Files
labelmain-demo/components/setting/PageSurface.tsx
zhangheng fb3ffe3dfd refactor: remove unused dead-code exports flagged by react-doctor
Low-risk subset only — each verified to have zero external references
(tsc + eslint green); API clients, stores, icons and constants left
untouched.

- libs/util.ts: unexport `is` (still used internally by isNumber)
- components/tree-select/libs/util.ts: drop unused recursive `findLabel`
- app/person/workload/config.ts: drop unused `dimensionOpts`
- components/login/libs/session-cache.ts: drop unused `getCachedSession`
- components/setting/PageSurface.tsx: drop 3 unused layout components
  (SettingTableViewport, SettingSectionHeader, SettingFilterStack)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 12:39:27 +08:00

703 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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<string | undefined>) {
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<CustomPageSizePersistedState> = {
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<CustomPageSizePersistedState>
| 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<CustomPageSizePersistedState> = {
...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<CustomPageSizeStoreState>()(
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<ButtonProps, "children" | "onClick">
confirmButtonProps?: Omit<ButtonProps, "children" | "onClick">
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<HTMLInputElement>) => {
if (event.key !== "Enter") return
event.preventDefault()
handleAddCustomPageSize()
},
[handleAddCustomPageSize]
)
const handleClearCachedPageSizes = useCallback(() => {
clearCustomPageSizeCache()
closeCustomPageSizeModal()
notifications.show({
color: "green",
title: "清除成功",
message: "已清除自定义每页条数缓存",
})
}, [clearCustomPageSizeCache, closeCustomPageSizeModal])
return (
<>
<Button
variant="light"
color="gray"
size="xs"
onClick={() => setOpened(true)}
{...triggerButtonProps}>
{triggerLabel}
</Button>
<Modal
opened={opened}
onClose={closeCustomPageSizeModal}
title="添加每页条数"
classNames={settingModalClassNames}
centered>
<Stack gap={12} className={settingModalFormClassName}>
<TextInput
data-autofocus
label="每页条数"
placeholder="请输入大于 0 的整数,例如 99"
value={customPageSizeInput}
onChange={(event) =>
setCustomPageSizeInput(event.currentTarget.value)
}
onKeyDown={handleCustomPageSizeEnter}
/>
<Text className={settingModalMetaTextClassName}>
</Text>
<Group
justify={hasCustomPageSizeCache ? "space-between" : "flex-end"}
className={settingModalActionsClassName}
wrap="wrap">
{hasCustomPageSizeCache ? (
<Button
variant="subtle"
color="red"
onClick={handleClearCachedPageSizes}>
</Button>
) : null}
<Group gap={12}>
<Button variant="default" onClick={closeCustomPageSizeModal}>
</Button>
<Button onClick={handleAddCustomPageSize} {...confirmButtonProps}>
</Button>
</Group>
</Group>
</Stack>
</Modal>
</>
)
}
export function SettingPage({
className,
...props
}: PropsWithChildren<StackProps>) {
return (
<Stack
w="100%"
h="calc(100vh - 60px)"
p="md"
gap="md"
className={mergeClassName(classes.page, className)}
{...props}
/>
)
}
export function SettingPanel({
className,
withBorder = true,
radius = "sm",
p = "md",
shadow,
...props
}: PropsWithChildren<PaperProps>) {
return (
<Paper
withBorder={withBorder}
radius={radius}
p={p}
shadow={shadow}
className={mergeClassName(classes.panel, className)}
{...props}
/>
)
}
export function SettingFilterPanel({
className,
...props
}: PropsWithChildren<PaperProps>) {
return (
<SettingPanel
className={mergeClassName(classes.filterPanel, className)}
{...props}
/>
)
}
export function SettingContentPanel({
className,
...props
}: PropsWithChildren<PaperProps>) {
return (
<SettingPanel
className={mergeClassName(classes.contentPanel, className)}
{...props}
/>
)
}
type SettingListHeaderProps = {
title: ReactNode
count?: ReactNode
actions?: ReactNode
className?: string
}
export function SettingListHeader({
title,
count,
actions,
className,
}: SettingListHeaderProps) {
return (
<Group
justify="space-between"
align="center"
wrap="wrap"
gap="sm"
className={mergeClassName(classes.listHeader, className)}>
<Group gap="xs">
<Text className={classes.listHeaderTitle}>{title}</Text>
{count !== undefined ? (
<Text className={classes.listHeaderCount}>{count}</Text>
) : null}
</Group>
{actions}
</Group>
)
}
export function SettingHeaderActions({
className,
gap = "sm",
...props
}: PropsWithChildren<GroupProps>) {
return (
<Group
gap={gap}
wrap="wrap"
className={mergeClassName(classes.headerActions, className)}
{...props}
/>
)
}
export function SettingFilterActions({
className,
gap = "sm",
justify = "flex-end",
mt = "md",
...props
}: PropsWithChildren<GroupProps>) {
return (
<Group
justify={justify}
gap={gap}
wrap="wrap"
mt={mt}
className={mergeClassName(classes.filterActions, className)}
{...props}
/>
)
}
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 (
<div
style={style}
className={mergeClassName(classes.filterField, className)}>
<Text className={mergeClassName(classes.filterLabel, labelClassName)}>
{label}
</Text>
<div className={mergeClassName(classes.filterControl, controlClassName)}>
{children}
</div>
</div>
)
}
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<string, unknown>
> = {
titleStyle: {
whiteSpace: "nowrap",
fontWeight: 500,
},
}
export function SettingDataTable<T = Record<string, unknown>>(
props: DataTableProps<T>
) {
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<T>),
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<T>
return <DataTable {...tableProps} />
}
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