Compare commits

..

12 Commits

Author SHA1 Message Date
zhangheng
4311cd67eb fix(table): fix table display 2026-04-22 14:06:20 +08:00
zhangheng
1211b79dc3 fix(eslint): fix 2026-04-20 20:31:32 +08:00
zhangheng
a4ccdc5589 fix(daily): report 2026-04-20 16:51:59 +08:00
刘耀勇
1b74c23dac Merge branch 'zh' into 'main'
fix(report): table filters

See merge request prism/lable/labelmain!12
2026-04-16 20:28:42 +08:00
zhangheng
02efe5023d fix(report): table filters 2026-04-16 15:13:10 +08:00
刘耀勇
2b53e70116 Merge branch 'zh' into 'main'
fix(common): prod hide lidar

See merge request prism/lable/labelmain!11
2026-04-15 18:39:31 +08:00
zhangheng
c7980e2d17 fix(common): prod hide lidar 2026-04-15 17:09:49 +08:00
刘耀勇
0ca274bc50 Merge branch 'zh' into 'main'
Zh

See merge request prism/lable/labelmain!10
2026-04-14 18:37:55 +08:00
zhangheng
2edc1f6b73 fix(ones): bug 2026-04-14 17:14:43 +08:00
zhangheng
86a4955c38 fix(daily): fix style 2026-04-14 16:10:13 +08:00
刘耀勇
15d23f0cc9 Merge branch 'zh' into 'main'
Zh

See merge request prism/lable/labelmain!9
2026-04-13 18:00:42 +08:00
刘耀勇
2ee893b0d6 Merge branch 'zh' into 'main'
Zh

See merge request prism/lable/labelmain!8
2026-04-10 14:00:07 +08:00
10 changed files with 1055 additions and 415 deletions

View File

@@ -103,7 +103,19 @@ export default function PersonalProjectTable() {
title: "项目名称", title: "项目名称",
width: 160, width: 160,
render: (record: Project.PersonProjectDashboardResponseItem) => { 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>
)
}, },
}, },
{ {

View File

@@ -78,9 +78,21 @@ export default function PersonalTaskTable() {
{ {
accessor: "project_name", accessor: "project_name",
title: "项目名称", title: "项目名称",
width: 150, width: 160,
render: (record: Task.SimpleTaskItem) => { 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>
)
}, },
}, },
{ {

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,
@@ -31,6 +32,7 @@ import {
setTypeOpts, setTypeOpts,
workTypeOpts, workTypeOpts,
} from "../config" } from "../config"
import classes from "./DailyReportModal.module.css"
function normalizeTimeValue(value?: string | null) { function normalizeTimeValue(value?: string | null) {
if (!value) return "" if (!value) return ""
@@ -86,25 +88,9 @@ function HourRightSection() {
) )
} }
export default function DailyReportModal(props: { function getEmptyFormValues(currentUserName?: string) {
opened: boolean return {
record: Daily.DailyWorkList | null user_name: currentUserName ?? "",
projectOptions: Array<{ label: string; value: string }>
onCloseAction: (refresh?: boolean) => void
}) {
const { opened, record, onCloseAction, projectOptions } = props
const user_id = usePermissionStore((s) => s.user_id)
const user_name = usePermissionStore((s) => s.user_name)
const treeData = useAllUserStore((s) => s.treeData)
const leaderOptions = useMemo(() => {
return flattenLeaderTree(treeData ?? [])
}, [treeData])
const form = useForm({
initialValues: {
user_name: user_name ?? "",
date: "", date: "",
start_time: "", start_time: "",
end_time: "", end_time: "",
@@ -124,49 +110,22 @@ export default function DailyReportModal(props: {
residual_work_time: undefined as number | undefined, residual_work_time: undefined as number | undefined,
residual_pm: "", residual_pm: "",
remarks: "", remarks: "",
}, }
validate: {
date: (v) => (v ? null : "请输入日期"),
// set_type: (v) => (v ? null : "请选择规定类型"),
actual_type: (v) => (v ? null : "请选择出勤类型"),
},
validateInputOnChange: true,
validateInputOnBlur: true,
clearInputErrorOnChange: true,
})
useEffect(() => {
if (!opened) 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.resetDirty()
return
} }
form.setValues({ function getCreateFormValues(currentUserName?: string) {
user_name: user_name ?? "", return {
...getEmptyFormValues(currentUserName),
date: new Date().toISOString().slice(0, 10),
}
}
function getRecordFormValues(
record: Daily.DailyWorkList,
currentUserName?: string
) {
return {
user_name: currentUserName ?? "",
date: record.date ?? "", date: record.date ?? "",
start_time: normalizeTimeValue(record.start_time), start_time: normalizeTimeValue(record.start_time),
end_time: normalizeTimeValue(record.end_time), end_time: normalizeTimeValue(record.end_time),
@@ -205,7 +164,58 @@ export default function DailyReportModal(props: {
? "负" ? "负"
: "", : "",
remarks: record.remarks ?? "", remarks: record.remarks ?? "",
}
}
export default function DailyReportModal(props: {
opened: boolean
record: Daily.DailyWorkList | null
projectOptions: Array<{ label: string; value: string }>
onCloseAction: (refresh?: boolean) => void
}) {
const { opened, record, onCloseAction, projectOptions } = props
const user_id = usePermissionStore((s) => s.user_id)
const user_name = usePermissionStore((s) => s.user_name)
const treeData = useAllUserStore((s) => s.treeData)
const leaderOptions = useMemo(() => {
return flattenLeaderTree(treeData ?? [])
}, [treeData])
const form = useForm({
initialValues: getEmptyFormValues(user_name),
validate: {
date: (v) => (v ? null : "请输入日期"),
set_type: (v) => (v ? null : "请选择规定类型"),
actual_type: (v) => (v ? null : "请选择出勤类型"),
},
validateInputOnChange: true,
validateInputOnBlur: true,
clearInputErrorOnChange: true,
}) })
const resetFormState = () => {
form.setValues(getEmptyFormValues(user_name))
form.clearErrors()
form.resetDirty()
}
useEffect(() => {
if (!opened) {
resetFormState()
return
}
if (!record) {
form.setValues(getCreateFormValues(user_name))
form.clearErrors()
form.resetDirty()
return
}
form.setValues(getRecordFormValues(record, user_name))
form.clearErrors()
form.resetDirty() form.resetDirty()
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [opened, record, user_name]) }, [opened, record, user_name])
@@ -280,12 +290,12 @@ export default function DailyReportModal(props: {
? false ? false
: undefined, : undefined,
obj_type: values.obj_type ? Number(values.obj_type) : undefined, obj_type: values.obj_type ? Number(values.obj_type) : undefined,
work_time: values.work_time, work_time: values.work_time || undefined,
rate: values.rate, rate: values.rate || undefined,
standard_size: values.standard_size, standard_size: values.standard_size || undefined,
completed_size: values.completed_size, completed_size: values.completed_size || undefined,
residual_size: values.residual_size, residual_size: values.residual_size || undefined,
residual_work_time: values.residual_work_time, residual_work_time: values.residual_work_time || undefined,
residual_pm: residual_pm:
values.residual_pm === "正" values.residual_pm === "正"
? true ? true
@@ -311,8 +321,8 @@ export default function DailyReportModal(props: {
message: "已提交日报", message: "已提交日报",
}) })
} }
resetFormState()
onCloseAction(true) onCloseAction(true)
form.reset()
} catch (e) { } catch (e) {
notifications.show({ notifications.show({
color: "red", color: "red",
@@ -326,27 +336,37 @@ export default function DailyReportModal(props: {
<Modal <Modal
opened={opened} opened={opened}
onClose={() => { onClose={() => {
form.reset() resetFormState()
onCloseAction() onCloseAction()
}} }}
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
withBorder
radius="lg"
p="md"
shadow="xs"
className={classes.formSurface}>
<Stack gap="md" className={classes.formPanel}>
<SimpleGrid cols={{ base: 1, sm: 3 }} spacing="sm"> <SimpleGrid cols={{ base: 1, sm: 3 }} spacing="sm">
<TextInput <TextInput label="姓名" value={form.values.user_name} readOnly />
label="姓名"
size="xs"
radius="xs"
value={form.values.user_name}
readOnly
/>
<TextInput <TextInput
label="日期" label="日期"
size="xs"
radius="xs"
type="date" type="date"
withAsterisk withAsterisk
{...form.getInputProps("date")} {...form.getInputProps("date")}
@@ -355,15 +375,11 @@ export default function DailyReportModal(props: {
<TextInput <TextInput
label="开始时间" label="开始时间"
size="xs"
radius="xs"
type="time" type="time"
{...form.getInputProps("start_time")} {...form.getInputProps("start_time")}
/> />
<TextInput <TextInput
label="结束时间" label="结束时间"
size="xs"
radius="xs"
type="time" type="time"
{...form.getInputProps("end_time")} {...form.getInputProps("end_time")}
/> />
@@ -371,86 +387,100 @@ export default function DailyReportModal(props: {
<Select <Select
label="规定类型" label="规定类型"
size="xs"
radius="xs"
withAsterisk withAsterisk
data={setTypeOpts} data={setTypeOpts}
searchable searchable
clearable clearable
{...form.getInputProps("set_type")} {...form.getInputProps("set_type")}
value={form.values.set_type || null}
onChange={(value) =>
form.setFieldValue("set_type", value ?? "")
}
/> />
<Select <Select
label="出勤类型" label="出勤类型"
size="xs"
radius="xs"
withAsterisk withAsterisk
data={actualTypeOpts} data={actualTypeOpts}
searchable searchable
clearable clearable
{...form.getInputProps("actual_type")} {...form.getInputProps("actual_type")}
value={form.values.actual_type || null}
onChange={(value) =>
form.setFieldValue("actual_type", value ?? "")
}
/> />
<div /> <div />
<Select <Select
label="项目名称" label="项目名称"
size="xs"
radius="xs"
data={projectOptions} data={projectOptions}
searchable searchable
clearable clearable
{...form.getInputProps("project_id")} {...form.getInputProps("project_id")}
value={form.values.project_id || null}
onChange={(value) =>
form.setFieldValue("project_id", value ?? "")
}
/> />
<Select <Select
label="任务组长" label="任务组长"
size="xs"
radius="xs"
data={leaderOptions} data={leaderOptions}
searchable searchable
clearable clearable
{...form.getInputProps("leader_uid")} {...form.getInputProps("leader_uid")}
value={form.values.leader_uid || null}
onChange={(value) =>
form.setFieldValue("leader_uid", value ?? "")
}
/> />
<Select <Select
label="工作类别" label="工作类别"
size="xs"
radius="xs"
data={workTypeOpts} data={workTypeOpts}
searchable searchable
clearable clearable
{...form.getInputProps("work_type")} {...form.getInputProps("work_type")}
value={form.values.work_type || null}
onChange={(value) =>
form.setFieldValue("work_type", value ?? "")
}
/> />
<Select <Select
label="对象类型" label="对象类型"
size="xs"
radius="xs"
data={objTypeOpts} data={objTypeOpts}
searchable searchable
clearable clearable
{...form.getInputProps("obj_type")} {...form.getInputProps("obj_type")}
value={form.values.obj_type || null}
onChange={(value) =>
form.setFieldValue("obj_type", value ?? "")
}
/> />
<Select <Select
label="绩效核算" label="绩效核算"
size="xs"
radius="xs"
data={performanceOpts} data={performanceOpts}
searchable searchable
clearable clearable
{...form.getInputProps("performance_accounting")} {...form.getInputProps("performance_accounting")}
value={form.values.performance_accounting || null}
onChange={(value) =>
form.setFieldValue("performance_accounting", value ?? "")
}
/> />
<Select <Select
label="核算方式" label="核算方式"
size="xs"
radius="xs"
data={accountTypeOpts} data={accountTypeOpts}
searchable searchable
clearable clearable
{...form.getInputProps("accounting_type")} {...form.getInputProps("accounting_type")}
value={form.values.accounting_type || null}
onChange={(value) =>
form.setFieldValue("accounting_type", value ?? "")
}
/> />
<NumberInput <NumberInput
label="工时" label="工时"
size="xs"
radius="xs"
min={0} min={0}
decimalScale={1} decimalScale={1}
allowNegative={false} allowNegative={false}
@@ -461,8 +491,6 @@ export default function DailyReportModal(props: {
/> />
<NumberInput <NumberInput
label="额定倍速" label="额定倍速"
size="xs"
radius="xs"
min={0} min={0}
allowNegative={false} allowNegative={false}
hideControls hideControls
@@ -472,16 +500,12 @@ export default function DailyReportModal(props: {
<NumberInput <NumberInput
label="标准量" label="标准量"
size="xs"
radius="xs"
value={form.values.standard_size} value={form.values.standard_size}
readOnly readOnly
hideControls hideControls
/> />
<NumberInput <NumberInput
label="完成量" label="完成量"
size="xs"
radius="xs"
min={0} min={0}
allowNegative={false} allowNegative={false}
hideControls hideControls
@@ -491,16 +515,12 @@ export default function DailyReportModal(props: {
<NumberInput <NumberInput
label="余量" label="余量"
size="xs"
radius="xs"
value={form.values.residual_size} value={form.values.residual_size}
readOnly readOnly
hideControls hideControls
/> />
<NumberInput <NumberInput
label="余量工时" label="余量工时"
size="xs"
radius="xs"
value={form.values.residual_work_time} value={form.values.residual_work_time}
readOnly readOnly
hideControls hideControls
@@ -509,36 +529,26 @@ export default function DailyReportModal(props: {
/> />
<TextInput <TextInput
label="余量正负" label="余量正负"
size="xs"
radius="xs"
value={form.values.residual_pm} value={form.values.residual_pm}
readOnly readOnly
/> />
</SimpleGrid> </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"
radius="xs"
onClick={() => { onClick={() => {
form.reset() resetFormState()
onCloseAction() onCloseAction()
}}> }}>
</Button> </Button>
<Button size="xs" radius="xs" type="submit"> <Button type="submit"></Button>
</Button>
</Group> </Group>
</Stack> </Stack>
</Paper>
</form> </form>
</Modal> </Modal>
) )

View File

@@ -7,15 +7,25 @@ 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, Select,
SimpleGrid, SimpleGrid,
Stack, Stack,
Text, Text,
@@ -34,11 +44,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 +111,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 +145,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 +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 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 +490,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 +510,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 +523,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 +531,12 @@ export default function PersonReportPage() {
}}> }}>
</Button> </Button>
</SettingFilterActions>
</Group> </Group>
</Group> <Collapse in={searchExpanded} transitionDuration={150} animateOpacity>
<Collapse
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 +548,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 +556,6 @@ export default function PersonReportPage() {
/> />
<MultiSelect <MultiSelect
label="项目名称" label="项目名称"
size="xs"
radius="xs"
data={projectOptions} data={projectOptions}
searchable searchable
clearable clearable
@@ -525,132 +564,116 @@ export default function PersonReportPage() {
/> />
<MultiSelect <MultiSelect
label="任务组长" label="任务组长"
size="xs"
radius="xs"
data={leaderOptions} data={leaderOptions}
searchable searchable
clearable clearable
value={filters.leader_uid} value={filters.leader_uid}
onChange={(v) => setFilters((s) => ({ ...s, leader_uid: v }))} onChange={(v) => setFilters((s) => ({ ...s, leader_uid: v }))}
/> />
<MultiSelect <Select
label="规定类型" label="规定类型"
size="xs"
radius="xs"
data={setTypeOpts} data={setTypeOpts}
searchable searchable
clearable clearable
value={filters.set_type ? [filters.set_type] : []} value={filters.set_type || null}
onChange={(v) => onChange={(value) =>
setFilters((s) => ({ ...s, set_type: v[0] ?? "" })) setFilters((s) => ({ ...s, set_type: value ?? "" }))
} }
/> />
<MultiSelect <Select
label="出勤类型" label="出勤类型"
size="xs"
radius="xs"
data={actualTypeOpts} data={actualTypeOpts}
searchable searchable
clearable clearable
value={filters.actual_type ? [filters.actual_type] : []} value={filters.actual_type || null}
onChange={(v) => onChange={(value) =>
setFilters((s) => ({ ...s, actual_type: v[0] ?? "" })) setFilters((s) => ({ ...s, actual_type: value ?? "" }))
} }
/> />
<MultiSelect <Select
label="工作类别" label="工作类别"
size="xs"
radius="xs"
data={workTypeOpts} data={workTypeOpts}
searchable searchable
clearable clearable
value={filters.work_type ? [filters.work_type] : []} value={filters.work_type || null}
onChange={(v) => onChange={(value) =>
setFilters((s) => ({ ...s, work_type: v[0] ?? "" })) setFilters((s) => ({ ...s, work_type: value ?? "" }))
} }
/> />
<MultiSelect <Select
label="核算方式" label="核算方式"
size="xs"
radius="xs"
data={accountTypeOpts} data={accountTypeOpts}
searchable searchable
clearable clearable
value={filters.accounting_type ? [filters.accounting_type] : []} value={filters.accounting_type || null}
onChange={(v) => onChange={(value) =>
setFilters((s) => ({ ...s, accounting_type: v[0] ?? "" })) setFilters((s) => ({ ...s, accounting_type: value ?? "" }))
} }
/> />
<MultiSelect <Select
label="绩效核算" label="绩效核算"
size="xs"
radius="xs"
data={performanceOpts} data={performanceOpts}
searchable searchable
clearable clearable
value={ value={filters.performance_accounting || null}
filters.performance_accounting onChange={(value) =>
? [filters.performance_accounting]
: []
}
onChange={(v) =>
setFilters((s) => ({ setFilters((s) => ({
...s, ...s,
performance_accounting: v[0] ?? "", performance_accounting: value ?? "",
})) }))
} }
/> />
<MultiSelect <Select
label="对象类型" label="对象类型"
size="xs"
radius="xs"
data={objTypeOpts} data={objTypeOpts}
searchable searchable
clearable clearable
value={filters.obj_type ? [filters.obj_type] : []} value={filters.obj_type || null}
onChange={(v) => onChange={(value) =>
setFilters((s) => ({ ...s, obj_type: v[0] ?? "" })) setFilters((s) => ({ ...s, obj_type: value ?? "" }))
} }
/> />
</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,
}}> }}>
<Stack gap={12} align="stretch" style={{ flex: 1, minHeight: 0 }}> <SettingListHeader
<Flex justify={"space-between"} align="center" w="100%"> title="我的日报"
count={`${totalItems}`}
actions={
<SettingHeaderActions>
<Button <Button
leftSection={<IconPlus size={12} />} leftSection={<IconPlus size={16} />}
size="xs"
radius={"xs"}
onClick={() => { onClick={() => {
setEditingRecord(null) setEditingRecord(null)
setModalOpened(true) setModalOpened(true)
}}> }}>
</Button> </Button>
<ActionIcon onClick={load} variant="transparent"> <Button
<IconRefresh size={16} /> variant="default"
</ActionIcon> leftSection={<IconRefresh size={16} />}
</Flex> onClick={load}
<DataTable<Daily.DailyWorkList> loading={loading}>
</Button>
</SettingHeaderActions>
}
/>
<Stack gap={12} align="stretch" style={{ flex: 1, minHeight: 0 }}>
<SettingDataTable<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 +688,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 +733,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 +747,31 @@ export default function PersonReportPage() {
if (refresh) load() if (refresh) load()
}} }}
/> />
<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> </Stack>
</Modal>
</SettingPage>
) )
} }

View File

@@ -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[] = [ export const componentList: MenuItem[] = [
...currentComponentList, ...currentComponentList,
{ {
@@ -98,11 +109,7 @@ export const componentList: MenuItem[] = [
url: "video", url: "video",
icon: "VideoAnnotationIcon", icon: "VideoAnnotationIcon",
}, },
{ ...lidarList,
title: "点云标注",
url: "lidar",
icon: "LidarAnnotationIcon",
},
{ {
title: "管理中心", title: "管理中心",
url: "mgt", url: "mgt",

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

View File

@@ -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 { dirname } from "path"
import { fileURLToPath } from "url" 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 __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename) const __dirname = dirname(__filename)
@@ -13,7 +13,6 @@ const compat = new FlatCompat({
}) })
const eslintConfig = [ const eslintConfig = [
...compat.extends("next/core-web-vitals"),
{ {
ignores: [ ignores: [
"node_modules/**", "node_modules/**",
@@ -22,6 +21,9 @@ const eslintConfig = [
"build/**", "build/**",
"next-env.d.ts", "next-env.d.ts",
], ],
},
...compat.extends("next/core-web-vitals"),
{
languageOptions: { languageOptions: {
parser: tsparser, parser: tsparser,
parserOptions: { parserOptions: {
@@ -52,6 +54,11 @@ const eslintConfig = [
], ],
"react-hooks/rules-of-hooks": "error", // 检查 Hooks 的声明 "react-hooks/rules-of-hooks": "error", // 检查 Hooks 的声明
"react-hooks/exhaustive-deps": "warn", // 检查依赖项的声明 "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", "react/display-name": "off",
"import/no-anonymous-default-export": "off", "import/no-anonymous-default-export": "off",
"@typescript-eslint/no-unused-vars": [ "@typescript-eslint/no-unused-vars": [

View File

@@ -11,4 +11,4 @@ module.exports = {
}, },
}, },
}, },
}; }