From 9d9bf4ff7fadafb5d2b279bb5823885e46a53ec3 Mon Sep 17 00:00:00 2001 From: zhangheng Date: Tue, 28 Apr 2026 14:36:36 +0800 Subject: [PATCH] perf(style): codex-skills/cowa-admin-list-ui --- .../components/PersonalProjectTable.tsx | 196 ++-- .../components/PersonalTaskTable.tsx | 167 ++-- app/person/dashboard/page.module.css | 328 ++++++ app/person/dashboard/page.tsx | 289 ++---- .../components/DailyReportModal.module.css | 97 +- .../report/components/DailyReportModal.tsx | 11 +- app/person/report/page.module.css | 177 ++++ app/person/report/page.tsx | 539 +++++----- app/theme.ts | 20 +- components/label/components/Icon.tsx | 946 ++++++++++++++++++ .../label/components/TaskStatusTag.module.css | 81 ++ components/label/components/TaskStatusTag.tsx | 84 ++ components/layout/AppLayout.module.css | 81 ++ components/layout/AppLayout.tsx | 73 +- components/layout/HeaderLink.module.css | 27 +- components/layout/components/NavbarLinks.tsx | 12 +- components/layout/navStyles.ts | 29 + components/setting/PageSurface.module.css | 259 ++++- components/setting/PageSurface.tsx | 54 +- next.config.ts | 14 +- 20 files changed, 2672 insertions(+), 812 deletions(-) create mode 100644 app/person/dashboard/page.module.css create mode 100644 app/person/report/page.module.css create mode 100644 components/label/components/Icon.tsx create mode 100644 components/label/components/TaskStatusTag.module.css create mode 100644 components/label/components/TaskStatusTag.tsx create mode 100644 components/layout/AppLayout.module.css create mode 100644 components/layout/navStyles.ts diff --git a/app/person/dashboard/components/PersonalProjectTable.tsx b/app/person/dashboard/components/PersonalProjectTable.tsx index df742ba..1bd1b4e 100644 --- a/app/person/dashboard/components/PersonalProjectTable.tsx +++ b/app/person/dashboard/components/PersonalProjectTable.tsx @@ -3,24 +3,20 @@ import { getPersonProjectDashBoard } from "@/components/label/api/project" import { Project } from "@/components/label/api/project/typing" import { - Button, - Flex, - Group, - Modal, - Stack, - Text, - TextInput, -} from "@mantine/core" + SettingDataTable, + SettingListHeader, +} from "@/components/setting/PageSurface" +import { Button, Group, Modal, Stack, Text, TextInput } from "@mantine/core" import { DatePickerInput, type DatesRangeValue } from "@mantine/dates" import { notifications } from "@mantine/notifications" -import { IconRefresh } from "@tabler/icons-react" +import { IconRefresh, IconSearch } from "@tabler/icons-react" import dayjs from "dayjs" import "dayjs/locale/zh-cn" import duration from "dayjs/plugin/duration" -import { DataTable, DataTableColumn } from "mantine-datatable" -import { useRouter } from "next/navigation" +import { DataTableColumn } from "mantine-datatable" import { useCallback, useEffect, useMemo, useState } from "react" +import classes from "../page.module.css" dayjs.extend(duration) @@ -51,14 +47,13 @@ function createInitialFilters(): FilterFormRecord { const DEFAULT_RECORDS_PER_PAGE_OPTIONS = [10, 15, 20, 50] export default function PersonalProjectTable() { - const router = useRouter() const [records, setRecords] = useState< Project.PersonProjectDashboardResponseItem[] >([]) const [loading, setLoading] = useState(false) const [page, setPage] = useState(1) - const [pageSize, setPageSize] = useState(50) + const [pageSize, setPageSize] = useState(10) const [recordsPerPageOptions, setRecordsPerPageOptions] = useState( () => [...DEFAULT_RECORDS_PER_PAGE_OPTIONS] ) @@ -123,12 +118,18 @@ export default function PersonalProjectTable() { setQueryTrigger((v) => v + 1) } + const handleReset = useCallback(() => { + const next = createInitialFilters() + setFilters(next) + setForm(next) + setPage(1) + setQueryTrigger((v) => v + 1) + }, []) + useEffect(() => { if (page > totalPages) setPage(totalPages) }, [page, totalPages]) - const [currentRowId, setCurrentRowId] = useState(0) - const openCustomPageSizeModal = useCallback(() => { setCustomPageSizeModalOpened(true) }, []) @@ -186,22 +187,8 @@ export default function PersonalProjectTable() { render: (record: Project.PersonProjectDashboardResponseItem) => { return ( { - false && setCurrentRowId(record.project_id) - false && - router.push(`/project/info/detail?id=${record.project_id}`) - }} title={record.project_id.toString()} - // c={"brand"} - style={{ - fontSize: "14px", - maxWidth: "100%", - whiteSpace: "nowrap", - overflow: "hidden", - textOverflow: "ellipsis", - }}> + className={classes.ellipsisText}> {record.project_id} ) @@ -215,13 +202,7 @@ export default function PersonalProjectTable() { return ( + className={classes.ellipsisText}> {record.project_name ?? "-"} ) @@ -235,13 +216,7 @@ export default function PersonalProjectTable() { return ( + className={classes.ellipsisText}> {record.project_type ?? "-"} ) @@ -252,7 +227,11 @@ export default function PersonalProjectTable() { title: "任务耗时", width: 140, render: (record: Project.PersonProjectDashboardResponseItem) => { - return formatDurationSeconds(record.work_time) + return ( + + {formatDurationSeconds(record.work_time)} + + ) }, }, { @@ -260,74 +239,63 @@ export default function PersonalProjectTable() { title: "标注类别", width: 160, render: (record: Project.PersonProjectDashboardResponseItem) => { - return record.project_label_type ?? "-" + return ( + + {record.project_label_type ?? "-"} + + ) }, }, ] return column - }, [router]) + }, []) return ( - - - - - 参与项目 - - - setFilters((s) => ({ - ...s, - query_date: value, - })) - } - /> - - - - - - + + +
+
+ 统计周期 +
+ + setFilters((s) => ({ + ...s, + query_date: value, + })) + } + /> +
+
+
+ + +
+
+
+ width="100%" - style={{ width: "100%" }} - styles={{ - header: { - color: "var(--mantine-color-grey-text)", - backgroundColor: "var(--mantine-datatable-striped-color)", - }, - }} - withTableBorder - withRowBorders + minHeight={0} + style={{ width: "100%", flex: 1 }} pinFirstColumn - rowBackgroundColor={(record) => { - if (record.project_id === currentRowId) - return "var(--mantine-datatable-highlight-on-hover-color-light, var(--mantine-datatable-highlight-on-hover-color))" - }} - idAccessor="project_id" + fetching={loading} scrollAreaProps={{ type: "auto" }} records={pageRecords} onRecordsPerPageChange={setPageSize} @@ -338,7 +306,10 @@ export default function PersonalProjectTable() { page={page} onPageChange={setPage} noRecordsText="暂无数据" - recordsPerPageLabel="当前页数" + recordsPerPageLabel="每页条数" + paginationText={({ from, to, totalRecords }) => + `显示 ${from}-${to} 条,共 ${totalRecords} 条记录` + } renderPagination={({ Controls }) => ( @@ -361,7 +331,7 @@ export default function PersonalProjectTable() { )} /> - +
取消 - +
-
+ ) } diff --git a/app/person/dashboard/components/PersonalTaskTable.tsx b/app/person/dashboard/components/PersonalTaskTable.tsx index 8be7f5a..043feb3 100644 --- a/app/person/dashboard/components/PersonalTaskTable.tsx +++ b/app/person/dashboard/components/PersonalTaskTable.tsx @@ -2,13 +2,17 @@ import { getUserTaskList } from "@/components/label/api/task" import { Task } from "@/components/label/api/task/typing" +import TaskStatusTag from "@/components/label/components/TaskStatusTag" import { useBackUrlStore, usePermissionStore, } from "@/components/label/store/auth" -import { TaskStatusEnum } from "@/components/label/utils/constants" import { - Badge, + SettingDataTable, + SettingHeaderActions, + SettingListHeader, +} from "@/components/setting/PageSurface" +import { Button, Group, Modal, @@ -19,25 +23,9 @@ import { } from "@mantine/core" import { notifications } from "@mantine/notifications" import { IconRefresh } from "@tabler/icons-react" -import { DataTable, DataTableColumn } from "mantine-datatable" +import { DataTableColumn } from "mantine-datatable" import { useCallback, useEffect, useMemo, useState } from "react" - -function getStatusColor(status?: number | null) { - switch (status) { - case 2: - return "blue" - case 4: - return "yellow" - case 6: - return "orange" - case 7: - return "green" - case 8: - return "red" - default: - return "gray" - } -} +import classes from "../page.module.css" const DEFAULT_RECORDS_PER_PAGE_OPTIONS = [10, 15, 20, 50] @@ -46,7 +34,7 @@ export default function PersonalTaskTable() { const setBackProps = useBackUrlStore((s) => s.setBackProps) const [page, setPage] = useState(1) - const [pageSize, setPageSize] = useState(50) + const [pageSize, setPageSize] = useState(10) const [recordsPerPageOptions, setRecordsPerPageOptions] = useState( () => [...DEFAULT_RECORDS_PER_PAGE_OPTIONS] ) @@ -148,13 +136,7 @@ export default function PersonalTaskTable() { return ( + className={classes.ellipsisText}> {record.project_name ?? "-"} ) @@ -167,8 +149,6 @@ export default function PersonalTaskTable() { render: (record: Task.SimpleTaskItem) => { return ( { setCurrentRowId(record.id) if ([4, 5, 6, 7].includes(record.label_type)) { @@ -193,14 +173,7 @@ export default function PersonalTaskTable() { } }} title={record.id.toString()} - c={"brand"} - style={{ - fontSize: "14px", - maxWidth: "100%", - whiteSpace: "nowrap", - overflow: "hidden", - textOverflow: "ellipsis", - }}> + className={classes.tableLink}> {record.id} ) @@ -211,86 +184,84 @@ export default function PersonalTaskTable() { title: "对象数", width: 80, render: (record: Task.SimpleTaskItem) => { - return <>{record.label_object_size?.object_size || "-"} + return ( + + {record.label_object_size?.object_size || "-"} + + ) }, }, { accessor: "label_status", title: "状态", - width: 80, - render: (record: Task.SimpleTaskItem) => { - return ( - - {TaskStatusEnum.get(record.label_status)} - - ) - }, + width: 160, + render: (record) => ( + + ), }, { accessor: "label_username", title: "标注员", width: 80, + render: (record: Task.SimpleTaskItem) => ( + + {record.label_username || "-"} + + ), }, { accessor: "review1_username", title: "审核员", width: 80, + render: (record: Task.SimpleTaskItem) => ( + + {record.review1_username || "-"} + + ), }, { accessor: "review2_username", title: "复审员", width: 80, + render: (record: Task.SimpleTaskItem) => ( + + {record.review2_username || "-"} + + ), }, ] return column }, [setBackProps]) return ( - - - - 我的任务 - - - - - + + + + + } + /> +
+ width="100%" - style={{ width: "100%" }} - styles={{ - header: { - color: "var(--mantine-color-grey-text)", - backgroundColor: "var(--mantine-datatable-striped-color)", - }, - }} - withTableBorder - withRowBorders + minHeight={0} + style={{ width: "100%", flex: 1 }} pinFirstColumn - pinLastColumn + fetching={loading} rowBackgroundColor={({ id }) => { - if (id === currentRowId) - return "var(--mantine-datatable-highlight-on-hover-color-light, var(--mantine-datatable-highlight-on-hover-color))" + if (id === currentRowId) return "#F7FBFF" }} scrollAreaProps={{ type: "auto" }} records={records} @@ -302,7 +273,10 @@ export default function PersonalTaskTable() { page={page} onPageChange={setPage} noRecordsText="暂无数据" - recordsPerPageLabel="当前页数" + recordsPerPageLabel="每页条数" + paginationText={({ from, to, totalRecords }) => + `显示 ${from}-${to} 条,共 ${totalRecords} 条记录` + } renderPagination={({ Controls }) => ( @@ -325,7 +298,7 @@ export default function PersonalTaskTable() { )} /> - +
取消 - +
-
+ ) } diff --git a/app/person/dashboard/page.module.css b/app/person/dashboard/page.module.css new file mode 100644 index 0000000..2bf0bf8 --- /dev/null +++ b/app/person/dashboard/page.module.css @@ -0,0 +1,328 @@ +.pageIntro { + gap: 4px; +} + +.breadcrumb { + color: #86909c; + font-size: 13px; + line-height: 20px; +} + +.breadcrumbCurrent { + color: #4e5969; +} + +.summaryGrid { + display: grid; + grid-template-columns: 180px repeat(3, minmax(0, 1fr)); +} + +.summaryDateCell, +.summaryMetricCell { + display: flex; + min-width: 0; + min-height: 116px; + flex-direction: column; + gap: 8px; + padding: 16px 20px; +} + +.summaryDateCell, +.summaryMetricCell:not(:last-child) { + border-right: 1px solid #e5e6eb; +} + +.summaryDateCell { + justify-content: center; + background: #f7f8fa; +} + +.summaryDateLabel, +.summaryMetricLabel { + color: #86909c; + font-size: 12px; + font-weight: 500; + line-height: 20px; +} + +.summaryDateValue { + color: #1d2129; + font-size: 28px; + font-weight: 500; + line-height: 32px; +} + +.summaryDateMeta { + color: #4e5969; + font-size: 14px; + line-height: 20px; +} + +.summaryMetricCell { + justify-content: center; +} + +.summaryMetricValue { + color: #1d2129; + font-size: 24px; + font-weight: 500; + line-height: 32px; +} + +.summaryMetricMeta { + display: inline-flex; + width: fit-content; + align-items: center; + gap: 8px; + border-radius: 4px; + background: #f7f8fa; + padding: 6px 10px; +} + +.summaryMetricMetaLabel { + color: #86909c; + font-size: 12px; + line-height: 20px; +} + +.summaryMetricMetaValue { + color: #1d2129; + font-size: 14px; + font-weight: 500; + line-height: 20px; +} + +.tableGrid { + display: grid; + flex: 1; + min-height: 0; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; +} + +.dashboardPanel { + display: flex; + min-height: 0; + flex-direction: column; +} + +.dashboardPanelBody { + display: flex; + min-height: 0; + flex: 1; + flex-direction: column; + padding: 16px; +} + +.dashboardSection { + display: flex; + min-height: 0; + height: 100%; + flex-direction: column; + gap: 12px; +} + +.dashboardTableArea { + display: flex; + min-height: 0; + flex: 1; + flex-direction: column; +} + +.dashboardFilterBar { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 12px 16px; + align-items: center; + padding-bottom: 12px; + border-bottom: 1px solid #e5e6eb; +} + +.dashboardFilterField { + display: flex; + min-width: 0; + align-items: center; + gap: 10px; +} + +.dashboardFilterLabel { + flex: 0 0 80px; + color: #4e5969; + font-size: 14px; + font-weight: 500; + line-height: 20px; +} + +.dashboardFilterControl { + flex: 1; + min-width: 0; +} + +.dashboardFilterControl :global(.mantine-InputWrapper-root) { + width: 100%; +} + +.dashboardFilterBar :global(.mantine-Input-input), +.dashboardFilterBar :global(.mantine-DateInput-input) { + min-height: 32px; + border-color: #e5e6eb; + border-radius: 4px; + background: #f7f8fa; + color: #1d2129; + font-size: 14px; + box-shadow: none; + transition: + border-color 0.2s ease, + color 0.2s ease, + background-color 0.2s ease; +} + +.dashboardFilterBar :global(.mantine-Input-input::placeholder), +.dashboardFilterBar :global(.mantine-DateInput-input::placeholder) { + color: #86909c; +} + +.dashboardFilterBar :global(.mantine-Input-input:focus), +.dashboardFilterBar :global(.mantine-DateInput-input:focus) { + border-color: #1874ff; + background: #ffffff; + box-shadow: 0 0 0 2px rgba(24, 116, 255, 0.12); +} + +.dashboardFilterBar :global(.mantine-Input-section), +.dashboardFilterBar :global(.mantine-DateInput-section) { + color: #86909c; +} + +.dashboardFilterActions { + display: inline-flex; + align-items: center; + justify-self: end; + gap: 8px; +} + +.dashboardFilterActions :global(.mantine-Button-root) { + min-height: 32px; + border-radius: 4px; + padding-inline: 12px; + font-size: 14px; + font-weight: 500; + box-shadow: none; +} + +.dashboardFilterActions :global(.mantine-Button-root[data-variant="filled"]) { + --button-bg: #1874ff; + --button-hover: #0f6ae8; + --button-color: #ffffff; + --button-bd: transparent; + background: #1874ff !important; + color: #ffffff !important; +} + +.dashboardFilterActions + :global(.mantine-Button-root[data-variant="filled"]:hover) { + background: #0f6ae8 !important; +} + +.dashboardFilterActions :global(.mantine-Button-root[data-variant="default"]) { + border-color: #e5e6eb; + background: #ffffff; + color: #4e5969; +} + +.tableLink { + border: 0; + padding: 0; + background: transparent; + color: #1874ff; + font-size: 14px; + line-height: 20px; + cursor: pointer; +} + +.tableLink:disabled { + color: #c9cdd4; + cursor: not-allowed; +} + +.tableCellText { + display: block; + color: #1d2129; + font-size: 14px; + line-height: 20px; +} + +.ellipsisText { + display: block; + color: #1d2129; + font-size: 14px; + line-height: 20px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.statusBadge { + min-height: 24px; + border: 0; + border-radius: 999px; + padding: 0 10px; + font-size: 12px; + font-weight: 500; +} + +@media (max-width: 1200px) { + .summaryGrid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .summaryMetricCell:nth-child(2) { + border-right: 0; + } + + .summaryDateCell, + .summaryMetricCell { + border-bottom: 1px solid #e5e6eb; + } + + .summaryMetricCell:nth-last-child(-n + 2) { + border-bottom: 0; + } + + .tableGrid { + grid-template-columns: 1fr; + } +} + +@media (max-width: 720px) { + .summaryGrid { + grid-template-columns: 1fr; + } + + .summaryDateCell, + .summaryMetricCell { + border-right: 0; + } + + .summaryMetricCell:not(:last-child) { + border-bottom: 1px solid #e5e6eb; + } + + .dashboardFilterBar { + grid-template-columns: 1fr; + } + + .dashboardFilterField { + flex-direction: column; + align-items: flex-start; + gap: 6px; + } + + .dashboardFilterLabel { + flex-basis: auto; + } + + .dashboardFilterActions { + justify-self: stretch; + } +} diff --git a/app/person/dashboard/page.tsx b/app/person/dashboard/page.tsx index 0640037..86c1166 100644 --- a/app/person/dashboard/page.tsx +++ b/app/person/dashboard/page.tsx @@ -2,27 +2,17 @@ import { getCurrentPersonData } from "@/components/label/api/project" import { - BackgroundImage, - Center, - Group, - Paper, - SimpleGrid, - Stack, - Text, - Title, -} from "@mantine/core" + SettingContentPanel, + SettingPage, + SettingPanel, +} from "@/components/setting/PageSurface" +import { Stack, Text } from "@mantine/core" import dayjs from "dayjs" import duration from "dayjs/plugin/duration" -import { JSX, useEffect, useMemo, useState } from "react" -import { DataIcon, HourIcon, ReviewIcon } from "./components/Icon" +import { useEffect, useMemo, useState } from "react" import PersonalProjectTable from "./components/PersonalProjectTable" import PersonalTaskTable from "./components/PersonalTaskTable" -import DataImage from "./resource/images/data.png" -import DataLogoImage from "./resource/images/data_logo.png" -import HourImage from "./resource/images/hour.png" -import HourLogoImage from "./resource/images/hour_logo.png" -import ReviewImage from "./resource/images/review.png" -import ReviewLogoImage from "./resource/images/review_logo.png" +import classes from "./page.module.css" dayjs.extend(duration) @@ -56,95 +46,6 @@ function formatDurationSeconds(seconds?: number | null) { return "-" } -function StatCard(props: { - title: string - timeText: string - sizeTitle: string - sizeValue: number - sizeColor: string - background: string - backgroundLogo: string - Icon: () => JSX.Element -}) { - const { - title, - timeText, - sizeTitle, - sizeValue, - sizeColor, - background, - backgroundLogo, - Icon, - } = props - return ( - - - - -
- -
- -
- - {title} - - - {timeText} - -
- - - {sizeTitle} - - - {sizeValue} - - -
-
-
- ) -} - export default function PersonDashboardPage() { const [data, setData] = useState<{ label_data_size: number @@ -173,116 +74,86 @@ export default function PersonDashboardPage() { } }, [data]) + const summaryItems = useMemo( + () => [ + { + title: "标注工时", + value: dataText.label, + metaLabel: "标注数据量", + metaValue: data?.label_data_size ?? 0, + }, + { + title: "审核工时", + value: dataText.review1, + metaLabel: "审核数据量", + metaValue: data?.review1_data_size ?? 0, + }, + { + title: "复审工时", + value: dataText.review2, + metaLabel: "复审数据量", + metaValue: data?.review2_data_size ?? 0, + }, + ], + [ + data?.label_data_size, + data?.review1_data_size, + data?.review2_data_size, + dataText, + ] + ) + return ( - - - - - 今日 - -
- - - {dayjs().format("DD")} - - {monthText(dayjs().format("MM"))} - -
-
+ + + + 个人工作台 /{" "} + 个人看板 + + - - - - - -
+ +
+
+ 今日 + + {dayjs().format("DD")} + + + {monthText(dayjs().format("MM"))} · {dayjs().format("YYYY")} + +
- - -
+ {summaryItems.map((item) => ( +
+ {item.title} + {item.value} +
+ + {item.metaLabel} + + + {item.metaValue} + +
+
+ ))} +
+ + +
+ +
- +
- -
+ +
- - - +
+
+ ) } diff --git a/app/person/report/components/DailyReportModal.module.css b/app/person/report/components/DailyReportModal.module.css index 92add3b..8c31cf4 100644 --- a/app/person/report/components/DailyReportModal.module.css +++ b/app/person/report/components/DailyReportModal.module.css @@ -1,81 +1,116 @@ .modalContent { - border-radius: 16px; + border-radius: 8px; overflow: hidden; - box-shadow: 0 24px 60px rgba(15, 23, 42, 0.16); + box-shadow: 0 12px 24px rgba(15, 23, 42, 0.08); } .modalHeader { - padding: 20px 24px 0; + padding: 16px 20px 0; background: #ffffff; } .modalTitle { - color: #1f2329; - font-size: 18px; - font-weight: 700; - line-height: 28px; + color: #1d2129; + font-size: 16px; + font-weight: 500; + line-height: 24px; } .modalBody { - padding: 16px 24px 24px; - background: #f8fafc; + padding: 16px 20px 20px; + background: #ffffff; } .formSurface { - border-color: #e6ebf2; - background: linear-gradient(180deg, #ffffff 0%, #fcfdff 100%); - box-shadow: 0 12px 28px rgba(15, 23, 42, 0.05); + border-color: #e5e6eb; + border-radius: 4px; + background: #ffffff; + box-shadow: none; } .formPanel :global(.mantine-InputWrapper-label) { - color: #1f2329; + color: #4e5969; font-size: 14px; font-weight: 500; - line-height: 22px; - margin-bottom: 6px; + line-height: 20px; + margin-bottom: 4px; } .formPanel :global(.mantine-Input-input), +.formPanel :global(.mantine-DateInput-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; + min-height: 32px; + border-color: #e5e6eb; + border-radius: 4px; + background: #f7f8fa; + color: #1d2129; font-size: 14px; box-shadow: none; transition: - border-color 0.15s ease, - box-shadow 0.15s ease, - background-color 0.15s ease; + border-color 0.2s ease, + color 0.2s ease, + background-color 0.2s ease; } .formPanel :global(.mantine-Input-input::placeholder), +.formPanel :global(.mantine-DateInput-input::placeholder), .formPanel :global(.mantine-Select-input::placeholder), .formPanel :global(.mantine-NumberInput-input::placeholder), .formPanel :global(.mantine-Textarea-input::placeholder) { - color: #98a2b3; + color: #86909c; } .formPanel :global(.mantine-Input-input:focus), +.formPanel :global(.mantine-DateInput-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); + border-color: #1874ff; + background: #ffffff; + box-shadow: 0 0 0 2px rgba(24, 116, 255, 0.12); } .formPanel :global(.mantine-Input-input[readonly]), -.formPanel :global(.mantine-NumberInput-input[readonly]) { - background: #f8fafc; - color: #667085; +.formPanel :global(.mantine-DateInput-input[readonly]), +.formPanel :global(.mantine-NumberInput-input[readonly]), +.formPanel :global(.mantine-Textarea-input[readonly]) { + background: #f7f8fa; + color: #86909c; +} + +.formPanel :global(.mantine-Input-section), +.formPanel :global(.mantine-DateInput-section), +.formPanel :global(.mantine-Select-section), +.formPanel :global(.mantine-NumberInput-section) { + color: #86909c; } .formPanel :global(.mantine-Button-root) { - min-height: 36px; - border-radius: 8px; - padding-inline: 14px; + min-height: 32px; + border-radius: 4px; + padding-inline: 12px; font-size: 14px; font-weight: 500; + box-shadow: none; +} + +.formPanel :global(.mantine-Button-root[data-variant="filled"]) { + --button-bg: #1874ff; + --button-hover: #0f6ae8; + --button-color: #ffffff; + --button-bd: transparent; + background: #1874ff !important; + color: #ffffff !important; +} + +.formPanel :global(.mantine-Button-root[data-variant="filled"]:hover) { + background: #0f6ae8 !important; +} + +.formPanel :global(.mantine-Button-root[data-variant="default"]) { + border-color: #e5e6eb; + background: #ffffff; + color: #4e5969; } diff --git a/app/person/report/components/DailyReportModal.tsx b/app/person/report/components/DailyReportModal.tsx index deab87b..7435666 100644 --- a/app/person/report/components/DailyReportModal.tsx +++ b/app/person/report/components/DailyReportModal.tsx @@ -358,12 +358,7 @@ export default function DailyReportModal(props: { }} closeOnClickOutside={false}>
- + @@ -547,7 +542,9 @@ export default function DailyReportModal(props: { }}> 取消 - + diff --git a/app/person/report/page.module.css b/app/person/report/page.module.css new file mode 100644 index 0000000..554873d --- /dev/null +++ b/app/person/report/page.module.css @@ -0,0 +1,177 @@ +.pageIntro { + gap: 4px; +} + +.breadcrumb { + color: #86909c; + font-size: 13px; + line-height: 20px; +} + +.breadcrumbCurrent { + color: #4e5969; +} + +.filterTopRow { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 12px 20px; + align-items: center; +} + +.filterExpandedGrid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 12px 20px; + padding-top: 12px; + border-top: 1px solid #e5e6eb; +} + +.filterField { + display: flex; + align-items: center; + gap: 10px; + min-width: 0; +} + +.filterLabel { + flex: 0 0 80px; + color: #4e5969; + font-size: 14px; + font-weight: 500; + line-height: 20px; +} + +.filterControl { + flex: 1; + min-width: 0; +} + +.filterControl :global(.mantine-InputWrapper-root) { + width: 100%; +} + +.filterControl :global(.mantine-PillsInput-root) { + width: 100%; +} + +.filterControl :global(.mantine-InputWrapper-label) { + display: none; +} + +.filterActions { + display: inline-flex; + justify-self: end; + align-items: center; +} + +.collapseToggle { + align-self: center; + justify-self: start; + min-width: auto; + padding-inline: 0; +} + +.contentPanelBody { + flex: 1; + min-height: 0; + display: flex; + flex-direction: column; + gap: 12px; +} + +.tableActionBar { + padding: 4px 16px 0; +} + +.operationGroup { + display: inline-flex; + align-items: center; + gap: 8px; + white-space: nowrap; +} + +.operationLink { + border: 0; + padding: 0; + background: transparent; + color: #1874ff; + font-size: 12px; + line-height: 20px; + cursor: pointer; + transition: + color 0.2s ease, + opacity 0.2s ease; +} + +.operationLinkDanger { + color: #f53f3f; +} + +.operationLinkDisabled { + color: #c9cdd4; + cursor: not-allowed; +} + +.operationDivider { + width: 1px; + height: 12px; + background: #e5e6eb; +} + +.statusBadge { + min-height: 24px; + padding: 0 10px; + border: 0; + border-radius: 999px; + font-size: 12px; + font-weight: 500; +} + +.statusPositive { + background: #e8ffea; + color: #00b42a; +} + +.statusNegative { + background: #ffece8; + color: #f53f3f; +} + +.ellipsisText { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +@media (max-width: 1200px) { + .filterTopRow { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .filterActions { + justify-self: stretch; + } + + .filterExpandedGrid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (max-width: 720px) { + .filterTopRow, + .filterExpandedGrid { + grid-template-columns: 1fr; + } + + .filterField { + flex-direction: column; + align-items: flex-start; + gap: 6px; + } + + .filterLabel { + flex-basis: auto; + } +} diff --git a/app/person/report/page.tsx b/app/person/report/page.tsx index 1e3e61f..f7a5767 100644 --- a/app/person/report/page.tsx +++ b/app/person/report/page.tsx @@ -17,7 +17,6 @@ import { SettingPage, } from "@/components/setting/PageSurface" import { - ActionIcon, Badge, Button, Collapse, @@ -25,7 +24,6 @@ import { Modal, MultiSelect, Select, - SimpleGrid, Stack, Text, TextInput, @@ -35,11 +33,9 @@ import { notifications } from "@mantine/notifications" import { IconChevronDown, IconChevronUp, - IconPencil, IconPlus, IconRefresh, IconSearch, - IconTrash, } from "@tabler/icons-react" import { DateInput } from "@mantine/dates" @@ -58,6 +54,7 @@ import { setTypeOpts, workTypeOpts, } from "./config" +import classes from "./page.module.css" type LeaderTreeNode = { title?: string @@ -114,6 +111,21 @@ function toNumber(value?: string) { const DEFAULT_RECORDS_PER_PAGE_OPTIONS = [10, 15, 20, 50] +function InlineFilterField({ + label, + children, +}: { + label: string + children: React.ReactNode +}) { + return ( +
+ {label} +
{children}
+
+ ) +} + export default function PersonReportPage() { const treeData = useAllUserStore((s) => s.treeData) @@ -342,12 +354,8 @@ export default function PersonReportPage() { render: (record) => ( + className={classes.ellipsisText} + style={{ maxWidth: 220 }}> {record.project_name ?? "-"} ), @@ -358,9 +366,19 @@ export default function PersonReportPage() { width: 110, render: (record) => { if (record.performance_accounting === true) - return {performanceTextMap.get(true)} + return ( + + {performanceTextMap.get(true)} + + ) if (record.performance_accounting === false) - return {performanceTextMap.get(false)} + return ( + + {performanceTextMap.get(false)} + + ) return "-" }, }, @@ -419,12 +437,8 @@ export default function PersonReportPage() { render: (record) => ( + className={classes.ellipsisText} + style={{ maxWidth: 200 }}> {record.remarks ?? "-"} ), @@ -437,47 +451,54 @@ export default function PersonReportPage() { render: (record) => { const disabled = !!record.lock_status return ( - - { - if (disabled) return - setEditingRecord(record) - setModalOpened(true) - }}> - - - { - if (disabled || !record.id) return - modals.openConfirmModal({ - title: "删除日报", - centered: true, - children: ( - - 删除后不可恢复,你确定删除该日报吗? - - ), - labels: { confirm: "删除", cancel: "取消" }, - confirmProps: { color: "red" }, - onConfirm: async () => { - await deleteDailyWorkData(record.id as number) - notifications.show({ - color: "green", - title: "操作成功", - message: "已删除日报", - }) - load() - }, - }) - }}> - - + +
+ + + +
) }, @@ -509,13 +530,55 @@ export default function PersonReportPage() { return ( + + + 个人工作台 / 日报管理 /{" "} + 我的日报 + + + - - - - 筛选条件 + +
+ + + setFilters((s) => ({ + ...s, + date_start: dayjs(e).format("YYYY-MM-DD"), + })) + } + /> + + + { + setFilters((s) => ({ + ...s, + date_end: dayjs(e).format("YYYY-MM-DD"), + })) + }} + /> + + + setFilters((s) => ({ ...s, project_id: v }))} + /> + + + - - - - +
- - - setFilters((s) => ({ - ...s, - date_start: dayjs(e).format("YYYY-MM-DD"), - })) - } - /> - { - setFilters((s) => ({ - ...s, - date_end: dayjs(e).format("YYYY-MM-DD"), - })) - }} - /> - setFilters((s) => ({ ...s, project_id: v }))} - /> - setFilters((s) => ({ ...s, leader_uid: v }))} - /> - - setFilters((s) => ({ ...s, actual_type: value ?? "" })) - } - /> - - setFilters((s) => ({ ...s, accounting_type: value ?? "" })) - } - /> - - setFilters((s) => ({ ...s, obj_type: value ?? "" })) - } - /> - +
+ + setFilters((s) => ({ ...s, leader_uid: v }))} + /> + + + + setFilters((s) => ({ ...s, actual_type: value ?? "" })) + } + /> + + + + setFilters((s) => ({ + ...s, + accounting_type: value ?? "", + })) + } + /> + + + + setFilters((s) => ({ ...s, obj_type: value ?? "" })) + } + /> + +
@@ -663,81 +711,87 @@ export default function PersonReportPage() { style={{ flex: 1, minHeight: 0, + padding: 0, display: "flex", flexDirection: "column", - gap: 12, }}> - - - - - } - /> - - - width="100%" - minHeight={0} - style={{ width: "100%", flex: 1 }} - pinFirstColumn - pinLastColumn - scrollAreaProps={{ type: "auto" }} - fetching={loading} - records={records} - columns={columns} - totalRecords={totalItems} - recordsPerPage={pageSize} - page={page} - onPageChange={setPage} - onRecordsPerPageChange={(v) => { - setPageSize(v) - setPage(1) - }} - recordsPerPageOptions={recordsPerPageOptions} - sortStatus={sortStatus} - onSortStatusChange={(s) => { - setSortStatus(s) - setPage(1) - }} - renderPagination={({ Controls }) => ( - - - - +
+
+ + + + } + /> +
+ + + width="100%" + minHeight={0} + style={{ width: "100%", flex: 1 }} + pinFirstColumn + pinLastColumn + scrollAreaProps={{ type: "auto" }} + fetching={loading} + records={records} + columns={columns} + totalRecords={totalItems} + recordsPerPage={pageSize} + page={page} + paginationText={({ from, to, totalRecords }) => + `显示 ${from}-${to} 条,共 ${totalRecords} 条记录` + } + onPageChange={setPage} + onRecordsPerPageChange={(v) => { + setPageSize(v) + setPage(1) + }} + recordsPerPageOptions={recordsPerPageOptions} + sortStatus={sortStatus} + onSortStatusChange={(s) => { + setSortStatus(s) + setPage(1) + }} + renderPagination={({ Controls }) => ( + + + + + + + - - - )} - /> - {/* + )} + /> + {/* 当前页总计 @@ -755,7 +809,8 @@ export default function PersonReportPage() { */} - + +
取消 - +
diff --git a/app/theme.ts b/app/theme.ts index 2b0888f..dfebec0 100644 --- a/app/theme.ts +++ b/app/theme.ts @@ -3,16 +3,16 @@ import { Button, createTheme, MantineColorsTuple } from "@mantine/core" /* -------- 1. 自定义颜色(可删/可改) -------- */ const brandColors: MantineColorsTuple = [ - "#E8F4FC", - "#CEECFD", - "#A7DAFF", - "#69C0FF", - "#4ABAFF", - "#09ADFF", - "#00A1FF", - "#168DE8", - "#0D73D0", - "#0256A4", + "#E8F3FF", + "#D6E8FF", + "#B8D5FF", + "#8DBAFF", + "#5C9AFF", + "#3283FF", + "#1874FF", + "#0F68EB", + "#0D59C9", + "#0B4AA6", ] const greyColors: MantineColorsTuple = [ diff --git a/components/label/components/Icon.tsx b/components/label/components/Icon.tsx new file mode 100644 index 0000000..404c552 --- /dev/null +++ b/components/label/components/Icon.tsx @@ -0,0 +1,946 @@ +import { SVGProps } from "react" + +export type IconSvgProps = SVGProps & { + size?: number +} + +export const Logo: React.FC = () => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +) + +export const LoginUserIcon = () => ( + + + +) + +export const LoginPasswordIcon = () => ( + + + + +) + +export const LoginCodeIcon = () => ( + + + +) + +export const DashboardMenuIcon = () => ( + + + + + + + + + + +) + +export const TaskMenuIcon = () => ( + + + + + + + + + + +) + +export const DeviceMenuIcon = () => ( + + + + + + + + + + + + +) + +export const CountMenuIcon = () => ( + + + +) + +export const UserMenuIcon = () => ( + + + + + + + + + + +) + +export const AuthMenuIcon = () => ( + + + + + + + + + + + +) + +export const PersonalMenuIcon = () => ( + + + +) + +export const DevLogo = ({ ...props }) => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +) + +export const ExportIcon = () => ( + + + +) + +export const BasicInfoIcon = () => ( + + + +) + +export const BankCardIcon = () => ( + + + + +) + +export const BusinessCardIcon = () => ( + + + + + + + + + + +) + +export const CollectionCardIcon = () => ( + + + + +) + +export const BillRecordCardIcon = () => ( + + + + +) + +export const ToolTipIcon = () => ( + + + +) + +export const IDLogoIcon = () => ( + + + + +) + +export const ChevronLeftIcon: React.FC = ({ ...props }) => ( + + + +) + +export const DesktopIcon: React.FC = ({ ...props }) => ( + + + +) + +export const LabelStatusIcon: React.FC = ({ ...props }) => ( + + + +) + +export const LabelStatusWaitIcon = ({ ...props }) => ( + + + +) + +// 待验收 +export const LabelStatusAcceptanceIcon = ({ ...props }) => ( + + + +) + +// 审核失败 +export const LabelStatusFailIcon = ({ ...props }) => ( + + + +) + +// 试标 +export const LabelPretestIcon = () => ( + + + +) + +export const LabelStatusStopIcon = ({ ...props }) => ( + + + +) + +export const InfoCircleIcon = ({ ...props }) => ( + + + +) + +/************************* 标注状态icon ****************************/ +// 待领取 +export const TaskIcon1 = ({ ...props }) => ( + + + +) + +// 标注中 +export const TaskIcon2 = ({ ...props }) => ( + + + +) + +// 标注完成 +export const TaskIcon3 = ({ ...props }) => ( + + + +) + +// 审核中 +export const TaskIcon4 = ({ ...props }) => ( + + + +) + +// 审核完成 +export const TaskIcon5 = ({ ...props }) => ( + + + +) + +// 复审中 / 复审完成 +export const TaskIcon6 = ({ ...props }) => ( + + + +) + +// 无法标注 +export const TaskIcon7 = ({ ...props }) => ( + + + +) + +export const DispatchTaskIcon = () => ( + + + +) + +export const FreeTaskIcon = () => ( + + + +) diff --git a/components/label/components/TaskStatusTag.module.css b/components/label/components/TaskStatusTag.module.css new file mode 100644 index 0000000..bb1474e --- /dev/null +++ b/components/label/components/TaskStatusTag.module.css @@ -0,0 +1,81 @@ +.root { + display: inline-flex; + max-width: 100%; + align-items: center; + gap: 6px; + flex-wrap: wrap; +} + +.center { + width: 100%; + justify-content: center; +} + +.statusPill, +.rejectedPill { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 4px; + border-radius: 4px; + border: 1px solid transparent; + white-space: nowrap; + transition: + color 0.33s ease, + border-color 0.33s ease, + background-color 0.33s ease, + opacity 0.33s ease; +} + +.statusPill { + min-height: 24px; + padding: 0 8px; + font-size: 12px; + font-weight: 500; + line-height: 18px; +} + +.statusText, +.rejectedText { + font-size: 12px; + line-height: 20px; +} + +.statusWaiting { + border-color: #e7d2a7; + background: #fbf7ee; + color: #92641e; +} + +.statusActive { + border-color: rgba(62, 106, 225, 0.18); + background: rgba(62, 106, 225, 0.08); + color: #3e6ae1; +} + +.statusComplete { + border-color: #e4e8ee; + background: #f7f8fa; + color: #394150; +} + +.statusDanger { + border-color: #edced2; + background: #fbf1f2; + color: #a33d4b; +} + +.rejectedPill { + min-height: 20px; + padding: 0 7px; + border-color: #e8c98a; + background: #fff8ed; + color: #a46b12; + font-size: 11px; + font-weight: 500; + line-height: 16px; +} + +.statusPill :global(svg) { + flex: none; +} diff --git a/components/label/components/TaskStatusTag.tsx b/components/label/components/TaskStatusTag.tsx new file mode 100644 index 0000000..db3515c --- /dev/null +++ b/components/label/components/TaskStatusTag.tsx @@ -0,0 +1,84 @@ +"use client" + +import { Box, Text } from "@mantine/core" +import { isValidElement, useMemo } from "react" +import { TaskStatusEnum } from "../utils/constants" +import { + TaskIcon1, + TaskIcon2, + TaskIcon3, + TaskIcon4, + TaskIcon5, + TaskIcon6, + TaskIcon7, +} from "./Icon" +import classes from "./TaskStatusTag.module.css" + +interface ComponentProps { + status: number + rejected?: boolean + center?: boolean +} + +const statusToneClassMap: Record = { + 1: classes.statusWaiting, + 2: classes.statusActive, + 3: classes.statusComplete, + 4: classes.statusActive, + 5: classes.statusComplete, + 6: classes.statusActive, + 7: classes.statusComplete, + 8: classes.statusDanger, +} + +const TaskStatusTag = ({ status, rejected, center }: ComponentProps) => { + const text = TaskStatusEnum.get(status) || "默认" + const icon = useMemo(() => { + switch (status) { + case 1: + return + case 2: + return + case 3: + return + case 4: + return + case 5: + return + case 6: + case 7: + return + case 8: + return + default: + return null + } + }, [status]) + + const containerClassName = center + ? `${classes.root} ${classes.center}` + : classes.root + const statusClassName = `${classes.statusPill} ${ + statusToneClassMap[status] ?? classes.statusComplete + }` + + return ( + + + {isValidElement(icon) ? icon : null} + + {text} + + + {rejected ? ( + + + 返工 + + + ) : null} + + ) +} + +export default TaskStatusTag diff --git a/components/layout/AppLayout.module.css b/components/layout/AppLayout.module.css new file mode 100644 index 0000000..1eb0175 --- /dev/null +++ b/components/layout/AppLayout.module.css @@ -0,0 +1,81 @@ +.header { + border-bottom: 1px solid #e5e6eb; + background: #ffffff; +} + +.headerInner { + height: 100%; + padding: 0 16px; +} + +.brand { + display: flex; + width: 196px; + align-items: center; + gap: 8px; + color: #1d2129; + font-size: 14px; + font-weight: 500; + line-height: 22px; +} + +.brandMark { + display: inline-flex; + width: 20px; + height: 20px; + align-items: center; + justify-content: center; + border-radius: 4px; + background: #1874ff; + color: #ffffff; + font-size: 11px; + font-weight: 600; +} + +.headerNav { + display: flex; + flex: 1; + align-items: center; + gap: 8px; + height: 100%; +} + +.headerUser { + display: flex; + align-items: center; + gap: 6px; +} + +.navbar { + border-right: 1px solid #e5e6eb; + background: #ffffff; + transition: width 0.2s ease; +} + +.navbarScroll { + height: calc(100vh - 108px); + padding: 8px 8px 0; +} + +.navbarFooter { + border-top: 1px solid #e5e6eb; + padding: 6px 0 0; +} + +.main { + background: #f7f8fa; +} + + +.userName { + color: #1d2129; + font-size: 14px; + font-weight: 500; + line-height: 18px; +} + +.userBaseCity { + color: #86909c; + font-size: 12px; + line-height: 14px; +} \ No newline at end of file diff --git a/components/layout/AppLayout.tsx b/components/layout/AppLayout.tsx index 9ec9b02..17d82fd 100644 --- a/components/layout/AppLayout.tsx +++ b/components/layout/AppLayout.tsx @@ -20,7 +20,12 @@ import { import Avvvatars from "avvvatars-react" import { useDisclosure, useMediaQuery } from "@mantine/hooks" -import { IconLogout, IconMoon, IconSun } from "@tabler/icons-react" +import { + IconChevronDown, + IconLogout, + IconMoon, + IconSun, +} from "@tabler/icons-react" import cx from "clsx" import { Suspense, useEffect, useMemo, useState } from "react" import actionToggleClasses from "./ActionToggle.module.css" @@ -30,9 +35,11 @@ import { useUserStore } from "@/app/store/user" import { usePathname, useRouter } from "next/navigation" import { usePermissionStore } from "../label/store/auth" import { deleteCookie } from "../login/api" +import classes from "./AppLayout.module.css" import { MenuItem, withoutLayoutRoutes } from "./common" import { ClientIcon } from "./components/ClientIcon" import { LinksGroup } from "./components/NavbarLinks" +import { getShellNavLinkStyles } from "./navStyles" import { useAppLayoutStore } from "./store" const COMPOUND_SURNAMES = new Set([ @@ -182,7 +189,7 @@ export default function AppLayout({ window.location.href = toPathname // router.replace(toPathname) }}> - + {item.title} )) @@ -243,6 +250,7 @@ export default function AppLayout({ } const matches = useMediaQuery("(min-width: 48em)") const userName = usePermissionStore((s) => s.user_name) + const detailInfo = usePermissionStore((s) => s.detailInfo) const avatarDisplayValue = useMemo( () => getAvatarDisplayValue(userName), [userName] @@ -253,28 +261,32 @@ export default function AppLayout({ }>{children} ) : ( - - + + - - + + + C 标注平台 {headItems} - + @@ -301,12 +313,28 @@ export default function AppLayout({ - + - + + {userName} + + {detailInfo?.base_city || "-"} + + + + 用户信息 @@ -327,9 +355,9 @@ export default function AppLayout({ - + - + {activeHeaderMenu?.items?.map((item) => { if (item.items?.length) { if (matches) { @@ -359,11 +387,12 @@ export default function AppLayout({ item.icon && ( ) } label={item.title} + styles={getShellNavLinkStyles(false)} /> @@ -390,6 +419,10 @@ export default function AppLayout({ ) } } else { + const isActive = pathname.startsWith( + `/${activeHeaderMenu.url}/${item.url}` + ) + return ( } label={item.title} + styles={getShellNavLinkStyles(isActive)} /> ) @@ -427,7 +459,7 @@ export default function AppLayout({ })} - + - + }>{children} diff --git a/components/layout/HeaderLink.module.css b/components/layout/HeaderLink.module.css index 0b77fc7..3cb0256 100644 --- a/components/layout/HeaderLink.module.css +++ b/components/layout/HeaderLink.module.css @@ -1,27 +1,30 @@ .link { display: flex; - height: 100%; + height: 40px; justify-content: center; align-items: center; - padding: 8px 16px; - gap: 0.5rem; + padding: 0 14px; + gap: 6px; text-decoration: none; - color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-dark-0)); - font-size: var(--mantine-font-size-lg); + border-radius: 4px; + color: #1d2129; + font-size: 16px; font-weight: 500; + line-height: 22px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + transition: + background-color 0.2s ease, + color 0.2s ease; @mixin hover { - background-color: light-dark( - var(--mantine-color-gray-0), - var(--mantine-color-dark-6) - ); + background-color: #eef4ff; + color: #1874ff; } - [data-mantine-color-scheme] &[data-active] { - background-color: var(--mantine-primary-color-light); - color: var(--mantine-primary-color-light-color); + &[data-active] { + background-color: #1874ff; + color: #ffffff; } } diff --git a/components/layout/components/NavbarLinks.tsx b/components/layout/components/NavbarLinks.tsx index 98ae795..58b07e2 100644 --- a/components/layout/components/NavbarLinks.tsx +++ b/components/layout/components/NavbarLinks.tsx @@ -5,6 +5,7 @@ import { NavLink, Space } from "@mantine/core" import { usePathname, useRouter } from "next/navigation" import { useState } from "react" import { MenuItem } from "../common" +import { getShellNavLinkStyles } from "../navStyles" import { ClientIcon } from "./ClientIcon" interface LinksGroupProps { @@ -50,6 +51,10 @@ export function LinksGroup({ )) : null + const isActive = hasLinks + ? false + : pathname.startsWith(`/${routerUrl}/${item.url}`) + return ( <> @@ -77,7 +80,8 @@ export function LinksGroup({ )} } - childrenOffset={0}> + childrenOffset={0} + styles={getShellNavLinkStyles(isActive)}> {children} diff --git a/components/layout/navStyles.ts b/components/layout/navStyles.ts new file mode 100644 index 0000000..b6fc890 --- /dev/null +++ b/components/layout/navStyles.ts @@ -0,0 +1,29 @@ +export function getShellNavLinkStyles(active: boolean) { + return { + root: { + minHeight: 40, + borderRadius: 4, + padding: "8px 12px", + color: active ? "#1874FF" : "#4E5969", + backgroundColor: active ? "#E7EFFF" : "transparent", + transition: "background-color 0.2s ease, color 0.2s ease", + }, + body: { + gap: 8, + }, + label: { + fontSize: 14, + fontWeight: 500, + lineHeight: "22px", + }, + section: { + color: active ? "#1874FF" : "inherit", + }, + chevron: { + color: active ? "#1874FF" : "#86909C", + }, + children: { + paddingLeft: 0, + }, + } as const +} diff --git a/components/setting/PageSurface.module.css b/components/setting/PageSurface.module.css index dcfb36d..55b1248 100644 --- a/components/setting/PageSurface.module.css +++ b/components/setting/PageSurface.module.css @@ -1,15 +1,16 @@ .page { width: 100%; - height: calc(100vh - 56px); + height: calc(100vh - 60px); padding: 16px; - gap: 16px; + gap: 12px; + background: #f7f8fa; } .panel { - border-color: #e6ebf2; - border-radius: 12px; + border-color: #e5e6eb; + border-radius: 4px; background: #ffffff; - box-shadow: 0 10px 30px rgba(15, 23, 42, 0.05); + box-shadow: none; } .filterPanel { @@ -17,28 +18,40 @@ } .filterPanel :global(.mantine-InputWrapper-label) { - color: #1f2329; + color: #4e5969; font-size: 14px; font-weight: 500; - line-height: 22px; - margin-bottom: 6px; + line-height: 20px; + margin-bottom: 4px; } .filterPanel :global(.mantine-Input-input), .filterPanel :global(.mantine-Select-input), +.filterPanel :global(.mantine-DateInput-input), .filterPanel :global(.mantine-NumberInput-input), -.filterPanel :global(.mantine-PillsInput-input), +.filterPanel :global(.mantine-PillsInput-root), .filterPanel :global(.mantine-Textarea-input) { - min-height: 36px; - border-color: #d0d5dd; - background: #ffffff; - color: #31373d; + min-height: 32px; + border-color: #e5e6eb; + border-radius: 4px; + background: #f7f8fa; + color: #1d2129; font-size: 14px; box-shadow: none; transition: - border-color 0.15s ease, - box-shadow 0.15s ease, - background-color 0.15s ease; + border-color 0.2s ease, + color 0.2s ease, + background-color 0.2s ease; +} + +.filterPanel :global(.mantine-PillsInput-root) { + padding-inline: 12px; +} + +.filterPanel :global(.mantine-PillsInput-input) { + min-height: 30px; + background: transparent; + color: #1d2129; } .filterPanel :global(.mantine-Textarea-input) { @@ -47,82 +60,246 @@ .filterPanel :global(.mantine-Input-input::placeholder), .filterPanel :global(.mantine-Select-input::placeholder), +.filterPanel :global(.mantine-DateInput-input::placeholder), .filterPanel :global(.mantine-NumberInput-input::placeholder), .filterPanel :global(.mantine-PillsInput-input::placeholder), .filterPanel :global(.mantine-Textarea-input::placeholder) { - color: #98a2b3; + color: #86909c; } .filterPanel :global(.mantine-Input-input:focus), .filterPanel :global(.mantine-Select-input:focus), +.filterPanel :global(.mantine-DateInput-input:focus), .filterPanel :global(.mantine-NumberInput-input:focus), -.filterPanel :global(.mantine-PillsInput-input:focus-within), +.filterPanel :global(.mantine-PillsInput-root:focus-within), .filterPanel :global(.mantine-Textarea-input:focus) { - border-color: #69c0ff; - box-shadow: 0 0 0 3px rgba(105, 192, 255, 0.14); + border-color: #1874ff; + background: #ffffff; + box-shadow: 0 0 0 2px rgba(24, 116, 255, 0.12); } .filterPanel :global(.mantine-Button-root[data-variant="transparent"]) { - color: #168de8; + min-height: auto; + height: auto; + padding-inline: 0; + color: #1874ff; } -.filterPanel :global(.mantine-Button-root:not([data-variant="transparent"])), +.filterPanel :global(.mantine-Button-root), .headerActions :global(.mantine-Button-root) { - min-height: 36px; - border-radius: 8px; - padding-inline: 14px; + min-height: 32px; + border-radius: 4px; + padding-inline: 12px; font-size: 14px; font-weight: 500; + box-shadow: none; + transition: + background-color 0.2s ease, + border-color 0.2s ease, + color 0.2s ease; +} + +.filterPanel :global(.mantine-Button-root[data-variant="filled"]), +.headerActions :global(.mantine-Button-root[data-variant="filled"]) { + --button-bg: #1874ff; + --button-hover: #0f6ae8; + --button-color: #ffffff; + --button-bd: transparent; + background: #1874ff !important; + color: #ffffff !important; +} + +.filterPanel :global(.mantine-Button-root[data-variant="filled"]:hover), +.headerActions :global(.mantine-Button-root[data-variant="filled"]:hover) { + background: #0f6ae8 !important; +} + +.filterPanel :global(.mantine-Button-root[data-variant="default"]), +.headerActions :global(.mantine-Button-root[data-variant="default"]) { + border-color: #e5e6eb; + background: #ffffff; + color: #4e5969; } .filterPanel :global(.mantine-ActionIcon-root), .headerActions :global(.mantine-ActionIcon-root) { - width: 36px; - height: 36px; - border-radius: 8px; + width: 32px; + height: 32px; + border-radius: 4px; } .filterPanel :global(.mantine-Pill-root) { - background: #eef7ff; - color: #168de8; + background: #e7efff; + color: #1874ff; +} + +.filterPanel :global(.mantine-PillsInput-field), +.filterPanel :global(.mantine-Select-section), +.filterPanel :global(.mantine-DateInput-section) { + color: #86909c; } .contentPanel { - border-color: #e6ebf2; + border-color: #e5e6eb; +} + +.contentPanel :global(.mantine-datatable-pagination) { + padding: 12px 16px !important; + color: #4e5969; +} + +.contentPanel :global(.mantine-datatable-pagination-text), +.contentPanel :global(.mantine-datatable-pagination .mantine-Text-root) { + color: #4e5969; + font-size: 12px; + line-height: 20px; +} + +.contentPanel + :global(.mantine-datatable-pagination button[data-variant="default"]), +.contentPanel + :global(.mantine-datatable-pagination button[data-variant="light"]) { + min-height: 28px !important; + height: 28px !important; + padding-inline: 10px !important; + border-radius: 4px !important; + border-color: #e5e6eb; + background: #ffffff; + color: #4e5969; + font-size: 12px; + box-shadow: none; +} + +.contentPanel + :global(.mantine-datatable-pagination button[data-variant="default"]:hover), +.contentPanel + :global(.mantine-datatable-pagination button[data-variant="light"]:hover) { + background: #f7f8fa; + color: #1d2129; +} + +.contentPanel :global(.mantine-datatable-pagination-pages) { + --pagination-control-size: 28px; + --pagination-control-fz: 12px; +} + +.contentPanel :global(.mantine-Pagination-dots) { + color: #86909c; +} + +.contentPanel :global(.mantine-Pagination-control), +.contentPanel :global(.mantine-datatable-pagination-pages-control) { + min-width: 28px !important; + height: 28px !important; + border: 0; + border-radius: 4px !important; + background: transparent; + color: #4e5969; + box-shadow: none; +} + +.contentPanel :global(.mantine-Pagination-control:hover), +.contentPanel :global(.mantine-datatable-pagination-pages-control:hover) { + background: #f2f3f5; + color: #1d2129; +} + +.contentPanel :global(.mantine-Pagination-control[data-active]), +.contentPanel + :global(.mantine-datatable-pagination-pages-control[data-active]) { + background: #e7efff; + border-color: transparent; + color: #1874ff; +} + +.contentPanel :global(.mantine-Pagination-control[data-active]:hover), +.contentPanel + :global(.mantine-datatable-pagination-pages-control[data-active]:hover) { + background: #dbe8ff; + border-color: transparent; + color: #1874ff; +} + +.contentPanel :global(.mantine-Pagination-control[data-disabled]), +.contentPanel + :global(.mantine-datatable-pagination-pages-control[data-disabled]) { + background: transparent; + color: #c9cdd4; } .filterActions, .headerActions { - gap: 12px; + gap: 8px; } .listHeader { - margin-bottom: 16px; + margin-bottom: 12px; } .sectionHeader { padding-bottom: 12px; - border-bottom: 1px solid #eef2f6; + border-bottom: 1px solid #e5e6eb; } .sectionEyebrow { - color: #168de8; + color: #1874ff; font-size: 12px; - font-weight: 700; + font-weight: 500; line-height: 18px; letter-spacing: 0.08em; text-transform: uppercase; } .sectionTitle { - color: #1f2329; - font-size: 18px; - font-weight: 700; - line-height: 28px; + color: #1d2129; + font-size: 16px; + font-weight: 500; + line-height: 24px; } .sectionDescription { - color: #667085; + color: #86909c; font-size: 13px; line-height: 20px; } + +.listHeaderTitle { + color: #1d2129; + font-size: 14px; + font-weight: 500; + line-height: 22px; +} + +.listHeaderCount { + color: #86909c; + font-size: 12px; + line-height: 20px; +} + +.settingTabsList { + padding: 4px; + gap: 8px; + border: 1px solid #eef2f6; + border-radius: 10px; + background: #f8f9fb; +} + +.settingTabsTab { + min-height: 36px; + border-radius: 8px; + color: #56606a; + font-weight: 600; +} + +.settingTabsTab[data-active] { + background: #ffffff; + color: #1f2329; + box-shadow: 0 6px 18px rgba(15, 23, 42, 0.08); +} + +.settingTabsPanel { + flex: 1; + min-width: 0; + min-height: 0; + padding-top: 16px; +} diff --git a/components/setting/PageSurface.tsx b/components/setting/PageSurface.tsx index 65a06df..690c1b7 100644 --- a/components/setting/PageSurface.tsx +++ b/components/setting/PageSurface.tsx @@ -1,7 +1,6 @@ "use client" import { - Badge, Group, Paper, Stack, @@ -29,7 +28,7 @@ export function SettingPage({ return ( ) { return ( @@ -140,11 +139,9 @@ export function SettingListHeader({ gap="sm" className={mergeClassName(classes.listHeader, className)}> - {title} + {title} {count !== undefined ? ( - - {count} - + {count} ) : null} {actions} @@ -188,25 +185,33 @@ export function SettingFilterActions({ const settingDataTableStyles = { header: { - backgroundColor: "#F8F9FB", - borderBottom: "1px solid #EEF2F6", - color: "#56606A", + backgroundColor: "#F7F8FA", + borderBottom: "1px solid #E5E6EB", + color: "#4E5969", fontSize: "14px", + fontWeight: 500, }, root: { height: "100%", maxWidth: "100%", minWidth: 0, fontSize: "14px", - color: "#31373D", - borderBottom: "1px solid #E8EDF3", - borderRadius: 10, + color: "#1D2129", + borderBottom: "1px solid #E5E6EB", + borderRadius: 4, overflow: "hidden", boxSizing: "border-box", backgroundColor: "#FFFFFF", }, + table: { + backgroundColor: "#FFFFFF", + }, + footer: { + backgroundColor: "#FFFFFF", + }, pagination: { - padding: "14px 16px 18px", + padding: "12px 16px", + borderTop: "1px solid #E5E6EB", }, } as const @@ -215,7 +220,7 @@ const settingDataTableDefaultColumnProps: DataTableDefaultColumnProps< > = { titleStyle: { whiteSpace: "nowrap", - fontWeight: 600, + fontWeight: 500, }, } @@ -270,12 +275,12 @@ export function SettingDataTable>( const tableProps = { withTableBorder: withTableBorder ?? true, withRowBorders: withRowBorders ?? true, - striped: striped ?? true, - highlightOnHover: highlightOnHover ?? true, + striped: striped ?? false, + highlightOnHover: highlightOnHover ?? false, defaultColumnProps: defaultColumnProps ?? (settingDataTableDefaultColumnProps as DataTableDefaultColumnProps), - loaderBackgroundBlur: loaderBackgroundBlur ?? 1, + loaderBackgroundBlur: loaderBackgroundBlur ?? 0, noRecordsText: noRecordsText ?? "暂无数据", minHeight: minHeight ?? 360, styles: styles @@ -301,11 +306,6 @@ export const settingTabsStyles = { 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, @@ -314,3 +314,9 @@ export const settingTabsStyles = { paddingTop: 16, }, } as const + +export const settingTabsClassNames = { + list: classes.settingTabsList, + tab: classes.settingTabsTab, + panel: classes.settingTabsPanel, +} as const diff --git a/next.config.ts b/next.config.ts index 078ab2e..263d12a 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,13 +1,19 @@ import type { NextConfig } from "next" import { subConfigList } from "./components/layout/common" +const isDevelopment = process.env.NODE_ENV === "development" + const nextConfig: NextConfig = { /* config options here */ basePath: process.env.NEXT_PUBLIC_BASE_PATH, - output: process.env.NEXT_PUBLIC_OUTPUT_TYPE as - | "standalone" - | "export" - | undefined, + // Keep build-time output selection for production builds, but avoid + // surfacing export/middleware warnings in `next dev`. + output: isDevelopment + ? undefined + : (process.env.NEXT_PUBLIC_OUTPUT_TYPE as + | "standalone" + | "export" + | undefined), async rewrites() { // 应用代理baseAppProxy const repoProxy = subConfigList.map(({ source, url }) => {