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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user