422 lines
8.8 KiB
TypeScript
422 lines
8.8 KiB
TypeScript
"use client"
|
|
|
|
import {
|
|
Group,
|
|
Paper,
|
|
Stack,
|
|
Text,
|
|
type GroupProps,
|
|
type PaperProps,
|
|
type StackProps,
|
|
} from "@mantine/core"
|
|
import {
|
|
DataTable,
|
|
type DataTableDefaultColumnProps,
|
|
type DataTableProps,
|
|
} from "mantine-datatable"
|
|
import type { CSSProperties, PropsWithChildren, ReactNode } from "react"
|
|
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
|
|
|
|
type SettingTableViewportProps = PropsWithChildren<{
|
|
maxHeight: number | string
|
|
className?: string
|
|
style?: CSSProperties
|
|
}>
|
|
|
|
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 SettingTableViewport({
|
|
maxHeight,
|
|
className,
|
|
style,
|
|
children,
|
|
}: SettingTableViewportProps) {
|
|
return (
|
|
<div
|
|
style={{ maxHeight, ...style }}
|
|
className={mergeClassName(classes.tableViewport, className)}>
|
|
{children}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
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 SettingSectionHeaderProps = GroupProps & {
|
|
eyebrow?: string
|
|
title: ReactNode
|
|
description?: ReactNode
|
|
actions?: ReactNode
|
|
}
|
|
|
|
export function SettingSectionHeader({
|
|
eyebrow,
|
|
title,
|
|
description,
|
|
actions,
|
|
className,
|
|
...props
|
|
}: SettingSectionHeaderProps) {
|
|
return (
|
|
<Group
|
|
justify="space-between"
|
|
align="flex-start"
|
|
gap="sm"
|
|
wrap="wrap"
|
|
className={mergeClassName(classes.sectionHeader, className)}
|
|
{...props}>
|
|
<Stack gap={2}>
|
|
{eyebrow ? (
|
|
<Text className={classes.sectionEyebrow}>{eyebrow}</Text>
|
|
) : null}
|
|
<Text className={classes.sectionTitle}>{title}</Text>
|
|
{description ? (
|
|
<Text className={classes.sectionDescription}>{description}</Text>
|
|
) : null}
|
|
</Stack>
|
|
{actions}
|
|
</Group>
|
|
)
|
|
}
|
|
|
|
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}
|
|
/>
|
|
)
|
|
}
|
|
|
|
export function SettingFilterStack({
|
|
className,
|
|
gap = 12,
|
|
...props
|
|
}: PropsWithChildren<StackProps>) {
|
|
return (
|
|
<Stack
|
|
gap={gap}
|
|
className={mergeClassName(classes.filterStack, 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
|