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}>
)
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 (
-
-
+
+
- 搜索条件
+ 筛选条件
-
+
}
onClick={() => {
setFilters(defaultFilters)
@@ -472,8 +522,6 @@ export default function PersonReportPage() {
重置
}
onClick={() => {
setAppliedFilters(filters)
@@ -482,18 +530,12 @@ export default function PersonReportPage() {
}}>
查询
-
+
-
+
@@ -505,8 +547,6 @@ export default function PersonReportPage() {
/>
{
@@ -515,8 +555,6 @@ export default function PersonReportPage() {
/>
-
+
-
+
+ }
+ onClick={() => {
+ setEditingRecord(null)
+ setModalOpened(true)
+ }}>
+ 填写日报
+
+ }
+ onClick={load}
+ loading={loading}>
+ 刷新
+
+
+ }
+ />
-
- }
- size="xs"
- radius={"xs"}
- onClick={() => {
- setEditingRecord(null)
- setModalOpened(true)
- }}>
- 填写日报
-
-
-
-
-
-
+
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