Compare commits

...

2 Commits

Author SHA1 Message Date
iven
4cfbdec5fc refactor(web): 统一 dayjs 导入为集中初始化 — 11 个文件
Some checks failed
CI / security-audit (push) Has been cancelled
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
所有 health 页面从 import dayjs from 'dayjs' 迁移到
import { dayjs } from '.../utils/dayjs',确保 relativeTime
和 zh-cn locale 全局生效。
2026-04-28 01:47:13 +08:00
iven
5b47f13ecf feat(web): 提取共享基础组件 — dayjs/format/EntityName/FilterBar/PageContainer/DrawerForm
- 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 导入
2026-04-28 01:45:48 +08:00
18 changed files with 264 additions and 17 deletions

View File

@@ -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<string, unknown>) => Promise<void>;
initialValues?: Record<string, unknown>;
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 (
<Drawer
title={title}
open={open}
onClose={onClose}
width={width}
styles={{
body: { background: isDark ? '#141414' : undefined },
}}
extra={
<Space>
<Button onClick={onClose}></Button>
<Button type="primary" onClick={handleSubmit} loading={loading}>
</Button>
</Space>
}
>
<Form form={form} layout="vertical" initialValues={initialValues}>
{sections
? sections.map((s, i) => (
<div key={i}>
{i > 0 && <Divider style={{ margin: '16px 0' }} />}
<Typography.Text
strong
style={{ fontSize: 14, marginBottom: 12, display: 'block' }}
>
{s.title}
</Typography.Text>
<div style={gridStyle}>{s.fields}</div>
</div>
))
: children && <div style={gridStyle}>{children}</div>}
</Form>
</Drawer>
);
}

View File

@@ -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 <span>{name}</span>;
if (id) {
return (
<Tooltip title={`ID: ${id.slice(0, 8)}...`}>
<Typography.Text type="secondary" style={{ cursor: 'help' }}>
{fallbackLabel}
</Typography.Text>
</Tooltip>
);
}
return <Typography.Text type="secondary">{fallbackLabel}</Typography.Text>;
}

View File

@@ -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 (
<Flex
justify="space-between"
align="center"
style={{
padding: '12px 16px',
background: isDark ? '#1f1f1f' : '#fafafa',
borderRadius: 8,
marginBottom: 16,
}}
>
<Space wrap size="middle">
{children}
</Space>
<Space>
{onReset && (
<Button icon={<ReloadOutlined />} onClick={onReset}>
</Button>
)}
{extra}
</Space>
</Flex>
);
}

View File

@@ -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 (
<div style={{ padding: 24 }}>
<Flex justify="space-between" align="center" style={{ marginBottom: 16 }}>
<div>
<Typography.Title level={4} style={{ margin: 0 }}>
{title}
</Typography.Title>
{subtitle && (
<Typography.Text type="secondary" style={{ fontSize: 13 }}>
{subtitle}
</Typography.Text>
)}
</div>
<Space>
{selectedCount ? batchActions : actions}
{selectedCount ? (
<Button size="small" onClick={onClearSelection}>
({selectedCount})
</Button>
) : null}
</Space>
</Flex>
{filters && (
<FilterBar onReset={onResetFilters} extra={filterExtra}>
{filters}
</FilterBar>
)}
<Card
styles={{ body: { padding: 0 } }}
style={{ background: isDark ? '#141414' : '#fff' }}
loading={loading}
>
{children}
</Card>
</div>
);
}

View File

@@ -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 = [

View File

@@ -20,7 +20,7 @@ import {
EditOutlined,
DeleteOutlined,
} from '@ant-design/icons';
import dayjs from 'dayjs';
import { dayjs } from '../../utils/dayjs';
import { doctorApi, type Doctor, type CreateDoctorReq, type UpdateDoctorReq } from '../../api/health/doctors';
import { AuthButton } from '../../components/AuthButton';

View File

@@ -18,7 +18,7 @@ import {
Empty,
} from 'antd';
import { PlusOutlined, EditOutlined } from '@ant-design/icons';
import dayjs from 'dayjs';
import { dayjs } from '../../utils/dayjs';
import type { Dayjs } from 'dayjs';
import {
appointmentApi,

View File

@@ -1,7 +1,7 @@
import { useState, useEffect, useCallback } from 'react';
import { Table, DatePicker, message } from 'antd';
import type { ColumnsType, TablePaginationConfig } from 'antd/es/table';
import dayjs from 'dayjs';
import { dayjs } from '../../utils/dayjs';
import { followUpApi, type FollowUpRecord } from '../../api/health/followUp';
import { PatientSelect } from './components/PatientSelect';
import { useThemeMode } from '../../hooks/useThemeMode';

View File

@@ -13,7 +13,7 @@ import {
} from 'antd';
import { PlusOutlined, EditOutlined, SwapOutlined, DeleteOutlined } from '@ant-design/icons';
import type { ColumnsType, TablePaginationConfig } from 'antd/es/table';
import dayjs from 'dayjs';
import { dayjs } from '../../utils/dayjs';
import { followUpApi, type FollowUpTask, type CreateFollowUpTaskReq, type UpdateFollowUpTaskReq } from '../../api/health/followUp';
import { StatusTag } from './components/StatusTag';
import { PatientSelect } from './components/PatientSelect';

View File

@@ -23,7 +23,7 @@ import {
DeleteOutlined,
CheckCircleOutlined,
} from '@ant-design/icons';
import dayjs from 'dayjs';
import { dayjs } from '../../utils/dayjs';
import type { Dayjs } from 'dayjs';
import {
pointsApi,

View File

@@ -17,7 +17,7 @@ import {
import {
CheckCircleOutlined,
} from '@ant-design/icons';
import dayjs from 'dayjs';
import { dayjs } from '../../utils/dayjs';
import {
pointsApi,
type PointsOrder,

View File

@@ -21,7 +21,7 @@ import {
EditOutlined,
DeleteOutlined,
} from '@ant-design/icons';
import dayjs from 'dayjs';
import { dayjs } from '../../utils/dayjs';
import {
pointsApi,
type PointsProduct,

View File

@@ -21,7 +21,7 @@ import {
EditOutlined,
DeleteOutlined,
} from '@ant-design/icons';
import dayjs from 'dayjs';
import { dayjs } from '../../utils/dayjs';
import {
pointsApi,
type PointsRule,

View File

@@ -1,7 +1,7 @@
import { useCallback, useState } from 'react';
import { Table, Tag, Button, Modal, Form, Select, DatePicker, Input, message, Popconfirm, Space } from 'antd';
import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons';
import dayjs from 'dayjs';
import { dayjs } from '../../../utils/dayjs';
import type { Dayjs } from 'dayjs';
import { healthDataApi } from '../../../api/health/healthData';
import type { HealthRecord } from '../../../api/health/healthData';

View File

@@ -1,7 +1,7 @@
import { useCallback, useState } from 'react';
import { Table, Tag, Button, Modal, Form, Input, DatePicker, message, Popconfirm, Space } from 'antd';
import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons';
import dayjs from 'dayjs';
import { dayjs } from '../../../utils/dayjs';
import type { Dayjs } from 'dayjs';
import { healthDataApi } from '../../../api/health/healthData';
import type { LabReport } from '../../../api/health/healthData';

View File

@@ -2,7 +2,7 @@ import { useCallback, useState, useMemo } from 'react';
import { Table, Button, Modal, Form, InputNumber, DatePicker, Input, message, Typography, Tooltip, Popconfirm, Space } from 'antd';
import { PlusOutlined, InfoCircleOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons';
import type { Dayjs } from 'dayjs';
import dayjs from 'dayjs';
import { dayjs } from '../../../utils/dayjs';
import { healthDataApi } from '../../../api/health/healthData';
import type { VitalSigns } from '../../../api/health/healthData';
import { VitalSignsChart } from './VitalSignsChart';

View File

@@ -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;

View File

@@ -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}`;
};