Compare commits
12 Commits
f6af93df12
...
4311cd67eb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4311cd67eb | ||
|
|
1211b79dc3 | ||
|
|
a4ccdc5589 | ||
|
|
1b74c23dac | ||
|
|
02efe5023d | ||
|
|
2b53e70116 | ||
|
|
c7980e2d17 | ||
|
|
0ca274bc50 | ||
|
|
2edc1f6b73 | ||
|
|
86a4955c38 | ||
|
|
15d23f0cc9 | ||
|
|
2ee893b0d6 |
@@ -103,7 +103,19 @@ export default function PersonalProjectTable() {
|
||||
title: "项目名称",
|
||||
width: 160,
|
||||
render: (record: Project.PersonProjectDashboardResponseItem) => {
|
||||
return record.project_name ?? "-"
|
||||
return (
|
||||
<Text
|
||||
title={record.project_name ?? "-"}
|
||||
style={{
|
||||
fontSize: "14px",
|
||||
maxWidth: "100%",
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
}}>
|
||||
{record.project_name ?? "-"}
|
||||
</Text>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -78,9 +78,21 @@ export default function PersonalTaskTable() {
|
||||
{
|
||||
accessor: "project_name",
|
||||
title: "项目名称",
|
||||
width: 150,
|
||||
width: 160,
|
||||
render: (record: Task.SimpleTaskItem) => {
|
||||
return <Text>{record.project_name}</Text>
|
||||
return (
|
||||
<Text
|
||||
title={record.project_name ?? "-"}
|
||||
style={{
|
||||
fontSize: "14px",
|
||||
maxWidth: "100%",
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
}}>
|
||||
{record.project_name ?? "-"}
|
||||
</Text>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
81
app/person/report/components/DailyReportModal.module.css
Normal file
81
app/person/report/components/DailyReportModal.module.css
Normal 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;
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
Group,
|
||||
Modal,
|
||||
NumberInput,
|
||||
Paper,
|
||||
Select,
|
||||
SimpleGrid,
|
||||
Stack,
|
||||
@@ -31,6 +32,7 @@ import {
|
||||
setTypeOpts,
|
||||
workTypeOpts,
|
||||
} from "../config"
|
||||
import classes from "./DailyReportModal.module.css"
|
||||
|
||||
function normalizeTimeValue(value?: string | null) {
|
||||
if (!value) return ""
|
||||
@@ -86,6 +88,85 @@ function HourRightSection() {
|
||||
)
|
||||
}
|
||||
|
||||
function getEmptyFormValues(currentUserName?: string) {
|
||||
return {
|
||||
user_name: currentUserName ?? "",
|
||||
date: "",
|
||||
start_time: "",
|
||||
end_time: "",
|
||||
set_type: "",
|
||||
actual_type: "",
|
||||
project_id: "",
|
||||
leader_uid: "",
|
||||
work_type: "",
|
||||
accounting_type: "",
|
||||
performance_accounting: "",
|
||||
obj_type: "",
|
||||
work_time: undefined as number | undefined,
|
||||
rate: undefined as number | undefined,
|
||||
standard_size: undefined as number | undefined,
|
||||
completed_size: undefined as number | undefined,
|
||||
residual_size: undefined as number | undefined,
|
||||
residual_work_time: undefined as number | undefined,
|
||||
residual_pm: "",
|
||||
remarks: "",
|
||||
}
|
||||
}
|
||||
|
||||
function getCreateFormValues(currentUserName?: string) {
|
||||
return {
|
||||
...getEmptyFormValues(currentUserName),
|
||||
date: new Date().toISOString().slice(0, 10),
|
||||
}
|
||||
}
|
||||
|
||||
function getRecordFormValues(
|
||||
record: Daily.DailyWorkList,
|
||||
currentUserName?: string
|
||||
) {
|
||||
return {
|
||||
user_name: currentUserName ?? "",
|
||||
date: record.date ?? "",
|
||||
start_time: normalizeTimeValue(record.start_time),
|
||||
end_time: normalizeTimeValue(record.end_time),
|
||||
set_type: record.set_type ?? "",
|
||||
actual_type: record.actual_type ?? "",
|
||||
project_id:
|
||||
record.project_id === undefined || record.project_id === null
|
||||
? ""
|
||||
: String(record.project_id),
|
||||
leader_uid:
|
||||
record.leader_uid === undefined || record.leader_uid === null
|
||||
? ""
|
||||
: String(record.leader_uid),
|
||||
work_type: record.work_type ?? "",
|
||||
accounting_type: record.accounting_type ?? "",
|
||||
performance_accounting:
|
||||
record.performance_accounting === true
|
||||
? "true"
|
||||
: record.performance_accounting === false
|
||||
? "false"
|
||||
: "",
|
||||
obj_type:
|
||||
record.obj_type === undefined || record.obj_type === null
|
||||
? ""
|
||||
: String(record.obj_type),
|
||||
work_time: record.work_time ?? undefined,
|
||||
rate: record.rate ?? undefined,
|
||||
standard_size: record.standard_size ?? undefined,
|
||||
completed_size: record.completed_size ?? undefined,
|
||||
residual_size: record.residual_size ?? undefined,
|
||||
residual_work_time: record.residual_work_time ?? undefined,
|
||||
residual_pm:
|
||||
record.residual_pm === true
|
||||
? "正"
|
||||
: record.residual_pm === false
|
||||
? "负"
|
||||
: "",
|
||||
remarks: record.remarks ?? "",
|
||||
}
|
||||
}
|
||||
|
||||
export default function DailyReportModal(props: {
|
||||
opened: boolean
|
||||
record: Daily.DailyWorkList | null
|
||||
@@ -103,31 +184,10 @@ export default function DailyReportModal(props: {
|
||||
}, [treeData])
|
||||
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
user_name: user_name ?? "",
|
||||
date: "",
|
||||
start_time: "",
|
||||
end_time: "",
|
||||
set_type: "",
|
||||
actual_type: "",
|
||||
project_id: "",
|
||||
leader_uid: "",
|
||||
work_type: "",
|
||||
accounting_type: "",
|
||||
performance_accounting: "",
|
||||
obj_type: "",
|
||||
work_time: undefined as number | undefined,
|
||||
rate: undefined as number | undefined,
|
||||
standard_size: undefined as number | undefined,
|
||||
completed_size: undefined as number | undefined,
|
||||
residual_size: undefined as number | undefined,
|
||||
residual_work_time: undefined as number | undefined,
|
||||
residual_pm: "",
|
||||
remarks: "",
|
||||
},
|
||||
initialValues: getEmptyFormValues(user_name),
|
||||
validate: {
|
||||
date: (v) => (v ? null : "请输入日期"),
|
||||
// set_type: (v) => (v ? null : "请选择规定类型"),
|
||||
set_type: (v) => (v ? null : "请选择规定类型"),
|
||||
actual_type: (v) => (v ? null : "请选择出勤类型"),
|
||||
},
|
||||
validateInputOnChange: true,
|
||||
@@ -135,77 +195,27 @@ export default function DailyReportModal(props: {
|
||||
clearInputErrorOnChange: true,
|
||||
})
|
||||
|
||||
const resetFormState = () => {
|
||||
form.setValues(getEmptyFormValues(user_name))
|
||||
form.clearErrors()
|
||||
form.resetDirty()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!opened) return
|
||||
if (!opened) {
|
||||
resetFormState()
|
||||
return
|
||||
}
|
||||
|
||||
if (!record) {
|
||||
form.setValues({
|
||||
user_name: user_name ?? "",
|
||||
date: new Date().toISOString().slice(0, 10),
|
||||
start_time: "",
|
||||
end_time: "",
|
||||
set_type: "",
|
||||
actual_type: "",
|
||||
project_id: "",
|
||||
leader_uid: "",
|
||||
work_type: "",
|
||||
accounting_type: "",
|
||||
performance_accounting: "",
|
||||
obj_type: "",
|
||||
work_time: undefined,
|
||||
rate: undefined,
|
||||
standard_size: undefined,
|
||||
completed_size: undefined,
|
||||
residual_size: undefined,
|
||||
residual_work_time: undefined,
|
||||
residual_pm: "",
|
||||
remarks: "",
|
||||
})
|
||||
form.setValues(getCreateFormValues(user_name))
|
||||
form.clearErrors()
|
||||
form.resetDirty()
|
||||
return
|
||||
}
|
||||
|
||||
form.setValues({
|
||||
user_name: user_name ?? "",
|
||||
date: record.date ?? "",
|
||||
start_time: normalizeTimeValue(record.start_time),
|
||||
end_time: normalizeTimeValue(record.end_time),
|
||||
set_type: record.set_type ?? "",
|
||||
actual_type: record.actual_type ?? "",
|
||||
project_id:
|
||||
record.project_id === undefined || record.project_id === null
|
||||
? ""
|
||||
: String(record.project_id),
|
||||
leader_uid:
|
||||
record.leader_uid === undefined || record.leader_uid === null
|
||||
? ""
|
||||
: String(record.leader_uid),
|
||||
work_type: record.work_type ?? "",
|
||||
accounting_type: record.accounting_type ?? "",
|
||||
performance_accounting:
|
||||
record.performance_accounting === true
|
||||
? "true"
|
||||
: record.performance_accounting === false
|
||||
? "false"
|
||||
: "",
|
||||
obj_type:
|
||||
record.obj_type === undefined || record.obj_type === null
|
||||
? ""
|
||||
: String(record.obj_type),
|
||||
work_time: record.work_time ?? undefined,
|
||||
rate: record.rate ?? undefined,
|
||||
standard_size: record.standard_size ?? undefined,
|
||||
completed_size: record.completed_size ?? undefined,
|
||||
residual_size: record.residual_size ?? undefined,
|
||||
residual_work_time: record.residual_work_time ?? undefined,
|
||||
residual_pm:
|
||||
record.residual_pm === true
|
||||
? "正"
|
||||
: record.residual_pm === false
|
||||
? "负"
|
||||
: "",
|
||||
remarks: record.remarks ?? "",
|
||||
})
|
||||
form.setValues(getRecordFormValues(record, user_name))
|
||||
form.clearErrors()
|
||||
form.resetDirty()
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [opened, record, user_name])
|
||||
@@ -280,12 +290,12 @@ export default function DailyReportModal(props: {
|
||||
? false
|
||||
: undefined,
|
||||
obj_type: values.obj_type ? Number(values.obj_type) : undefined,
|
||||
work_time: values.work_time,
|
||||
rate: values.rate,
|
||||
standard_size: values.standard_size,
|
||||
completed_size: values.completed_size,
|
||||
residual_size: values.residual_size,
|
||||
residual_work_time: values.residual_work_time,
|
||||
work_time: values.work_time || undefined,
|
||||
rate: values.rate || undefined,
|
||||
standard_size: values.standard_size || undefined,
|
||||
completed_size: values.completed_size || undefined,
|
||||
residual_size: values.residual_size || undefined,
|
||||
residual_work_time: values.residual_work_time || undefined,
|
||||
residual_pm:
|
||||
values.residual_pm === "正"
|
||||
? true
|
||||
@@ -311,8 +321,8 @@ export default function DailyReportModal(props: {
|
||||
message: "已提交日报",
|
||||
})
|
||||
}
|
||||
resetFormState()
|
||||
onCloseAction(true)
|
||||
form.reset()
|
||||
} catch (e) {
|
||||
notifications.show({
|
||||
color: "red",
|
||||
@@ -326,219 +336,219 @@ export default function DailyReportModal(props: {
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={() => {
|
||||
form.reset()
|
||||
resetFormState()
|
||||
onCloseAction()
|
||||
}}
|
||||
title={record ? "编辑日报" : "填写日报"}
|
||||
size={840}
|
||||
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}>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Stack gap="sm">
|
||||
<SimpleGrid cols={{ base: 1, sm: 3 }} spacing="sm">
|
||||
<TextInput
|
||||
label="姓名"
|
||||
size="xs"
|
||||
radius="xs"
|
||||
value={form.values.user_name}
|
||||
readOnly
|
||||
/>
|
||||
<TextInput
|
||||
label="日期"
|
||||
size="xs"
|
||||
radius="xs"
|
||||
type="date"
|
||||
withAsterisk
|
||||
{...form.getInputProps("date")}
|
||||
/>
|
||||
<div />
|
||||
<Paper
|
||||
withBorder
|
||||
radius="lg"
|
||||
p="md"
|
||||
shadow="xs"
|
||||
className={classes.formSurface}>
|
||||
<Stack gap="md" className={classes.formPanel}>
|
||||
<SimpleGrid cols={{ base: 1, sm: 3 }} spacing="sm">
|
||||
<TextInput label="姓名" value={form.values.user_name} readOnly />
|
||||
<TextInput
|
||||
label="日期"
|
||||
type="date"
|
||||
withAsterisk
|
||||
{...form.getInputProps("date")}
|
||||
/>
|
||||
<div />
|
||||
|
||||
<TextInput
|
||||
label="开始时间"
|
||||
size="xs"
|
||||
radius="xs"
|
||||
type="time"
|
||||
{...form.getInputProps("start_time")}
|
||||
/>
|
||||
<TextInput
|
||||
label="结束时间"
|
||||
size="xs"
|
||||
radius="xs"
|
||||
type="time"
|
||||
{...form.getInputProps("end_time")}
|
||||
/>
|
||||
<div />
|
||||
<TextInput
|
||||
label="开始时间"
|
||||
type="time"
|
||||
{...form.getInputProps("start_time")}
|
||||
/>
|
||||
<TextInput
|
||||
label="结束时间"
|
||||
type="time"
|
||||
{...form.getInputProps("end_time")}
|
||||
/>
|
||||
<div />
|
||||
|
||||
<Select
|
||||
label="规定类型"
|
||||
size="xs"
|
||||
radius="xs"
|
||||
withAsterisk
|
||||
data={setTypeOpts}
|
||||
searchable
|
||||
clearable
|
||||
{...form.getInputProps("set_type")}
|
||||
/>
|
||||
<Select
|
||||
label="出勤类型"
|
||||
size="xs"
|
||||
radius="xs"
|
||||
withAsterisk
|
||||
data={actualTypeOpts}
|
||||
searchable
|
||||
clearable
|
||||
{...form.getInputProps("actual_type")}
|
||||
/>
|
||||
<div />
|
||||
<Select
|
||||
label="规定类型"
|
||||
withAsterisk
|
||||
data={setTypeOpts}
|
||||
searchable
|
||||
clearable
|
||||
{...form.getInputProps("set_type")}
|
||||
value={form.values.set_type || null}
|
||||
onChange={(value) =>
|
||||
form.setFieldValue("set_type", value ?? "")
|
||||
}
|
||||
/>
|
||||
<Select
|
||||
label="出勤类型"
|
||||
withAsterisk
|
||||
data={actualTypeOpts}
|
||||
searchable
|
||||
clearable
|
||||
{...form.getInputProps("actual_type")}
|
||||
value={form.values.actual_type || null}
|
||||
onChange={(value) =>
|
||||
form.setFieldValue("actual_type", value ?? "")
|
||||
}
|
||||
/>
|
||||
<div />
|
||||
|
||||
<Select
|
||||
label="项目名称"
|
||||
size="xs"
|
||||
radius="xs"
|
||||
data={projectOptions}
|
||||
searchable
|
||||
clearable
|
||||
{...form.getInputProps("project_id")}
|
||||
/>
|
||||
<Select
|
||||
label="任务组长"
|
||||
size="xs"
|
||||
radius="xs"
|
||||
data={leaderOptions}
|
||||
searchable
|
||||
clearable
|
||||
{...form.getInputProps("leader_uid")}
|
||||
/>
|
||||
<Select
|
||||
label="工作类别"
|
||||
size="xs"
|
||||
radius="xs"
|
||||
data={workTypeOpts}
|
||||
searchable
|
||||
clearable
|
||||
{...form.getInputProps("work_type")}
|
||||
/>
|
||||
<Select
|
||||
label="项目名称"
|
||||
data={projectOptions}
|
||||
searchable
|
||||
clearable
|
||||
{...form.getInputProps("project_id")}
|
||||
value={form.values.project_id || null}
|
||||
onChange={(value) =>
|
||||
form.setFieldValue("project_id", value ?? "")
|
||||
}
|
||||
/>
|
||||
<Select
|
||||
label="任务组长"
|
||||
data={leaderOptions}
|
||||
searchable
|
||||
clearable
|
||||
{...form.getInputProps("leader_uid")}
|
||||
value={form.values.leader_uid || null}
|
||||
onChange={(value) =>
|
||||
form.setFieldValue("leader_uid", value ?? "")
|
||||
}
|
||||
/>
|
||||
<Select
|
||||
label="工作类别"
|
||||
data={workTypeOpts}
|
||||
searchable
|
||||
clearable
|
||||
{...form.getInputProps("work_type")}
|
||||
value={form.values.work_type || null}
|
||||
onChange={(value) =>
|
||||
form.setFieldValue("work_type", value ?? "")
|
||||
}
|
||||
/>
|
||||
|
||||
<Select
|
||||
label="对象类型"
|
||||
size="xs"
|
||||
radius="xs"
|
||||
data={objTypeOpts}
|
||||
searchable
|
||||
clearable
|
||||
{...form.getInputProps("obj_type")}
|
||||
/>
|
||||
<Select
|
||||
label="绩效核算"
|
||||
size="xs"
|
||||
radius="xs"
|
||||
data={performanceOpts}
|
||||
searchable
|
||||
clearable
|
||||
{...form.getInputProps("performance_accounting")}
|
||||
/>
|
||||
<Select
|
||||
label="核算方式"
|
||||
size="xs"
|
||||
radius="xs"
|
||||
data={accountTypeOpts}
|
||||
searchable
|
||||
clearable
|
||||
{...form.getInputProps("accounting_type")}
|
||||
/>
|
||||
<Select
|
||||
label="对象类型"
|
||||
data={objTypeOpts}
|
||||
searchable
|
||||
clearable
|
||||
{...form.getInputProps("obj_type")}
|
||||
value={form.values.obj_type || null}
|
||||
onChange={(value) =>
|
||||
form.setFieldValue("obj_type", value ?? "")
|
||||
}
|
||||
/>
|
||||
<Select
|
||||
label="绩效核算"
|
||||
data={performanceOpts}
|
||||
searchable
|
||||
clearable
|
||||
{...form.getInputProps("performance_accounting")}
|
||||
value={form.values.performance_accounting || null}
|
||||
onChange={(value) =>
|
||||
form.setFieldValue("performance_accounting", value ?? "")
|
||||
}
|
||||
/>
|
||||
<Select
|
||||
label="核算方式"
|
||||
data={accountTypeOpts}
|
||||
searchable
|
||||
clearable
|
||||
{...form.getInputProps("accounting_type")}
|
||||
value={form.values.accounting_type || null}
|
||||
onChange={(value) =>
|
||||
form.setFieldValue("accounting_type", value ?? "")
|
||||
}
|
||||
/>
|
||||
|
||||
<NumberInput
|
||||
label="工时"
|
||||
size="xs"
|
||||
radius="xs"
|
||||
min={0}
|
||||
decimalScale={1}
|
||||
allowNegative={false}
|
||||
hideControls
|
||||
rightSection={<HourRightSection />}
|
||||
rightSectionWidth={78}
|
||||
{...form.getInputProps("work_time")}
|
||||
/>
|
||||
<NumberInput
|
||||
label="额定倍速"
|
||||
size="xs"
|
||||
radius="xs"
|
||||
min={0}
|
||||
allowNegative={false}
|
||||
hideControls
|
||||
{...form.getInputProps("rate")}
|
||||
/>
|
||||
<div />
|
||||
<NumberInput
|
||||
label="工时"
|
||||
min={0}
|
||||
decimalScale={1}
|
||||
allowNegative={false}
|
||||
hideControls
|
||||
rightSection={<HourRightSection />}
|
||||
rightSectionWidth={78}
|
||||
{...form.getInputProps("work_time")}
|
||||
/>
|
||||
<NumberInput
|
||||
label="额定倍速"
|
||||
min={0}
|
||||
allowNegative={false}
|
||||
hideControls
|
||||
{...form.getInputProps("rate")}
|
||||
/>
|
||||
<div />
|
||||
|
||||
<NumberInput
|
||||
label="标准量"
|
||||
size="xs"
|
||||
radius="xs"
|
||||
value={form.values.standard_size}
|
||||
readOnly
|
||||
hideControls
|
||||
/>
|
||||
<NumberInput
|
||||
label="完成量"
|
||||
size="xs"
|
||||
radius="xs"
|
||||
min={0}
|
||||
allowNegative={false}
|
||||
hideControls
|
||||
{...form.getInputProps("completed_size")}
|
||||
/>
|
||||
<div />
|
||||
<NumberInput
|
||||
label="标准量"
|
||||
value={form.values.standard_size}
|
||||
readOnly
|
||||
hideControls
|
||||
/>
|
||||
<NumberInput
|
||||
label="完成量"
|
||||
min={0}
|
||||
allowNegative={false}
|
||||
hideControls
|
||||
{...form.getInputProps("completed_size")}
|
||||
/>
|
||||
<div />
|
||||
|
||||
<NumberInput
|
||||
label="余量"
|
||||
size="xs"
|
||||
radius="xs"
|
||||
value={form.values.residual_size}
|
||||
readOnly
|
||||
hideControls
|
||||
/>
|
||||
<NumberInput
|
||||
label="余量工时"
|
||||
size="xs"
|
||||
radius="xs"
|
||||
value={form.values.residual_work_time}
|
||||
readOnly
|
||||
hideControls
|
||||
rightSection={<HourRightSection />}
|
||||
rightSectionWidth={78}
|
||||
/>
|
||||
<TextInput
|
||||
label="余量正负"
|
||||
size="xs"
|
||||
radius="xs"
|
||||
value={form.values.residual_pm}
|
||||
readOnly
|
||||
/>
|
||||
</SimpleGrid>
|
||||
<NumberInput
|
||||
label="余量"
|
||||
value={form.values.residual_size}
|
||||
readOnly
|
||||
hideControls
|
||||
/>
|
||||
<NumberInput
|
||||
label="余量工时"
|
||||
value={form.values.residual_work_time}
|
||||
readOnly
|
||||
hideControls
|
||||
rightSection={<HourRightSection />}
|
||||
rightSectionWidth={78}
|
||||
/>
|
||||
<TextInput
|
||||
label="余量正负"
|
||||
value={form.values.residual_pm}
|
||||
readOnly
|
||||
/>
|
||||
</SimpleGrid>
|
||||
|
||||
<TextInput
|
||||
label="备注"
|
||||
size="xs"
|
||||
radius="xs"
|
||||
{...form.getInputProps("remarks")}
|
||||
/>
|
||||
<TextInput label="备注" {...form.getInputProps("remarks")} />
|
||||
|
||||
<Group justify="flex-end" gap="sm">
|
||||
<Button
|
||||
variant="default"
|
||||
size="xs"
|
||||
radius="xs"
|
||||
onClick={() => {
|
||||
form.reset()
|
||||
onCloseAction()
|
||||
}}>
|
||||
取消
|
||||
</Button>
|
||||
<Button size="xs" radius="xs" type="submit">
|
||||
提交
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
<Group justify="flex-end" gap="sm">
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={() => {
|
||||
resetFormState()
|
||||
onCloseAction()
|
||||
}}>
|
||||
取消
|
||||
</Button>
|
||||
<Button type="submit">提交</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</form>
|
||||
</Modal>
|
||||
)
|
||||
|
||||
@@ -7,15 +7,25 @@ import {
|
||||
import { Daily } from "@/components/label/api/daily/typing"
|
||||
import { getSelfProjectList } from "@/components/label/api/project"
|
||||
import { useAllUserStore } from "@/components/label/store/auth"
|
||||
import {
|
||||
SettingContentPanel,
|
||||
SettingDataTable,
|
||||
SettingFilterActions,
|
||||
SettingFilterPanel,
|
||||
SettingHeaderActions,
|
||||
SettingListHeader,
|
||||
SettingPage,
|
||||
SettingPanel,
|
||||
} from "@/components/setting/PageSurface"
|
||||
import {
|
||||
ActionIcon,
|
||||
Badge,
|
||||
Button,
|
||||
Collapse,
|
||||
Flex,
|
||||
Group,
|
||||
Modal,
|
||||
MultiSelect,
|
||||
Paper,
|
||||
Select,
|
||||
SimpleGrid,
|
||||
Stack,
|
||||
Text,
|
||||
@@ -34,11 +44,7 @@ import {
|
||||
} from "@tabler/icons-react"
|
||||
|
||||
import dayjs from "dayjs"
|
||||
import {
|
||||
DataTable,
|
||||
DataTableColumn,
|
||||
DataTableSortStatus,
|
||||
} from "mantine-datatable"
|
||||
import { DataTableColumn, DataTableSortStatus } from "mantine-datatable"
|
||||
import { useCallback, useEffect, useMemo, useState } from "react"
|
||||
import DailyReportModal from "./components/DailyReportModal"
|
||||
import {
|
||||
@@ -105,6 +111,8 @@ function toNumber(value?: string) {
|
||||
return Number.isFinite(n) ? n : undefined
|
||||
}
|
||||
|
||||
const DEFAULT_RECORDS_PER_PAGE_OPTIONS = [10, 15, 20, 50]
|
||||
|
||||
export default function PersonReportPage() {
|
||||
const treeData = useAllUserStore((s) => s.treeData)
|
||||
|
||||
@@ -137,6 +145,12 @@ export default function PersonReportPage() {
|
||||
|
||||
const [page, setPage] = useState(1)
|
||||
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 [loading, setLoading] = useState(false)
|
||||
const [records, setRecords] = useState<Daily.DailyWorkList[]>([])
|
||||
@@ -229,6 +243,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 cols: DataTableColumn<Daily.DailyWorkList>[] = [
|
||||
{ accessor: "id", title: "ID", width: 80, sortable: true },
|
||||
@@ -429,21 +490,13 @@ export default function PersonReportPage() {
|
||||
}, [asyncGetSelfProjectList])
|
||||
|
||||
return (
|
||||
<Stack w="100%" h="calc(100vh - 56px)" p="md" gap="md">
|
||||
<Paper
|
||||
withBorder
|
||||
p="md"
|
||||
radius="md"
|
||||
style={{
|
||||
borderColor:
|
||||
"light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-4))",
|
||||
}}>
|
||||
<SettingPage>
|
||||
<SettingFilterPanel>
|
||||
<Stack gap="sm">
|
||||
<Group justify="space-between" wrap="wrap" gap="sm">
|
||||
<Group gap="xs">
|
||||
<Text fw={700}>搜索条件</Text>
|
||||
<Text fw={700}>筛选条件</Text>
|
||||
<Button
|
||||
size="xs"
|
||||
variant="transparent"
|
||||
rightSection={
|
||||
searchExpanded ? (
|
||||
@@ -457,11 +510,9 @@ export default function PersonReportPage() {
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
<Group gap="sm">
|
||||
<SettingFilterActions mt={0}>
|
||||
<Button
|
||||
variant="default"
|
||||
size="xs"
|
||||
radius="xs"
|
||||
leftSection={<IconRefresh size={16} />}
|
||||
onClick={() => {
|
||||
setFilters(defaultFilters)
|
||||
@@ -472,8 +523,6 @@ export default function PersonReportPage() {
|
||||
重置
|
||||
</Button>
|
||||
<Button
|
||||
size="xs"
|
||||
radius="xs"
|
||||
leftSection={<IconSearch size={16} />}
|
||||
onClick={() => {
|
||||
setAppliedFilters(filters)
|
||||
@@ -482,18 +531,12 @@ export default function PersonReportPage() {
|
||||
}}>
|
||||
查询
|
||||
</Button>
|
||||
</Group>
|
||||
</SettingFilterActions>
|
||||
</Group>
|
||||
<Collapse
|
||||
in={searchExpanded}
|
||||
transitionDuration={150}
|
||||
transitionTimingFunction="ease"
|
||||
animateOpacity>
|
||||
<Collapse in={searchExpanded} transitionDuration={150} animateOpacity>
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, md: 3, lg: 4 }} spacing="sm">
|
||||
<TextInput
|
||||
label="开始日期"
|
||||
size="xs"
|
||||
radius="xs"
|
||||
type="date"
|
||||
value={filters.date_start}
|
||||
onChange={(e) =>
|
||||
@@ -505,8 +548,6 @@ export default function PersonReportPage() {
|
||||
/>
|
||||
<TextInput
|
||||
label="结束日期"
|
||||
size="xs"
|
||||
radius="xs"
|
||||
type="date"
|
||||
value={filters.date_end}
|
||||
onChange={(e) => {
|
||||
@@ -515,8 +556,6 @@ export default function PersonReportPage() {
|
||||
/>
|
||||
<MultiSelect
|
||||
label="项目名称"
|
||||
size="xs"
|
||||
radius="xs"
|
||||
data={projectOptions}
|
||||
searchable
|
||||
clearable
|
||||
@@ -525,132 +564,116 @@ export default function PersonReportPage() {
|
||||
/>
|
||||
<MultiSelect
|
||||
label="任务组长"
|
||||
size="xs"
|
||||
radius="xs"
|
||||
data={leaderOptions}
|
||||
searchable
|
||||
clearable
|
||||
value={filters.leader_uid}
|
||||
onChange={(v) => setFilters((s) => ({ ...s, leader_uid: v }))}
|
||||
/>
|
||||
<MultiSelect
|
||||
<Select
|
||||
label="规定类型"
|
||||
size="xs"
|
||||
radius="xs"
|
||||
data={setTypeOpts}
|
||||
searchable
|
||||
clearable
|
||||
value={filters.set_type ? [filters.set_type] : []}
|
||||
onChange={(v) =>
|
||||
setFilters((s) => ({ ...s, set_type: v[0] ?? "" }))
|
||||
value={filters.set_type || null}
|
||||
onChange={(value) =>
|
||||
setFilters((s) => ({ ...s, set_type: value ?? "" }))
|
||||
}
|
||||
/>
|
||||
<MultiSelect
|
||||
<Select
|
||||
label="出勤类型"
|
||||
size="xs"
|
||||
radius="xs"
|
||||
data={actualTypeOpts}
|
||||
searchable
|
||||
clearable
|
||||
value={filters.actual_type ? [filters.actual_type] : []}
|
||||
onChange={(v) =>
|
||||
setFilters((s) => ({ ...s, actual_type: v[0] ?? "" }))
|
||||
value={filters.actual_type || null}
|
||||
onChange={(value) =>
|
||||
setFilters((s) => ({ ...s, actual_type: value ?? "" }))
|
||||
}
|
||||
/>
|
||||
<MultiSelect
|
||||
<Select
|
||||
label="工作类别"
|
||||
size="xs"
|
||||
radius="xs"
|
||||
data={workTypeOpts}
|
||||
searchable
|
||||
clearable
|
||||
value={filters.work_type ? [filters.work_type] : []}
|
||||
onChange={(v) =>
|
||||
setFilters((s) => ({ ...s, work_type: v[0] ?? "" }))
|
||||
value={filters.work_type || null}
|
||||
onChange={(value) =>
|
||||
setFilters((s) => ({ ...s, work_type: value ?? "" }))
|
||||
}
|
||||
/>
|
||||
<MultiSelect
|
||||
<Select
|
||||
label="核算方式"
|
||||
size="xs"
|
||||
radius="xs"
|
||||
data={accountTypeOpts}
|
||||
searchable
|
||||
clearable
|
||||
value={filters.accounting_type ? [filters.accounting_type] : []}
|
||||
onChange={(v) =>
|
||||
setFilters((s) => ({ ...s, accounting_type: v[0] ?? "" }))
|
||||
value={filters.accounting_type || null}
|
||||
onChange={(value) =>
|
||||
setFilters((s) => ({ ...s, accounting_type: value ?? "" }))
|
||||
}
|
||||
/>
|
||||
<MultiSelect
|
||||
<Select
|
||||
label="绩效核算"
|
||||
size="xs"
|
||||
radius="xs"
|
||||
data={performanceOpts}
|
||||
searchable
|
||||
clearable
|
||||
value={
|
||||
filters.performance_accounting
|
||||
? [filters.performance_accounting]
|
||||
: []
|
||||
}
|
||||
onChange={(v) =>
|
||||
value={filters.performance_accounting || null}
|
||||
onChange={(value) =>
|
||||
setFilters((s) => ({
|
||||
...s,
|
||||
performance_accounting: v[0] ?? "",
|
||||
performance_accounting: value ?? "",
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<MultiSelect
|
||||
<Select
|
||||
label="对象类型"
|
||||
size="xs"
|
||||
radius="xs"
|
||||
data={objTypeOpts}
|
||||
searchable
|
||||
clearable
|
||||
value={filters.obj_type ? [filters.obj_type] : []}
|
||||
onChange={(v) =>
|
||||
setFilters((s) => ({ ...s, obj_type: v[0] ?? "" }))
|
||||
value={filters.obj_type || null}
|
||||
onChange={(value) =>
|
||||
setFilters((s) => ({ ...s, obj_type: value ?? "" }))
|
||||
}
|
||||
/>
|
||||
</SimpleGrid>
|
||||
</Collapse>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</SettingFilterPanel>
|
||||
|
||||
<Paper
|
||||
withBorder
|
||||
p="md"
|
||||
radius="md"
|
||||
<SettingContentPanel
|
||||
style={{
|
||||
borderColor:
|
||||
"light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-4))",
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
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 }}>
|
||||
<Flex justify={"space-between"} align="center" w="100%">
|
||||
<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>
|
||||
<SettingDataTable<Daily.DailyWorkList>
|
||||
width="100%"
|
||||
style={{ width: "100%" }}
|
||||
withTableBorder
|
||||
withRowBorders
|
||||
minHeight={0}
|
||||
style={{ width: "100%", flex: 1 }}
|
||||
pinFirstColumn
|
||||
pinLastColumn
|
||||
scrollAreaProps={{ type: "auto" }}
|
||||
@@ -665,16 +688,35 @@ export default function PersonReportPage() {
|
||||
setPageSize(v)
|
||||
setPage(1)
|
||||
}}
|
||||
recordsPerPageOptions={[10, 15, 20, 50]}
|
||||
noRecordsText="暂无数据"
|
||||
recordsPerPageLabel="当前页数"
|
||||
recordsPerPageOptions={recordsPerPageOptions}
|
||||
sortStatus={sortStatus}
|
||||
onSortStatusChange={(s) => {
|
||||
setSortStatus(s)
|
||||
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">
|
||||
<Text size="sm" fw={700}>
|
||||
当前页总计
|
||||
@@ -691,9 +733,9 @@ export default function PersonReportPage() {
|
||||
</Text>
|
||||
</Group>
|
||||
</Group>
|
||||
</Paper>
|
||||
</SettingPanel>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</SettingContentPanel>
|
||||
|
||||
<DailyReportModal
|
||||
opened={modalOpened}
|
||||
@@ -705,6 +747,31 @@ export default function PersonReportPage() {
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -86,6 +86,17 @@ const currentComponentList: MenuItem[] = [
|
||||
},
|
||||
]
|
||||
|
||||
const lidarList: MenuItem[] =
|
||||
process.env.NEXT_PUBLIC_ENV === "production"
|
||||
? []
|
||||
: [
|
||||
{
|
||||
title: "点云标注",
|
||||
url: "lidar",
|
||||
icon: "LidarAnnotationIcon",
|
||||
},
|
||||
]
|
||||
|
||||
export const componentList: MenuItem[] = [
|
||||
...currentComponentList,
|
||||
{
|
||||
@@ -98,11 +109,7 @@ export const componentList: MenuItem[] = [
|
||||
url: "video",
|
||||
icon: "VideoAnnotationIcon",
|
||||
},
|
||||
{
|
||||
title: "点云标注",
|
||||
url: "lidar",
|
||||
icon: "LidarAnnotationIcon",
|
||||
},
|
||||
...lidarList,
|
||||
{
|
||||
title: "管理中心",
|
||||
url: "mgt",
|
||||
|
||||
128
components/setting/PageSurface.module.css
Normal file
128
components/setting/PageSurface.module.css
Normal 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;
|
||||
}
|
||||
316
components/setting/PageSurface.tsx
Normal file
316
components/setting/PageSurface.tsx
Normal 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
|
||||
@@ -1,9 +1,9 @@
|
||||
import { FlatCompat } from "@eslint/eslintrc"
|
||||
import tseslint from "@typescript-eslint/eslint-plugin"
|
||||
import tsparser from "@typescript-eslint/parser"
|
||||
import prettierPlugin from "eslint-plugin-prettier"
|
||||
import { dirname } from "path"
|
||||
import { fileURLToPath } from "url"
|
||||
import { FlatCompat } from "@eslint/eslintrc"
|
||||
import tsparser from "@typescript-eslint/parser"
|
||||
import tseslint from "@typescript-eslint/eslint-plugin"
|
||||
import prettierPlugin from "eslint-plugin-prettier"
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = dirname(__filename)
|
||||
@@ -13,7 +13,6 @@ const compat = new FlatCompat({
|
||||
})
|
||||
|
||||
const eslintConfig = [
|
||||
...compat.extends("next/core-web-vitals"),
|
||||
{
|
||||
ignores: [
|
||||
"node_modules/**",
|
||||
@@ -22,6 +21,9 @@ const eslintConfig = [
|
||||
"build/**",
|
||||
"next-env.d.ts",
|
||||
],
|
||||
},
|
||||
...compat.extends("next/core-web-vitals"),
|
||||
{
|
||||
languageOptions: {
|
||||
parser: tsparser,
|
||||
parserOptions: {
|
||||
@@ -52,6 +54,11 @@ const eslintConfig = [
|
||||
],
|
||||
"react-hooks/rules-of-hooks": "error", // 检查 Hooks 的声明
|
||||
"react-hooks/exhaustive-deps": "warn", // 检查依赖项的声明
|
||||
"react-hooks/immutability": "off", // 现有交互层大量依赖 ref/mutable 对象,暂不启用 React Compiler 约束
|
||||
"react-hooks/preserve-manual-memoization": "off", // 当前项目未接入 React Compiler,先保留现有 useCallback/useMemo 依赖写法
|
||||
"react-hooks/refs": "off", // 现有画布能力通过 ref 暴露 imperative API,后续如接入 Compiler 再系统治理
|
||||
"react-hooks/set-state-in-effect": "off", // 页面初始化和同步逻辑仍依赖 effect 驱动,避免一次升级触发大规模重构
|
||||
"react-hooks/static-components": "off", // 先允许局部 render helper,后续再按需拆分组件
|
||||
"react/display-name": "off",
|
||||
"import/no-anonymous-default-export": "off",
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
|
||||
@@ -11,4 +11,4 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user