fix(daily): fix style

This commit is contained in:
zhangheng
2026-04-14 16:10:13 +08:00
parent f6af93df12
commit 86a4955c38
5 changed files with 843 additions and 278 deletions

View File

@@ -0,0 +1,81 @@
.modalContent {
border-radius: 16px;
overflow: hidden;
box-shadow: 0 24px 60px rgba(15, 23, 42, 0.16);
}
.modalHeader {
padding: 20px 24px 0;
background: #ffffff;
}
.modalTitle {
color: #1f2329;
font-size: 18px;
font-weight: 700;
line-height: 28px;
}
.modalBody {
padding: 16px 24px 24px;
background: #f8fafc;
}
.formSurface {
border-color: #e6ebf2;
background: linear-gradient(180deg, #ffffff 0%, #fcfdff 100%);
box-shadow: 0 12px 28px rgba(15, 23, 42, 0.05);
}
.formPanel :global(.mantine-InputWrapper-label) {
color: #1f2329;
font-size: 14px;
font-weight: 500;
line-height: 22px;
margin-bottom: 6px;
}
.formPanel :global(.mantine-Input-input),
.formPanel :global(.mantine-Select-input),
.formPanel :global(.mantine-NumberInput-input),
.formPanel :global(.mantine-Textarea-input) {
min-height: 36px;
border-color: #d0d5dd;
background: #ffffff;
color: #31373d;
font-size: 14px;
box-shadow: none;
transition:
border-color 0.15s ease,
box-shadow 0.15s ease,
background-color 0.15s ease;
}
.formPanel :global(.mantine-Input-input::placeholder),
.formPanel :global(.mantine-Select-input::placeholder),
.formPanel :global(.mantine-NumberInput-input::placeholder),
.formPanel :global(.mantine-Textarea-input::placeholder) {
color: #98a2b3;
}
.formPanel :global(.mantine-Input-input:focus),
.formPanel :global(.mantine-Select-input:focus),
.formPanel :global(.mantine-NumberInput-input:focus),
.formPanel :global(.mantine-Textarea-input:focus) {
border-color: #69c0ff;
box-shadow: 0 0 0 3px rgba(105, 192, 255, 0.14);
}
.formPanel :global(.mantine-Input-input[readonly]),
.formPanel :global(.mantine-NumberInput-input[readonly]) {
background: #f8fafc;
color: #667085;
}
.formPanel :global(.mantine-Button-root) {
min-height: 36px;
border-radius: 8px;
padding-inline: 14px;
font-size: 14px;
font-weight: 500;
}

View File

@@ -14,6 +14,7 @@ import {
Group, Group,
Modal, Modal,
NumberInput, NumberInput,
Paper,
Select, Select,
SimpleGrid, SimpleGrid,
Stack, Stack,
@@ -23,6 +24,7 @@ import {
import { useForm } from "@mantine/form" import { useForm } from "@mantine/form"
import { notifications } from "@mantine/notifications" import { notifications } from "@mantine/notifications"
import { useEffect, useMemo } from "react" import { useEffect, useMemo } from "react"
import classes from "./DailyReportModal.module.css"
import { import {
accountTypeOpts, accountTypeOpts,
actualTypeOpts, actualTypeOpts,
@@ -332,213 +334,181 @@ export default function DailyReportModal(props: {
title={record ? "编辑日报" : "填写日报"} title={record ? "编辑日报" : "填写日报"}
size={840} size={840}
centered centered
classNames={{
content: classes.modalContent,
header: classes.modalHeader,
title: classes.modalTitle,
body: classes.modalBody,
}}
overlayProps={{ backgroundOpacity: 0.22 }}
transitionProps={{
transition: "fade",
duration: 100,
timingFunction: "ease",
}}
closeOnClickOutside={false}> closeOnClickOutside={false}>
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<Stack gap="sm"> <Paper
<SimpleGrid cols={{ base: 1, sm: 3 }} spacing="sm"> withBorder
<TextInput radius="lg"
label="姓名" p="md"
size="xs" shadow="xs"
radius="xs" className={classes.formSurface}>
value={form.values.user_name} <Stack gap="md" className={classes.formPanel}>
readOnly <SimpleGrid cols={{ base: 1, sm: 3 }} spacing="sm">
/> <TextInput label="姓名" value={form.values.user_name} readOnly />
<TextInput <TextInput
label="日期" label="日期"
size="xs" type="date"
radius="xs" withAsterisk
type="date" {...form.getInputProps("date")}
withAsterisk />
{...form.getInputProps("date")} <div />
/>
<div />
<TextInput <TextInput
label="开始时间" label="开始时间"
size="xs" type="time"
radius="xs" {...form.getInputProps("start_time")}
type="time" />
{...form.getInputProps("start_time")} <TextInput
/> label="结束时间"
<TextInput type="time"
label="结束时间" {...form.getInputProps("end_time")}
size="xs" />
radius="xs" <div />
type="time"
{...form.getInputProps("end_time")}
/>
<div />
<Select <Select
label="规定类型" label="规定类型"
size="xs" withAsterisk
radius="xs" data={setTypeOpts}
withAsterisk searchable
data={setTypeOpts} clearable
searchable {...form.getInputProps("set_type")}
clearable />
{...form.getInputProps("set_type")} <Select
/> label="出勤类型"
<Select withAsterisk
label="出勤类型" data={actualTypeOpts}
size="xs" searchable
radius="xs" clearable
withAsterisk {...form.getInputProps("actual_type")}
data={actualTypeOpts} />
searchable <div />
clearable
{...form.getInputProps("actual_type")}
/>
<div />
<Select <Select
label="项目名称" label="项目名称"
size="xs" data={projectOptions}
radius="xs" searchable
data={projectOptions} clearable
searchable {...form.getInputProps("project_id")}
clearable />
{...form.getInputProps("project_id")} <Select
/> label="任务组长"
<Select data={leaderOptions}
label="任务组长" searchable
size="xs" clearable
radius="xs" {...form.getInputProps("leader_uid")}
data={leaderOptions} />
searchable <Select
clearable label="工作类别"
{...form.getInputProps("leader_uid")} data={workTypeOpts}
/> searchable
<Select clearable
label="工作类别" {...form.getInputProps("work_type")}
size="xs" />
radius="xs"
data={workTypeOpts}
searchable
clearable
{...form.getInputProps("work_type")}
/>
<Select <Select
label="对象类型" label="对象类型"
size="xs" data={objTypeOpts}
radius="xs" searchable
data={objTypeOpts} clearable
searchable {...form.getInputProps("obj_type")}
clearable />
{...form.getInputProps("obj_type")} <Select
/> label="绩效核算"
<Select data={performanceOpts}
label="绩效核算" searchable
size="xs" clearable
radius="xs" {...form.getInputProps("performance_accounting")}
data={performanceOpts} />
searchable <Select
clearable label="核算方式"
{...form.getInputProps("performance_accounting")} data={accountTypeOpts}
/> searchable
<Select clearable
label="核算方式" {...form.getInputProps("accounting_type")}
size="xs" />
radius="xs"
data={accountTypeOpts}
searchable
clearable
{...form.getInputProps("accounting_type")}
/>
<NumberInput <NumberInput
label="工时" label="工时"
size="xs" min={0}
radius="xs" decimalScale={1}
min={0} allowNegative={false}
decimalScale={1} hideControls
allowNegative={false} rightSection={<HourRightSection />}
hideControls rightSectionWidth={78}
rightSection={<HourRightSection />} {...form.getInputProps("work_time")}
rightSectionWidth={78} />
{...form.getInputProps("work_time")} <NumberInput
/> label="额定倍速"
<NumberInput min={0}
label="额定倍速" allowNegative={false}
size="xs" hideControls
radius="xs" {...form.getInputProps("rate")}
min={0} />
allowNegative={false} <div />
hideControls
{...form.getInputProps("rate")}
/>
<div />
<NumberInput <NumberInput
label="标准量" label="标准量"
size="xs" value={form.values.standard_size}
radius="xs" readOnly
value={form.values.standard_size} hideControls
readOnly />
hideControls <NumberInput
/> label="完成量"
<NumberInput min={0}
label="完成量" allowNegative={false}
size="xs" hideControls
radius="xs" {...form.getInputProps("completed_size")}
min={0} />
allowNegative={false} <div />
hideControls
{...form.getInputProps("completed_size")}
/>
<div />
<NumberInput <NumberInput
label="余量" label="余量"
size="xs" value={form.values.residual_size}
radius="xs" readOnly
value={form.values.residual_size} hideControls
readOnly />
hideControls <NumberInput
/> label="余量工时"
<NumberInput value={form.values.residual_work_time}
label="余量工时" readOnly
size="xs" hideControls
radius="xs" rightSection={<HourRightSection />}
value={form.values.residual_work_time} rightSectionWidth={78}
readOnly />
hideControls <TextInput
rightSection={<HourRightSection />} label="余量正负"
rightSectionWidth={78} value={form.values.residual_pm}
/> readOnly
<TextInput />
label="余量正负" </SimpleGrid>
size="xs"
radius="xs"
value={form.values.residual_pm}
readOnly
/>
</SimpleGrid>
<TextInput <TextInput label="备注" {...form.getInputProps("remarks")} />
label="备注"
size="xs"
radius="xs"
{...form.getInputProps("remarks")}
/>
<Group justify="flex-end" gap="sm"> <Group justify="flex-end" gap="sm">
<Button <Button
variant="default" variant="default"
size="xs" onClick={() => {
radius="xs" form.reset()
onClick={() => { onCloseAction()
form.reset() }}>
onCloseAction()
}}> </Button>
<Button type="submit"></Button>
</Button> </Group>
<Button size="xs" radius="xs" type="submit"> </Stack>
</Paper>
</Button>
</Group>
</Stack>
</form> </form>
</Modal> </Modal>
) )

View File

@@ -7,15 +7,24 @@ import {
import { Daily } from "@/components/label/api/daily/typing" import { Daily } from "@/components/label/api/daily/typing"
import { getSelfProjectList } from "@/components/label/api/project" import { getSelfProjectList } from "@/components/label/api/project"
import { useAllUserStore } from "@/components/label/store/auth" import { useAllUserStore } from "@/components/label/store/auth"
import {
SettingContentPanel,
SettingDataTable,
SettingFilterActions,
SettingFilterPanel,
SettingHeaderActions,
SettingListHeader,
SettingPage,
SettingPanel,
} from "@/components/setting/PageSurface"
import { import {
ActionIcon, ActionIcon,
Badge, Badge,
Button, Button,
Collapse, Collapse,
Flex,
Group, Group,
Modal,
MultiSelect, MultiSelect,
Paper,
SimpleGrid, SimpleGrid,
Stack, Stack,
Text, Text,
@@ -34,11 +43,7 @@ import {
} from "@tabler/icons-react" } from "@tabler/icons-react"
import dayjs from "dayjs" import dayjs from "dayjs"
import { import { DataTableColumn, DataTableSortStatus } from "mantine-datatable"
DataTable,
DataTableColumn,
DataTableSortStatus,
} from "mantine-datatable"
import { useCallback, useEffect, useMemo, useState } from "react" import { useCallback, useEffect, useMemo, useState } from "react"
import DailyReportModal from "./components/DailyReportModal" import DailyReportModal from "./components/DailyReportModal"
import { import {
@@ -105,6 +110,8 @@ function toNumber(value?: string) {
return Number.isFinite(n) ? n : undefined return Number.isFinite(n) ? n : undefined
} }
const DEFAULT_RECORDS_PER_PAGE_OPTIONS = [10, 15, 20, 50]
export default function PersonReportPage() { export default function PersonReportPage() {
const treeData = useAllUserStore((s) => s.treeData) const treeData = useAllUserStore((s) => s.treeData)
@@ -137,6 +144,12 @@ export default function PersonReportPage() {
const [page, setPage] = useState(1) const [page, setPage] = useState(1)
const [pageSize, setPageSize] = useState(20) const [pageSize, setPageSize] = useState(20)
const [recordsPerPageOptions, setRecordsPerPageOptions] = useState<number[]>(
() => [...DEFAULT_RECORDS_PER_PAGE_OPTIONS]
)
const [customPageSizeInput, setCustomPageSizeInput] = useState("")
const [customPageSizeModalOpened, setCustomPageSizeModalOpened] =
useState(false)
const [totalItems, setTotalItems] = useState(0) const [totalItems, setTotalItems] = useState(0)
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [records, setRecords] = useState<Daily.DailyWorkList[]>([]) const [records, setRecords] = useState<Daily.DailyWorkList[]>([])
@@ -229,6 +242,53 @@ 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<HTMLInputElement>) => {
if (event.key !== "Enter") return
event.preventDefault()
handleAddCustomPageSize()
},
[handleAddCustomPageSize]
)
const columns = useMemo(() => { const columns = useMemo(() => {
const cols: DataTableColumn<Daily.DailyWorkList>[] = [ const cols: DataTableColumn<Daily.DailyWorkList>[] = [
{ accessor: "id", title: "ID", width: 80, sortable: true }, { accessor: "id", title: "ID", width: 80, sortable: true },
@@ -429,21 +489,13 @@ export default function PersonReportPage() {
}, [asyncGetSelfProjectList]) }, [asyncGetSelfProjectList])
return ( return (
<Stack w="100%" h="calc(100vh - 56px)" p="md" gap="md"> <SettingPage>
<Paper <SettingFilterPanel>
withBorder
p="md"
radius="md"
style={{
borderColor:
"light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-4))",
}}>
<Stack gap="sm"> <Stack gap="sm">
<Group justify="space-between" wrap="wrap" gap="sm"> <Group justify="space-between" wrap="wrap" gap="sm">
<Group gap="xs"> <Group gap="xs">
<Text fw={700}></Text> <Text fw={700}></Text>
<Button <Button
size="xs"
variant="transparent" variant="transparent"
rightSection={ rightSection={
searchExpanded ? ( searchExpanded ? (
@@ -457,11 +509,9 @@ export default function PersonReportPage() {
</Button> </Button>
</Group> </Group>
<Group gap="sm"> <SettingFilterActions mt={0}>
<Button <Button
variant="default" variant="default"
size="xs"
radius="xs"
leftSection={<IconRefresh size={16} />} leftSection={<IconRefresh size={16} />}
onClick={() => { onClick={() => {
setFilters(defaultFilters) setFilters(defaultFilters)
@@ -472,8 +522,6 @@ export default function PersonReportPage() {
</Button> </Button>
<Button <Button
size="xs"
radius="xs"
leftSection={<IconSearch size={16} />} leftSection={<IconSearch size={16} />}
onClick={() => { onClick={() => {
setAppliedFilters(filters) setAppliedFilters(filters)
@@ -482,18 +530,12 @@ export default function PersonReportPage() {
}}> }}>
</Button> </Button>
</Group> </SettingFilterActions>
</Group> </Group>
<Collapse <Collapse in={searchExpanded} transitionDuration={150} animateOpacity>
in={searchExpanded}
transitionDuration={150}
transitionTimingFunction="ease"
animateOpacity>
<SimpleGrid cols={{ base: 1, sm: 2, md: 3, lg: 4 }} spacing="sm"> <SimpleGrid cols={{ base: 1, sm: 2, md: 3, lg: 4 }} spacing="sm">
<TextInput <TextInput
label="开始日期" label="开始日期"
size="xs"
radius="xs"
type="date" type="date"
value={filters.date_start} value={filters.date_start}
onChange={(e) => onChange={(e) =>
@@ -505,8 +547,6 @@ export default function PersonReportPage() {
/> />
<TextInput <TextInput
label="结束日期" label="结束日期"
size="xs"
radius="xs"
type="date" type="date"
value={filters.date_end} value={filters.date_end}
onChange={(e) => { onChange={(e) => {
@@ -515,8 +555,6 @@ export default function PersonReportPage() {
/> />
<MultiSelect <MultiSelect
label="项目名称" label="项目名称"
size="xs"
radius="xs"
data={projectOptions} data={projectOptions}
searchable searchable
clearable clearable
@@ -525,8 +563,6 @@ export default function PersonReportPage() {
/> />
<MultiSelect <MultiSelect
label="任务组长" label="任务组长"
size="xs"
radius="xs"
data={leaderOptions} data={leaderOptions}
searchable searchable
clearable clearable
@@ -535,8 +571,6 @@ export default function PersonReportPage() {
/> />
<MultiSelect <MultiSelect
label="规定类型" label="规定类型"
size="xs"
radius="xs"
data={setTypeOpts} data={setTypeOpts}
searchable searchable
clearable clearable
@@ -547,8 +581,6 @@ export default function PersonReportPage() {
/> />
<MultiSelect <MultiSelect
label="出勤类型" label="出勤类型"
size="xs"
radius="xs"
data={actualTypeOpts} data={actualTypeOpts}
searchable searchable
clearable clearable
@@ -559,8 +591,6 @@ export default function PersonReportPage() {
/> />
<MultiSelect <MultiSelect
label="工作类别" label="工作类别"
size="xs"
radius="xs"
data={workTypeOpts} data={workTypeOpts}
searchable searchable
clearable clearable
@@ -571,8 +601,6 @@ export default function PersonReportPage() {
/> />
<MultiSelect <MultiSelect
label="核算方式" label="核算方式"
size="xs"
radius="xs"
data={accountTypeOpts} data={accountTypeOpts}
searchable searchable
clearable clearable
@@ -583,8 +611,6 @@ export default function PersonReportPage() {
/> />
<MultiSelect <MultiSelect
label="绩效核算" label="绩效核算"
size="xs"
radius="xs"
data={performanceOpts} data={performanceOpts}
searchable searchable
clearable clearable
@@ -602,8 +628,6 @@ export default function PersonReportPage() {
/> />
<MultiSelect <MultiSelect
label="对象类型" label="对象类型"
size="xs"
radius="xs"
data={objTypeOpts} data={objTypeOpts}
searchable searchable
clearable clearable
@@ -615,42 +639,44 @@ export default function PersonReportPage() {
</SimpleGrid> </SimpleGrid>
</Collapse> </Collapse>
</Stack> </Stack>
</Paper> </SettingFilterPanel>
<Paper <SettingContentPanel
withBorder
p="md"
radius="md"
style={{ style={{
borderColor:
"light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-4))",
flex: 1, flex: 1,
minHeight: 0, minHeight: 0,
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
gap: 12, gap: 12,
}}> }}>
<SettingListHeader
title="我的日报"
count={`${totalItems}`}
actions={
<SettingHeaderActions>
<Button
leftSection={<IconPlus size={16} />}
onClick={() => {
setEditingRecord(null)
setModalOpened(true)
}}>
</Button>
<Button
variant="default"
leftSection={<IconRefresh size={16} />}
onClick={load}
loading={loading}>
</Button>
</SettingHeaderActions>
}
/>
<Stack gap={12} align="stretch" style={{ flex: 1, minHeight: 0 }}> <Stack gap={12} align="stretch" style={{ flex: 1, minHeight: 0 }}>
<Flex justify={"space-between"} align="center" w="100%"> <SettingDataTable<Daily.DailyWorkList>
<Button
leftSection={<IconPlus size={12} />}
size="xs"
radius={"xs"}
onClick={() => {
setEditingRecord(null)
setModalOpened(true)
}}>
</Button>
<ActionIcon onClick={load} variant="transparent">
<IconRefresh size={16} />
</ActionIcon>
</Flex>
<DataTable<Daily.DailyWorkList>
width="100%" width="100%"
style={{ width: "100%" }} minHeight={0}
withTableBorder style={{ width: "100%", flex: 1 }}
withRowBorders
pinFirstColumn pinFirstColumn
pinLastColumn pinLastColumn
scrollAreaProps={{ type: "auto" }} scrollAreaProps={{ type: "auto" }}
@@ -665,16 +691,35 @@ export default function PersonReportPage() {
setPageSize(v) setPageSize(v)
setPage(1) setPage(1)
}} }}
recordsPerPageOptions={[10, 15, 20, 50]} recordsPerPageOptions={recordsPerPageOptions}
noRecordsText="暂无数据"
recordsPerPageLabel="当前页数"
sortStatus={sortStatus} sortStatus={sortStatus}
onSortStatusChange={(s) => { onSortStatusChange={(s) => {
setSortStatus(s) setSortStatus(s)
setPage(1) setPage(1)
}} }}
renderPagination={({ Controls }) => (
<Group
justify="space-between"
align="center"
w="100%"
gap="sm"
wrap="wrap">
<Group gap="xs" align="center" wrap="wrap">
<Controls.Text />
<Controls.PageSizeSelector />
<Button
variant="light"
color="gray"
size="xs"
onClick={openCustomPageSizeModal}>
</Button>
</Group>
<Controls.Pagination />
</Group>
)}
/> />
<Paper withBorder radius="xs" p="sm"> <SettingPanel radius="md" p="sm">
<Group justify="space-between" wrap="wrap"> <Group justify="space-between" wrap="wrap">
<Text size="sm" fw={700}> <Text size="sm" fw={700}>
@@ -691,9 +736,9 @@ export default function PersonReportPage() {
</Text> </Text>
</Group> </Group>
</Group> </Group>
</Paper> </SettingPanel>
</Stack> </Stack>
</Paper> </SettingContentPanel>
<DailyReportModal <DailyReportModal
opened={modalOpened} opened={modalOpened}
@@ -705,6 +750,31 @@ export default function PersonReportPage() {
if (refresh) load() if (refresh) load()
}} }}
/> />
</Stack>
<Modal
opened={customPageSizeModalOpened}
onClose={closeCustomPageSizeModal}
title="添加每页条数"
centered>
<Stack gap="sm">
<TextInput
data-autofocus
label="每页条数"
placeholder="请输入大于 0 的整数,例如 99"
value={customPageSizeInput}
onChange={(event) =>
setCustomPageSizeInput(event.currentTarget.value)
}
onKeyDown={handleCustomPageSizeEnter}
/>
<Group justify="flex-end">
<Button variant="default" onClick={closeCustomPageSizeModal}>
</Button>
<Button onClick={handleAddCustomPageSize}></Button>
</Group>
</Stack>
</Modal>
</SettingPage>
) )
} }

View File

@@ -0,0 +1,128 @@
.page {
width: 100%;
height: calc(100vh - 56px);
padding: 16px;
gap: 16px;
}
.panel {
border-color: #e6ebf2;
border-radius: 12px;
background: #ffffff;
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.05);
}
.filterPanel {
position: relative;
}
.filterPanel :global(.mantine-InputWrapper-label) {
color: #1f2329;
font-size: 14px;
font-weight: 500;
line-height: 22px;
margin-bottom: 6px;
}
.filterPanel :global(.mantine-Input-input),
.filterPanel :global(.mantine-Select-input),
.filterPanel :global(.mantine-NumberInput-input),
.filterPanel :global(.mantine-PillsInput-input),
.filterPanel :global(.mantine-Textarea-input) {
min-height: 36px;
border-color: #d0d5dd;
background: #ffffff;
color: #31373d;
font-size: 14px;
box-shadow: none;
transition:
border-color 0.15s ease,
box-shadow 0.15s ease,
background-color 0.15s ease;
}
.filterPanel :global(.mantine-Textarea-input) {
min-height: 96px;
}
.filterPanel :global(.mantine-Input-input::placeholder),
.filterPanel :global(.mantine-Select-input::placeholder),
.filterPanel :global(.mantine-NumberInput-input::placeholder),
.filterPanel :global(.mantine-PillsInput-input::placeholder),
.filterPanel :global(.mantine-Textarea-input::placeholder) {
color: #98a2b3;
}
.filterPanel :global(.mantine-Input-input:focus),
.filterPanel :global(.mantine-Select-input:focus),
.filterPanel :global(.mantine-NumberInput-input:focus),
.filterPanel :global(.mantine-PillsInput-input:focus-within),
.filterPanel :global(.mantine-Textarea-input:focus) {
border-color: #69c0ff;
box-shadow: 0 0 0 3px rgba(105, 192, 255, 0.14);
}
.filterPanel :global(.mantine-Button-root[data-variant="transparent"]) {
color: #168de8;
}
.filterPanel :global(.mantine-Button-root:not([data-variant="transparent"])),
.headerActions :global(.mantine-Button-root) {
min-height: 36px;
border-radius: 8px;
padding-inline: 14px;
font-size: 14px;
font-weight: 500;
}
.filterPanel :global(.mantine-ActionIcon-root),
.headerActions :global(.mantine-ActionIcon-root) {
width: 36px;
height: 36px;
border-radius: 8px;
}
.filterPanel :global(.mantine-Pill-root) {
background: #eef7ff;
color: #168de8;
}
.contentPanel {
border-color: #e6ebf2;
}
.filterActions,
.headerActions {
gap: 12px;
}
.listHeader {
margin-bottom: 16px;
}
.sectionHeader {
padding-bottom: 12px;
border-bottom: 1px solid #eef2f6;
}
.sectionEyebrow {
color: #168de8;
font-size: 12px;
font-weight: 700;
line-height: 18px;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.sectionTitle {
color: #1f2329;
font-size: 18px;
font-weight: 700;
line-height: 28px;
}
.sectionDescription {
color: #667085;
font-size: 13px;
line-height: 20px;
}

View File

@@ -0,0 +1,316 @@
"use client"
import {
Badge,
Group,
Paper,
Stack,
Text,
type GroupProps,
type PaperProps,
type StackProps,
} from "@mantine/core"
import type { PropsWithChildren, ReactNode } from "react"
import {
DataTable,
type DataTableDefaultColumnProps,
type DataTableProps,
} from "mantine-datatable"
import classes from "./PageSurface.module.css"
function mergeClassName(...classNames: Array<string | undefined>) {
return classNames.filter(Boolean).join(" ")
}
export function SettingPage({
className,
...props
}: PropsWithChildren<StackProps>) {
return (
<Stack
w="100%"
h="calc(100vh - 56px)"
p="md"
gap="md"
className={mergeClassName(classes.page, className)}
{...props}
/>
)
}
export function SettingPanel({
className,
withBorder = true,
radius = "lg",
p = "md",
shadow = "xs",
...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 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 fw={700}>{title}</Text>
{count !== undefined ? (
<Badge variant="light" color="gray">
{count}
</Badge>
) : 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}
/>
)
}
const settingDataTableStyles = {
header: {
backgroundColor: "#F8F9FB",
borderBottom: "1px solid #EEF2F6",
color: "#56606A",
fontSize: "14px",
},
root: {
height: "100%",
maxWidth: "100%",
minWidth: 0,
fontSize: "14px",
color: "#31373D",
borderBottom: "1px solid #E8EDF3",
borderRadius: 10,
overflow: "hidden",
boxSizing: "border-box",
backgroundColor: "#FFFFFF",
},
pagination: {
padding: "14px 16px 18px",
},
} as const
const settingDataTableDefaultColumnProps: DataTableDefaultColumnProps<
Record<string, unknown>
> = {
titleStyle: {
whiteSpace: "nowrap",
fontWeight: 600,
},
}
export function SettingDataTable<T = Record<string, unknown>>(
props: DataTableProps<T>
) {
const {
styles,
defaultColumnProps,
withTableBorder,
withRowBorders,
striped,
highlightOnHover,
loaderBackgroundBlur,
loadingText,
noRecordsText,
minHeight,
page,
onPageChange,
totalRecords,
recordsPerPage,
recordsPerPageLabel,
paginationText,
...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 ?? true,
highlightOnHover: highlightOnHover ?? true,
defaultColumnProps:
defaultColumnProps ??
(settingDataTableDefaultColumnProps as DataTableDefaultColumnProps<T>),
loaderBackgroundBlur: loaderBackgroundBlur ?? 1,
noRecordsText: noRecordsText ?? "暂无数据",
minHeight: minHeight ?? 360,
styles: styles
? { ...settingDataTableStyles, ...styles }
: settingDataTableStyles,
...paginationProps,
...rest,
} as DataTableProps<T>
return <DataTable {...tableProps} />
}
export const settingTabsStyles = {
list: {
padding: 4,
backgroundColor: "#F8F9FB",
border: "1px solid #EEF2F6",
borderRadius: 10,
gap: 8,
},
tab: {
minHeight: 36,
borderRadius: 8,
color: "#56606A",
fontWeight: 600,
"&[data-active]": {
backgroundColor: "#FFFFFF",
color: "#1F2329",
boxShadow: "0 6px 18px rgba(15, 23, 42, 0.08)",
},
},
panel: {
flex: 1,
minHeight: 0,
minWidth: 0,
paddingTop: 16,
},
} as const