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:
2026-09-23 11:14:54 +08:00
parent 7172f6aa44
commit e0fa1abec5
39 changed files with 1042 additions and 62 deletions
@@ -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
}