"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, 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) { return classNames.filter(Boolean).join(" ") } export const settingModalClassNames = { content: classes.modalContent, header: classes.modalHeader, title: classes.modalTitle, body: classes.modalBody, } export const settingModalFormClassName = classes.modalForm 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} ) } export function SettingPage({ className, ...props }: PropsWithChildren) { return ( ) } export function SettingPanel({ className, withBorder = true, radius = "sm", p = "md", shadow, ...props }: PropsWithChildren) { return ( ) } export function SettingFilterPanel({ className, ...props }: PropsWithChildren) { return ( ) } export function SettingContentPanel({ className, ...props }: PropsWithChildren) { return ( ) } type SettingListHeaderProps = { title: ReactNode count?: ReactNode actions?: ReactNode className?: string } export function SettingListHeader({ title, count, actions, className, }: SettingListHeaderProps) { const isPlainCount = typeof count === "string" || typeof count === "number" return ( {title} {count !== undefined ? ( isPlainCount ? ( {count} ) : (
{count}
) ) : null}
{actions}
) } export function SettingHeaderActions({ className, gap = "sm", ...props }: PropsWithChildren) { return ( ) } export function SettingFilterActions({ className, gap = "sm", justify = "flex-end", mt = "md", ...props }: PropsWithChildren) { return ( ) } type SettingInlineFilterFieldProps = { label: ReactNode children: ReactNode className?: string labelClassName?: string controlClassName?: string labelWidth?: number | string } export function SettingInlineFilterField({ label, children, className, labelClassName, controlClassName, labelWidth = 80, }: SettingInlineFilterFieldProps) { const style = { "--setting-inline-filter-label-width": typeof labelWidth === "number" ? `${labelWidth}px` : labelWidth, } as CSSProperties return (
{label}
{children}
) } const settingDataTableStyles = { header: { backgroundColor: "#F7F8FA", borderBottom: "1px solid #E5E6EB", color: "#4E5969", fontSize: "14px", fontWeight: 500, }, root: { maxWidth: "100%", minWidth: 0, fontSize: "14px", color: "#1D2129", borderBottom: "1px solid #E5E6EB", borderRadius: 4, overflow: "hidden", boxSizing: "border-box", backgroundColor: "#FFFFFF", }, table: { backgroundColor: "#FFFFFF", }, footer: { backgroundColor: "#FFFFFF", }, pagination: { padding: "12px 16px", borderTop: "1px solid #E5E6EB", }, } as const const settingDataTableDefaultColumnProps: DataTableDefaultColumnProps< Record > = { titleStyle: { whiteSpace: "nowrap", fontWeight: 500, }, } export function SettingDataTable>( props: DataTableProps ) { const { styles, defaultColumnProps, withTableBorder, withRowBorders, striped, highlightOnHover, loaderBackgroundBlur, loadingText, noRecordsText, minHeight, maxHeight, height, page, onPageChange, totalRecords, recordsPerPage, recordsPerPageLabel, paginationText, scrollAreaProps, ...rest } = props const paginationProps = page !== undefined && onPageChange !== undefined && recordsPerPage !== undefined ? { page, onPageChange, totalRecords, recordsPerPage, loadingText: loadingText ?? "正在加载数据...", recordsPerPageLabel: recordsPerPageLabel ?? "每页条数", paginationText: paginationText ?? (({ from, to, totalRecords, }: { from: number to: number totalRecords: number }) => `显示 ${from}-${to} 条,共 ${totalRecords} 条记录`), } : {} const tableProps = { withTableBorder: withTableBorder ?? true, withRowBorders: withRowBorders ?? true, striped: striped ?? false, highlightOnHover: highlightOnHover ?? false, defaultColumnProps: defaultColumnProps ?? (settingDataTableDefaultColumnProps as DataTableDefaultColumnProps), loaderBackgroundBlur: loaderBackgroundBlur ?? 0, noRecordsText: noRecordsText ?? "暂无数据", minHeight: minHeight ?? 360, maxHeight, height: height ?? (maxHeight !== undefined ? "auto" : undefined), scrollAreaProps: { type: "auto", ...scrollAreaProps, }, styles: styles ? { ...settingDataTableStyles, ...styles } : settingDataTableStyles, ...paginationProps, ...rest, } as DataTableProps return } export const settingTabsStyles = { list: { padding: 4, backgroundColor: "#F7F8FA", border: "1px solid #E5E6EB", borderRadius: 4, gap: 8, }, tab: { minHeight: 32, borderRadius: 4, padding: "0 12px", fontSize: 14, fontWeight: 500, lineHeight: "20px", }, tabLabel: { color: "inherit", fontSize: 14, lineHeight: "20px", }, panel: { flex: 1, minHeight: 0, minWidth: 0, paddingTop: 16, }, } as const export const settingTabsClassNames = { list: classes.settingTabsList, tab: classes.settingTabsTab, tabLabel: classes.settingTabsTabLabel, panel: classes.settingTabsPanel, } as const