mirror of
https://gitee.com/wanwujie/sub2api
synced 2026-04-02 22:42:14 +08:00
Merge pull request #1147 from DaydreamCoding/feat/persisted-page-size
feat(frontend): 分页 pageSize 持久化到 localStorage,刷新后自动恢复
This commit is contained in:
@@ -69,6 +69,7 @@ import { adminAPI } from '@/api/admin'
|
|||||||
import { formatDateTime } from '@/utils/format'
|
import { formatDateTime } from '@/utils/format'
|
||||||
import type { AnnouncementUserReadStatus } from '@/types'
|
import type { AnnouncementUserReadStatus } from '@/types'
|
||||||
import type { Column } from '@/components/common/types'
|
import type { Column } from '@/components/common/types'
|
||||||
|
import { getPersistedPageSize } from '@/composables/usePersistedPageSize'
|
||||||
|
|
||||||
import BaseDialog from '@/components/common/BaseDialog.vue'
|
import BaseDialog from '@/components/common/BaseDialog.vue'
|
||||||
import DataTable from '@/components/common/DataTable.vue'
|
import DataTable from '@/components/common/DataTable.vue'
|
||||||
@@ -92,7 +93,7 @@ const search = ref('')
|
|||||||
|
|
||||||
const pagination = reactive({
|
const pagination = reactive({
|
||||||
page: 1,
|
page: 1,
|
||||||
page_size: 20,
|
page_size: getPersistedPageSize(),
|
||||||
total: 0,
|
total: 0,
|
||||||
pages: 0
|
pages: 0
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ import { computed, ref } from 'vue'
|
|||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import Icon from '@/components/icons/Icon.vue'
|
import Icon from '@/components/icons/Icon.vue'
|
||||||
import Select from './Select.vue'
|
import Select from './Select.vue'
|
||||||
|
import { setPersistedPageSize } from '@/composables/usePersistedPageSize'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
@@ -216,6 +217,7 @@ const goToPage = (newPage: number) => {
|
|||||||
const handlePageSizeChange = (value: string | number | boolean | null) => {
|
const handlePageSizeChange = (value: string | number | boolean | null) => {
|
||||||
if (value === null || typeof value === 'boolean') return
|
if (value === null || typeof value === 'boolean') return
|
||||||
const newPageSize = typeof value === 'string' ? parseInt(value) : value
|
const newPageSize = typeof value === 'string' ? parseInt(value) : value
|
||||||
|
setPersistedPageSize(newPageSize)
|
||||||
emit('update:pageSize', newPageSize)
|
emit('update:pageSize', newPageSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -126,6 +126,7 @@
|
|||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import soraAPI, { type SoraGeneration } from '@/api/sora'
|
import soraAPI, { type SoraGeneration } from '@/api/sora'
|
||||||
|
import { getPersistedPageSize } from '@/composables/usePersistedPageSize'
|
||||||
import SoraMediaPreview from './SoraMediaPreview.vue'
|
import SoraMediaPreview from './SoraMediaPreview.vue'
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -190,7 +191,7 @@ async function loadItems(pageNum: number) {
|
|||||||
status: 'completed',
|
status: 'completed',
|
||||||
storage_type: 's3,local',
|
storage_type: 's3,local',
|
||||||
page: pageNum,
|
page: pageNum,
|
||||||
page_size: 20
|
page_size: getPersistedPageSize()
|
||||||
})
|
})
|
||||||
const rows = Array.isArray(res.data) ? res.data : []
|
const rows = Array.isArray(res.data) ? res.data : []
|
||||||
if (pageNum === 1) {
|
if (pageNum === 1) {
|
||||||
|
|||||||
27
frontend/src/composables/usePersistedPageSize.ts
Normal file
27
frontend/src/composables/usePersistedPageSize.ts
Normal file
@@ -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 {
|
||||||
|
// 静默失败
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { ref, reactive, onUnmounted, toRaw } from 'vue'
|
import { ref, reactive, onUnmounted, toRaw } from 'vue'
|
||||||
import { useDebounceFn } from '@vueuse/core'
|
import { useDebounceFn } from '@vueuse/core'
|
||||||
import type { BasePaginationResponse, FetchOptions } from '@/types'
|
import type { BasePaginationResponse, FetchOptions } from '@/types'
|
||||||
|
import { getPersistedPageSize, setPersistedPageSize } from './usePersistedPageSize'
|
||||||
|
|
||||||
interface PaginationState {
|
interface PaginationState {
|
||||||
page: number
|
page: number
|
||||||
@@ -21,14 +22,14 @@ interface TableLoaderOptions<T, P> {
|
|||||||
* 统一处理分页、筛选、搜索防抖和请求取消
|
* 统一处理分页、筛选、搜索防抖和请求取消
|
||||||
*/
|
*/
|
||||||
export function useTableLoader<T, P extends Record<string, any>>(options: TableLoaderOptions<T, P>) {
|
export function useTableLoader<T, P extends Record<string, any>>(options: TableLoaderOptions<T, P>) {
|
||||||
const { fetchFn, initialParams, pageSize = 20, debounceMs = 300 } = options
|
const { fetchFn, initialParams, pageSize, debounceMs = 300 } = options
|
||||||
|
|
||||||
const items = ref<T[]>([])
|
const items = ref<T[]>([])
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const params = reactive<P>({ ...(initialParams || {}) } as P)
|
const params = reactive<P>({ ...(initialParams || {}) } as P)
|
||||||
const pagination = reactive<PaginationState>({
|
const pagination = reactive<PaginationState>({
|
||||||
page: 1,
|
page: 1,
|
||||||
page_size: pageSize,
|
page_size: pageSize ?? getPersistedPageSize(),
|
||||||
total: 0,
|
total: 0,
|
||||||
pages: 0
|
pages: 0
|
||||||
})
|
})
|
||||||
@@ -87,6 +88,7 @@ export function useTableLoader<T, P extends Record<string, any>>(options: TableL
|
|||||||
const handlePageSizeChange = (size: number) => {
|
const handlePageSizeChange = (size: number) => {
|
||||||
pagination.page_size = size
|
pagination.page_size = size
|
||||||
pagination.page = 1
|
pagination.page = 1
|
||||||
|
setPersistedPageSize(size)
|
||||||
load()
|
load()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -239,6 +239,7 @@
|
|||||||
import { computed, onMounted, reactive, ref } from 'vue'
|
import { computed, onMounted, reactive, ref } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useAppStore } from '@/stores/app'
|
import { useAppStore } from '@/stores/app'
|
||||||
|
import { getPersistedPageSize } from '@/composables/usePersistedPageSize'
|
||||||
import { adminAPI } from '@/api/admin'
|
import { adminAPI } from '@/api/admin'
|
||||||
import { formatDateTime, formatDateTimeLocalInput, parseDateTimeLocalInput } from '@/utils/format'
|
import { formatDateTime, formatDateTimeLocalInput, parseDateTimeLocalInput } from '@/utils/format'
|
||||||
import type { AdminGroup, Announcement, AnnouncementTargeting } from '@/types'
|
import type { AdminGroup, Announcement, AnnouncementTargeting } from '@/types'
|
||||||
@@ -270,7 +271,7 @@ const searchQuery = ref('')
|
|||||||
|
|
||||||
const pagination = reactive({
|
const pagination = reactive({
|
||||||
page: 1,
|
page: 1,
|
||||||
page_size: 20,
|
page_size: getPersistedPageSize(),
|
||||||
total: 0,
|
total: 0,
|
||||||
pages: 0
|
pages: 0
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1855,6 +1855,7 @@ import GroupCapacityBadge from '@/components/common/GroupCapacityBadge.vue'
|
|||||||
import { VueDraggable } from 'vue-draggable-plus'
|
import { VueDraggable } from 'vue-draggable-plus'
|
||||||
import { createStableObjectKeyResolver } from '@/utils/stableObjectKey'
|
import { createStableObjectKeyResolver } from '@/utils/stableObjectKey'
|
||||||
import { useKeyedDebouncedSearch } from '@/composables/useKeyedDebouncedSearch'
|
import { useKeyedDebouncedSearch } from '@/composables/useKeyedDebouncedSearch'
|
||||||
|
import { getPersistedPageSize } from '@/composables/usePersistedPageSize'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
@@ -2016,7 +2017,7 @@ const filters = reactive({
|
|||||||
})
|
})
|
||||||
const pagination = reactive({
|
const pagination = reactive({
|
||||||
page: 1,
|
page: 1,
|
||||||
page_size: 20,
|
page_size: getPersistedPageSize(),
|
||||||
total: 0,
|
total: 0,
|
||||||
pages: 0
|
pages: 0
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -383,6 +383,7 @@ import { ref, reactive, computed, onMounted, onUnmounted } from 'vue'
|
|||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useAppStore } from '@/stores/app'
|
import { useAppStore } from '@/stores/app'
|
||||||
import { useClipboard } from '@/composables/useClipboard'
|
import { useClipboard } from '@/composables/useClipboard'
|
||||||
|
import { getPersistedPageSize } from '@/composables/usePersistedPageSize'
|
||||||
import { adminAPI } from '@/api/admin'
|
import { adminAPI } from '@/api/admin'
|
||||||
import { formatDateTime } from '@/utils/format'
|
import { formatDateTime } from '@/utils/format'
|
||||||
import type { PromoCode, PromoCodeUsage } from '@/types'
|
import type { PromoCode, PromoCodeUsage } from '@/types'
|
||||||
@@ -414,7 +415,7 @@ const filters = reactive({
|
|||||||
|
|
||||||
const pagination = reactive({
|
const pagination = reactive({
|
||||||
page: 1,
|
page: 1,
|
||||||
page_size: 20,
|
page_size: getPersistedPageSize(),
|
||||||
total: 0
|
total: 0
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -884,6 +884,7 @@ import PlatformTypeBadge from '@/components/common/PlatformTypeBadge.vue'
|
|||||||
import { useClipboard } from '@/composables/useClipboard'
|
import { useClipboard } from '@/composables/useClipboard'
|
||||||
import { useSwipeSelect } from '@/composables/useSwipeSelect'
|
import { useSwipeSelect } from '@/composables/useSwipeSelect'
|
||||||
import { useTableSelection } from '@/composables/useTableSelection'
|
import { useTableSelection } from '@/composables/useTableSelection'
|
||||||
|
import { getPersistedPageSize } from '@/composables/usePersistedPageSize'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
@@ -941,7 +942,7 @@ const filters = reactive({
|
|||||||
})
|
})
|
||||||
const pagination = reactive({
|
const pagination = reactive({
|
||||||
page: 1,
|
page: 1,
|
||||||
page_size: 20,
|
page_size: getPersistedPageSize(),
|
||||||
total: 0,
|
total: 0,
|
||||||
pages: 0
|
pages: 0
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -395,6 +395,7 @@ import { ref, reactive, computed, onMounted, onUnmounted, watch } from 'vue'
|
|||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useAppStore } from '@/stores/app'
|
import { useAppStore } from '@/stores/app'
|
||||||
import { useClipboard } from '@/composables/useClipboard'
|
import { useClipboard } from '@/composables/useClipboard'
|
||||||
|
import { getPersistedPageSize } from '@/composables/usePersistedPageSize'
|
||||||
import { adminAPI } from '@/api/admin'
|
import { adminAPI } from '@/api/admin'
|
||||||
import { formatDateTime } from '@/utils/format'
|
import { formatDateTime } from '@/utils/format'
|
||||||
import type { RedeemCode, RedeemCodeType, Group, GroupPlatform, SubscriptionType } from '@/types'
|
import type { RedeemCode, RedeemCodeType, Group, GroupPlatform, SubscriptionType } from '@/types'
|
||||||
@@ -532,7 +533,7 @@ const filters = reactive({
|
|||||||
})
|
})
|
||||||
const pagination = reactive({
|
const pagination = reactive({
|
||||||
page: 1,
|
page: 1,
|
||||||
page_size: 20,
|
page_size: getPersistedPageSize(),
|
||||||
total: 0,
|
total: 0,
|
||||||
pages: 0
|
pages: 0
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -744,6 +744,7 @@ import type { UserSubscription, Group, GroupPlatform, SubscriptionType } from '@
|
|||||||
import type { SimpleUser } from '@/api/admin/usage'
|
import type { SimpleUser } from '@/api/admin/usage'
|
||||||
import type { Column } from '@/components/common/types'
|
import type { Column } from '@/components/common/types'
|
||||||
import { formatDateOnly } from '@/utils/format'
|
import { formatDateOnly } from '@/utils/format'
|
||||||
|
import { getPersistedPageSize } from '@/composables/usePersistedPageSize'
|
||||||
import AppLayout from '@/components/layout/AppLayout.vue'
|
import AppLayout from '@/components/layout/AppLayout.vue'
|
||||||
import TablePageLayout from '@/components/layout/TablePageLayout.vue'
|
import TablePageLayout from '@/components/layout/TablePageLayout.vue'
|
||||||
import DataTable from '@/components/common/DataTable.vue'
|
import DataTable from '@/components/common/DataTable.vue'
|
||||||
@@ -928,7 +929,7 @@ const sortState = reactive({
|
|||||||
|
|
||||||
const pagination = reactive({
|
const pagination = reactive({
|
||||||
page: 1,
|
page: 1,
|
||||||
page_size: 20,
|
page_size: getPersistedPageSize(),
|
||||||
total: 0,
|
total: 0,
|
||||||
pages: 0
|
pages: 0
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -124,6 +124,7 @@ import { useI18n } from 'vue-i18n'
|
|||||||
import { saveAs } from 'file-saver'
|
import { saveAs } from 'file-saver'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { useAppStore } from '@/stores/app'; import { adminAPI } from '@/api/admin'; import { adminUsageAPI } from '@/api/admin/usage'
|
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 { formatReasoningEffort } from '@/utils/format'
|
||||||
import { resolveUsageRequestType, requestTypeToLegacyStream } from '@/utils/usageRequestType'
|
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'
|
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 defaultRange = getLast24HoursRangeDates()
|
||||||
const startDate = ref(defaultRange.start); const endDate = ref(defaultRange.end)
|
const startDate = ref(defaultRange.start); const endDate = ref(defaultRange.end)
|
||||||
const filters = ref<AdminUsageQueryParams>({ user_id: undefined, model: undefined, group_id: undefined, request_type: undefined, billing_type: null, start_date: startDate.value, end_date: endDate.value })
|
const filters = ref<AdminUsageQueryParams>({ 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<string | null> | undefined): string | undefined => {
|
const getSingleQueryValue = (value: string | null | Array<string | null> | undefined): string | undefined => {
|
||||||
if (Array.isArray(value)) return value.find((item): item is string => typeof item === 'string' && item.length > 0)
|
if (Array.isArray(value)) return value.find((item): item is string => typeof item === 'string' && item.length > 0)
|
||||||
|
|||||||
@@ -521,6 +521,7 @@
|
|||||||
import { ref, reactive, computed, onMounted, onUnmounted } from 'vue'
|
import { ref, reactive, computed, onMounted, onUnmounted } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useAppStore } from '@/stores/app'
|
import { useAppStore } from '@/stores/app'
|
||||||
|
import { getPersistedPageSize } from '@/composables/usePersistedPageSize'
|
||||||
import { formatDateTime } from '@/utils/format'
|
import { formatDateTime } from '@/utils/format'
|
||||||
import Icon from '@/components/icons/Icon.vue'
|
import Icon from '@/components/icons/Icon.vue'
|
||||||
|
|
||||||
@@ -774,7 +775,7 @@ const attributeDefinitions = ref<UserAttributeDefinition[]>([])
|
|||||||
const userAttributeValues = ref<Record<number, Record<number, string>>>({})
|
const userAttributeValues = ref<Record<number, Record<number, string>>>({})
|
||||||
const pagination = reactive({
|
const pagination = reactive({
|
||||||
page: 1,
|
page: 1,
|
||||||
page_size: 20,
|
page_size: getPersistedPageSize(),
|
||||||
total: 0,
|
total: 0,
|
||||||
pages: 0
|
pages: 0
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1035,6 +1035,7 @@
|
|||||||
import { useAppStore } from '@/stores/app'
|
import { useAppStore } from '@/stores/app'
|
||||||
import { useOnboardingStore } from '@/stores/onboarding'
|
import { useOnboardingStore } from '@/stores/onboarding'
|
||||||
import { useClipboard } from '@/composables/useClipboard'
|
import { useClipboard } from '@/composables/useClipboard'
|
||||||
|
import { getPersistedPageSize } from '@/composables/usePersistedPageSize'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
import { keysAPI, authAPI, usageAPI, userGroupsAPI } from '@/api'
|
import { keysAPI, authAPI, usageAPI, userGroupsAPI } from '@/api'
|
||||||
@@ -1101,7 +1102,7 @@ const userGroupRates = ref<Record<number, number>>({})
|
|||||||
|
|
||||||
const pagination = ref({
|
const pagination = ref({
|
||||||
page: 1,
|
page: 1,
|
||||||
page_size: 10,
|
page_size: getPersistedPageSize(),
|
||||||
total: 0,
|
total: 0,
|
||||||
pages: 0
|
pages: 0
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -496,6 +496,7 @@ import Icon from '@/components/icons/Icon.vue'
|
|||||||
import type { UsageLog, ApiKey, UsageQueryParams, UsageStatsResponse } from '@/types'
|
import type { UsageLog, ApiKey, UsageQueryParams, UsageStatsResponse } from '@/types'
|
||||||
import type { Column } from '@/components/common/types'
|
import type { Column } from '@/components/common/types'
|
||||||
import { formatDateTime, formatReasoningEffort } from '@/utils/format'
|
import { formatDateTime, formatReasoningEffort } from '@/utils/format'
|
||||||
|
import { getPersistedPageSize } from '@/composables/usePersistedPageSize'
|
||||||
import { formatTokenPricePerMillion } from '@/utils/usagePricing'
|
import { formatTokenPricePerMillion } from '@/utils/usagePricing'
|
||||||
import { getUsageServiceTierLabel } from '@/utils/usageServiceTier'
|
import { getUsageServiceTierLabel } from '@/utils/usageServiceTier'
|
||||||
import { resolveUsageRequestType } from '@/utils/usageRequestType'
|
import { resolveUsageRequestType } from '@/utils/usageRequestType'
|
||||||
@@ -584,7 +585,7 @@ const onDateRangeChange = (range: {
|
|||||||
|
|
||||||
const pagination = reactive({
|
const pagination = reactive({
|
||||||
page: 1,
|
page: 1,
|
||||||
page_size: 20,
|
page_size: getPersistedPageSize(),
|
||||||
total: 0,
|
total: 0,
|
||||||
pages: 0
|
pages: 0
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user