diff --git a/frontend/src/components/admin/announcements/AnnouncementReadStatusDialog.vue b/frontend/src/components/admin/announcements/AnnouncementReadStatusDialog.vue index e7d991a8..a0d9de3c 100644 --- a/frontend/src/components/admin/announcements/AnnouncementReadStatusDialog.vue +++ b/frontend/src/components/admin/announcements/AnnouncementReadStatusDialog.vue @@ -69,6 +69,7 @@ import { adminAPI } from '@/api/admin' import { formatDateTime } from '@/utils/format' import type { AnnouncementUserReadStatus } from '@/types' import type { Column } from '@/components/common/types' +import { getPersistedPageSize } from '@/composables/usePersistedPageSize' import BaseDialog from '@/components/common/BaseDialog.vue' import DataTable from '@/components/common/DataTable.vue' @@ -92,7 +93,7 @@ const search = ref('') const pagination = reactive({ page: 1, - page_size: 20, + page_size: getPersistedPageSize(), total: 0, pages: 0 }) diff --git a/frontend/src/components/common/Pagination.vue b/frontend/src/components/common/Pagination.vue index 5110c8a3..abb0e566 100644 --- a/frontend/src/components/common/Pagination.vue +++ b/frontend/src/components/common/Pagination.vue @@ -122,6 +122,7 @@ import { computed, ref } from 'vue' import { useI18n } from 'vue-i18n' import Icon from '@/components/icons/Icon.vue' import Select from './Select.vue' +import { setPersistedPageSize } from '@/composables/usePersistedPageSize' const { t } = useI18n() @@ -216,6 +217,7 @@ const goToPage = (newPage: number) => { const handlePageSizeChange = (value: string | number | boolean | null) => { if (value === null || typeof value === 'boolean') return const newPageSize = typeof value === 'string' ? parseInt(value) : value + setPersistedPageSize(newPageSize) emit('update:pageSize', newPageSize) } diff --git a/frontend/src/components/sora/SoraLibraryPage.vue b/frontend/src/components/sora/SoraLibraryPage.vue index 0e2b5e1d..1d49fe60 100644 --- a/frontend/src/components/sora/SoraLibraryPage.vue +++ b/frontend/src/components/sora/SoraLibraryPage.vue @@ -126,6 +126,7 @@ import { ref, computed, onMounted } from 'vue' import { useI18n } from 'vue-i18n' import soraAPI, { type SoraGeneration } from '@/api/sora' +import { getPersistedPageSize } from '@/composables/usePersistedPageSize' import SoraMediaPreview from './SoraMediaPreview.vue' const emit = defineEmits<{ @@ -190,7 +191,7 @@ async function loadItems(pageNum: number) { status: 'completed', storage_type: 's3,local', page: pageNum, - page_size: 20 + page_size: getPersistedPageSize() }) const rows = Array.isArray(res.data) ? res.data : [] if (pageNum === 1) { diff --git a/frontend/src/composables/usePersistedPageSize.ts b/frontend/src/composables/usePersistedPageSize.ts new file mode 100644 index 00000000..9e173a1d --- /dev/null +++ b/frontend/src/composables/usePersistedPageSize.ts @@ -0,0 +1,27 @@ +const STORAGE_KEY = 'table-page-size' +const DEFAULT_PAGE_SIZE = 20 + +/** + * 从 localStorage 读取/写入 pageSize + * 全局共享一个 key,所有表格统一偏好 + */ +export function getPersistedPageSize(fallback = DEFAULT_PAGE_SIZE): number { + try { + const stored = localStorage.getItem(STORAGE_KEY) + if (stored) { + const parsed = Number(stored) + if (Number.isFinite(parsed) && parsed > 0) return parsed + } + } catch { + // localStorage 不可用(隐私模式等) + } + return fallback +} + +export function setPersistedPageSize(size: number): void { + try { + localStorage.setItem(STORAGE_KEY, String(size)) + } catch { + // 静默失败 + } +} diff --git a/frontend/src/composables/useTableLoader.ts b/frontend/src/composables/useTableLoader.ts index 5fb6c5e0..67c1dcdb 100644 --- a/frontend/src/composables/useTableLoader.ts +++ b/frontend/src/composables/useTableLoader.ts @@ -1,6 +1,7 @@ import { ref, reactive, onUnmounted, toRaw } from 'vue' import { useDebounceFn } from '@vueuse/core' import type { BasePaginationResponse, FetchOptions } from '@/types' +import { getPersistedPageSize, setPersistedPageSize } from './usePersistedPageSize' interface PaginationState { page: number @@ -21,14 +22,14 @@ interface TableLoaderOptions { * 统一处理分页、筛选、搜索防抖和请求取消 */ export function useTableLoader>(options: TableLoaderOptions) { - const { fetchFn, initialParams, pageSize = 20, debounceMs = 300 } = options + const { fetchFn, initialParams, pageSize, debounceMs = 300 } = options const items = ref([]) const loading = ref(false) const params = reactive

({ ...(initialParams || {}) } as P) const pagination = reactive({ page: 1, - page_size: pageSize, + page_size: pageSize ?? getPersistedPageSize(), total: 0, pages: 0 }) @@ -87,6 +88,7 @@ export function useTableLoader>(options: TableL const handlePageSizeChange = (size: number) => { pagination.page_size = size pagination.page = 1 + setPersistedPageSize(size) load() } diff --git a/frontend/src/views/admin/AnnouncementsView.vue b/frontend/src/views/admin/AnnouncementsView.vue index 1c716807..fcb7e283 100644 --- a/frontend/src/views/admin/AnnouncementsView.vue +++ b/frontend/src/views/admin/AnnouncementsView.vue @@ -239,6 +239,7 @@ import { computed, onMounted, reactive, ref } from 'vue' import { useI18n } from 'vue-i18n' import { useAppStore } from '@/stores/app' +import { getPersistedPageSize } from '@/composables/usePersistedPageSize' import { adminAPI } from '@/api/admin' import { formatDateTime, formatDateTimeLocalInput, parseDateTimeLocalInput } from '@/utils/format' import type { AdminGroup, Announcement, AnnouncementTargeting } from '@/types' @@ -270,7 +271,7 @@ const searchQuery = ref('') const pagination = reactive({ page: 1, - page_size: 20, + page_size: getPersistedPageSize(), total: 0, pages: 0 }) diff --git a/frontend/src/views/admin/GroupsView.vue b/frontend/src/views/admin/GroupsView.vue index ddd7e672..a7c1a10d 100644 --- a/frontend/src/views/admin/GroupsView.vue +++ b/frontend/src/views/admin/GroupsView.vue @@ -1855,6 +1855,7 @@ import GroupCapacityBadge from '@/components/common/GroupCapacityBadge.vue' import { VueDraggable } from 'vue-draggable-plus' import { createStableObjectKeyResolver } from '@/utils/stableObjectKey' import { useKeyedDebouncedSearch } from '@/composables/useKeyedDebouncedSearch' +import { getPersistedPageSize } from '@/composables/usePersistedPageSize' const { t } = useI18n() const appStore = useAppStore() @@ -2016,7 +2017,7 @@ const filters = reactive({ }) const pagination = reactive({ page: 1, - page_size: 20, + page_size: getPersistedPageSize(), total: 0, pages: 0 }) diff --git a/frontend/src/views/admin/PromoCodesView.vue b/frontend/src/views/admin/PromoCodesView.vue index 73499f80..7bf670f7 100644 --- a/frontend/src/views/admin/PromoCodesView.vue +++ b/frontend/src/views/admin/PromoCodesView.vue @@ -383,6 +383,7 @@ import { ref, reactive, computed, onMounted, onUnmounted } from 'vue' import { useI18n } from 'vue-i18n' import { useAppStore } from '@/stores/app' import { useClipboard } from '@/composables/useClipboard' +import { getPersistedPageSize } from '@/composables/usePersistedPageSize' import { adminAPI } from '@/api/admin' import { formatDateTime } from '@/utils/format' import type { PromoCode, PromoCodeUsage } from '@/types' @@ -414,7 +415,7 @@ const filters = reactive({ const pagination = reactive({ page: 1, - page_size: 20, + page_size: getPersistedPageSize(), total: 0 }) diff --git a/frontend/src/views/admin/ProxiesView.vue b/frontend/src/views/admin/ProxiesView.vue index 22694d07..032a0a2f 100644 --- a/frontend/src/views/admin/ProxiesView.vue +++ b/frontend/src/views/admin/ProxiesView.vue @@ -884,6 +884,7 @@ import PlatformTypeBadge from '@/components/common/PlatformTypeBadge.vue' import { useClipboard } from '@/composables/useClipboard' import { useSwipeSelect } from '@/composables/useSwipeSelect' import { useTableSelection } from '@/composables/useTableSelection' +import { getPersistedPageSize } from '@/composables/usePersistedPageSize' const { t } = useI18n() const appStore = useAppStore() @@ -941,7 +942,7 @@ const filters = reactive({ }) const pagination = reactive({ page: 1, - page_size: 20, + page_size: getPersistedPageSize(), total: 0, pages: 0 }) diff --git a/frontend/src/views/admin/RedeemView.vue b/frontend/src/views/admin/RedeemView.vue index 17e612c5..ad4fde5c 100644 --- a/frontend/src/views/admin/RedeemView.vue +++ b/frontend/src/views/admin/RedeemView.vue @@ -395,6 +395,7 @@ import { ref, reactive, computed, onMounted, onUnmounted, watch } from 'vue' import { useI18n } from 'vue-i18n' import { useAppStore } from '@/stores/app' import { useClipboard } from '@/composables/useClipboard' +import { getPersistedPageSize } from '@/composables/usePersistedPageSize' import { adminAPI } from '@/api/admin' import { formatDateTime } from '@/utils/format' import type { RedeemCode, RedeemCodeType, Group, GroupPlatform, SubscriptionType } from '@/types' @@ -532,7 +533,7 @@ const filters = reactive({ }) const pagination = reactive({ page: 1, - page_size: 20, + page_size: getPersistedPageSize(), total: 0, pages: 0 }) diff --git a/frontend/src/views/admin/SubscriptionsView.vue b/frontend/src/views/admin/SubscriptionsView.vue index 0b4a627f..bb9b5d9a 100644 --- a/frontend/src/views/admin/SubscriptionsView.vue +++ b/frontend/src/views/admin/SubscriptionsView.vue @@ -744,6 +744,7 @@ import type { UserSubscription, Group, GroupPlatform, SubscriptionType } from '@ import type { SimpleUser } from '@/api/admin/usage' import type { Column } from '@/components/common/types' import { formatDateOnly } from '@/utils/format' +import { getPersistedPageSize } from '@/composables/usePersistedPageSize' import AppLayout from '@/components/layout/AppLayout.vue' import TablePageLayout from '@/components/layout/TablePageLayout.vue' import DataTable from '@/components/common/DataTable.vue' @@ -928,7 +929,7 @@ const sortState = reactive({ const pagination = reactive({ page: 1, - page_size: 20, + page_size: getPersistedPageSize(), total: 0, pages: 0 }) diff --git a/frontend/src/views/admin/UsageView.vue b/frontend/src/views/admin/UsageView.vue index dec1e043..4911f7b3 100644 --- a/frontend/src/views/admin/UsageView.vue +++ b/frontend/src/views/admin/UsageView.vue @@ -124,6 +124,7 @@ import { useI18n } from 'vue-i18n' import { saveAs } from 'file-saver' import { useRoute } from 'vue-router' import { useAppStore } from '@/stores/app'; import { adminAPI } from '@/api/admin'; import { adminUsageAPI } from '@/api/admin/usage' +import { getPersistedPageSize } from '@/composables/usePersistedPageSize' import { formatReasoningEffort } from '@/utils/format' import { resolveUsageRequestType, requestTypeToLegacyStream } from '@/utils/usageRequestType' import AppLayout from '@/components/layout/AppLayout.vue'; import Pagination from '@/components/common/Pagination.vue'; import Select from '@/components/common/Select.vue'; import DateRangePicker from '@/components/common/DateRangePicker.vue' @@ -203,7 +204,7 @@ const getGranularityForRange = (start: string, end: string): 'day' | 'hour' => { const defaultRange = getLast24HoursRangeDates() const startDate = ref(defaultRange.start); const endDate = ref(defaultRange.end) const filters = ref({ user_id: undefined, model: undefined, group_id: undefined, request_type: undefined, billing_type: null, start_date: startDate.value, end_date: endDate.value }) -const pagination = reactive({ page: 1, page_size: 20, total: 0 }) +const pagination = reactive({ page: 1, page_size: getPersistedPageSize(), total: 0 }) const getSingleQueryValue = (value: string | null | Array | undefined): string | undefined => { if (Array.isArray(value)) return value.find((item): item is string => typeof item === 'string' && item.length > 0) diff --git a/frontend/src/views/admin/UsersView.vue b/frontend/src/views/admin/UsersView.vue index 06310888..eea6ed33 100644 --- a/frontend/src/views/admin/UsersView.vue +++ b/frontend/src/views/admin/UsersView.vue @@ -521,6 +521,7 @@ import { ref, reactive, computed, onMounted, onUnmounted } from 'vue' import { useI18n } from 'vue-i18n' import { useAppStore } from '@/stores/app' +import { getPersistedPageSize } from '@/composables/usePersistedPageSize' import { formatDateTime } from '@/utils/format' import Icon from '@/components/icons/Icon.vue' @@ -774,7 +775,7 @@ const attributeDefinitions = ref([]) const userAttributeValues = ref>>({}) const pagination = reactive({ page: 1, - page_size: 20, + page_size: getPersistedPageSize(), total: 0, pages: 0 }) diff --git a/frontend/src/views/user/KeysView.vue b/frontend/src/views/user/KeysView.vue index 3068cb7f..836fd2cb 100644 --- a/frontend/src/views/user/KeysView.vue +++ b/frontend/src/views/user/KeysView.vue @@ -1035,6 +1035,7 @@ import { useAppStore } from '@/stores/app' import { useOnboardingStore } from '@/stores/onboarding' import { useClipboard } from '@/composables/useClipboard' +import { getPersistedPageSize } from '@/composables/usePersistedPageSize' const { t } = useI18n() import { keysAPI, authAPI, usageAPI, userGroupsAPI } from '@/api' @@ -1101,7 +1102,7 @@ const userGroupRates = ref>({}) const pagination = ref({ page: 1, - page_size: 10, + page_size: getPersistedPageSize(), total: 0, pages: 0 }) diff --git a/frontend/src/views/user/UsageView.vue b/frontend/src/views/user/UsageView.vue index a7952096..3b8ef2e0 100644 --- a/frontend/src/views/user/UsageView.vue +++ b/frontend/src/views/user/UsageView.vue @@ -496,6 +496,7 @@ import Icon from '@/components/icons/Icon.vue' import type { UsageLog, ApiKey, UsageQueryParams, UsageStatsResponse } from '@/types' import type { Column } from '@/components/common/types' import { formatDateTime, formatReasoningEffort } from '@/utils/format' +import { getPersistedPageSize } from '@/composables/usePersistedPageSize' import { formatTokenPricePerMillion } from '@/utils/usagePricing' import { getUsageServiceTierLabel } from '@/utils/usageServiceTier' import { resolveUsageRequestType } from '@/utils/usageRequestType' @@ -584,7 +585,7 @@ const onDateRangeChange = (range: { const pagination = reactive({ page: 1, - page_size: 20, + page_size: getPersistedPageSize(), total: 0, pages: 0 })