diff --git a/pkg/models/transaction.go b/pkg/models/transaction.go index 24a54eea..eab98482 100644 --- a/pkg/models/transaction.go +++ b/pkg/models/transaction.go @@ -126,8 +126,8 @@ type TransactionListInMonthByPageRequest struct { CategoryId int64 `form:"category_id" binding:"min=0"` AccountId int64 `form:"account_id" binding:"min=0"` Keyword string `form:"keyword"` - Page int32 `form:"page" binding:"required,min=1"` - Count int32 `form:"count" binding:"required,min=1,max=50"` + Page int32 `form:"page" binding:"min=0"` + Count int32 `form:"count" binding:"min=0,max=50"` TrimAccount bool `form:"trim_account"` TrimCategory bool `form:"trim_category"` TrimTag bool `form:"trim_tag"` diff --git a/pkg/services/transactions.go b/pkg/services/transactions.go index e1277c85..b458f617 100644 --- a/pkg/services/transactions.go +++ b/pkg/services/transactions.go @@ -98,11 +98,11 @@ func (s *TransactionService) GetTransactionsInMonthByPage(uid int64, year int32, return nil, errs.ErrUserIdInvalid } - if page < 1 { + if page < 0 || (count > 0 && page < 1) { return nil, errs.ErrPageIndexInvalid } - if count < 1 { + if count < 0 { return nil, errs.ErrPageCountInvalid } @@ -120,7 +120,13 @@ func (s *TransactionService) GetTransactionsInMonthByPage(uid int64, year int32, var transactions []*models.Transaction condition, conditionParams := s.getTransactionQueryCondition(uid, maxTransactionTime, minTransactionTime, transactionType, categoryIds, accountIds, keyword, true) - err = s.UserDataDB(uid).Where(condition, conditionParams...).Limit(int(count), int(count*(page-1))).OrderBy("transaction_time desc").Find(&transactions) + sess := s.UserDataDB(uid).Where(condition, conditionParams...) + + if count > 0 { + sess = sess.Limit(int(count), int(count*(page-1))) + } + + err = sess.OrderBy("transaction_time desc").Find(&transactions) return transactions, err } diff --git a/src/lib/datetime.js b/src/lib/datetime.js index 4560f386..fdf14801 100644 --- a/src/lib/datetime.js +++ b/src/lib/datetime.js @@ -331,6 +331,70 @@ export function getDateRangeByDateType(dateType, firstDayOfWeek) { }; } +export function getRecentMonthDateRanges(monthCount) { + const recentDateRanges = []; + const thisMonthFirstUnixTime = getThisMonthFirstUnixTime(); + + for (let i = 0; i < monthCount; i++) { + let minTime = thisMonthFirstUnixTime; + + if (i > 0) { + minTime = getUnixTimeBeforeUnixTime(thisMonthFirstUnixTime, i, 'months'); + } + + let maxTime = getUnixTimeBeforeUnixTime(getUnixTimeAfterUnixTime(minTime, 1, 'months'), 1, 'seconds'); + let dateType = dateTimeConstants.allDateRanges.Custom.type; + let year = getYear(parseDateFromUnixTime(minTime)); + let month = getMonth(parseDateFromUnixTime(minTime)); + + if (i === 0) { + dateType = dateTimeConstants.allDateRanges.ThisMonth.type; + } else if (i === 1) { + dateType = dateTimeConstants.allDateRanges.LastMonth.type; + } + + recentDateRanges.push({ + dateType: dateType, + minTime: minTime, + maxTime: maxTime, + year: year, + month: month + }); + } + + return recentDateRanges; +} + +export function getRecentDateRangeType(allRecentMonthDateRanges, dateType, minTime, maxTime, firstDayOfWeek) { + let dateRange = getDateRangeByDateType(dateType, firstDayOfWeek); + + if (dateRange && dateRange.dateType === dateTimeConstants.allDateRanges.All.type) { + return allRecentMonthDateRanges.length - 1; // Custom + } + + if (!dateRange && (!maxTime || !minTime)) { + return allRecentMonthDateRanges.length - 1; // Custom + } + + if (!dateRange) { + dateRange = { + dateType: dateTimeConstants.allDateRanges.Custom.type, + maxTime: maxTime, + minTime: minTime + }; + } + + for (let i = 0; i < allRecentMonthDateRanges.length - 1; i++) { + const recentDateRange = allRecentMonthDateRanges[i]; + + if (recentDateRange.minTime === dateRange.minTime && recentDateRange.maxTime === dateRange.maxTime) { + return i; + } + } + + return allRecentMonthDateRanges.length - 1; // Custom +} + export function isDateRangeMatchFullYears(minTime, maxTime) { const minDateTime = parseDateFromUnixTime(minTime).set({ second: 0, millisecond: 0 }); const maxDateTime = parseDateFromUnixTime(maxTime).set({ second: 59, millisecond: 999 }); diff --git a/src/lib/i18n.js b/src/lib/i18n.js index d3e7b756..4b9bd83c 100644 --- a/src/lib/i18n.js +++ b/src/lib/i18n.js @@ -22,6 +22,7 @@ import { getBrowserTimezoneOffset, getBrowserTimezoneOffsetMinutes, getDateTimeFormatType, + getRecentMonthDateRanges, isDateRangeMatchFullYears, isDateRangeMatchFullMonths } from './datetime.js'; @@ -388,10 +389,18 @@ function getMonthShortName(month, translateFn) { return translateFn(`datetime.${month}.short`); } +function getMonthLongName(month, translateFn) { + return translateFn(`datetime.${month}.long`); +} + function getWeekdayShortName(weekDay, translateFn) { return translateFn(`datetime.${weekDay}.short`); } +function getWeekdayLongName(weekDay, translateFn) { + return translateFn(`datetime.${weekDay}.long`); +} + function getI18nLongDateFormat(translateFn, formatTypeValue) { const defaultLongDateFormatTypeName = translateFn('default.longDateFormat'); return getDateTimeFormat(translateFn, datetime.allLongDateFormat, datetime.allLongDateFormatArray, 'format.longDate', defaultLongDateFormatTypeName, datetime.defaultLongDateFormat, formatTypeValue); @@ -588,6 +597,35 @@ function getAllDateRanges(includeCustom, translateFn) { return allDateRanges; } +function getAllRecentMonthDateRanges(userStore, includeCustom, translateFn) { + const allRecentMonthDateRanges = []; + const recentDateRanges = getRecentMonthDateRanges(12); + + for (let i = 0; i < recentDateRanges.length; i++) { + const recentDateRange = recentDateRanges[i]; + + allRecentMonthDateRanges.push({ + dateType: recentDateRange.dateType, + minTime: recentDateRange.minTime, + maxTime: recentDateRange.maxTime, + year: recentDateRange.year, + month: recentDateRange.month, + displayName: formatUnixTime(recentDateRange.minTime, getI18nLongYearMonthFormat(translateFn, userStore.currentUserLongDateFormat)) + }); + } + + if (includeCustom) { + allRecentMonthDateRanges.push({ + dateType: datetime.allDateRanges.Custom.type, + minTime: 0, + maxTime: 0, + displayName: translateFn('Custom Date') + }); + } + + return allRecentMonthDateRanges; +} + function getDateRangeDisplayName(userStore, dateType, startTime, endTime, translateFn) { if (dateType === datetime.allDateRanges.All.type) { return translateFn(datetime.allDateRanges.All.name); @@ -993,7 +1031,9 @@ export function i18nFunctions(i18nGlobal) { getAllLongTimeFormats: () => getAllLongTimeFormats(i18nGlobal.t), getAllShortTimeFormats: () => getAllShortTimeFormats(i18nGlobal.t), getMonthShortName: (month) => getMonthShortName(month, i18nGlobal.t), + getMonthLongName: (month) => getMonthLongName(month, i18nGlobal.t), getWeekdayShortName: (weekDay) => getWeekdayShortName(weekDay, i18nGlobal.t), + getWeekdayLongName: (weekDay) => getWeekdayLongName(weekDay, i18nGlobal.t), formatUnixTimeToLongDateTime: (userStore, unixTime, utcOffset, currentUtcOffset) => formatUnixTime(unixTime, getI18nLongDateFormat(i18nGlobal.t, userStore.currentUserLongDateFormat) + ' ' + getI18nLongTimeFormat(i18nGlobal.t, userStore.currentUserLongTimeFormat), utcOffset, currentUtcOffset), formatUnixTimeToShortDateTime: (userStore, unixTime, utcOffset, currentUtcOffset) => formatUnixTime(unixTime, getI18nShortDateFormat(i18nGlobal.t, userStore.currentUserShortDateFormat) + ' ' + getI18nShortTimeFormat(i18nGlobal.t, userStore.currentUserShortTimeFormat), utcOffset, currentUtcOffset), formatUnixTimeToLongDate: (userStore, unixTime, utcOffset, currentUtcOffset) => formatUnixTime(unixTime, getI18nLongDateFormat(i18nGlobal.t, userStore.currentUserLongDateFormat), utcOffset, currentUtcOffset), @@ -1014,6 +1054,7 @@ export function i18nFunctions(i18nGlobal) { getAllCurrencies: () => getAllCurrencies(i18nGlobal.t), getAllWeekDays: () => getAllWeekDays(i18nGlobal.t), getAllDateRanges: (includeCustom) => getAllDateRanges(includeCustom, i18nGlobal.t), + getAllRecentMonthDateRanges: (userStore, includeCustom) => getAllRecentMonthDateRanges(userStore, includeCustom, i18nGlobal.t), getDateRangeDisplayName: (userStore, dateType, startTime, endTime) => getDateRangeDisplayName(userStore, dateType, startTime, endTime, i18nGlobal.t), getAllStatisticsChartDataTypes: () => getAllStatisticsChartDataTypes(i18nGlobal.t), getAllStatisticsSortingTypes: () => getAllStatisticsSortingTypes(i18nGlobal.t), diff --git a/src/lib/services.js b/src/lib/services.js index 0cd3a153..4ca3780d 100644 --- a/src/lib/services.js +++ b/src/lib/services.js @@ -13,7 +13,7 @@ let needBlockRequest = false; let blockedRequests = []; axios.defaults.baseURL = api.baseApiUrlPath; -axios.defaults.timeout = 10000; // 10s +axios.defaults.timeout = 100000; // 10s axios.interceptors.request.use(config => { const token = userState.getToken(); @@ -240,6 +240,9 @@ export default { getTransactions: ({ maxTime, minTime, type, categoryId, accountId, keyword }) => { return axios.get(`v1/transactions/list.json?max_time=${maxTime}&min_time=${minTime}&type=${type}&category_id=${categoryId}&account_id=${accountId}&keyword=${keyword}&count=50&trim_account=true&trim_category=true&trim_tag=true`); }, + getAllTransactionsByMonth: ({ year, month, type, categoryId, accountId, keyword }) => { + return axios.get(`v1/transactions/list/by_month.json?year=${year}&month=${month}&type=${type}&category_id=${categoryId}&account_id=${accountId}&keyword=${keyword}&trim_account=true&trim_category=true&trim_tag=true`); + }, getTransactionStatistics: ({ startTime, endTime }) => { const queryParams = []; diff --git a/src/lib/ui.desktop.js b/src/lib/ui.desktop.js new file mode 100644 index 00000000..b9a868fa --- /dev/null +++ b/src/lib/ui.desktop.js @@ -0,0 +1,52 @@ +export function getOuterHeight(element) { + if (!element) { + return 0; + } + + const computedStyle = window.getComputedStyle(element); + + return ['height', 'padding-top', 'padding-bottom', 'margin-top', 'margin-bottom'] + .map((key) => parseInt(computedStyle.getPropertyValue(key), 10)) + .reduce((prev, cur) => prev + cur); +} + +export function getCssValue(element, name) { + if (!element) { + return 0; + } + + const computedStyle = window.getComputedStyle(element); + return computedStyle.getPropertyValue(name); +} + +export function scrollToMenuListItem(listContentEl) { + if (!listContentEl) { + return; + } + + const lists = listContentEl.querySelectorAll('div.v-list'); + + if (!lists.length || !lists[0]) { + return; + } + + const container = lists[0]; + const selectedItems = container.querySelectorAll('div.v-list-item.list-item-selected'); + + if (!selectedItems.length || !selectedItems[0]) { + return; + } + + const selectedItem = selectedItems[0]; + const containerOuterHeight = getOuterHeight(container); + const selectedItemOuterHeight = getOuterHeight(selectedItem); + + const targetPos = selectedItem.offsetTop - container.offsetTop - parseInt(getCssValue(container, 'padding-top'), 10) + - (containerOuterHeight - selectedItemOuterHeight) / 2; + + if (targetPos <= 0) { + return; + } + + container.scrollTop = targetPos; +} diff --git a/src/locales/en.js b/src/locales/en.js index b02ee79d..97004aaa 100644 --- a/src/locales/en.js +++ b/src/locales/en.js @@ -730,7 +730,9 @@ export default { 'Duplicate': 'Duplicate', 'Sort': 'Sort', 'Date': 'Date', + 'Time': 'Time', 'Type': 'Type', + 'All Types': 'All Types', 'More': 'More', 'All': 'All', 'Today': 'Today', @@ -862,6 +864,7 @@ export default { 'Unable to get account list': 'Unable to get account list', 'Account list is up to date': 'Account list is up to date', 'Account list has been updated': 'Account list has been updated', + 'All Accounts': 'All Accounts', 'No available account': 'No available account', 'Add Account': 'Add Account', 'Edit Account': 'Edit Account', @@ -1071,6 +1074,7 @@ export default { 'Income Secondary Categories': 'Income Secondary Categories', 'Transfer Secondary Categories': 'Transfer Secondary Categories', 'Transaction Secondary Categories': 'Transaction Secondary Categories', + 'All Categories': 'All Categories', 'No available category': 'No available category', 'Add Default Categories': 'Add Default Categories', 'Default Categories': 'Default Categories', diff --git a/src/locales/zh_Hans.js b/src/locales/zh_Hans.js index 243bf430..0ef5e399 100644 --- a/src/locales/zh_Hans.js +++ b/src/locales/zh_Hans.js @@ -730,7 +730,9 @@ export default { 'Duplicate': '复制', 'Sort': '排序', 'Date': '日期', + 'Time': '时间', 'Type': '类型', + 'All Types': '全部类型', 'More': '更多', 'All': '全部', 'Today': '今天', @@ -862,6 +864,7 @@ export default { 'Unable to get account list': '无法获取账户列表', 'Account list is up to date': '账户列表已是最新', 'Account list has been updated': '账户列表已更新', + 'All Accounts': '全部账户', 'No available account': '没有可用的账户', 'Add Account': '添加账户', 'Edit Account': '编辑账户', @@ -1071,6 +1074,7 @@ export default { 'Income Secondary Categories': '收入二级分类', 'Transfer Secondary Categories': '转账二级分类', 'Transaction Secondary Categories': '交易二级分类', + 'All Categories': '全部分类', 'No available category': '没有可用的分类', 'Add Default Categories': '添加默认分类', 'Default Categories': '默认分类', diff --git a/src/router/desktop.js b/src/router/desktop.js index 7fa4bd8f..b76b458e 100644 --- a/src/router/desktop.js +++ b/src/router/desktop.js @@ -82,7 +82,15 @@ const router = createRouter({ { path: '/transactions', component: TransactionsPage, - beforeEnter: checkLogin + beforeEnter: checkLogin, + props: route => ({ + initDateType: route.query.dateType, + initMaxTime: route.query.maxTime, + initMinTime: route.query.minTime, + initType: route.query.type, + initCategoryId: route.query.categoryId, + initAccountId: route.query.accountId + }) }, { path: '/statistics/transaction', diff --git a/src/stores/transaction.js b/src/stores/transaction.js index bfe31d2a..aacfe3f9 100644 --- a/src/stores/transaction.js +++ b/src/stores/transaction.js @@ -377,7 +377,31 @@ export const useTransactionsStore = defineStore('transactions', { this.transactionsFilter.keyword = filter.keyword; } }, - loadTransactions({ reload, autoExpand, defaultCurrency }) { + getTransactionListPageParams() { + const querys = []; + + if (this.transactionsFilter.type) { + querys.push('type=' + this.transactionsFilter.type); + } + + if (this.transactionsFilter.accountId && this.transactionsFilter.accountId !== '0') { + querys.push('accountId=' + this.transactionsFilter.accountId); + } + + if (this.transactionsFilter.categoryId && this.transactionsFilter.categoryId !== '0') { + querys.push('categoryId=' + this.transactionsFilter.categoryId); + } + + querys.push('dateType=' + this.transactionsFilter.dateType); + + if (this.transactionsFilter.dateType === datetimeConstants.allDateRanges.Custom.type) { + querys.push('maxTime=' + this.transactionsFilter.maxTime); + querys.push('minTime=' + this.transactionsFilter.minTime); + } + + return querys.join('&'); + }, + loadTransactions({ reload, yearMonth, autoExpand, defaultCurrency }) { const self = this; const settingsStore = useSettingsStore(); const exchangeRatesStore = useExchangeRatesStore(); @@ -390,14 +414,29 @@ export const useTransactionsStore = defineStore('transactions', { } return new Promise((resolve, reject) => { - services.getTransactions({ + let promise = null; + + if (yearMonth) { + promise = services.getAllTransactionsByMonth({ + year: yearMonth.year, + month: yearMonth.month, + type: self.transactionsFilter.type, + categoryId: self.transactionsFilter.categoryId, + accountId: self.transactionsFilter.accountId, + keyword: self.transactionsFilter.keyword + }); + } else { + promise = services.getTransactions({ maxTime: actualMaxTime, minTime: self.transactionsFilter.minTime * 1000, type: self.transactionsFilter.type, categoryId: self.transactionsFilter.categoryId, accountId: self.transactionsFilter.accountId, keyword: self.transactionsFilter.keyword - }).then(response => { + }); + } + + promise.then(response => { const data = response.data; if (!data || !data.success || !data.result) { diff --git a/src/styles/desktop/global.scss b/src/styles/desktop/global.scss index 15ac90b8..9ab17348 100644 --- a/src/styles/desktop/global.scss +++ b/src/styles/desktop/global.scss @@ -113,16 +113,22 @@ input[type=number] { tr.even-row { background: #fcfcfc; } + + tbody.has-bottom-border > tr:last-child > td { + border-bottom: thin solid rgba(var(--v-border-color), var(--v-border-opacity)); + } +} + +.v-table.v-table--hover { + tbody > tr.no-hover:hover td { + background: inherit !important; + } } .v-table.table-striped { tr:nth-child(even) { background: #fcfcfc; } - - tbody.has-bottom-border > tr:last-child > td { - border-bottom: thin solid rgba(var(--v-border-color), var(--v-border-opacity)); - } } .v-theme--dark { diff --git a/src/views/desktop/MainLayout.vue b/src/views/desktop/MainLayout.vue index 7b257ab8..38340499 100644 --- a/src/views/desktop/MainLayout.vue +++ b/src/views/desktop/MainLayout.vue @@ -27,7 +27,7 @@