From 5b47f13ecfdfdbae2f9c7d2092d03d12596c0add Mon Sep 17 00:00:00 2001 From: iven Date: Tue, 28 Apr 2026 01:45:48 +0800 Subject: [PATCH] =?UTF-8?q?feat(web):=20=E6=8F=90=E5=8F=96=E5=85=B1?= =?UTF-8?q?=E4=BA=AB=E5=9F=BA=E7=A1=80=E7=BB=84=E4=BB=B6=20=E2=80=94=20day?= =?UTF-8?q?js/format/EntityName/FilterBar/PageContainer/DrawerForm?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - utils/dayjs.ts: 集中初始化 relativeTime 插件 + zh-cn locale - utils/format.ts: formatDate/formatDateTime/formatRelative/calcAge - components/EntityName.tsx: UUID→姓名兜底显示 - components/FilterBar.tsx: 统一筛选栏容器 - components/PageContainer.tsx: 统一页面容器(标题+筛选+表格+暗色模式) - components/DrawerForm.tsx: 抽屉式表单容器(分组+双列网格) - AlertList.tsx: 迁移到集中 dayjs 导入 --- apps/web/src/components/DrawerForm.tsx | 94 +++++++++++++++++++++++ apps/web/src/components/EntityName.tsx | 23 ++++++ apps/web/src/components/FilterBar.tsx | 38 +++++++++ apps/web/src/components/PageContainer.tsx | 72 +++++++++++++++++ apps/web/src/pages/health/AlertList.tsx | 7 +- apps/web/src/utils/dayjs.ts | 9 +++ apps/web/src/utils/format.ts | 16 ++++ 7 files changed, 253 insertions(+), 6 deletions(-) create mode 100644 apps/web/src/components/DrawerForm.tsx create mode 100644 apps/web/src/components/EntityName.tsx create mode 100644 apps/web/src/components/FilterBar.tsx create mode 100644 apps/web/src/components/PageContainer.tsx create mode 100644 apps/web/src/utils/dayjs.ts create mode 100644 apps/web/src/utils/format.ts diff --git a/apps/web/src/components/DrawerForm.tsx b/apps/web/src/components/DrawerForm.tsx new file mode 100644 index 0000000..cb51e01 --- /dev/null +++ b/apps/web/src/components/DrawerForm.tsx @@ -0,0 +1,94 @@ +import React from 'react'; +import { Drawer, Form, Typography, Divider, Button, Space } from 'antd'; +import { useThemeMode } from '../hooks/useThemeMode'; + +export interface FormSection { + title: string; + fields: React.ReactNode; + defaultCollapsed?: boolean; +} + +interface DrawerFormProps { + title: string; + open: boolean; + onClose: () => void; + onSubmit: (values: Record) => Promise; + initialValues?: Record; + loading?: boolean; + width?: number | string; + sections?: FormSection[]; + children?: React.ReactNode; + columns?: 1 | 2; +} + +export function DrawerForm({ + title, + open, + onClose, + onSubmit, + initialValues, + loading, + width = 640, + sections, + children, + columns = 2, +}: DrawerFormProps) { + const [form] = Form.useForm(); + const isDark = useThemeMode(); + + React.useEffect(() => { + if (open) { + form.resetFields(); + if (initialValues) { + form.setFieldsValue(initialValues); + } + } + }, [open, initialValues, form]); + + const handleSubmit = async () => { + const values = await form.validateFields(); + await onSubmit(values); + }; + + const gridStyle: React.CSSProperties = + columns === 2 + ? { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '0 16px' } + : {}; + + return ( + + + + + } + > +
+ {sections + ? sections.map((s, i) => ( +
+ {i > 0 && } + + {s.title} + +
{s.fields}
+
+ )) + : children &&
{children}
} +
+
+ ); +} diff --git a/apps/web/src/components/EntityName.tsx b/apps/web/src/components/EntityName.tsx new file mode 100644 index 0000000..0e9afeb --- /dev/null +++ b/apps/web/src/components/EntityName.tsx @@ -0,0 +1,23 @@ +import { Tooltip, Typography } from 'antd'; + +interface EntityNameProps { + name?: string | null; + id?: string; + fallbackLabel?: string; +} + +export function EntityName({ name, id, fallbackLabel = '未知' }: EntityNameProps) { + if (name) return {name}; + + if (id) { + return ( + + + {fallbackLabel} + + + ); + } + + return {fallbackLabel}; +} diff --git a/apps/web/src/components/FilterBar.tsx b/apps/web/src/components/FilterBar.tsx new file mode 100644 index 0000000..93e3ade --- /dev/null +++ b/apps/web/src/components/FilterBar.tsx @@ -0,0 +1,38 @@ +import { Button, Flex, Space } from 'antd'; +import { ReloadOutlined } from '@ant-design/icons'; +import { useThemeMode } from '../hooks/useThemeMode'; + +interface FilterBarProps { + children: React.ReactNode; + onReset?: () => void; + extra?: React.ReactNode; +} + +export function FilterBar({ children, onReset, extra }: FilterBarProps) { + const isDark = useThemeMode(); + + return ( + + + {children} + + + {onReset && ( + + )} + {extra} + + + ); +} diff --git a/apps/web/src/components/PageContainer.tsx b/apps/web/src/components/PageContainer.tsx new file mode 100644 index 0000000..bf85f25 --- /dev/null +++ b/apps/web/src/components/PageContainer.tsx @@ -0,0 +1,72 @@ +import { Card, Flex, Space, Typography, Button } from 'antd'; +import { useThemeMode } from '../hooks/useThemeMode'; +import { FilterBar } from './FilterBar'; + +interface PageContainerProps { + title: string; + subtitle?: string; + filters?: React.ReactNode; + onResetFilters?: () => void; + filterExtra?: React.ReactNode; + actions?: React.ReactNode; + batchActions?: React.ReactNode; + selectedCount?: number; + onClearSelection?: () => void; + children: React.ReactNode; + loading?: boolean; +} + +export function PageContainer({ + title, + subtitle, + filters, + onResetFilters, + filterExtra, + actions, + batchActions, + selectedCount, + onClearSelection, + children, + loading, +}: PageContainerProps) { + const isDark = useThemeMode(); + + return ( +
+ +
+ + {title} + + {subtitle && ( + + {subtitle} + + )} +
+ + {selectedCount ? batchActions : actions} + {selectedCount ? ( + + ) : null} + +
+ + {filters && ( + + {filters} + + )} + + + {children} + +
+ ); +} diff --git a/apps/web/src/pages/health/AlertList.tsx b/apps/web/src/pages/health/AlertList.tsx index 1d56555..0e7e00d 100644 --- a/apps/web/src/pages/health/AlertList.tsx +++ b/apps/web/src/pages/health/AlertList.tsx @@ -10,9 +10,7 @@ import { } from 'antd'; import { CheckOutlined, StopOutlined } from '@ant-design/icons'; import type { ColumnsType, TablePaginationConfig } from 'antd/es/table'; -import dayjs from 'dayjs'; -import relativeTime from 'dayjs/plugin/relativeTime'; -import 'dayjs/locale/zh-cn'; +import { dayjs } from '../../utils/dayjs'; import { listAlerts, acknowledgeAlert, @@ -22,9 +20,6 @@ import { import { AuthButton } from '../../components/AuthButton'; import { useThemeMode } from '../../hooks/useThemeMode'; -dayjs.extend(relativeTime); -dayjs.locale('zh-cn'); - // --- 常量映射 --- const STATUS_OPTIONS = [ diff --git a/apps/web/src/utils/dayjs.ts b/apps/web/src/utils/dayjs.ts new file mode 100644 index 0000000..6eaa785 --- /dev/null +++ b/apps/web/src/utils/dayjs.ts @@ -0,0 +1,9 @@ +import dayjs from 'dayjs'; +import relativeTime from 'dayjs/plugin/relativeTime'; +import 'dayjs/locale/zh-cn'; + +dayjs.extend(relativeTime); +dayjs.locale('zh-cn'); + +export { dayjs }; +export default dayjs; diff --git a/apps/web/src/utils/format.ts b/apps/web/src/utils/format.ts new file mode 100644 index 0000000..64ab2a1 --- /dev/null +++ b/apps/web/src/utils/format.ts @@ -0,0 +1,16 @@ +import { dayjs } from './dayjs'; + +export const formatDate = (v: string | null | undefined): string => + v ? dayjs(v).format('YYYY-MM-DD') : '--'; + +export const formatDateTime = (v: string | null | undefined): string => + v ? dayjs(v).format('YYYY-MM-DD HH:mm') : '--'; + +export const formatRelative = (v: string | null | undefined): string => + v ? dayjs(v).fromNow() : '--'; + +export const calcAge = (birthDate: string | null | undefined): string => { + if (!birthDate) return '--'; + const age = dayjs().diff(dayjs(birthDate), 'year'); + return `${age}岁`; +};