From ba06bb1852c303e41f4f5a226b406e02d437f07e Mon Sep 17 00:00:00 2001 From: zhangheng Date: Thu, 14 May 2026 19:49:31 +0800 Subject: [PATCH] feat(page): size --- .../components/PersonalProjectTable.tsx | 103 +---- .../components/PersonalTaskTable.tsx | 111 +----- app/person/report/page.tsx | 103 +---- components/setting/PageSurface.tsx | 355 +++++++++++++++++- 4 files changed, 401 insertions(+), 271 deletions(-) diff --git a/app/person/dashboard/components/PersonalProjectTable.tsx b/app/person/dashboard/components/PersonalProjectTable.tsx index f875591..bcc5717 100644 --- a/app/person/dashboard/components/PersonalProjectTable.tsx +++ b/app/person/dashboard/components/PersonalProjectTable.tsx @@ -5,10 +5,11 @@ import { Project } from "@/components/label/api/project/typing" import { SettingDataTable, SettingListHeader, + SettingCustomPageSizeControl, + usePersistentPageSizeOptions, } from "@/components/setting/PageSurface" -import { Button, Group, Modal, Stack, Text, TextInput } from "@mantine/core" +import { Button, Group, Stack, Text } from "@mantine/core" import { DatePickerInput, type DatesRangeValue } from "@mantine/dates" -import { notifications } from "@mantine/notifications" import { IconRefresh, IconSearch } from "@tabler/icons-react" import dayjs from "dayjs" @@ -66,12 +67,12 @@ export default function PersonalProjectTable() { const [page, setPage] = useState(1) const [pageSize, setPageSize] = useState(10) - const [recordsPerPageOptions, setRecordsPerPageOptions] = useState( - () => [...DEFAULT_RECORDS_PER_PAGE_OPTIONS] - ) - const [customPageSizeInput, setCustomPageSizeInput] = useState("") - const [customPageSizeModalOpened, setCustomPageSizeModalOpened] = - useState(false) + const { + recordsPerPageOptions, + addCustomPageSize, + clearCustomPageSizeCache, + hasCustomPageSizeCache, + } = usePersistentPageSizeOptions(DEFAULT_RECORDS_PER_PAGE_OPTIONS, pageSize) const [totalItems, setTotalItems] = useState(0) const [filters, setFilters] = useState(() => createInitialFilters()) @@ -138,53 +139,6 @@ export default function PersonalProjectTable() { if (page > totalPages) setPage(totalPages) }, [page, totalPages]) - const openCustomPageSizeModal = useCallback(() => { - setCustomPageSizeModalOpened(true) - }, []) - - const closeCustomPageSizeModal = useCallback(() => { - setCustomPageSizeModalOpened(false) - setCustomPageSizeInput("") - }, []) - - const handleAddCustomPageSize = useCallback(() => { - const rawValue = customPageSizeInput.trim() - if (!rawValue) return - const size = Number(rawValue) - if (!Number.isInteger(size) || size <= 0) { - notifications.show({ - color: "red", - title: "输入无效", - message: "请填写大于 0 的整数条数", - }) - return - } - if (recordsPerPageOptions.includes(size)) { - notifications.show({ - color: "blue", - title: "已存在", - message: `每页 ${size} 条已在可选项中`, - }) - return - } - setRecordsPerPageOptions((prev) => [...prev, size].sort((a, b) => a - b)) - closeCustomPageSizeModal() - notifications.show({ - color: "green", - title: "添加成功", - message: `已新增每页 ${size} 条`, - }) - }, [closeCustomPageSizeModal, customPageSizeInput, recordsPerPageOptions]) - - const handleCustomPageSizeEnter = useCallback( - (event: React.KeyboardEvent) => { - if (event.key !== "Enter") return - event.preventDefault() - handleAddCustomPageSize() - }, - [handleAddCustomPageSize] - ) - const originColumn = useMemo(() => { let column: DataTableColumn[] = [ @@ -329,44 +283,19 @@ export default function PersonalProjectTable() { - + )} /> - - - - setCustomPageSizeInput(event.currentTarget.value) - } - onKeyDown={handleCustomPageSizeEnter} - /> - - - - - - ) } diff --git a/app/person/dashboard/components/PersonalTaskTable.tsx b/app/person/dashboard/components/PersonalTaskTable.tsx index 043feb3..e7888bf 100644 --- a/app/person/dashboard/components/PersonalTaskTable.tsx +++ b/app/person/dashboard/components/PersonalTaskTable.tsx @@ -11,17 +11,10 @@ import { SettingDataTable, SettingHeaderActions, SettingListHeader, + SettingCustomPageSizeControl, + usePersistentPageSizeOptions, } from "@/components/setting/PageSurface" -import { - Button, - Group, - Modal, - Stack, - Text, - TextInput, - UnstyledButton, -} from "@mantine/core" -import { notifications } from "@mantine/notifications" +import { Button, Group, Stack, Text, UnstyledButton } from "@mantine/core" import { IconRefresh } from "@tabler/icons-react" import { DataTableColumn } from "mantine-datatable" import { useCallback, useEffect, useMemo, useState } from "react" @@ -35,12 +28,12 @@ export default function PersonalTaskTable() { const [page, setPage] = useState(1) const [pageSize, setPageSize] = useState(10) - const [recordsPerPageOptions, setRecordsPerPageOptions] = useState( - () => [...DEFAULT_RECORDS_PER_PAGE_OPTIONS] - ) - const [customPageSizeInput, setCustomPageSizeInput] = useState("") - const [customPageSizeModalOpened, setCustomPageSizeModalOpened] = - useState(false) + const { + recordsPerPageOptions, + addCustomPageSize, + clearCustomPageSizeCache, + hasCustomPageSizeCache, + } = usePersistentPageSizeOptions(DEFAULT_RECORDS_PER_PAGE_OPTIONS, pageSize) const [totalItems, setTotalItems] = useState(0) const [records, setRecords] = useState([]) const [loading, setLoading] = useState(false) @@ -79,53 +72,6 @@ export default function PersonalTaskTable() { const [currentRowId, setCurrentRowId] = useState(0) - const openCustomPageSizeModal = useCallback(() => { - setCustomPageSizeModalOpened(true) - }, []) - - const closeCustomPageSizeModal = useCallback(() => { - setCustomPageSizeModalOpened(false) - setCustomPageSizeInput("") - }, []) - - const handleAddCustomPageSize = useCallback(() => { - const rawValue = customPageSizeInput.trim() - if (!rawValue) return - const size = Number(rawValue) - if (!Number.isInteger(size) || size <= 0) { - notifications.show({ - color: "red", - title: "输入无效", - message: "请填写大于 0 的整数条数", - }) - return - } - if (recordsPerPageOptions.includes(size)) { - notifications.show({ - color: "blue", - title: "已存在", - message: `每页 ${size} 条已在可选项中`, - }) - return - } - setRecordsPerPageOptions((prev) => [...prev, size].sort((a, b) => a - b)) - closeCustomPageSizeModal() - notifications.show({ - color: "green", - title: "添加成功", - message: `已新增每页 ${size} 条`, - }) - }, [closeCustomPageSizeModal, customPageSizeInput, recordsPerPageOptions]) - - const handleCustomPageSizeEnter = useCallback( - (event: React.KeyboardEvent) => { - if (event.key !== "Enter") return - event.preventDefault() - handleAddCustomPageSize() - }, - [handleAddCustomPageSize] - ) - const originColumn = useMemo(() => { let column: DataTableColumn[] = [ { @@ -287,44 +233,19 @@ export default function PersonalTaskTable() { - + )} /> - - - - setCustomPageSizeInput(event.currentTarget.value) - } - onKeyDown={handleCustomPageSizeEnter} - /> - - - - - - ) } diff --git a/app/person/report/page.tsx b/app/person/report/page.tsx index 46a5d2b..2ca0612 100644 --- a/app/person/report/page.tsx +++ b/app/person/report/page.tsx @@ -9,24 +9,24 @@ import { getSelfProjectList } from "@/components/label/api/project" import { useAllUserStore } from "@/components/label/store/auth" import { SettingContentPanel, + SettingCustomPageSizeControl, SettingDataTable, SettingFilterActions, SettingFilterPanel, SettingHeaderActions, SettingListHeader, SettingPage, + usePersistentPageSizeOptions, } from "@/components/setting/PageSurface" import { Badge, Button, Collapse, Group, - Modal, MultiSelect, Select, Stack, Text, - TextInput, } from "@mantine/core" import { modals } from "@mantine/modals" import { notifications } from "@mantine/notifications" @@ -127,12 +127,12 @@ export default function PersonReportPage() { const [page, setPage] = useState(1) const [pageSize, setPageSize] = useState(20) - const [recordsPerPageOptions, setRecordsPerPageOptions] = useState( - () => [...DEFAULT_RECORDS_PER_PAGE_OPTIONS] - ) - const [customPageSizeInput, setCustomPageSizeInput] = useState("") - const [customPageSizeModalOpened, setCustomPageSizeModalOpened] = - useState(false) + const { + recordsPerPageOptions, + addCustomPageSize, + clearCustomPageSizeCache, + hasCustomPageSizeCache, + } = usePersistentPageSizeOptions(DEFAULT_RECORDS_PER_PAGE_OPTIONS, pageSize) const [totalItems, setTotalItems] = useState(0) const [loading, setLoading] = useState(false) const [records, setRecords] = useState([]) @@ -225,53 +225,6 @@ export default function PersonReportPage() { }) }, []) - const openCustomPageSizeModal = useCallback(() => { - setCustomPageSizeModalOpened(true) - }, []) - - const closeCustomPageSizeModal = useCallback(() => { - setCustomPageSizeModalOpened(false) - setCustomPageSizeInput("") - }, []) - - const handleAddCustomPageSize = useCallback(() => { - const rawValue = customPageSizeInput.trim() - if (!rawValue) return - const size = Number(rawValue) - if (!Number.isInteger(size) || size <= 0) { - notifications.show({ - color: "red", - title: "输入无效", - message: "请填写大于 0 的整数条数", - }) - return - } - if (recordsPerPageOptions.includes(size)) { - notifications.show({ - color: "blue", - title: "已存在", - message: `每页 ${size} 条已在可选项中`, - }) - return - } - setRecordsPerPageOptions((prev) => [...prev, size].sort((a, b) => a - b)) - closeCustomPageSizeModal() - notifications.show({ - color: "green", - title: "添加成功", - message: `已新增每页 ${size} 条`, - }) - }, [closeCustomPageSizeModal, customPageSizeInput, recordsPerPageOptions]) - - const handleCustomPageSizeEnter = useCallback( - (event: React.KeyboardEvent) => { - if (event.key !== "Enter") return - event.preventDefault() - handleAddCustomPageSize() - }, - [handleAddCustomPageSize] - ) - const sum = (arr: number[]) => arr.reduce((accumulator, currentValue) => { const value = Number(currentValue ?? 0) @@ -754,12 +707,13 @@ export default function PersonReportPage() { - + @@ -797,33 +751,6 @@ export default function PersonReportPage() { if (refresh) load() }} /> - - - - - setCustomPageSizeInput(event.currentTarget.value) - } - onKeyDown={handleCustomPageSizeEnter} - /> - - - - - - ) } diff --git a/components/setting/PageSurface.tsx b/components/setting/PageSurface.tsx index 30fee19..84fcfe3 100644 --- a/components/setting/PageSurface.tsx +++ b/components/setting/PageSurface.tsx @@ -1,20 +1,40 @@ "use client" import { + Button, Group, + Modal, Paper, Stack, Text, + TextInput, + type ButtonProps, type GroupProps, type PaperProps, type StackProps, } from "@mantine/core" +import { notifications } from "@mantine/notifications" import { DataTable, type DataTableDefaultColumnProps, type DataTableProps, } from "mantine-datatable" -import type { CSSProperties, PropsWithChildren, ReactNode } from "react" +import { + type CSSProperties, + type KeyboardEvent, + type PropsWithChildren, + type ReactNode, + useCallback, + useEffect, + useMemo, + useState, +} from "react" +import { create } from "zustand" +import { + persist, + type PersistStorage, + type StorageValue, +} from "zustand/middleware" import classes from "./PageSurface.module.css" function mergeClassName(...classNames: Array) { @@ -33,6 +53,339 @@ export const settingModalActionsClassName = classes.modalActions export const settingModalMetaTextClassName = classes.modalMetaText export const settingSurfaceClassNames = classes +const CUSTOM_PAGE_SIZE_STORAGE_KEY = "cowa:custom-records-per-page-options:v1" + +function normalizePageSizeOptions(values: number[]) { + return Array.from( + new Set(values.filter((value) => Number.isInteger(value) && value > 0)) + ).sort((a, b) => a - b) +} + +type AddCustomPageSizeResult = "added" | "exists" | "invalid" + +type CustomPageSizePersistedState = { + customPageSizeCache: number[] +} + +type CustomPageSizeStoreState = CustomPageSizePersistedState & { + appendCustomPageSize: (size: number) => void + clearCustomPageSizeCache: () => void +} + +function normalizeCustomPageSizePersistedState( + value: unknown +): CustomPageSizePersistedState { + if (!value || typeof value !== "object") { + return { customPageSizeCache: [] } + } + + const { customPageSizeCache } = value as { + customPageSizeCache?: unknown + } + if (!Array.isArray(customPageSizeCache)) { + return { customPageSizeCache: [] } + } + + return { + customPageSizeCache: normalizePageSizeOptions( + customPageSizeCache.map((item) => Number(item)) + ), + } +} + +const customPageSizeStorage: PersistStorage = { + getItem: (name) => { + if (typeof window === "undefined") return null + + try { + const rawValue = window.localStorage.getItem(name) + if (!rawValue) return null + + const parsedValue = JSON.parse(rawValue) as + | StorageValue + | number[] + | null + + if (Array.isArray(parsedValue)) { + return { + state: { + customPageSizeCache: normalizePageSizeOptions( + parsedValue.map((item) => Number(item)) + ), + }, + version: 0, + } + } + + if (!parsedValue || typeof parsedValue !== "object") return null + + return { + ...parsedValue, + state: normalizeCustomPageSizePersistedState(parsedValue.state), + } + } catch { + return null + } + }, + setItem: (name, value) => { + if (typeof window === "undefined") return + + try { + const normalizedValue: StorageValue = { + ...value, + state: normalizeCustomPageSizePersistedState(value.state), + } + + if (normalizedValue.state.customPageSizeCache.length === 0) { + window.localStorage.removeItem(name) + return + } + + window.localStorage.setItem(name, JSON.stringify(normalizedValue)) + } catch { + // Ignore persistence failures and keep the UI usable. + } + }, + removeItem: (name) => { + if (typeof window === "undefined") return + + try { + window.localStorage.removeItem(name) + } catch { + // Ignore persistence failures and keep the UI usable. + } + }, +} + +const useCustomPageSizeStore = create()( + persist( + (set) => ({ + customPageSizeCache: [], + appendCustomPageSize: (size) => + set((state) => ({ + customPageSizeCache: normalizePageSizeOptions([ + ...state.customPageSizeCache, + size, + ]), + })), + clearCustomPageSizeCache: () => set({ customPageSizeCache: [] }), + }), + { + name: CUSTOM_PAGE_SIZE_STORAGE_KEY, + storage: customPageSizeStorage, + partialize: (state) => ({ + customPageSizeCache: state.customPageSizeCache, + }), + } + ) +) + +export function usePersistentPageSizeOptions( + defaultOptions: number[], + currentPageSize?: number +) { + const normalizedDefaultOptions = useMemo( + () => normalizePageSizeOptions(defaultOptions), + [defaultOptions] + ) + const customPageSizeCache = useCustomPageSizeStore( + (state) => state.customPageSizeCache + ) + const appendCustomPageSize = useCustomPageSizeStore( + (state) => state.appendCustomPageSize + ) + const clearCustomPageSizeCache = useCustomPageSizeStore( + (state) => state.clearCustomPageSizeCache + ) + + useEffect(() => { + void useCustomPageSizeStore.persist.rehydrate() + + const handleStorage = (event: StorageEvent) => { + if (event.key !== CUSTOM_PAGE_SIZE_STORAGE_KEY) return + void useCustomPageSizeStore.persist.rehydrate() + } + + window.addEventListener("storage", handleStorage) + return () => { + window.removeEventListener("storage", handleStorage) + } + }, []) + + const persistedRecordsPerPageOptions = useMemo( + () => + normalizePageSizeOptions([ + ...normalizedDefaultOptions, + ...customPageSizeCache, + ]), + [customPageSizeCache, normalizedDefaultOptions] + ) + + const recordsPerPageOptions = useMemo(() => { + const nextOptions = [...persistedRecordsPerPageOptions] + if ( + typeof currentPageSize === "number" && + Number.isInteger(currentPageSize) && + currentPageSize > 0 + ) { + nextOptions.push(currentPageSize) + } + return normalizePageSizeOptions(nextOptions) + }, [currentPageSize, persistedRecordsPerPageOptions]) + + const addCustomPageSize = useCallback( + (size: number): AddCustomPageSizeResult => { + if (!Number.isInteger(size) || size <= 0) return "invalid" + if (persistedRecordsPerPageOptions.includes(size)) return "exists" + + appendCustomPageSize(size) + return "added" + }, + [appendCustomPageSize, persistedRecordsPerPageOptions] + ) + + return { + recordsPerPageOptions, + hasCustomPageSizeCache: customPageSizeCache.length > 0, + addCustomPageSize, + clearCustomPageSizeCache, + } +} + +type SettingCustomPageSizeControlProps = { + addCustomPageSize: (size: number) => AddCustomPageSizeResult + clearCustomPageSizeCache: () => void + hasCustomPageSizeCache: boolean + triggerButtonProps?: Omit + confirmButtonProps?: Omit + triggerLabel?: ReactNode +} + +export function SettingCustomPageSizeControl({ + addCustomPageSize, + clearCustomPageSizeCache, + hasCustomPageSizeCache, + triggerButtonProps, + confirmButtonProps, + triggerLabel = "自定义每页条数", +}: SettingCustomPageSizeControlProps) { + const [opened, setOpened] = useState(false) + const [customPageSizeInput, setCustomPageSizeInput] = useState("") + + const closeCustomPageSizeModal = useCallback(() => { + setOpened(false) + setCustomPageSizeInput("") + }, []) + + const handleAddCustomPageSize = useCallback(() => { + const rawValue = customPageSizeInput.trim() + if (!rawValue) return + + const size = Number(rawValue) + const result = addCustomPageSize(size) + + if (result === "invalid") { + notifications.show({ + color: "red", + title: "输入无效", + message: "请填写大于 0 的整数条数", + }) + return + } + + if (result === "exists") { + notifications.show({ + color: "blue", + title: "已存在", + message: `每页 ${size} 条已在可选项中`, + }) + return + } + + closeCustomPageSizeModal() + notifications.show({ + color: "green", + title: "添加成功", + message: `已新增每页 ${size} 条`, + }) + }, [addCustomPageSize, closeCustomPageSizeModal, customPageSizeInput]) + + const handleCustomPageSizeEnter = useCallback( + (event: KeyboardEvent) => { + if (event.key !== "Enter") return + event.preventDefault() + handleAddCustomPageSize() + }, + [handleAddCustomPageSize] + ) + + const handleClearCachedPageSizes = useCallback(() => { + clearCustomPageSizeCache() + closeCustomPageSizeModal() + notifications.show({ + color: "green", + title: "清除成功", + message: "已清除自定义每页条数缓存", + }) + }, [clearCustomPageSizeCache, closeCustomPageSizeModal]) + + return ( + <> + + + + + setCustomPageSizeInput(event.currentTarget.value) + } + onKeyDown={handleCustomPageSizeEnter} + /> + + 已添加的自定义条数会缓存在当前浏览器中,可随时清除。 + + + {hasCustomPageSizeCache ? ( + + ) : null} + + + + + + + + + ) +} + type SettingTableViewportProps = PropsWithChildren<{ maxHeight: number | string className?: string