diff --git a/pkg/api/transactions.go b/pkg/api/transactions.go index 9ee2065b..73a9272b 100644 --- a/pkg/api/transactions.go +++ b/pkg/api/transactions.go @@ -290,7 +290,7 @@ func (a *TransactionsApi) TransactionAmountsHandler(c *core.Context) (interface{ return nil, errs.ErrQueryItemsEmpty } - if len(requestItems) > 5 { + if len(requestItems) > 10 { log.WarnfWithRequestId(c, "[transactions.TransactionAmountsHandler] parse request failed, because there are too many items") return nil, errs.ErrQueryItemsTooMuch } diff --git a/public/img/desktop/card-background.png b/public/img/desktop/card-background.png new file mode 100644 index 00000000..cb321bf3 Binary files /dev/null and b/public/img/desktop/card-background.png differ diff --git a/public/img/desktop/document.svg b/public/img/desktop/document.svg new file mode 100644 index 00000000..41ba961c --- /dev/null +++ b/public/img/desktop/document.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/src/consts/datetime.js b/src/consts/datetime.js index ff98acc4..0e28f4d1 100644 --- a/src/consts/datetime.js +++ b/src/consts/datetime.js @@ -1,3 +1,18 @@ +const allMonthsArray = [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December' +]; + const allWeekDays = { Sunday: { type: 0, @@ -190,6 +205,7 @@ const defaultDateTimeFormatValue = 0; export default { allWeekDays: allWeekDays, allWeekDaysArray: allWeekDaysArray, + allMonthsArray: allMonthsArray, allLongDateFormat: allLongDateFormat, allLongDateFormatArray: allLongDateFormatArray, allShortDateFormat: allShortDateFormat, diff --git a/src/desktop-main.js b/src/desktop-main.js index 4ac4b6ec..1603ce34 100644 --- a/src/desktop-main.js +++ b/src/desktop-main.js @@ -46,8 +46,9 @@ import 'vuetify/styles'; import * as echarts from 'echarts/core'; import { CanvasRenderer } from 'echarts/renderers'; -import { PieChart } from 'echarts/charts'; +import { BarChart, PieChart } from 'echarts/charts'; import { + GridComponent, TooltipComponent, LegendComponent, } from 'echarts/components'; @@ -363,7 +364,9 @@ const vuetify = createVuetify({ echarts.use([ CanvasRenderer, + BarChart, PieChart, + GridComponent, TooltipComponent, LegendComponent ]); diff --git a/src/lib/datetime.js b/src/lib/datetime.js index 02ee4194..c9f8ef5f 100644 --- a/src/lib/datetime.js +++ b/src/lib/datetime.js @@ -133,6 +133,11 @@ export function getDayOfWeekName(date) { return dateTimeConstants.allWeekDaysArray[dayOfWeek].name; } +export function getMonthName(date) { + const dayOfWeek = moment(date).month(); + return dateTimeConstants.allMonthsArray[dayOfWeek]; +} + export function getHour(date) { return moment(date).hour(); } diff --git a/src/lib/services.js b/src/lib/services.js index 3334c938..be202737 100644 --- a/src/lib/services.js +++ b/src/lib/services.js @@ -258,7 +258,7 @@ export default { return axios.get('v1/transactions/statistics.json' + (queryParams.length ? '?' + queryParams.join('&') : '')); }, - getTransactionAmounts: ({ today, thisWeek, thisMonth, thisYear }) => { + getTransactionAmounts: ({ today, thisWeek, thisMonth, thisYear, lastMonth, monthBeforeLastMonth, monthBeforeLast2Months, monthBeforeLast3Months, monthBeforeLast4Months }) => { const queryParams = []; if (today) { @@ -277,6 +277,26 @@ export default { queryParams.push(`thisYear_${thisYear.startTime}_${thisYear.endTime}`); } + if (lastMonth) { + queryParams.push(`lastMonth_${lastMonth.startTime}_${lastMonth.endTime}`); + } + + if (monthBeforeLastMonth) { + queryParams.push(`monthBeforeLastMonth_${monthBeforeLastMonth.startTime}_${monthBeforeLastMonth.endTime}`); + } + + if (monthBeforeLast2Months) { + queryParams.push(`monthBeforeLast2Months_${monthBeforeLast2Months.startTime}_${monthBeforeLast2Months.endTime}`); + } + + if (monthBeforeLast3Months) { + queryParams.push(`monthBeforeLast3Months_${monthBeforeLast3Months.startTime}_${monthBeforeLast3Months.endTime}`); + } + + if (monthBeforeLast4Months) { + queryParams.push(`monthBeforeLast4Months_${monthBeforeLast4Months.startTime}_${monthBeforeLast4Months.endTime}`); + } + return axios.get('v1/transactions/amounts.json' + (queryParams.length ? '?query=' + queryParams.join('|') : '')); }, getTransaction: ({ id }) => { diff --git a/src/locales/en.js b/src/locales/en.js index 97004aaa..9c51cf04 100644 --- a/src/locales/en.js +++ b/src/locales/en.js @@ -837,6 +837,7 @@ export default { 'PIN code is wrong': 'PIN code is wrong', 'Sign Up': 'Sign Up', 'Overview': 'Overview', + 'Trend in Income and Expense': 'Trend in Income and Expense', 'View Details': 'View Details', 'Transaction List': 'Transaction List', 'Account List': 'Account List', diff --git a/src/locales/zh_Hans.js b/src/locales/zh_Hans.js index 0ef5e399..a1747981 100644 --- a/src/locales/zh_Hans.js +++ b/src/locales/zh_Hans.js @@ -837,6 +837,7 @@ export default { 'PIN code is wrong': 'PIN码错误', 'Sign Up': '注册', 'Overview': '总览', + 'Trend in Income and Expense': '收入与支出趋势', 'View Details': '查看详情', 'Transaction List': '交易列表', 'Account List': '账户列表', diff --git a/src/stores/overview.js b/src/stores/overview.js index 2e4b7066..ddf6ae80 100644 --- a/src/stores/overview.js +++ b/src/stores/overview.js @@ -5,6 +5,7 @@ import { useExchangeRatesStore } from './exchangeRates.js'; import { isNumber, isEquals } from '@/lib/common.js'; import { + getUnixTimeBeforeUnixTime, getTodayFirstUnixTime, getTodayLastUnixTime, getThisWeekFirstUnixTime, @@ -31,6 +32,21 @@ function updateTransactionDateRange(state) { state.transactionDataRange.thisYear.startTime = getThisYearFirstUnixTime(); state.transactionDataRange.thisYear.endTime = getThisYearLastUnixTime(); + + state.transactionDataRange.lastMonth.startTime = getUnixTimeBeforeUnixTime(getThisMonthFirstUnixTime(), 1, 'months'); + state.transactionDataRange.lastMonth.endTime = getUnixTimeBeforeUnixTime(getThisMonthLastUnixTime(), 1, 'months'); + + state.transactionDataRange.monthBeforeLastMonth.startTime = getUnixTimeBeforeUnixTime(getThisMonthFirstUnixTime(), 2, 'months'); + state.transactionDataRange.monthBeforeLastMonth.endTime = getUnixTimeBeforeUnixTime(getThisMonthLastUnixTime(), 2, 'months'); + + state.transactionDataRange.monthBeforeLast2Months.startTime = getUnixTimeBeforeUnixTime(getThisMonthFirstUnixTime(), 3, 'months'); + state.transactionDataRange.monthBeforeLast2Months.endTime = getUnixTimeBeforeUnixTime(getThisMonthLastUnixTime(), 3, 'months'); + + state.transactionDataRange.monthBeforeLast3Months.startTime = getUnixTimeBeforeUnixTime(getThisMonthFirstUnixTime(), 4, 'months'); + state.transactionDataRange.monthBeforeLast3Months.endTime = getUnixTimeBeforeUnixTime(getThisMonthLastUnixTime(), 4, 'months'); + + state.transactionDataRange.monthBeforeLast4Months.startTime = getUnixTimeBeforeUnixTime(getThisMonthFirstUnixTime(), 5, 'months'); + state.transactionDataRange.monthBeforeLast4Months.endTime = getUnixTimeBeforeUnixTime(getThisMonthLastUnixTime(), 5, 'months'); } export const useOverviewStore = defineStore('overview', { @@ -51,8 +67,31 @@ export const useOverviewStore = defineStore('overview', { thisYear: { startTime: getThisYearFirstUnixTime(), endTime: getThisYearLastUnixTime() + }, + lastMonth: { + startTime: getUnixTimeBeforeUnixTime(getThisMonthFirstUnixTime(), 1, 'months'), + endTime: getUnixTimeBeforeUnixTime(getThisMonthLastUnixTime(), 1, 'months') + }, + monthBeforeLastMonth: { + startTime: getUnixTimeBeforeUnixTime(getThisMonthFirstUnixTime(), 2, 'months'), + endTime: getUnixTimeBeforeUnixTime(getThisMonthLastUnixTime(), 2, 'months') + }, + monthBeforeLast2Months: { + startTime: getUnixTimeBeforeUnixTime(getThisMonthFirstUnixTime(), 3, 'months'), + endTime: getUnixTimeBeforeUnixTime(getThisMonthLastUnixTime(), 3, 'months') + }, + monthBeforeLast3Months: { + startTime: getUnixTimeBeforeUnixTime(getThisMonthFirstUnixTime(), 4, 'months'), + endTime: getUnixTimeBeforeUnixTime(getThisMonthLastUnixTime(), 4, 'months') + }, + monthBeforeLast4Months: { + startTime: getUnixTimeBeforeUnixTime(getThisMonthFirstUnixTime(), 5, 'months'), + endTime: getUnixTimeBeforeUnixTime(getThisMonthLastUnixTime(), 5, 'months') } }, + transactionOverviewOptions: { + loadLast5Months: false + }, transactionOverviewData: {}, transactionOverviewStateInvalid: true }), @@ -78,7 +117,7 @@ export const useOverviewStore = defineStore('overview', { const finalOverviewData = {}; const defaultCurrency = userStore.currentUserDefaultCurrency; - [ 'today', 'thisWeek', 'thisMonth', 'thisYear' ].forEach(field => { + [ 'today', 'thisWeek', 'thisMonth', 'thisYear', 'lastMonth', 'monthBeforeLastMonth', 'monthBeforeLast2Months', 'monthBeforeLast3Months', 'monthBeforeLast4Months' ].forEach(field => { if (!Object.prototype.hasOwnProperty.call(overviewData, field)) { return; } @@ -139,31 +178,47 @@ export const useOverviewStore = defineStore('overview', { }, resetTransactionOverview() { updateTransactionDateRange(this); + this.transactionOverviewOptions.loadLast5Months = false; this.transactionOverviewData = {}; this.transactionOverviewStateInvalid = true; }, - loadTransactionOverview({ force }) { + loadTransactionOverview({ force, loadLast5Months }) { const self = this; let dateChanged = false; + let rangeChanged = false; if (self.transactionDataRange.today.startTime !== getTodayFirstUnixTime()) { dateChanged = true; updateTransactionDateRange(self); } - if (!dateChanged && !force && !self.transactionOverviewStateInvalid) { + if (loadLast5Months && !self.transactionOverviewOptions.loadLast5Months) { + rangeChanged = true; + } + + if (!dateChanged && !rangeChanged && !force && !self.transactionOverviewStateInvalid) { return new Promise((resolve) => { resolve(self.transactionOverviewData); }); } + const requestParams = { + today: self.transactionDataRange.today, + thisWeek: self.transactionDataRange.thisWeek, + thisMonth: self.transactionDataRange.thisMonth, + thisYear: self.transactionDataRange.thisYear + }; + + if (loadLast5Months) { + requestParams.lastMonth = self.transactionDataRange.lastMonth; + requestParams.monthBeforeLastMonth = self.transactionDataRange.monthBeforeLastMonth; + requestParams.monthBeforeLast2Months = self.transactionDataRange.monthBeforeLast2Months; + requestParams.monthBeforeLast3Months = self.transactionDataRange.monthBeforeLast3Months; + requestParams.monthBeforeLast4Months = self.transactionDataRange.monthBeforeLast4Months; + } + return new Promise((resolve, reject) => { - services.getTransactionAmounts({ - today: self.transactionDataRange.today, - thisWeek: self.transactionDataRange.thisWeek, - thisMonth: self.transactionDataRange.thisMonth, - thisYear: self.transactionDataRange.thisYear - }).then(response => { + services.getTransactionAmounts(requestParams).then(response => { const data = response.data; if (!data || !data.success || !data.result) { @@ -181,6 +236,7 @@ export const useOverviewStore = defineStore('overview', { } self.transactionOverviewData = data.result; + self.transactionOverviewOptions.loadLast5Months = loadLast5Months; resolve(data.result); }).catch(error => { diff --git a/src/views/desktop/HomePage.vue b/src/views/desktop/HomePage.vue index 0776a9a1..03649e4f 100644 --- a/src/views/desktop/HomePage.vue +++ b/src/views/desktop/HomePage.vue @@ -4,9 +4,11 @@ + + + + + + diff --git a/src/views/desktop/overview/MonthlyIncomeAndExpenseCard.vue b/src/views/desktop/overview/MonthlyIncomeAndExpenseCard.vue new file mode 100644 index 00000000..4ffb7507 --- /dev/null +++ b/src/views/desktop/overview/MonthlyIncomeAndExpenseCard.vue @@ -0,0 +1,258 @@ + + + + + diff --git a/third-patry-dependencies.json b/third-patry-dependencies.json index 8f25f71a..d85733cb 100644 --- a/third-patry-dependencies.json +++ b/third-patry-dependencies.json @@ -270,6 +270,11 @@ "url": "https://materialdesignicons.com", "licenseUrl": "https://github.com/Templarian/MaterialDesign-JS/blob/v7.2.96/LICENSE" }, + { + "name": "Solar Icons Set", + "url": "https://www.figma.com/community/file/1166831539721848736/Solar-Icons-Set", + "licenseUrl": "https://creativecommons.org/licenses/by/4.0" + }, { "name": "Hand drawn minimal background", "url": "https://www.freepik.com/free-vector/hand-drawn-minimal-background_15441932.htm",