feat: 表格分页支持统一配置并记忆各表格每页条数
- 后端新增聚合用户偏好 paginationPrefs(每页条数候选列表与默认每页条数),提供 GET/PUT /api/setting/pagination-prefs,默认候选 [10,20,50,100]、默认每页 20,非法值自动过滤并回退 - 后端新增 /category/page、/tag/page、/channel/page、/email-bill/configs/page、/email-bill/sender-rules/page 分页接口,原全量接口保持不变以免影响下拉、筛选、AI 与导出 - 前端新增共享 composables/usePageSizeOptions.ts 与 useTablePageSize.ts,所有表格统一取用候选列表并记住各自上次选择的每页条数,优先级为表格记忆 > 全局默认 > 20 - 各表格上次选择以聚合偏好 finance.pagination.tablePageSizes 存于本机 localStorage,收支记录页旧的 filterPrefs.pageSize 自动迁移 - 设置页外观设置新增「表格每页条数选项」分组,可增删候选值并配置默认每页条数,保存后全局即时生效 - 路由守卫在已登录时预加载分页偏好,保证表格页首次渲染即使用配置值 - 前端分类、标签、渠道、邮箱配置、发件人规则表格接入后端分页 - 补充中文、英文、日文三语文案
This commit is contained in:
@@ -6,6 +6,11 @@ export function getCategoryList(type?: string) {
|
||||
return request.get('/category/list', { params: type ? { type } : {} })
|
||||
}
|
||||
|
||||
/** 分页获取分类列表,可按类型筛选 */
|
||||
export function getCategoryPage(params: { type?: string; page?: number; size?: number }) {
|
||||
return request.get('/category/page', { params })
|
||||
}
|
||||
|
||||
/** 创建新分类 */
|
||||
export function createCategory(data: { name: string; type: string; icon?: string; sortOrder?: number }) {
|
||||
return request.post('/category', data)
|
||||
|
||||
@@ -14,6 +14,11 @@ export function getChannelList() {
|
||||
return request.get('/channel')
|
||||
}
|
||||
|
||||
/** 分页查询当前用户的自定义渠道列表(含使用账单数) */
|
||||
export function getChannelPage(params: { page?: number; size?: number }) {
|
||||
return request.get('/channel/page', { params })
|
||||
}
|
||||
|
||||
/** 新建自定义渠道 */
|
||||
export function createChannel(name: string) {
|
||||
return request.post('/channel', { name })
|
||||
|
||||
@@ -99,6 +99,11 @@ export function listEmailBillConfigs() {
|
||||
return request.get('/email-bill/configs')
|
||||
}
|
||||
|
||||
/** 分页查询邮箱配置列表 */
|
||||
export function pageEmailBillConfigs(params: { page?: number; size?: number }) {
|
||||
return request.get('/email-bill/configs/page', { params })
|
||||
}
|
||||
|
||||
/** 新增邮箱配置 */
|
||||
export function createEmailBillConfig(data: EmailBillConfigPayload) {
|
||||
return request.post('/email-bill/configs', data)
|
||||
@@ -134,6 +139,11 @@ export function listEmailBillSenderRules() {
|
||||
return request.get('/email-bill/sender-rules')
|
||||
}
|
||||
|
||||
/** 分页查询发件人规则列表 */
|
||||
export function pageEmailBillSenderRules(params: { page?: number; size?: number }) {
|
||||
return request.get('/email-bill/sender-rules/page', { params })
|
||||
}
|
||||
|
||||
/** 新增发件人规则 */
|
||||
export function createEmailBillSenderRule(data: EmailBillSenderRulePayload) {
|
||||
return request.post('/email-bill/sender-rules', data)
|
||||
|
||||
@@ -30,3 +30,13 @@ export function getShowHiddenData() {
|
||||
export function updateShowHiddenData(show: boolean) {
|
||||
return request.put('/setting/show-hidden-data', { show })
|
||||
}
|
||||
|
||||
/** 查询当前用户配置的表格分页偏好(每页条数候选列表与默认每页条数) */
|
||||
export function getPaginationPrefs() {
|
||||
return request.get('/setting/pagination-prefs')
|
||||
}
|
||||
|
||||
/** 更新当前用户配置的表格分页偏好 */
|
||||
export function updatePaginationPrefs(data: { options: number[]; defaultSize: number }) {
|
||||
return request.put('/setting/pagination-prefs', data)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,11 @@ export function getTagList() {
|
||||
return request.get('/tag/list')
|
||||
}
|
||||
|
||||
/** 分页获取标签列表 */
|
||||
export function getTagPage(params: { page?: number; size?: number }) {
|
||||
return request.get('/tag/page', { params })
|
||||
}
|
||||
|
||||
/** 创建新标签 */
|
||||
export function createTag(data: { name: string; color?: string; sortOrder?: number }) {
|
||||
return request.post('/tag', data)
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
// 外观设置面板:配置「不计入统计」记录在收支记录列表中的展示样式,以及是否显示已隐藏备注的记录
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { ref, onMounted, computed, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import {
|
||||
useMessage, NCard, NSpace, NButton, NIcon, NText, NRadioGroup, NRadio, NSwitch,
|
||||
useMessage, NCard, NSpace, NButton, NIcon, NText, NRadioGroup, NRadio, NSwitch, NDynamicTags, NSelect,
|
||||
} from 'naive-ui'
|
||||
import { ColorPaletteOutline, SaveOutline } from '@vicons/ionicons5'
|
||||
import { getExcludedDisplayStyle, updateExcludedDisplayStyle, getShowHiddenData, updateShowHiddenData } from '../api/setting'
|
||||
import {
|
||||
getExcludedDisplayStyle, updateExcludedDisplayStyle, getShowHiddenData, updateShowHiddenData,
|
||||
getPaginationPrefs, updatePaginationPrefs,
|
||||
} from '../api/setting'
|
||||
import { DEFAULT_PAGE_SIZE, DEFAULT_PAGE_SIZE_OPTIONS, loadPageSizeOptions } from '../composables/usePageSizeOptions'
|
||||
|
||||
const message = useMessage()
|
||||
const { t } = useI18n()
|
||||
@@ -21,6 +25,29 @@ const showHiddenData = ref(false)
|
||||
const loadingHidden = ref(false)
|
||||
const savingHidden = ref(false)
|
||||
|
||||
/** 表格分页每页条数候选列表(字符串标签形式,保存时解析为数字) */
|
||||
const pageSizeTags = ref<string[]>([])
|
||||
/** 默认每页条数 */
|
||||
const pageSizeDefault = ref<number>(DEFAULT_PAGE_SIZE)
|
||||
const savingPageSizes = ref(false)
|
||||
|
||||
/** 候选列表解析为合法正整数(升序去重),用于默认值下拉 */
|
||||
const parsedPageSizes = computed(() =>
|
||||
Array.from(
|
||||
new Set(pageSizeTags.value.map((v) => Number(String(v).trim())).filter((v) => Number.isInteger(v) && v > 0)),
|
||||
).sort((a, b) => a - b),
|
||||
)
|
||||
|
||||
/** 默认每页条数下拉选项(来自候选列表) */
|
||||
const defaultSizeOptions = computed(() => parsedPageSizes.value.map((v) => ({ label: String(v), value: v })))
|
||||
|
||||
// 候选列表变更后,若默认值不在其中则回退为首项
|
||||
watch(parsedPageSizes, (list) => {
|
||||
if (list.length && !list.includes(pageSizeDefault.value)) {
|
||||
pageSizeDefault.value = list[0]
|
||||
}
|
||||
})
|
||||
|
||||
/** 展示样式选项(随语言切换响应式更新) */
|
||||
const styleOptions = computed(() => [
|
||||
{ label: t('display.strikethrough'), value: 'strikethrough' },
|
||||
@@ -85,9 +112,49 @@ async function handleHiddenChange(value: string | number | boolean) {
|
||||
}
|
||||
}
|
||||
|
||||
/** 应用后端返回的分页偏好到表单 */
|
||||
function applyPaginationPrefs(data: any, fallbackOptions: number[], fallbackDefault: number) {
|
||||
const list: number[] = Array.isArray(data?.options) ? data.options : fallbackOptions
|
||||
pageSizeTags.value = list.map((v) => String(v))
|
||||
const def = Number(data?.defaultSize)
|
||||
pageSizeDefault.value = list.includes(def) ? def : (list[0] ?? fallbackDefault)
|
||||
}
|
||||
|
||||
/** 加载表格分页偏好,失败时回退默认值 */
|
||||
async function loadPageSizes() {
|
||||
try {
|
||||
const res: any = await getPaginationPrefs()
|
||||
applyPaginationPrefs(res?.data, DEFAULT_PAGE_SIZE_OPTIONS, DEFAULT_PAGE_SIZE)
|
||||
} catch (e: any) {
|
||||
applyPaginationPrefs(null, DEFAULT_PAGE_SIZE_OPTIONS, DEFAULT_PAGE_SIZE)
|
||||
}
|
||||
}
|
||||
|
||||
/** 保存表格分页偏好(候选列表 + 默认每页条数),成功后刷新全局共享配置 */
|
||||
async function handleSavePageSizes() {
|
||||
const options = parsedPageSizes.value
|
||||
if (!options.length) {
|
||||
message.error(t('display.pageSizeEmpty'))
|
||||
return
|
||||
}
|
||||
const defaultSize = options.includes(pageSizeDefault.value) ? pageSizeDefault.value : options[0]
|
||||
savingPageSizes.value = true
|
||||
try {
|
||||
const res: any = await updatePaginationPrefs({ options, defaultSize })
|
||||
applyPaginationPrefs(res?.data, options, defaultSize)
|
||||
await loadPageSizeOptions(true)
|
||||
message.success(t('display.pageSizeSaved'))
|
||||
} catch (e: any) {
|
||||
message.error(e.message || t('display.pageSizeFailed'))
|
||||
} finally {
|
||||
savingPageSizes.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadStyle()
|
||||
loadHiddenData()
|
||||
loadPageSizes()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -147,6 +214,21 @@ onMounted(() => {
|
||||
</NSpace>
|
||||
</div>
|
||||
|
||||
<div class="config-group">
|
||||
<div class="group-title">{{ t('display.pageSizeLabel') }}</div>
|
||||
<NText depth="3" style="font-size: 12px; display: block; margin-bottom: 8px;">{{ t('display.pageSizeHint') }}</NText>
|
||||
<NSpace align="center" :size="12">
|
||||
<NDynamicTags v-model:value="pageSizeTags" :max="10" style="max-width: 360px;" />
|
||||
</NSpace>
|
||||
<NSpace align="center" :size="12" style="margin-top: 8px;">
|
||||
<NText depth="3" style="font-size: 12px;">{{ t('display.defaultPageSizeLabel') }}</NText>
|
||||
<NSelect v-model:value="pageSizeDefault" :options="defaultSizeOptions" style="width: 120px;" />
|
||||
<NButton size="small" :loading="savingPageSizes" @click="handleSavePageSizes">
|
||||
{{ t('display.pageSizeSave') }}
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</div>
|
||||
|
||||
<NSpace justify="end" style="margin-top: 16px; width: 100%;">
|
||||
<NButton type="primary" :loading="saving" @click="handleSave">
|
||||
<template #icon>
|
||||
|
||||
@@ -11,6 +11,8 @@ import { getAiConfig, parseRelationByAi } from '../api/ai'
|
||||
import { getChannelLabel } from '../utils/channel'
|
||||
import { readString, writeString } from '../utils/storage'
|
||||
import { STORAGE_KEYS } from '../utils/storageKeys'
|
||||
import { usePageSizeOptions } from '../composables/usePageSizeOptions'
|
||||
import { useTablePageSize } from '../composables/useTablePageSize'
|
||||
|
||||
/** 关系类型偏好存储键(界面偏好统一以 finance. 前缀命名) */
|
||||
const RELATION_TYPE_KEY = STORAGE_KEYS.transactionRelationType
|
||||
@@ -34,6 +36,9 @@ const emit = defineEmits<{
|
||||
const { t } = useI18n()
|
||||
const message = useMessage()
|
||||
|
||||
// 所有表格共用的每页条数候选列表
|
||||
const { pageSizeOptions } = usePageSizeOptions()
|
||||
|
||||
// 编辑状态
|
||||
const editingGroupId = ref<number | null>(null)
|
||||
const editorType = ref<TransactionRelationType>('PARENT_CHILD')
|
||||
@@ -52,7 +57,7 @@ const showPicker = ref(false)
|
||||
const pickerRows = ref<RelationMemberDraft[]>([])
|
||||
const pickerChecked = ref<number[]>([])
|
||||
const pickerPage = ref(1)
|
||||
const pickerSize = ref(10)
|
||||
const pickerSize = useTablePageSize('relationPicker')
|
||||
const pickerTotal = ref(0)
|
||||
const pickerRemark = ref('')
|
||||
const pickerLoading = ref(false)
|
||||
@@ -287,6 +292,12 @@ async function loadPicker(page: number) {
|
||||
}
|
||||
}
|
||||
|
||||
/** 候选账单每页条数变化,回到第一页重新加载 */
|
||||
function handlePickerPageSizeChange(size: number) {
|
||||
pickerSize.value = size
|
||||
loadPicker(1)
|
||||
}
|
||||
|
||||
/** 确认选择候选账单 */
|
||||
function confirmPick() {
|
||||
const existing = new Set(editorMembers.value.map((item) => item.transactionId))
|
||||
@@ -423,9 +434,12 @@ const pickerColumns = computed<DataTableColumns<RelationMemberDraft>>(() => [
|
||||
<NSpace justify="space-between" align="center" style="margin-top: 12px;">
|
||||
<NPagination
|
||||
v-model:page="pickerPage"
|
||||
:page-size="pickerSize"
|
||||
v-model:page-size="pickerSize"
|
||||
:item-count="pickerTotal"
|
||||
:page-sizes="pageSizeOptions"
|
||||
show-size-picker
|
||||
@update:page="loadPicker"
|
||||
@update:page-size="handlePickerPageSizeChange"
|
||||
/>
|
||||
<NSpace>
|
||||
<NButton @click="showPicker = false">{{ t('common.cancel') }}</NButton>
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
// 表格分页偏好:所有表格共用的每页条数候选列表与默认每页条数,统一从后端用户设置读取,未配置时回退默认值
|
||||
import { ref } from 'vue'
|
||||
import { getPaginationPrefs } from '../api/setting'
|
||||
|
||||
/** 默认每页条数候选列表 */
|
||||
export const DEFAULT_PAGE_SIZE_OPTIONS: number[] = [10, 20, 50, 100]
|
||||
|
||||
/** 默认每页条数 */
|
||||
export const DEFAULT_PAGE_SIZE = 20
|
||||
|
||||
// 模块级单例,供所有表格响应式共享
|
||||
const pageSizeOptions = ref<number[]>([...DEFAULT_PAGE_SIZE_OPTIONS])
|
||||
const defaultPageSize = ref<number>(DEFAULT_PAGE_SIZE)
|
||||
let loaded = false
|
||||
|
||||
/** 将后端返回值规范化为升序去重的正整数数组,非法时回退默认 */
|
||||
function normalizeOptions(data: unknown): number[] {
|
||||
if (!Array.isArray(data)) return [...DEFAULT_PAGE_SIZE_OPTIONS]
|
||||
const list = data.map((v) => Number(v)).filter((v) => Number.isInteger(v) && v > 0)
|
||||
return list.length ? Array.from(new Set(list)).sort((a, b) => a - b) : [...DEFAULT_PAGE_SIZE_OPTIONS]
|
||||
}
|
||||
|
||||
/** 规范化默认每页条数:须为候选列表内的正整数,否则回退候选列表首项 */
|
||||
function normalizeDefault(value: unknown, options: number[]): number {
|
||||
const size = Number(value)
|
||||
return Number.isInteger(size) && options.includes(size) ? size : options[0]
|
||||
}
|
||||
|
||||
/**
|
||||
* 从后端加载分页偏好;force 为 true 时强制刷新(设置页保存后调用)。
|
||||
*
|
||||
* :param force: 是否强制重新加载
|
||||
* :returns: 当前的候选列表
|
||||
*/
|
||||
export async function loadPageSizeOptions(force = false): Promise<number[]> {
|
||||
if (loaded && !force) return pageSizeOptions.value
|
||||
try {
|
||||
const res: any = await getPaginationPrefs()
|
||||
const options = normalizeOptions(res?.data?.options)
|
||||
pageSizeOptions.value = options
|
||||
defaultPageSize.value = normalizeDefault(res?.data?.defaultSize, options)
|
||||
loaded = true
|
||||
} catch (e) {
|
||||
// 加载失败时保持默认值
|
||||
}
|
||||
return pageSizeOptions.value
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取共享的分页偏好(候选列表与默认每页条数),首次调用时异步加载。
|
||||
*
|
||||
* :returns: 候选列表 ref 与默认每页条数 ref
|
||||
*/
|
||||
export function usePageSizeOptions() {
|
||||
if (!loaded) {
|
||||
void loadPageSizeOptions()
|
||||
}
|
||||
return { pageSizeOptions, defaultPageSize }
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// 表格每页大小偏好:优先取本机上次选择,其次取全局默认值,变更自动持久化
|
||||
import { ref, watch } from 'vue'
|
||||
import type { Ref } from 'vue'
|
||||
import { usePageSizeOptions } from './usePageSizeOptions'
|
||||
import { readTablePageSize, writeTablePageSize } from '../utils/tablePageSizePrefs'
|
||||
|
||||
/**
|
||||
* 管理指定表格的每页大小:初始化时读取本机偏好(须在候选列表内),否则回退全局默认值,变更自动持久化。
|
||||
*
|
||||
* :param tableKey: 表格唯一键
|
||||
* :returns: 与存储同步的每页大小 ref
|
||||
*/
|
||||
export function useTablePageSize(tableKey: string): Ref<number> {
|
||||
const { pageSizeOptions, defaultPageSize } = usePageSizeOptions()
|
||||
const saved = readTablePageSize(tableKey)
|
||||
const initial = saved !== null && pageSizeOptions.value.includes(saved) ? saved : defaultPageSize.value
|
||||
const size = ref<number>(initial)
|
||||
watch(size, (next) => writeTablePageSize(tableKey, next))
|
||||
return size
|
||||
}
|
||||
@@ -196,6 +196,13 @@ export default {
|
||||
hiddenDataHint: { zh: '关闭时,已隐藏备注的记录不会出现在收支记录列表与导出中', en: 'When off, records with hidden remarks are excluded from the transaction list and export', ja: 'オフの場合、備考を非表示にした記録は収支リストとエクスポートに表示されません' },
|
||||
hiddenDataSaved: { zh: '已更新隐藏备注记录的显示设置', en: 'Hidden remark record display updated', ja: '非表示備考の記録表示を更新しました' },
|
||||
hiddenDataFailed: { zh: '更新隐藏备注记录的显示设置失败', en: 'Failed to update hidden remark record display', ja: '非表示備考の記録表示の更新に失敗しました' },
|
||||
pageSizeLabel: { zh: '表格每页条数选项', en: 'Table page size options', ja: 'テーブルの1ページ件数オプション' },
|
||||
pageSizeHint: { zh: '所有表格分页共用的每页条数候选列表,未配置时使用默认 10 / 20 / 50 / 100', en: 'Page size choices shared by all table pagination; defaults to 10 / 20 / 50 / 100 when unset', ja: 'すべてのテーブルで共通の1ページ件数候補。未設定時は 10 / 20 / 50 / 100 を使用' },
|
||||
defaultPageSizeLabel: { zh: '默认每页条数', en: 'Default page size', ja: '既定の1ページ件数' },
|
||||
pageSizeSave: { zh: '保存', en: 'Save', ja: '保存' },
|
||||
pageSizeEmpty: { zh: '请至少填写一个每页条数', en: 'Enter at least one page size', ja: '1ページ件数を1つ以上入力してください' },
|
||||
pageSizeSaved: { zh: '每页条数选项已保存', en: 'Page size options saved', ja: '1ページ件数オプションを保存しました' },
|
||||
pageSizeFailed: { zh: '保存每页条数选项失败', en: 'Failed to save page size options', ja: '1ページ件数オプションの保存に失敗しました' },
|
||||
},
|
||||
|
||||
// 注册设置
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useDatabaseStore } from '../store/database'
|
||||
import { useRegistrationStore } from '../store/registration'
|
||||
import { migrateKey, readString } from '../utils/storage'
|
||||
import { LEGACY_STORAGE_KEYS, STORAGE_KEYS } from '../utils/storageKeys'
|
||||
import { loadPageSizeOptions } from '../composables/usePageSizeOptions'
|
||||
|
||||
// 创建路由实例,使用 HTML5 History 模式
|
||||
const router = createRouter({
|
||||
@@ -165,9 +166,14 @@ router.beforeEach(async (to, _from, next) => {
|
||||
// 如果目标页面不是登录/注册页,且没有 token,则重定向到登录页并记录来源,登录后可回跳
|
||||
if (to.path !== '/login' && to.path !== '/register' && !userStore.token) {
|
||||
next({ path: '/login', query: { redirect: to.fullPath } })
|
||||
} else {
|
||||
next()
|
||||
return
|
||||
}
|
||||
|
||||
// 已登录:预加载表格分页偏好,保证表格页首次渲染即拿到默认每页条数
|
||||
if (userStore.token) {
|
||||
await loadPageSizeOptions()
|
||||
}
|
||||
next()
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
@@ -24,6 +24,8 @@ export const STORAGE_KEYS = {
|
||||
transactionRemarkHistory: 'finance.transaction.remarkHistory',
|
||||
// 收支记录筛选偏好(收支记录页与统计面板共用)
|
||||
transactionFilterPrefs: 'finance.transaction.filterPrefs',
|
||||
// 各表格每页大小偏好(聚合 JSON,按表格键区分,本机记忆)
|
||||
paginationTablePageSizes: 'finance.pagination.tablePageSizes',
|
||||
// 账单关系类型
|
||||
transactionRelationType: 'finance.transaction.relationType',
|
||||
// 收支记录 AI 查询历史
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
// 各表格每页大小偏好:以聚合 JSON 记录每个表格上次选择的每页条数,本机记忆
|
||||
import { readJson, writeJson } from './storage'
|
||||
import { STORAGE_KEYS } from './storageKeys'
|
||||
|
||||
/** 表格键到每页大小的映射 */
|
||||
export type TablePageSizes = Record<string, number>
|
||||
|
||||
/** 读取全部表格的每页大小偏好,仅保留合法正整数 */
|
||||
export function readTablePageSizes(): TablePageSizes {
|
||||
const raw = readJson<Record<string, unknown>>(STORAGE_KEYS.paginationTablePageSizes, {})
|
||||
const result: TablePageSizes = {}
|
||||
if (raw && typeof raw === 'object') {
|
||||
for (const [key, value] of Object.entries(raw)) {
|
||||
const size = Number(value)
|
||||
if (Number.isInteger(size) && size > 0) {
|
||||
result[key] = size
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/** 读取指定表格上次选择的每页大小,无有效值时返回 null */
|
||||
export function readTablePageSize(tableKey: string): number | null {
|
||||
const all = readTablePageSizes()
|
||||
return all[tableKey] ?? null
|
||||
}
|
||||
|
||||
/** 写入指定表格的每页大小,保留其它表格的值 */
|
||||
export function writeTablePageSize(tableKey: string, size: number): void {
|
||||
const all = readTablePageSizes()
|
||||
all[tableKey] = size
|
||||
writeJson(STORAGE_KEYS.paginationTablePageSizes, all)
|
||||
}
|
||||
@@ -4,14 +4,19 @@ import { ref, onMounted, computed, h } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useMessage, useDialog, NButton, NSpace, NDataTable, NModal, NForm, NFormItem, NInput, NInputNumber, NSelect } from 'naive-ui'
|
||||
import type { DataTableColumns } from 'naive-ui'
|
||||
import { getCategoryList, createCategory, updateCategory, deleteCategory } from '../api/category'
|
||||
import { getCategoryPage, createCategory, updateCategory, deleteCategory } from '../api/category'
|
||||
import { getAiConfig, suggestEmoji, parseCategoryByAi } from '../api/ai'
|
||||
import EmojiPicker from '../components/EmojiPicker.vue'
|
||||
import { usePageSizeOptions } from '../composables/usePageSizeOptions'
|
||||
import { useTablePageSize } from '../composables/useTablePageSize'
|
||||
|
||||
const message = useMessage() // 消息提示
|
||||
const dialog = useDialog() // 对话框
|
||||
const { t } = useI18n() // 国际化
|
||||
|
||||
// 所有表格共用的每页条数候选列表
|
||||
const { pageSizeOptions } = usePageSizeOptions()
|
||||
|
||||
/** 分类数据结构 */
|
||||
interface CategoryItem {
|
||||
id: number
|
||||
@@ -23,8 +28,36 @@ interface CategoryItem {
|
||||
}
|
||||
|
||||
// 列表和弹窗状态
|
||||
const categories = ref<CategoryItem[]>([]) // 全部分类列表
|
||||
const expenseList = ref<CategoryItem[]>([]) // 支出分类列表(当前页)
|
||||
const incomeList = ref<CategoryItem[]>([]) // 收入分类列表(当前页)
|
||||
const loading = ref(false) // 列表加载状态
|
||||
|
||||
// 支出分类分页
|
||||
const expensePage = ref(1)
|
||||
const expenseSize = useTablePageSize('categoryExpense')
|
||||
const expenseTotal = ref(0)
|
||||
// 收入分类分页
|
||||
const incomePage = ref(1)
|
||||
const incomeSize = useTablePageSize('categoryIncome')
|
||||
const incomeTotal = ref(0)
|
||||
|
||||
/** 支出分类分页配置 */
|
||||
const expensePagination = computed(() => ({
|
||||
page: expensePage.value,
|
||||
pageSize: expenseSize.value,
|
||||
itemCount: expenseTotal.value,
|
||||
pageSizes: pageSizeOptions.value,
|
||||
showSizePicker: true,
|
||||
}))
|
||||
|
||||
/** 收入分类分页配置 */
|
||||
const incomePagination = computed(() => ({
|
||||
page: incomePage.value,
|
||||
pageSize: incomeSize.value,
|
||||
itemCount: incomeTotal.value,
|
||||
pageSizes: pageSizeOptions.value,
|
||||
showSizePicker: true,
|
||||
}))
|
||||
const submitting = ref(false) // 表单提交状态
|
||||
const showModal = ref(false) // 新增/编辑弹窗显示状态
|
||||
const editingId = ref<number | null>(null) // 当前编辑的分类 ID(null 表示新增)
|
||||
@@ -40,10 +73,6 @@ const typeOptions = computed(() => [
|
||||
{ label: t('common.types.income'), value: 'INCOME' },
|
||||
])
|
||||
|
||||
// 按类型过滤分类列表
|
||||
const incomeList = computed(() => categories.value.filter(c => c.type === 'INCOME')) // 收入分类
|
||||
const expenseList = computed(() => categories.value.filter(c => c.type === 'EXPENSE')) // 支出分类
|
||||
|
||||
/** 创建表格列配置 */
|
||||
function createColumns(): DataTableColumns<CategoryItem> {
|
||||
return [
|
||||
@@ -85,19 +114,64 @@ function createColumns(): DataTableColumns<CategoryItem> {
|
||||
|
||||
const columns = computed(() => createColumns()) // 表格列配置(随语言切换响应式更新)
|
||||
|
||||
/** 加载分类列表数据 */
|
||||
/** 加载支出分类当前页 */
|
||||
async function fetchExpense() {
|
||||
try {
|
||||
const res: any = await getCategoryPage({ type: 'EXPENSE', page: expensePage.value, size: expenseSize.value })
|
||||
expenseList.value = res.data?.records || []
|
||||
expenseTotal.value = res.data?.total || 0
|
||||
} catch (e: any) {
|
||||
message.error(e.message || t('common.loadFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
/** 加载收入分类当前页 */
|
||||
async function fetchIncome() {
|
||||
try {
|
||||
const res: any = await getCategoryPage({ type: 'INCOME', page: incomePage.value, size: incomeSize.value })
|
||||
incomeList.value = res.data?.records || []
|
||||
incomeTotal.value = res.data?.total || 0
|
||||
} catch (e: any) {
|
||||
message.error(e.message || t('common.loadFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
/** 同时加载收入与支出分类列表 */
|
||||
async function fetchData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res: any = await getCategoryList()
|
||||
categories.value = res.data
|
||||
} catch (e: any) {
|
||||
message.error(e.message || t('common.loadFailed'))
|
||||
await Promise.all([fetchExpense(), fetchIncome()])
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 支出分类翻页 */
|
||||
function handleExpensePageChange(p: number) {
|
||||
expensePage.value = p
|
||||
fetchExpense()
|
||||
}
|
||||
|
||||
/** 支出分类每页条数变化 */
|
||||
function handleExpensePageSizeChange(size: number) {
|
||||
expenseSize.value = size
|
||||
expensePage.value = 1
|
||||
fetchExpense()
|
||||
}
|
||||
|
||||
/** 收入分类翻页 */
|
||||
function handleIncomePageChange(p: number) {
|
||||
incomePage.value = p
|
||||
fetchIncome()
|
||||
}
|
||||
|
||||
/** 收入分类每页条数变化 */
|
||||
function handleIncomePageSizeChange(size: number) {
|
||||
incomeSize.value = size
|
||||
incomePage.value = 1
|
||||
fetchIncome()
|
||||
}
|
||||
|
||||
/** 打开新增分类弹窗,重置表单 */
|
||||
function handleAdd() {
|
||||
editingId.value = null
|
||||
@@ -229,11 +303,29 @@ onMounted(() => {
|
||||
|
||||
<!-- 支出分类列表 -->
|
||||
<h3>{{ t('category.expenseSection') }}</h3>
|
||||
<NDataTable :columns="columns" :data="expenseList" :loading="loading" :bordered="false" />
|
||||
<NDataTable
|
||||
remote
|
||||
:columns="columns"
|
||||
:data="expenseList"
|
||||
:loading="loading"
|
||||
:bordered="false"
|
||||
:pagination="expensePagination"
|
||||
@update:page="handleExpensePageChange"
|
||||
@update:page-size="handleExpensePageSizeChange"
|
||||
/>
|
||||
|
||||
<!-- 收入分类列表 -->
|
||||
<h3 style="margin-top: 24px;">{{ t('category.incomeSection') }}</h3>
|
||||
<NDataTable :columns="columns" :data="incomeList" :loading="loading" :bordered="false" />
|
||||
<NDataTable
|
||||
remote
|
||||
:columns="columns"
|
||||
:data="incomeList"
|
||||
:loading="loading"
|
||||
:bordered="false"
|
||||
:pagination="incomePagination"
|
||||
@update:page="handleIncomePageChange"
|
||||
@update:page-size="handleIncomePageSizeChange"
|
||||
/>
|
||||
|
||||
<!-- 新增/编辑弹窗 -->
|
||||
<NModal v-model:show="showModal" preset="card" :title="editingId ? t('category.editTitle') : t('category.addTitle')" style="width: 450px;">
|
||||
|
||||
@@ -6,17 +6,36 @@ import { useRouter } from 'vue-router'
|
||||
import { useMessage, useDialog, NButton, NSpace, NDataTable, NModal, NForm, NFormItem, NInput, NAlert, NTag, NIcon } from 'naive-ui'
|
||||
import type { DataTableColumns } from 'naive-ui'
|
||||
import { ArrowBackOutline } from '@vicons/ionicons5'
|
||||
import { getChannelList, createChannel, renameChannel, deleteChannel } from '../api/channel'
|
||||
import { getChannelPage, createChannel, renameChannel, deleteChannel } from '../api/channel'
|
||||
import type { ChannelItem } from '../api/channel'
|
||||
import { usePageSizeOptions } from '../composables/usePageSizeOptions'
|
||||
import { useTablePageSize } from '../composables/useTablePageSize'
|
||||
|
||||
const message = useMessage() // 消息提示
|
||||
const dialog = useDialog() // 对话框
|
||||
const router = useRouter() // 路由
|
||||
const { t } = useI18n() // 国际化
|
||||
|
||||
// 所有表格共用的每页条数候选列表
|
||||
const { pageSizeOptions } = usePageSizeOptions()
|
||||
|
||||
// 列表和弹窗状态
|
||||
const channels = ref<ChannelItem[]>([]) // 自定义渠道列表
|
||||
const channels = ref<ChannelItem[]>([]) // 自定义渠道列表(当前页)
|
||||
const loading = ref(false) // 列表加载状态
|
||||
|
||||
// 渠道列表分页
|
||||
const page = ref(1)
|
||||
const pageSize = useTablePageSize('channel')
|
||||
const total = ref(0)
|
||||
|
||||
/** 渠道列表分页配置 */
|
||||
const pagination = computed(() => ({
|
||||
page: page.value,
|
||||
pageSize: pageSize.value,
|
||||
itemCount: total.value,
|
||||
pageSizes: pageSizeOptions.value,
|
||||
showSizePicker: true,
|
||||
}))
|
||||
const submitting = ref(false) // 表单提交状态
|
||||
const showModal = ref(false) // 新增/改名弹窗显示状态
|
||||
const editingId = ref<number | null>(null) // 当前改名的渠道 ID(null 表示新增)
|
||||
@@ -54,8 +73,9 @@ const columns = computed(() => createColumns()) // 表格列配置(随语言
|
||||
async function fetchData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res: any = await getChannelList()
|
||||
channels.value = res.data || []
|
||||
const res: any = await getChannelPage({ page: page.value, size: pageSize.value })
|
||||
channels.value = res.data?.records || []
|
||||
total.value = res.data?.total || 0
|
||||
} catch (e: any) {
|
||||
message.error(e.message || t('common.loadFailed'))
|
||||
} finally {
|
||||
@@ -63,6 +83,19 @@ async function fetchData() {
|
||||
}
|
||||
}
|
||||
|
||||
/** 渠道列表翻页 */
|
||||
function handlePageChange(p: number) {
|
||||
page.value = p
|
||||
fetchData()
|
||||
}
|
||||
|
||||
/** 渠道列表每页条数变化 */
|
||||
function handlePageSizeChange(size: number) {
|
||||
pageSize.value = size
|
||||
page.value = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
/** 打开新增渠道弹窗,重置表单 */
|
||||
function handleAdd() {
|
||||
editingId.value = null
|
||||
@@ -147,7 +180,16 @@ onMounted(fetchData)
|
||||
<NAlert type="info" :show-icon="true" style="margin-bottom: 16px;">{{ t('channel.hint') }}</NAlert>
|
||||
|
||||
<!-- 自定义渠道数据表格 -->
|
||||
<NDataTable :columns="columns" :data="channels" :loading="loading" :bordered="false" />
|
||||
<NDataTable
|
||||
remote
|
||||
:columns="columns"
|
||||
:data="channels"
|
||||
:loading="loading"
|
||||
:bordered="false"
|
||||
:pagination="pagination"
|
||||
@update:page="handlePageChange"
|
||||
@update:page-size="handlePageSizeChange"
|
||||
/>
|
||||
|
||||
<!-- 新增/改名弹窗 -->
|
||||
<NModal v-model:show="showModal" preset="card" :title="editingId ? t('channel.editTitle') : t('channel.addTitle')" style="width: 450px;">
|
||||
|
||||
@@ -9,9 +9,9 @@ import {
|
||||
} from 'naive-ui'
|
||||
import type { DataTableColumns } from 'naive-ui'
|
||||
import {
|
||||
listEmailBillConfigs, createEmailBillConfig, updateEmailBillConfig, deleteEmailBillConfig,
|
||||
pageEmailBillConfigs, createEmailBillConfig, updateEmailBillConfig, deleteEmailBillConfig,
|
||||
toggleEmailBillConfig, testEmailBillConfig,
|
||||
listEmailBillSenderRules, createEmailBillSenderRule, updateEmailBillSenderRule,
|
||||
pageEmailBillSenderRules, createEmailBillSenderRule, updateEmailBillSenderRule,
|
||||
deleteEmailBillSenderRule, toggleEmailBillSenderRule,
|
||||
pageEmailBillMessages, parseEmailBillMessage, importEmailBillMessage,
|
||||
ignoreEmailBillMessage, deleteEmailBillMessage,
|
||||
@@ -31,6 +31,8 @@ import BillImportResult from '../components/BillImportResult.vue'
|
||||
import { openSse } from '../utils/sse'
|
||||
import { migrateKey, writeString } from '../utils/storage'
|
||||
import { LEGACY_STORAGE_KEYS, STORAGE_KEYS } from '../utils/storageKeys'
|
||||
import { usePageSizeOptions } from '../composables/usePageSizeOptions'
|
||||
import { useTablePageSize } from '../composables/useTablePageSize'
|
||||
|
||||
const message = useMessage()
|
||||
const dialog = useDialog()
|
||||
@@ -38,6 +40,9 @@ const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
// 所有表格共用的每页条数候选列表
|
||||
const { pageSizeOptions } = usePageSizeOptions()
|
||||
|
||||
/** 待处理列表自动刷新间隔(毫秒) */
|
||||
const MESSAGE_REFRESH_MS = 30000
|
||||
|
||||
@@ -128,11 +133,23 @@ async function loadOptions() {
|
||||
// ===== 邮箱配置 =====
|
||||
const configs = ref<EmailBillConfig[]>([])
|
||||
const configLoading = ref(false)
|
||||
const configPage = ref(1)
|
||||
const configSize = useTablePageSize('emailConfig')
|
||||
const configTotal = ref(0)
|
||||
const configModalVisible = ref(false)
|
||||
const configEditingId = ref<number | null>(null)
|
||||
const configForm = ref<EmailBillConfigPayload>(emptyConfigForm())
|
||||
const configSaving = ref(false) // 邮箱配置保存状态
|
||||
|
||||
/** 邮箱配置分页配置 */
|
||||
const configPagination = computed(() => ({
|
||||
page: configPage.value,
|
||||
pageSize: configSize.value,
|
||||
itemCount: configTotal.value,
|
||||
pageSizes: pageSizeOptions.value,
|
||||
showSizePicker: true,
|
||||
}))
|
||||
|
||||
// ===== 立即检查实时进度 =====
|
||||
const checkLogs = ref<string[]>([])
|
||||
const checkRunning = ref(false)
|
||||
@@ -207,8 +224,9 @@ const configColumns = computed<DataTableColumns<EmailBillConfig>>(() => [
|
||||
async function loadConfigs() {
|
||||
configLoading.value = true
|
||||
try {
|
||||
const res: any = await listEmailBillConfigs()
|
||||
configs.value = res.data || []
|
||||
const res: any = await pageEmailBillConfigs({ page: configPage.value, size: configSize.value })
|
||||
configs.value = res.data?.records || []
|
||||
configTotal.value = res.data?.total || 0
|
||||
} catch (e: any) {
|
||||
message.error(e.message || t('emailBill.common.loadFailed'))
|
||||
} finally {
|
||||
@@ -216,6 +234,19 @@ async function loadConfigs() {
|
||||
}
|
||||
}
|
||||
|
||||
/** 邮箱配置翻页 */
|
||||
function handleConfigPageChange(p: number) {
|
||||
configPage.value = p
|
||||
loadConfigs()
|
||||
}
|
||||
|
||||
/** 邮箱配置每页条数变化 */
|
||||
function handleConfigPageSizeChange(size: number) {
|
||||
configSize.value = size
|
||||
configPage.value = 1
|
||||
loadConfigs()
|
||||
}
|
||||
|
||||
function openConfigModal(row?: EmailBillConfig) {
|
||||
if (row) {
|
||||
configEditingId.value = row.id
|
||||
@@ -354,11 +385,23 @@ function stopCheckStream() {
|
||||
// ===== 发件人规则 =====
|
||||
const senderRules = ref<EmailBillSenderRule[]>([])
|
||||
const ruleLoading = ref(false)
|
||||
const rulePage = ref(1)
|
||||
const ruleSize = useTablePageSize('emailRule')
|
||||
const ruleTotal = ref(0)
|
||||
const ruleModalVisible = ref(false)
|
||||
const ruleEditingId = ref<number | null>(null)
|
||||
const ruleForm = ref<Omit<EmailBillSenderRulePayload, 'senderPatterns'> & { patternsText: string }>(emptyRuleForm())
|
||||
const ruleSaving = ref(false) // 发件人规则保存状态
|
||||
|
||||
/** 发件人规则分页配置 */
|
||||
const rulePagination = computed(() => ({
|
||||
page: rulePage.value,
|
||||
pageSize: ruleSize.value,
|
||||
itemCount: ruleTotal.value,
|
||||
pageSizes: pageSizeOptions.value,
|
||||
showSizePicker: true,
|
||||
}))
|
||||
|
||||
function emptyRuleForm() {
|
||||
return {
|
||||
name: '', enabled: true, sortOrder: 0, patternsText: '', billType: defaultBillType(),
|
||||
@@ -398,8 +441,9 @@ const ruleColumns = computed<DataTableColumns<EmailBillSenderRule>>(() => [
|
||||
async function loadSenderRules() {
|
||||
ruleLoading.value = true
|
||||
try {
|
||||
const res: any = await listEmailBillSenderRules()
|
||||
senderRules.value = res.data || []
|
||||
const res: any = await pageEmailBillSenderRules({ page: rulePage.value, size: ruleSize.value })
|
||||
senderRules.value = res.data?.records || []
|
||||
ruleTotal.value = res.data?.total || 0
|
||||
} catch (e: any) {
|
||||
message.error(e.message || t('emailBill.common.loadFailed'))
|
||||
} finally {
|
||||
@@ -407,6 +451,19 @@ async function loadSenderRules() {
|
||||
}
|
||||
}
|
||||
|
||||
/** 发件人规则翻页 */
|
||||
function handleRulePageChange(p: number) {
|
||||
rulePage.value = p
|
||||
loadSenderRules()
|
||||
}
|
||||
|
||||
/** 发件人规则每页条数变化 */
|
||||
function handleRulePageSizeChange(size: number) {
|
||||
ruleSize.value = size
|
||||
rulePage.value = 1
|
||||
loadSenderRules()
|
||||
}
|
||||
|
||||
function openRuleModal(row?: EmailBillSenderRule) {
|
||||
if (row) {
|
||||
ruleEditingId.value = row.id
|
||||
@@ -490,7 +547,7 @@ const messages = ref<EmailBillMessage[]>([])
|
||||
const messageLoading = ref(false)
|
||||
const messageStatus = ref<string | null>('PULLED')
|
||||
const messagePage = ref(1)
|
||||
const messageSize = ref(20)
|
||||
const messageSize = useTablePageSize('emailMessage')
|
||||
const messageTotal = ref(0)
|
||||
|
||||
const messageColumns = computed<DataTableColumns<EmailBillMessage>>(() => [
|
||||
@@ -704,7 +761,18 @@ onUnmounted(() => {
|
||||
<NSpace justify="end">
|
||||
<NButton type="primary" @click="openConfigModal()">{{ t('emailBill.common.add') }}</NButton>
|
||||
</NSpace>
|
||||
<NDataTable :columns="configColumns" :data="configs" :loading="configLoading" :row-key="(r: EmailBillConfig) => r.id" size="small" :bordered="false" />
|
||||
<NDataTable
|
||||
remote
|
||||
:columns="configColumns"
|
||||
:data="configs"
|
||||
:loading="configLoading"
|
||||
:row-key="(r: EmailBillConfig) => r.id"
|
||||
size="small"
|
||||
:bordered="false"
|
||||
:pagination="configPagination"
|
||||
@update:page="handleConfigPageChange"
|
||||
@update:page-size="handleConfigPageSizeChange"
|
||||
/>
|
||||
<NCard
|
||||
v-if="checkRunning || checkLogs.length"
|
||||
size="small"
|
||||
@@ -727,7 +795,18 @@ onUnmounted(() => {
|
||||
<NSpace justify="end">
|
||||
<NButton type="primary" @click="openRuleModal()">{{ t('emailBill.common.add') }}</NButton>
|
||||
</NSpace>
|
||||
<NDataTable :columns="ruleColumns" :data="senderRules" :loading="ruleLoading" :row-key="(r: EmailBillSenderRule) => r.id" size="small" :bordered="false" />
|
||||
<NDataTable
|
||||
remote
|
||||
:columns="ruleColumns"
|
||||
:data="senderRules"
|
||||
:loading="ruleLoading"
|
||||
:row-key="(r: EmailBillSenderRule) => r.id"
|
||||
size="small"
|
||||
:bordered="false"
|
||||
:pagination="rulePagination"
|
||||
@update:page="handleRulePageChange"
|
||||
@update:page-size="handleRulePageSizeChange"
|
||||
/>
|
||||
</NSpace>
|
||||
</NTabPane>
|
||||
|
||||
@@ -758,8 +837,10 @@ onUnmounted(() => {
|
||||
:bordered="false"
|
||||
remote
|
||||
:pagination="{
|
||||
page: messagePage, pageSize: messageSize, itemCount: messageTotal, showSizePicker: false,
|
||||
page: messagePage, pageSize: messageSize, itemCount: messageTotal,
|
||||
pageSizes: pageSizeOptions, showSizePicker: true,
|
||||
onUpdatePage: (p: number) => { messagePage = p; loadMessages() },
|
||||
onUpdatePageSize: (s: number) => { messageSize = s; messagePage = 1; loadMessages() },
|
||||
}"
|
||||
/>
|
||||
</NSpace>
|
||||
|
||||
@@ -14,12 +14,17 @@ import type { HttpLog, LoginLog, AppLog } from '../api/log'
|
||||
import { openSse } from '../utils/sse'
|
||||
import { migrateKey, writeString } from '../utils/storage'
|
||||
import { LEGACY_STORAGE_KEYS, STORAGE_KEYS } from '../utils/storageKeys'
|
||||
import { usePageSizeOptions } from '../composables/usePageSizeOptions'
|
||||
import { useTablePageSize } from '../composables/useTablePageSize'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const message = useMessage()
|
||||
const { t } = useI18n()
|
||||
|
||||
// 所有表格共用的每页条数候选列表
|
||||
const { pageSizeOptions } = usePageSizeOptions()
|
||||
|
||||
// 当前激活的标签页,取自 URL 子路径(/logs/http、/logs/login、/logs/app)
|
||||
const activeTab = ref<string>((route.params.tab as string) || 'http')
|
||||
|
||||
@@ -43,7 +48,7 @@ const retentionOptions = computed(() => [
|
||||
const httpRecords = ref<HttpLog[]>([])
|
||||
const httpLoading = ref(false)
|
||||
const httpPage = ref(1)
|
||||
const httpSize = ref(20)
|
||||
const httpSize = useTablePageSize('logsHttp')
|
||||
const httpTotal = ref(0)
|
||||
const httpKeyword = ref('')
|
||||
const httpMethod = ref<string | null>(null)
|
||||
@@ -53,7 +58,7 @@ const httpStatus = ref<number | null>(null)
|
||||
const loginRecords = ref<LoginLog[]>([])
|
||||
const loginLoading = ref(false)
|
||||
const loginPage = ref(1)
|
||||
const loginSize = ref(20)
|
||||
const loginSize = useTablePageSize('logsLogin')
|
||||
const loginTotal = ref(0)
|
||||
const loginKeyword = ref('')
|
||||
const loginSuccess = ref<'true' | 'false' | null>(null)
|
||||
@@ -473,7 +478,7 @@ function lineClass(line: string) {
|
||||
v-model:page="httpPage"
|
||||
v-model:page-size="httpSize"
|
||||
:item-count="httpTotal"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:page-sizes="pageSizeOptions"
|
||||
show-size-picker
|
||||
@update:page="loadHttp"
|
||||
@update:page-size="searchHttp"
|
||||
@@ -527,7 +532,7 @@ function lineClass(line: string) {
|
||||
v-model:page="loginPage"
|
||||
v-model:page-size="loginSize"
|
||||
:item-count="loginTotal"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:page-sizes="pageSizeOptions"
|
||||
show-size-picker
|
||||
@update:page="loadLogin"
|
||||
@update:page-size="searchLogin"
|
||||
|
||||
@@ -16,11 +16,16 @@ import { getAiConfig, suggestRulesByAi, applyAiRules } from '../api/ai'
|
||||
import type { AiRuleSuggestion } from '../api/ai'
|
||||
import { getChannelLabel } from '../utils/channel'
|
||||
import { renderCategoryLabel } from '../utils/render'
|
||||
import { usePageSizeOptions } from '../composables/usePageSizeOptions'
|
||||
import { useTablePageSize } from '../composables/useTablePageSize'
|
||||
|
||||
const message = useMessage() // 消息提示
|
||||
const dialog = useDialog() // 对话框
|
||||
const { t } = useI18n() // 国际化
|
||||
|
||||
// 所有表格共用的每页条数候选列表
|
||||
const { pageSizeOptions } = usePageSizeOptions()
|
||||
|
||||
/** 分类选项 */
|
||||
interface CategoryOption {
|
||||
label: string
|
||||
@@ -47,7 +52,7 @@ const tags = ref<TagOption[]>([])
|
||||
|
||||
// 规则列表分页与筛选
|
||||
const page = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const pageSize = useTablePageSize('rule')
|
||||
const total = ref(0)
|
||||
const searchKeyword = ref('')
|
||||
const filterEnabled = ref<string | null>(null) // 'ON' | 'OFF' | null
|
||||
@@ -75,7 +80,7 @@ const aiCandidateRows = ref<CandidateRow[]>([])
|
||||
const aiCandidateLoading = ref(false)
|
||||
const checkedCandidateKeys = ref<number[]>([])
|
||||
const aiCandidatePage = ref(1)
|
||||
const aiCandidatePageSize = ref(20)
|
||||
const aiCandidatePageSize = useTablePageSize('ruleAiCandidate')
|
||||
const aiCandidateTotal = ref(0)
|
||||
const aiCandidateSearch = ref('')
|
||||
const aiCandidateType = ref<string | null>(null)
|
||||
@@ -319,7 +324,7 @@ const aiCandidatePagination = computed(() => ({
|
||||
page: aiCandidatePage.value,
|
||||
pageSize: aiCandidatePageSize.value,
|
||||
itemCount: aiCandidateTotal.value,
|
||||
pageSizes: [10, 20, 50, 100],
|
||||
pageSizes: pageSizeOptions.value,
|
||||
showSizePicker: true,
|
||||
}))
|
||||
|
||||
@@ -554,7 +559,7 @@ const pagination = computed(() => ({
|
||||
page: page.value,
|
||||
pageSize: pageSize.value,
|
||||
itemCount: total.value,
|
||||
pageSizes: [10, 20, 50, 100],
|
||||
pageSizes: pageSizeOptions.value,
|
||||
showSizePicker: true,
|
||||
}))
|
||||
|
||||
|
||||
@@ -4,13 +4,18 @@ import { ref, onMounted, computed, h } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useMessage, useDialog, NButton, NSpace, NDataTable, NModal, NForm, NFormItem, NInput, NInputNumber, NColorPicker, NTag } from 'naive-ui'
|
||||
import type { DataTableColumns } from 'naive-ui'
|
||||
import { getTagList, createTag, updateTag, deleteTag } from '../api/tag'
|
||||
import { getTagPage, createTag, updateTag, deleteTag } from '../api/tag'
|
||||
import { getAiConfig, suggestColor } from '../api/ai'
|
||||
import { usePageSizeOptions } from '../composables/usePageSizeOptions'
|
||||
import { useTablePageSize } from '../composables/useTablePageSize'
|
||||
|
||||
const message = useMessage() // 消息提示
|
||||
const dialog = useDialog() // 对话框
|
||||
const { t } = useI18n() // 国际化
|
||||
|
||||
// 所有表格共用的每页条数候选列表
|
||||
const { pageSizeOptions } = usePageSizeOptions()
|
||||
|
||||
/** 标签数据结构 */
|
||||
interface TagItem {
|
||||
id: number
|
||||
@@ -21,8 +26,22 @@ interface TagItem {
|
||||
}
|
||||
|
||||
// 列表和弹窗状态
|
||||
const tags = ref<TagItem[]>([]) // 标签列表
|
||||
const tags = ref<TagItem[]>([]) // 标签列表(当前页)
|
||||
const loading = ref(false) // 列表加载状态
|
||||
|
||||
// 标签列表分页
|
||||
const page = ref(1)
|
||||
const pageSize = useTablePageSize('tag')
|
||||
const total = ref(0)
|
||||
|
||||
/** 标签列表分页配置 */
|
||||
const pagination = computed(() => ({
|
||||
page: page.value,
|
||||
pageSize: pageSize.value,
|
||||
itemCount: total.value,
|
||||
pageSizes: pageSizeOptions.value,
|
||||
showSizePicker: true,
|
||||
}))
|
||||
const submitting = ref(false) // 表单提交状态
|
||||
const showModal = ref(false) // 新增/编辑弹窗显示状态
|
||||
const editingId = ref<number | null>(null) // 当前编辑的标签 ID(null 表示新增)
|
||||
@@ -82,8 +101,9 @@ const columns = computed(() => createColumns()) // 表格列配置(随语言
|
||||
async function fetchData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res: any = await getTagList()
|
||||
tags.value = res.data
|
||||
const res: any = await getTagPage({ page: page.value, size: pageSize.value })
|
||||
tags.value = res.data?.records || []
|
||||
total.value = res.data?.total || 0
|
||||
} catch (e: any) {
|
||||
message.error(e.message || t('common.loadFailed'))
|
||||
} finally {
|
||||
@@ -91,6 +111,19 @@ async function fetchData() {
|
||||
}
|
||||
}
|
||||
|
||||
/** 标签列表翻页 */
|
||||
function handlePageChange(p: number) {
|
||||
page.value = p
|
||||
fetchData()
|
||||
}
|
||||
|
||||
/** 标签列表每页条数变化 */
|
||||
function handlePageSizeChange(size: number) {
|
||||
pageSize.value = size
|
||||
page.value = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
/** 打开新增标签弹窗,重置表单 */
|
||||
function handleAdd() {
|
||||
editingId.value = null
|
||||
@@ -192,7 +225,16 @@ onMounted(() => {
|
||||
</NSpace>
|
||||
|
||||
<!-- 标签数据表格 -->
|
||||
<NDataTable :columns="columns" :data="tags" :loading="loading" :bordered="false" />
|
||||
<NDataTable
|
||||
remote
|
||||
:columns="columns"
|
||||
:data="tags"
|
||||
:loading="loading"
|
||||
:bordered="false"
|
||||
:pagination="pagination"
|
||||
@update:page="handlePageChange"
|
||||
@update:page-size="handlePageSizeChange"
|
||||
/>
|
||||
|
||||
<!-- 新增/编辑弹窗 -->
|
||||
<NModal v-model:show="showModal" preset="card" :title="editingId ? t('tag.editTitle') : t('tag.addTitle')" style="width: 450px;">
|
||||
|
||||
@@ -17,6 +17,9 @@ import { getChannelLabel } from '../utils/channel'
|
||||
import { renderCategoryLabel } from '../utils/render'
|
||||
import { useKeywordHistory } from '../composables/useKeywordHistory'
|
||||
import { useStringPreference } from '../composables/usePreference'
|
||||
import { usePageSizeOptions } from '../composables/usePageSizeOptions'
|
||||
import { useTablePageSize } from '../composables/useTablePageSize'
|
||||
import { readTablePageSize, writeTablePageSize } from '../utils/tablePageSizePrefs'
|
||||
import { openSse } from '../utils/sse'
|
||||
import { STORAGE_KEYS } from '../utils/storageKeys'
|
||||
import { loadTransactionFilterPrefs, saveTransactionFilterPrefs } from '../utils/transactionFilterPrefs'
|
||||
@@ -94,7 +97,7 @@ interface AiRecordEditRow {
|
||||
const transactions = ref<TransactionItem[]>([]) // 交易记录列表
|
||||
const total = ref(0) // 总记录数
|
||||
const page = ref(1) // 当前页码
|
||||
const pageSize = ref(20) // 每页条数
|
||||
const pageSize = useTablePageSize('transaction') // 每页条数
|
||||
const sortValue = ref('transactionTime-desc') // 排序方式:排序字段-方向
|
||||
const sortOptions = computed(() => [
|
||||
{ label: t('transaction.sort.timeDesc'), value: 'transactionTime-desc' },
|
||||
@@ -104,7 +107,7 @@ const sortOptions = computed(() => [
|
||||
])
|
||||
const sortField = computed(() => sortValue.value.split('-')[0]) // 服务端排序字段
|
||||
const sortOrder = computed(() => sortValue.value.split('-')[1]) // 服务端排序方向:asc/desc
|
||||
const pageSizes = [10, 20, 50, 100, 200, 500] // 可选的每页条数
|
||||
const { pageSizeOptions } = usePageSizeOptions() // 所有表格共用的每页条数候选列表
|
||||
const totalPages = computed(() => Math.max(1, Math.ceil(total.value / pageSize.value))) // 总页数
|
||||
// 当前页金额汇总:支出、收入与结余(含本页所有展示记录)
|
||||
const pageSummary = computed(() => {
|
||||
@@ -345,7 +348,12 @@ function loadFilterPrefs() {
|
||||
if (typeof p.filterRemark === 'string') filterRemark.value = p.filterRemark
|
||||
if (p.filterMinAmount !== undefined) filterMinAmount.value = p.filterMinAmount
|
||||
if (p.filterMaxAmount !== undefined) filterMaxAmount.value = p.filterMaxAmount
|
||||
if (typeof p.pageSize === 'number' && pageSizes.includes(p.pageSize)) pageSize.value = p.pageSize
|
||||
// 迁移旧筛选偏好中的每页条数到统一的表格分页偏好(新机制尚无值时)
|
||||
if (typeof p.pageSize === 'number' && Number.isInteger(p.pageSize) && p.pageSize > 0
|
||||
&& readTablePageSize('transaction') === null) {
|
||||
writeTablePageSize('transaction', p.pageSize)
|
||||
pageSize.value = p.pageSize
|
||||
}
|
||||
if (typeof p.sortValue === 'string' && sortOptions.value.some((o) => o.value === p.sortValue)) sortValue.value = p.sortValue
|
||||
if (p.filterTag) filterTag.value = p.filterTag
|
||||
}
|
||||
@@ -361,7 +369,6 @@ function saveFilterPrefs() {
|
||||
filterRemark: filterRemark.value,
|
||||
filterMinAmount: filterMinAmount.value,
|
||||
filterMaxAmount: filterMaxAmount.value,
|
||||
pageSize: pageSize.value,
|
||||
sortValue: sortValue.value,
|
||||
filterTag: filterTag.value,
|
||||
})
|
||||
@@ -369,7 +376,7 @@ function saveFilterPrefs() {
|
||||
|
||||
// 筛选条件变化时自动保存用户偏好
|
||||
watch(
|
||||
[filterType, filterCategory, filterChannel, filterDateRange, filterIncludeInStats, filterRemark, filterMinAmount, filterMaxAmount, pageSize, sortValue, filterTag],
|
||||
[filterType, filterCategory, filterChannel, filterDateRange, filterIncludeInStats, filterRemark, filterMinAmount, filterMaxAmount, sortValue, filterTag],
|
||||
saveFilterPrefs,
|
||||
{ deep: true },
|
||||
)
|
||||
@@ -2224,7 +2231,7 @@ onBeforeUnmount(() => {
|
||||
v-model:page="page"
|
||||
v-model:page-size="pageSize"
|
||||
:page-count="Math.ceil(total / pageSize)"
|
||||
:page-sizes="pageSizes"
|
||||
:page-sizes="pageSizeOptions"
|
||||
show-size-picker
|
||||
show-quick-jumper
|
||||
@update:page="handlePageChange"
|
||||
|
||||
@@ -16,6 +16,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import cn.rosercode.finance.common.PageResult;
|
||||
import cn.rosercode.finance.common.Result;
|
||||
import cn.rosercode.finance.dto.CategoryRequest;
|
||||
import cn.rosercode.finance.service.CategoryService;
|
||||
@@ -54,6 +55,24 @@ public class CategoryController {
|
||||
return Result.success(categoryService.list(userId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页获取分类列表
|
||||
*
|
||||
* @param type 分类类型(可选),用于按类型筛选
|
||||
* @param page 页码
|
||||
* @param size 每页大小
|
||||
* @return 分页结果
|
||||
*/
|
||||
@GetMapping("/page")
|
||||
@Operation(summary = "分页获取分类列表", description = "分页查询当前用户的分类,支持按类型筛选")
|
||||
public Result<PageResult<CategoryVO>> page(
|
||||
@Parameter(description = "分类类型,可选值:income/expense") @RequestParam(required = false) String type,
|
||||
@Parameter(description = "页码") @RequestParam(defaultValue = "1") int page,
|
||||
@Parameter(description = "每页大小") @RequestParam(defaultValue = "20") int size) {
|
||||
Long userId = UserContext.getUserId();
|
||||
return Result.success(categoryService.page(userId, type, page, size));
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建分类
|
||||
*
|
||||
|
||||
@@ -12,8 +12,10 @@ import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import cn.rosercode.finance.common.PageResult;
|
||||
import cn.rosercode.finance.common.Result;
|
||||
import cn.rosercode.finance.dto.ChannelRequest;
|
||||
import cn.rosercode.finance.service.ChannelService;
|
||||
@@ -49,6 +51,21 @@ public class ChannelController {
|
||||
return Result.success(channelService.list(UserContext.getUserId()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询当前用户的自定义渠道列表
|
||||
*
|
||||
* @param page 页码
|
||||
* @param size 每页大小
|
||||
* @return 分页结果(含使用账单数)
|
||||
*/
|
||||
@GetMapping("/page")
|
||||
@Operation(summary = "分页渠道列表", description = "分页查询当前用户的自定义交易渠道,含各自被使用的账单数")
|
||||
public Result<PageResult<ChannelVO>> page(
|
||||
@RequestParam(defaultValue = "1") int page,
|
||||
@RequestParam(defaultValue = "20") int size) {
|
||||
return Result.success(channelService.page(UserContext.getUserId(), page, size));
|
||||
}
|
||||
|
||||
/**
|
||||
* 新建自定义渠道
|
||||
*
|
||||
|
||||
@@ -79,6 +79,22 @@ public class EmailBillController {
|
||||
return Result.success(configService.list(userId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询邮箱配置列表
|
||||
*
|
||||
* @param page 页码
|
||||
* @param size 每页大小
|
||||
* @return 分页结果
|
||||
*/
|
||||
@GetMapping("/configs/page")
|
||||
@Operation(summary = "分页查询邮箱配置列表", description = "分页获取当前用户的 IMAP 邮箱配置,授权码已脱敏")
|
||||
public Result<PageResult<EmailBillConfigVO>> pageConfigs(
|
||||
@Parameter(description = "页码") @RequestParam(defaultValue = "1") int page,
|
||||
@Parameter(description = "每页大小") @RequestParam(defaultValue = "20") int size) {
|
||||
Long userId = UserContext.getUserId();
|
||||
return Result.success(configService.page(userId, page, size));
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建邮箱配置
|
||||
*
|
||||
@@ -164,6 +180,22 @@ public class EmailBillController {
|
||||
return Result.success(senderRuleService.list(userId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询发件人规则列表
|
||||
*
|
||||
* @param page 页码
|
||||
* @param size 每页大小
|
||||
* @return 分页结果
|
||||
*/
|
||||
@GetMapping("/sender-rules/page")
|
||||
@Operation(summary = "分页查询发件人规则列表", description = "分页获取当前用户的发件人识别规则,按优先级排序")
|
||||
public Result<PageResult<EmailBillSenderRuleVO>> pageSenderRules(
|
||||
@Parameter(description = "页码") @RequestParam(defaultValue = "1") int page,
|
||||
@Parameter(description = "每页大小") @RequestParam(defaultValue = "20") int size) {
|
||||
Long userId = UserContext.getUserId();
|
||||
return Result.success(senderRuleService.page(userId, page, size));
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建发件人规则
|
||||
*
|
||||
|
||||
@@ -13,8 +13,10 @@ import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import cn.rosercode.finance.common.PageResult;
|
||||
import cn.rosercode.finance.common.Result;
|
||||
import cn.rosercode.finance.dto.TagRequest;
|
||||
import cn.rosercode.finance.service.TagService;
|
||||
@@ -51,6 +53,22 @@ public class TagController {
|
||||
return Result.success(tagService.list(userId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页获取标签列表
|
||||
*
|
||||
* @param page 页码
|
||||
* @param size 每页大小
|
||||
* @return 分页结果
|
||||
*/
|
||||
@GetMapping("/page")
|
||||
@Operation(summary = "分页获取标签列表", description = "分页查询当前用户的标签")
|
||||
public Result<PageResult<TagVO>> page(
|
||||
@Parameter(description = "页码") @RequestParam(defaultValue = "1") int page,
|
||||
@Parameter(description = "每页大小") @RequestParam(defaultValue = "20") int size) {
|
||||
Long userId = UserContext.getUserId();
|
||||
return Result.success(tagService.page(userId, page, size));
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建标签
|
||||
*
|
||||
|
||||
@@ -13,15 +13,17 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
import cn.rosercode.finance.common.Result;
|
||||
import cn.rosercode.finance.dto.BillSourceStatsRequest;
|
||||
import cn.rosercode.finance.dto.ExcludedDisplayStyleRequest;
|
||||
import cn.rosercode.finance.dto.PaginationPrefsRequest;
|
||||
import cn.rosercode.finance.dto.ShowHiddenDataRequest;
|
||||
import cn.rosercode.finance.service.UserPreferenceService;
|
||||
import cn.rosercode.finance.util.UserContext;
|
||||
import cn.rosercode.finance.vo.PaginationPrefsVO;
|
||||
|
||||
/**
|
||||
* 用户设置控制器
|
||||
*
|
||||
* <p>提供按当前登录用户存储的偏好配置读写接口,包含账单来源统计默认值与
|
||||
* 「不计入统计」记录的展示样式。</p>
|
||||
* <p>提供按当前登录用户存储的偏好配置读写接口,包含账单来源统计默认值、
|
||||
* 「不计入统计」记录的展示样式与表格分页偏好。</p>
|
||||
*
|
||||
* @date 2026-09-21 10:14:12
|
||||
*/
|
||||
@@ -111,4 +113,32 @@ public class UserSettingController {
|
||||
userPreferenceService.saveShowHiddenData(userId, show);
|
||||
return Result.success(userPreferenceService.getShowHiddenData(userId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询当前用户配置的表格分页偏好。
|
||||
*
|
||||
* @return 分页偏好(每页条数候选列表与默认每页条数),未配置时返回系统默认
|
||||
*/
|
||||
@GetMapping("/pagination-prefs")
|
||||
@Operation(summary = "查询分页偏好", description = "读取当前用户所有表格分页的每页条数候选列表与默认每页条数")
|
||||
public Result<PaginationPrefsVO> getPaginationPrefs() {
|
||||
return Result.success(userPreferenceService.getPaginationPrefs(UserContext.getUserId()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新当前用户配置的表格分页偏好。
|
||||
*
|
||||
* @param request 分页偏好更新请求
|
||||
* @return 更新后的分页偏好,空或非法时回退系统默认
|
||||
*/
|
||||
@PutMapping("/pagination-prefs")
|
||||
@Operation(summary = "更新分页偏好", description = "设置当前用户所有表格分页的每页条数候选列表与默认每页条数")
|
||||
public Result<PaginationPrefsVO> updatePaginationPrefs(@RequestBody PaginationPrefsRequest request) {
|
||||
Long userId = UserContext.getUserId();
|
||||
PaginationPrefsVO prefs = new PaginationPrefsVO();
|
||||
prefs.setOptions(request.getOptions());
|
||||
prefs.setDefaultSize(request.getDefaultSize());
|
||||
userPreferenceService.savePaginationPrefs(userId, prefs);
|
||||
return Result.success(userPreferenceService.getPaginationPrefs(userId));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package cn.rosercode.finance.dto;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 表格分页偏好更新请求对象。
|
||||
*
|
||||
* @date 2026-09-23 11:05:25
|
||||
*/
|
||||
@Data
|
||||
public class PaginationPrefsRequest {
|
||||
|
||||
/** 每页条数候选列表 */
|
||||
private List<Integer> options;
|
||||
|
||||
/** 默认每页条数 */
|
||||
private Integer defaultSize;
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package cn.rosercode.finance.service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import cn.rosercode.finance.common.PageResult;
|
||||
import cn.rosercode.finance.dto.CategoryRequest;
|
||||
import cn.rosercode.finance.vo.CategoryVO;
|
||||
|
||||
@@ -27,6 +28,17 @@ public interface CategoryService {
|
||||
*/
|
||||
List<CategoryVO> listByType(Long userId, String type);
|
||||
|
||||
/**
|
||||
* 分页查询用户的分类,可按类型筛选。
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param type 分类类型(如 INCOME、EXPENSE),为空时查询全部
|
||||
* @param page 页码,从 1 开始
|
||||
* @param size 每页大小
|
||||
* @return 分页结果
|
||||
*/
|
||||
PageResult<CategoryVO> page(Long userId, String type, int page, int size);
|
||||
|
||||
/**
|
||||
* 创建新分类
|
||||
*
|
||||
|
||||
@@ -4,6 +4,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import cn.rosercode.finance.common.PageResult;
|
||||
import cn.rosercode.finance.dto.ChannelRequest;
|
||||
import cn.rosercode.finance.vo.ChannelOptionVO;
|
||||
import cn.rosercode.finance.vo.ChannelVO;
|
||||
@@ -23,6 +24,16 @@ public interface ChannelService {
|
||||
*/
|
||||
List<ChannelVO> list(Long userId);
|
||||
|
||||
/**
|
||||
* 分页查询当前用户的自定义渠道列表(含各自使用账单数)。
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param page 页码,从 1 开始
|
||||
* @param size 每页大小
|
||||
* @return 分页结果
|
||||
*/
|
||||
PageResult<ChannelVO> page(Long userId, int page, int size);
|
||||
|
||||
/**
|
||||
* 新建自定义渠道。
|
||||
*
|
||||
|
||||
@@ -2,6 +2,7 @@ package cn.rosercode.finance.service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import cn.rosercode.finance.common.PageResult;
|
||||
import cn.rosercode.finance.dto.EmailBillConfigRequest;
|
||||
import cn.rosercode.finance.entity.EmailBillConfig;
|
||||
import cn.rosercode.finance.vo.EmailBillConfigVO;
|
||||
@@ -21,6 +22,16 @@ public interface EmailBillConfigService {
|
||||
*/
|
||||
List<EmailBillConfigVO> list(Long userId);
|
||||
|
||||
/**
|
||||
* 分页查询当前用户的邮箱配置。
|
||||
*
|
||||
* @param userId 当前用户ID
|
||||
* @param page 页码,从 1 开始
|
||||
* @param size 每页大小
|
||||
* @return 分页结果
|
||||
*/
|
||||
PageResult<EmailBillConfigVO> page(Long userId, int page, int size);
|
||||
|
||||
/**
|
||||
* 新增邮箱配置。
|
||||
*
|
||||
|
||||
@@ -2,6 +2,7 @@ package cn.rosercode.finance.service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import cn.rosercode.finance.common.PageResult;
|
||||
import cn.rosercode.finance.dto.EmailBillSenderRuleRequest;
|
||||
import cn.rosercode.finance.entity.EmailBillSenderRule;
|
||||
import cn.rosercode.finance.vo.EmailBillSenderRuleVO;
|
||||
@@ -21,6 +22,16 @@ public interface EmailBillSenderRuleService {
|
||||
*/
|
||||
List<EmailBillSenderRuleVO> list(Long userId);
|
||||
|
||||
/**
|
||||
* 分页查询当前用户的发件人规则。
|
||||
*
|
||||
* @param userId 当前用户ID
|
||||
* @param page 页码,从 1 开始
|
||||
* @param size 每页大小
|
||||
* @return 分页结果,按优先级升序
|
||||
*/
|
||||
PageResult<EmailBillSenderRuleVO> page(Long userId, int page, int size);
|
||||
|
||||
/**
|
||||
* 新增发件人规则。
|
||||
*
|
||||
|
||||
@@ -2,6 +2,7 @@ package cn.rosercode.finance.service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import cn.rosercode.finance.common.PageResult;
|
||||
import cn.rosercode.finance.dto.TagRequest;
|
||||
import cn.rosercode.finance.vo.TagVO;
|
||||
|
||||
@@ -20,6 +21,16 @@ public interface TagService {
|
||||
*/
|
||||
List<TagVO> list(Long userId);
|
||||
|
||||
/**
|
||||
* 分页查询用户的标签列表。
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param page 页码,从 1 开始
|
||||
* @param size 每页大小
|
||||
* @return 分页结果
|
||||
*/
|
||||
PageResult<TagVO> page(Long userId, int page, int size);
|
||||
|
||||
/**
|
||||
* 创建新标签
|
||||
*
|
||||
|
||||
@@ -2,6 +2,8 @@ package cn.rosercode.finance.service;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import cn.rosercode.finance.vo.PaginationPrefsVO;
|
||||
|
||||
/**
|
||||
* 用户偏好服务接口,提供按用户存储的偏好配置读写。
|
||||
*
|
||||
@@ -56,4 +58,20 @@ public interface UserPreferenceService {
|
||||
* @param show 是否显示
|
||||
*/
|
||||
void saveShowHiddenData(Long userId, boolean show);
|
||||
|
||||
/**
|
||||
* 获取当前用户配置的表格分页偏好(每页条数候选列表与默认每页条数),未配置或非法时回退默认值。
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 分页偏好
|
||||
*/
|
||||
PaginationPrefsVO getPaginationPrefs(Long userId);
|
||||
|
||||
/**
|
||||
* 保存当前用户配置的表格分页偏好,空或非法时回退默认值。
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param prefs 分页偏好
|
||||
*/
|
||||
void savePaginationPrefs(Long userId, PaginationPrefsVO prefs);
|
||||
}
|
||||
|
||||
@@ -4,11 +4,13 @@ import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import cn.rosercode.finance.common.BusinessException;
|
||||
import cn.rosercode.finance.common.PageResult;
|
||||
import cn.rosercode.finance.converter.VoConverter;
|
||||
import cn.rosercode.finance.dto.CategoryRequest;
|
||||
import cn.rosercode.finance.entity.Category;
|
||||
@@ -60,6 +62,28 @@ public class CategoryServiceImpl implements CategoryService {
|
||||
return categories.stream().map(voConverter::toCategoryVO).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* 按排序字段和ID升序分页,type 为空时查询全部类型。
|
||||
*/
|
||||
@Override
|
||||
public PageResult<CategoryVO> page(Long userId, String type, int page, int size) {
|
||||
LambdaQueryWrapper<Category> wrapper = new LambdaQueryWrapper<Category>()
|
||||
.eq(Category::getUserId, userId);
|
||||
if (type != null && !type.isEmpty()) {
|
||||
wrapper.eq(Category::getType, type);
|
||||
}
|
||||
wrapper.orderByAsc(Category::getSortOrder).orderByAsc(Category::getId);
|
||||
int pageNo = page < 1 ? 1 : page;
|
||||
int pageSize = size < 1 ? 20 : size;
|
||||
Page<Category> result = categoryMapper.selectPage(new Page<>(pageNo, pageSize), wrapper);
|
||||
List<CategoryVO> records = result.getRecords().stream()
|
||||
.map(voConverter::toCategoryVO)
|
||||
.collect(Collectors.toList());
|
||||
return new PageResult<>(records, result.getTotal(), result.getCurrent(), result.getSize());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
|
||||
@@ -9,11 +9,13 @@ import java.util.Set;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import cn.rosercode.finance.common.BusinessException;
|
||||
import cn.rosercode.finance.common.PageResult;
|
||||
import cn.rosercode.finance.dto.ChannelRequest;
|
||||
import cn.rosercode.finance.entity.Channel;
|
||||
import cn.rosercode.finance.entity.Transaction;
|
||||
@@ -69,16 +71,48 @@ public class ChannelServiceImpl implements ChannelService {
|
||||
.orderByAsc(Channel::getId));
|
||||
List<ChannelVO> result = new ArrayList<>();
|
||||
for (Channel channel : channels) {
|
||||
ChannelVO vo = new ChannelVO();
|
||||
vo.setId(channel.getId());
|
||||
vo.setCode(channel.getCode());
|
||||
vo.setName(channel.getName());
|
||||
vo.setTransactionCount(countTransactions(userId, channel.getCode()));
|
||||
result.add(vo);
|
||||
result.add(toChannelVO(userId, channel));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* 按排序字段和ID升序分页,并对当前页渠道逐条统计使用账单数。
|
||||
*/
|
||||
@Override
|
||||
public PageResult<ChannelVO> page(Long userId, int page, int size) {
|
||||
LambdaQueryWrapper<Channel> wrapper = new LambdaQueryWrapper<Channel>()
|
||||
.eq(Channel::getUserId, userId)
|
||||
.orderByAsc(Channel::getSortOrder)
|
||||
.orderByAsc(Channel::getId);
|
||||
int pageNo = page < 1 ? 1 : page;
|
||||
int pageSize = size < 1 ? 20 : size;
|
||||
Page<Channel> result = channelMapper.selectPage(new Page<>(pageNo, pageSize), wrapper);
|
||||
List<ChannelVO> records = new ArrayList<>();
|
||||
for (Channel channel : result.getRecords()) {
|
||||
records.add(toChannelVO(userId, channel));
|
||||
}
|
||||
return new PageResult<>(records, result.getTotal(), result.getCurrent(), result.getSize());
|
||||
}
|
||||
|
||||
/**
|
||||
* 将渠道实体转换为视图对象,并填充使用账单数。
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param channel 渠道实体
|
||||
* @return 渠道视图对象
|
||||
*/
|
||||
private ChannelVO toChannelVO(Long userId, Channel channel) {
|
||||
ChannelVO vo = new ChannelVO();
|
||||
vo.setId(channel.getId());
|
||||
vo.setCode(channel.getCode());
|
||||
vo.setName(channel.getName());
|
||||
vo.setTransactionCount(countTransactions(userId, channel.getCode()));
|
||||
return vo;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
|
||||
@@ -5,6 +5,7 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
@@ -17,6 +18,7 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import cn.rosercode.finance.common.BusinessException;
|
||||
import cn.rosercode.finance.common.PageResult;
|
||||
import cn.rosercode.finance.converter.VoConverter;
|
||||
import cn.rosercode.finance.dto.EmailBillConfigRequest;
|
||||
import cn.rosercode.finance.email.EmailBillMonitorManager;
|
||||
@@ -105,6 +107,26 @@ public class EmailBillConfigServiceImpl implements EmailBillConfigService {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* 按配置ID升序分页。
|
||||
*/
|
||||
@Override
|
||||
public PageResult<EmailBillConfigVO> page(Long userId, int page, int size) {
|
||||
LambdaQueryWrapper<EmailBillConfig> wrapper = new LambdaQueryWrapper<EmailBillConfig>()
|
||||
.eq(EmailBillConfig::getUserId, userId)
|
||||
.orderByAsc(EmailBillConfig::getId);
|
||||
int pageNo = page < 1 ? 1 : page;
|
||||
int pageSize = size < 1 ? 20 : size;
|
||||
Page<EmailBillConfig> result = configMapper.selectPage(new Page<>(pageNo, pageSize), wrapper);
|
||||
List<EmailBillConfigVO> records = new ArrayList<>();
|
||||
for (EmailBillConfig config : result.getRecords()) {
|
||||
records.add(toVO(config));
|
||||
}
|
||||
return new PageResult<>(records, result.getTotal(), result.getCurrent(), result.getSize());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
|
||||
@@ -4,6 +4,7 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
@@ -13,6 +14,7 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import cn.rosercode.finance.common.BusinessException;
|
||||
import cn.rosercode.finance.common.PageResult;
|
||||
import cn.rosercode.finance.converter.VoConverter;
|
||||
import cn.rosercode.finance.dto.EmailBillSenderRuleRequest;
|
||||
import cn.rosercode.finance.entity.Category;
|
||||
@@ -72,6 +74,27 @@ public class EmailBillSenderRuleServiceImpl implements EmailBillSenderRuleServic
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* 按优先级和ID升序分页。
|
||||
*/
|
||||
@Override
|
||||
public PageResult<EmailBillSenderRuleVO> page(Long userId, int page, int size) {
|
||||
LambdaQueryWrapper<EmailBillSenderRule> wrapper = new LambdaQueryWrapper<EmailBillSenderRule>()
|
||||
.eq(EmailBillSenderRule::getUserId, userId)
|
||||
.orderByAsc(EmailBillSenderRule::getSortOrder)
|
||||
.orderByAsc(EmailBillSenderRule::getId);
|
||||
int pageNo = page < 1 ? 1 : page;
|
||||
int pageSize = size < 1 ? 20 : size;
|
||||
Page<EmailBillSenderRule> result = senderRuleMapper.selectPage(new Page<>(pageNo, pageSize), wrapper);
|
||||
List<EmailBillSenderRuleVO> records = new ArrayList<>();
|
||||
for (EmailBillSenderRule rule : result.getRecords()) {
|
||||
records.add(toVO(rule));
|
||||
}
|
||||
return new PageResult<>(records, result.getTotal(), result.getCurrent(), result.getSize());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
|
||||
@@ -4,11 +4,13 @@ import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import cn.rosercode.finance.common.BusinessException;
|
||||
import cn.rosercode.finance.common.PageResult;
|
||||
import cn.rosercode.finance.converter.VoConverter;
|
||||
import cn.rosercode.finance.dto.TagRequest;
|
||||
import cn.rosercode.finance.entity.RuleTag;
|
||||
@@ -56,6 +58,26 @@ public class TagServiceImpl implements TagService {
|
||||
return tags.stream().map(voConverter::toTagVO).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* 按排序字段和ID升序分页。
|
||||
*/
|
||||
@Override
|
||||
public PageResult<TagVO> page(Long userId, int page, int size) {
|
||||
LambdaQueryWrapper<Tag> wrapper = new LambdaQueryWrapper<Tag>()
|
||||
.eq(Tag::getUserId, userId)
|
||||
.orderByAsc(Tag::getSortOrder)
|
||||
.orderByAsc(Tag::getId);
|
||||
int pageNo = page < 1 ? 1 : page;
|
||||
int pageSize = size < 1 ? 20 : size;
|
||||
Page<Tag> result = tagMapper.selectPage(new Page<>(pageNo, pageSize), wrapper);
|
||||
List<TagVO> records = result.getRecords().stream()
|
||||
.map(voConverter::toTagVO)
|
||||
.collect(Collectors.toList());
|
||||
return new PageResult<>(records, result.getTotal(), result.getCurrent(), result.getSize());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
|
||||
@@ -2,8 +2,10 @@ package cn.rosercode.finance.service.impl;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
@@ -19,6 +21,7 @@ import cn.rosercode.finance.entity.UserPreference;
|
||||
import cn.rosercode.finance.enums.BillType;
|
||||
import cn.rosercode.finance.mapper.UserPreferenceMapper;
|
||||
import cn.rosercode.finance.service.UserPreferenceService;
|
||||
import cn.rosercode.finance.vo.PaginationPrefsVO;
|
||||
|
||||
/**
|
||||
* 用户偏好服务实现类,以键值形式读写每个用户的偏好配置。
|
||||
@@ -40,6 +43,24 @@ public class UserPreferenceServiceImpl implements UserPreferenceService {
|
||||
/** 偏好键:是否显示已隐藏备注的记录 */
|
||||
private static final String KEY_SHOW_HIDDEN_DATA = "showHiddenData";
|
||||
|
||||
/** 偏好键:表格分页偏好(每页条数候选列表与默认每页条数) */
|
||||
private static final String KEY_PAGINATION_PREFS = "paginationPrefs";
|
||||
|
||||
/** 默认的表格分页每页条数候选列表 */
|
||||
private static final List<Integer> DEFAULT_PAGE_SIZE_OPTIONS = List.of(10, 20, 50, 100);
|
||||
|
||||
/** 默认每页条数 */
|
||||
private static final int DEFAULT_PAGE_SIZE = 20;
|
||||
|
||||
/** 每页条数下限 */
|
||||
private static final int MIN_PAGE_SIZE = 1;
|
||||
|
||||
/** 每页条数上限 */
|
||||
private static final int MAX_PAGE_SIZE = 1000;
|
||||
|
||||
/** 每页条数候选值最大数量 */
|
||||
private static final int MAX_PAGE_SIZE_OPTIONS = 10;
|
||||
|
||||
/** 默认计入统计的账单来源,其余来源默认不计入 */
|
||||
private static final Set<String> DEFAULT_INCLUDED_SOURCES = Set.of("WECHAT", "ALIPAY");
|
||||
|
||||
@@ -146,6 +167,59 @@ public class UserPreferenceServiceImpl implements UserPreferenceService {
|
||||
log.info("保存隐藏备注记录显示偏好: userId={}, show={}", userId, show);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* 结果按用户缓存,未配置或取值非法时回退默认值。
|
||||
*/
|
||||
@Override
|
||||
@Cacheable(cacheNames = CacheConfig.USER_PREFERENCE_CACHE, key = "'paginationPrefs:' + #userId")
|
||||
public PaginationPrefsVO getPaginationPrefs(Long userId) {
|
||||
return normalizePaginationPrefs(parsePaginationPrefs(findValue(userId, KEY_PAGINATION_PREFS)));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* 保存后失效该用户的分页偏好缓存。
|
||||
*/
|
||||
@Override
|
||||
@CacheEvict(cacheNames = CacheConfig.USER_PREFERENCE_CACHE, key = "'paginationPrefs:' + #userId")
|
||||
public void savePaginationPrefs(Long userId, PaginationPrefsVO prefs) {
|
||||
PaginationPrefsVO normalized = normalizePaginationPrefs(prefs);
|
||||
upsert(userId, KEY_PAGINATION_PREFS, toJson(normalized));
|
||||
log.info("保存表格分页偏好: userId={}, options={}, defaultSize={}",
|
||||
userId, normalized.getOptions(), normalized.getDefaultSize());
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化分页偏好:候选列表过滤非正整数与越界值、去重升序并限制数量(为空回退默认);
|
||||
* 默认每页条数须为候选列表内的正整数,否则回退候选列表首项。
|
||||
*
|
||||
* @param prefs 原始分页偏好
|
||||
* @return 规范化后的分页偏好
|
||||
*/
|
||||
private PaginationPrefsVO normalizePaginationPrefs(PaginationPrefsVO prefs) {
|
||||
List<Integer> options = prefs != null ? prefs.getOptions() : null;
|
||||
List<Integer> normalizedOptions = options == null ? List.of() : options.stream()
|
||||
.filter(value -> value != null && value >= MIN_PAGE_SIZE && value <= MAX_PAGE_SIZE)
|
||||
.distinct()
|
||||
.sorted()
|
||||
.limit(MAX_PAGE_SIZE_OPTIONS)
|
||||
.collect(Collectors.toList());
|
||||
if (normalizedOptions.isEmpty()) {
|
||||
normalizedOptions = DEFAULT_PAGE_SIZE_OPTIONS;
|
||||
}
|
||||
// 未配置或未传默认值时回退系统默认每页条数,再校验其是否在候选列表内
|
||||
Integer rawDefault = prefs != null ? prefs.getDefaultSize() : null;
|
||||
int defaultSize = rawDefault != null ? rawDefault : DEFAULT_PAGE_SIZE;
|
||||
int resolvedDefault = normalizedOptions.contains(defaultSize) ? defaultSize : normalizedOptions.get(0);
|
||||
PaginationPrefsVO result = new PaginationPrefsVO();
|
||||
result.setOptions(List.copyOf(normalizedOptions));
|
||||
result.setDefaultSize(resolvedDefault);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询指定用户的某个偏好值。
|
||||
*
|
||||
@@ -202,6 +276,24 @@ public class UserPreferenceServiceImpl implements UserPreferenceService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 JSON 字符串解析为分页偏好,解析失败或为空时返回 null。
|
||||
*
|
||||
* @param json JSON 字符串
|
||||
* @return 分页偏好,解析失败时为 null
|
||||
*/
|
||||
private PaginationPrefsVO parsePaginationPrefs(String json) {
|
||||
if (json == null || json.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return objectMapper.readValue(json, PaginationPrefsVO.class);
|
||||
} catch (JsonProcessingException e) {
|
||||
log.warn("解析表格分页偏好失败:{}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将对象序列化为 JSON 字符串。
|
||||
*
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package cn.rosercode.finance.vo;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 表格分页用户偏好视图对象,包含每页条数候选列表与默认每页条数。
|
||||
*
|
||||
* @date 2026-09-23 11:05:25
|
||||
*/
|
||||
@Data
|
||||
public class PaginationPrefsVO {
|
||||
|
||||
/** 每页条数候选列表,升序去重 */
|
||||
private List<Integer> options;
|
||||
|
||||
/** 默认每页条数,必为候选列表中的值 */
|
||||
private Integer defaultSize;
|
||||
}
|
||||
Reference in New Issue
Block a user