diff --git a/app/person/report/components/DailyReportModal.module.css b/app/person/report/components/DailyReportModal.module.css new file mode 100644 index 0000000..92add3b --- /dev/null +++ b/app/person/report/components/DailyReportModal.module.css @@ -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; +} diff --git a/app/person/report/components/DailyReportModal.tsx b/app/person/report/components/DailyReportModal.tsx index 0d3a034..43d5e27 100644 --- a/app/person/report/components/DailyReportModal.tsx +++ b/app/person/report/components/DailyReportModal.tsx @@ -14,6 +14,7 @@ import { Group, Modal, NumberInput, + Paper, Select, SimpleGrid, Stack, @@ -23,6 +24,7 @@ import { import { useForm } from "@mantine/form" import { notifications } from "@mantine/notifications" import { useEffect, useMemo } from "react" +import classes from "./DailyReportModal.module.css" import { accountTypeOpts, actualTypeOpts, @@ -332,213 +334,181 @@ export default function DailyReportModal(props: { title={record ? "编辑日报" : "填写日报"} size={840} centered + classNames={{ + content: classes.modalContent, + header: classes.modalHeader, + title: classes.modalTitle, + body: classes.modalBody, + }} + overlayProps={{ backgroundOpacity: 0.22 }} + transitionProps={{ + transition: "fade", + duration: 100, + timingFunction: "ease", + }} closeOnClickOutside={false}>
- - - - -
+ + + + + +
- - -
+ + +
- -
+ +
- - + - - + - } - rightSectionWidth={78} - {...form.getInputProps("work_time")} - /> - -
+ } + rightSectionWidth={78} + {...form.getInputProps("work_time")} + /> + +
- - -
+ + +
- - } - rightSectionWidth={78} - /> - - + + } + rightSectionWidth={78} + /> + + - + - - - - - + + + + + + ) diff --git a/app/person/report/page.tsx b/app/person/report/page.tsx index 1c4aefd..4f69633 100644 --- a/app/person/report/page.tsx +++ b/app/person/report/page.tsx @@ -7,15 +7,24 @@ import { import { Daily } from "@/components/label/api/daily/typing" import { getSelfProjectList } from "@/components/label/api/project" import { useAllUserStore } from "@/components/label/store/auth" +import { + SettingContentPanel, + SettingDataTable, + SettingFilterActions, + SettingFilterPanel, + SettingHeaderActions, + SettingListHeader, + SettingPage, + SettingPanel, +} from "@/components/setting/PageSurface" import { ActionIcon, Badge, Button, Collapse, - Flex, Group, + Modal, MultiSelect, - Paper, SimpleGrid, Stack, Text, @@ -34,11 +43,7 @@ import { } from "@tabler/icons-react" import dayjs from "dayjs" -import { - DataTable, - DataTableColumn, - DataTableSortStatus, -} from "mantine-datatable" +import { DataTableColumn, DataTableSortStatus } from "mantine-datatable" import { useCallback, useEffect, useMemo, useState } from "react" import DailyReportModal from "./components/DailyReportModal" import { @@ -105,6 +110,8 @@ function toNumber(value?: string) { return Number.isFinite(n) ? n : undefined } +const DEFAULT_RECORDS_PER_PAGE_OPTIONS = [10, 15, 20, 50] + export default function PersonReportPage() { const treeData = useAllUserStore((s) => s.treeData) @@ -137,6 +144,12 @@ export default function PersonReportPage() { const [page, setPage] = useState(1) const [pageSize, setPageSize] = useState(20) + const [recordsPerPageOptions, setRecordsPerPageOptions] = useState( + () => [...DEFAULT_RECORDS_PER_PAGE_OPTIONS] + ) + const [customPageSizeInput, setCustomPageSizeInput] = useState("") + const [customPageSizeModalOpened, setCustomPageSizeModalOpened] = + useState(false) const [totalItems, setTotalItems] = useState(0) const [loading, setLoading] = useState(false) const [records, setRecords] = useState([]) @@ -229,6 +242,53 @@ export default function PersonReportPage() { }) }, []) + const openCustomPageSizeModal = useCallback(() => { + setCustomPageSizeModalOpened(true) + }, []) + + const closeCustomPageSizeModal = useCallback(() => { + setCustomPageSizeModalOpened(false) + setCustomPageSizeInput("") + }, []) + + const handleAddCustomPageSize = useCallback(() => { + const rawValue = customPageSizeInput.trim() + if (!rawValue) return + const size = Number(rawValue) + if (!Number.isInteger(size) || size <= 0) { + notifications.show({ + color: "red", + title: "输入无效", + message: "请填写大于 0 的整数条数", + }) + return + } + if (recordsPerPageOptions.includes(size)) { + notifications.show({ + color: "blue", + title: "已存在", + message: `每页 ${size} 条已在可选项中`, + }) + return + } + setRecordsPerPageOptions((prev) => [...prev, size].sort((a, b) => a - b)) + closeCustomPageSizeModal() + notifications.show({ + color: "green", + title: "添加成功", + message: `已新增每页 ${size} 条`, + }) + }, [closeCustomPageSizeModal, customPageSizeInput, recordsPerPageOptions]) + + const handleCustomPageSizeEnter = useCallback( + (event: React.KeyboardEvent) => { + if (event.key !== "Enter") return + event.preventDefault() + handleAddCustomPageSize() + }, + [handleAddCustomPageSize] + ) + const columns = useMemo(() => { const cols: DataTableColumn[] = [ { accessor: "id", title: "ID", width: 80, sortable: true }, @@ -429,21 +489,13 @@ export default function PersonReportPage() { }, [asyncGetSelfProjectList]) return ( - - + + - 搜索条件 + 筛选条件 - + - + @@ -505,8 +547,6 @@ export default function PersonReportPage() { /> { @@ -515,8 +555,6 @@ export default function PersonReportPage() { /> - + - + + + + + } + /> - - - - - - - + width="100%" - style={{ width: "100%" }} - withTableBorder - withRowBorders + minHeight={0} + style={{ width: "100%", flex: 1 }} pinFirstColumn pinLastColumn scrollAreaProps={{ type: "auto" }} @@ -665,16 +691,35 @@ export default function PersonReportPage() { setPageSize(v) setPage(1) }} - recordsPerPageOptions={[10, 15, 20, 50]} - noRecordsText="暂无数据" - recordsPerPageLabel="当前页数" + recordsPerPageOptions={recordsPerPageOptions} sortStatus={sortStatus} onSortStatusChange={(s) => { setSortStatus(s) setPage(1) }} + renderPagination={({ Controls }) => ( + + + + + + + + + )} /> - + 当前页总计 @@ -691,9 +736,9 @@ export default function PersonReportPage() { - + - + - + + + + + setCustomPageSizeInput(event.currentTarget.value) + } + onKeyDown={handleCustomPageSizeEnter} + /> + + + + + + + ) } diff --git a/components/setting/PageSurface.module.css b/components/setting/PageSurface.module.css new file mode 100644 index 0000000..dcfb36d --- /dev/null +++ b/components/setting/PageSurface.module.css @@ -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; +} diff --git a/components/setting/PageSurface.tsx b/components/setting/PageSurface.tsx new file mode 100644 index 0000000..65a06df --- /dev/null +++ b/components/setting/PageSurface.tsx @@ -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) { + return classNames.filter(Boolean).join(" ") +} + +export function SettingPage({ + className, + ...props +}: PropsWithChildren) { + return ( + + ) +} + +export function SettingPanel({ + className, + withBorder = true, + radius = "lg", + p = "md", + shadow = "xs", + ...props +}: PropsWithChildren) { + return ( + + ) +} + +export function SettingFilterPanel({ + className, + ...props +}: PropsWithChildren) { + return ( + + ) +} + +export function SettingContentPanel({ + className, + ...props +}: PropsWithChildren) { + return ( + + ) +} + +type SettingSectionHeaderProps = GroupProps & { + eyebrow?: string + title: ReactNode + description?: ReactNode + actions?: ReactNode +} + +export function SettingSectionHeader({ + eyebrow, + title, + description, + actions, + className, + ...props +}: SettingSectionHeaderProps) { + return ( + + + {eyebrow ? ( + {eyebrow} + ) : null} + {title} + {description ? ( + {description} + ) : null} + + {actions} + + ) +} + +type SettingListHeaderProps = { + title: ReactNode + count?: ReactNode + actions?: ReactNode + className?: string +} + +export function SettingListHeader({ + title, + count, + actions, + className, +}: SettingListHeaderProps) { + return ( + + + {title} + {count !== undefined ? ( + + {count} + + ) : null} + + {actions} + + ) +} + +export function SettingHeaderActions({ + className, + gap = "sm", + ...props +}: PropsWithChildren) { + return ( + + ) +} + +export function SettingFilterActions({ + className, + gap = "sm", + justify = "flex-end", + mt = "md", + ...props +}: PropsWithChildren) { + return ( + + ) +} + +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 +> = { + titleStyle: { + whiteSpace: "nowrap", + fontWeight: 600, + }, +} + +export function SettingDataTable>( + props: DataTableProps +) { + 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), + loaderBackgroundBlur: loaderBackgroundBlur ?? 1, + noRecordsText: noRecordsText ?? "暂无数据", + minHeight: minHeight ?? 360, + styles: styles + ? { ...settingDataTableStyles, ...styles } + : settingDataTableStyles, + ...paginationProps, + ...rest, + } as DataTableProps + + return +} + +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