医生和技师页面用于管理转诊和执行医生及技师的名单信息,支持按类别(姓名、医生、技师)查看和管理人员信息,包括查看、创建、编辑、删除等操作。该页面主要用于DICOM报告和检查流程中的人员信息管理。
Page (页面)
└── Main Container (主容器)
├── Header Region (头部区域)
│ ├── Title Component (标题组件)
│ │ └── Typography.Title: "转诊/执行医生和技师名单"
│ │
│ └── Tabs Component (选项卡组件)
│ ├── Tab Item: 姓名
│ ├── Tab Item: 医生
│ └── Tab Item: 技师
│
├── Content Region (内容区域)
│ └── Table Section (表格分区)
│ ├── Table Component (表格组件)
│ │ ├── Column: 选择框 (Checkbox)
│ │ ├── Column: 姓名/医生名称/技师名称
│ │ ├── Column: 工号/医生工号/技师工号
│ │ ├── Column: 科室
│ │ ├── Column: 职称
│ │ ├── Column: 联系电话
│ │ └── Column: 备注
│ │
│ ├── Empty State Component (空状态组件)
│ │ ├── Icon: Empty illustration
│ │ └── Text: "暂无数据"
│ │
│ └── Pagination Component (分页组件)
│ ├── Total Count Display (总数显示)
│ ├── Page Size Selector (每页条数选择器)
│ ├── Page Navigation (页码导航)
│ └── Page Jump Input (页码跳转输入)
│
└── Footer Region (底部区域)
└── Action Toolbar (操作工具栏)
├── Button Group Left (左侧按钮组)
│ ├── Button: 删除
│ └── Button: 编辑
│
└── Button Group Right (右侧按钮组)
└── Button: 新建 (Primary)
| 组件 | 用途 | 选择理由 |
|---|---|---|
| Typography.Title | 页面标题 | - 提供标准的标题样式 - 自动适配主题 - 语义化标记 - 层级清晰(level 4) |
| Tabs | 类别切换导航 | - 清晰的分类导航 - 自动处理激活状态 - 支持路由集成 - 适合管理同类数据的不同视图 - 视觉上区分不同类别的人员 |
| Table | 数据展示 | - 企业级表格组件 - 内置排序、筛选功能 - 支持行选择 - 自动处理分页数据 - 列宽自适应 - 固定列支持 |
| Checkbox | 行选择 | - 支持单选和多选 - 清晰的选中状态 - 便于批量操作 - 全选/取消全选支持 |
| Empty | 空状态展示 | - 标准的空状态组件 - 提供友好的视觉提示 - 可自定义描述文字 - 统一的用户体验 |
| Pagination | 分页控制 | - 完整的分页功能 - 支持页码跳转 - 每页条数选择 - 总数显示 - 国际化支持 |
| Button | 操作按钮 | - 提供多种按钮样式 - 支持加载状态 - 图标支持 - 自动防重复点击 - 主题一致性 |
| Space | 按钮组间距 | - 统一管理按钮间距 - 响应式布局 - 自动对齐 |
| Modal | 弹窗对话框 | - 用于编辑/新建人员信息 - 模态交互 - 表单提交 - 确认删除操作 |
| Form | 表单容器 | - 完整的表单验证 - 自动处理表单布局 - 内置错误显示 - 支持受控/非受控模式 |
| Input | 文本输入 | - 标准文本输入组件 - 支持前缀/后缀图标 - 提供清空功能 - 自动trim空格 |
| Popconfirm | 操作确认 | - 删除确认提示 - 轻量级确认 - 避免误操作 |
选择 Tabs 作为分类导航的原因:
选择 标准Table布局 的原因:
[x] 人员列表展示
[x] 分类视图
[x] 空状态展示
[x] 分页功能
[x] 数据加载
[x] 新建人员
[x] 编辑人员
[x] 删除人员
[x] 刷新列表
[x] 行选择
[x] 批量操作
需求描述:
交互流程:
数据结构:
// 人员类型枚举
enum StaffType {
ALL = 'all', // 姓名列表(所有人员)
DOCTOR = 'doctor', // 医生
TECHNICIAN = 'technician' // 技师
}
// 人员信息接口
interface StaffMember {
id: string;
name: string; // 姓名
staffNumber: string; // 工号
department: string; // 科室
title: string; // 职称
phone: string; // 联系电话
type: StaffType; // 人员类型
remark?: string; // 备注
createdAt: string; // 创建时间
updatedAt: string; // 更新时间
}
// 列表响应接口
interface StaffListResponse {
data: StaffMember[];
total: number;
page: number;
pageSize: number;
}
Tab设计说明:
边界情况:
需求描述:
交互流程:
表单字段设计:
interface CreateStaffForm {
name: string; // 2-20字符
type: StaffType; // 医生或技师
staffNumber: string; // 工号,3-20字符,字母数字
department: string; // 科室,2-50字符
title?: string; // 职称,可选
phone?: string; // 11位手机号
remark?: string; // 备注,最多200字符
}
验证规则:
业务规则:
边界情况:
需求描述:
交互流程:
可编辑字段:
不可编辑字段:
按钮状态:
边界情况:
需求描述:
交互流程:
确认对话框内容:
确定要删除 "${name}" 吗?确定要删除选中的 ${count} 个人员吗?删除后将无法恢复。删除限制:
级联影响:
边界情况:
需求描述:
列配置:
| 列名 | 宽度 | 对齐 | 排序 | 筛选 | 说明 |
|---|---|---|---|---|---|
| 选择框 | 50px | Center | - | - | Checkbox,用于行选择 |
| 姓名 | 120px | Left | ✅ | ✅ | 主要标识,支持搜索 |
| 工号 | 120px | Left | ✅ | ✅ | 唯一标识,支持搜索 |
| 科室 | 150px | Left | ✅ | ✅ | 支持筛选下拉 |
| 职称 | 120px | Left | ✅ | ✅ | 支持筛选下拉 |
| 联系电话 | 130px | Left | - | ✅ | 可为空 |
| 备注 | Auto | Left | - | - | 可为空,超长省略 |
排序功能:
筛选功能:
响应式处理:
需求描述:
空状态设计:
触发场景:
交互:
需求描述:
分页配置:
共 ${total} 条分页状态管理:
interface PaginationState {
current: number; // 当前页码
pageSize: number; // 每页条数
total: number; // 总条数
}
分页行为:
性能优化:
需求描述:
搜索方式:
快速搜索:
高级筛选:
筛选组合:
清空筛选:
筛选状态指示:
需求描述:
导入功能:
导出功能:
// states/staffManagementSlice.ts
interface StaffManagementState {
// 各Tab的数据
tabs: {
[key in StaffType]: {
data: StaffMember[];
total: number;
loading: boolean;
error: string | null;
pagination: {
current: number;
pageSize: number;
};
filters: {
keyword: string;
department: string[];
title: string[];
};
sorter: {
field: string | null;
order: 'ascend' | 'descend' | null;
};
};
};
// 当前激活的Tab
activeTab: StaffType;
// 选择状态
selection: {
selectedRowKeys: string[];
selectedRows: StaffMember[];
};
// 模态框状态
modals: {
createStaff: boolean;
editStaff: boolean;
};
// 当前编辑的人员
currentStaff: StaffMember | null;
// 操作状态
operations: {
creating: boolean;
updating: boolean;
deleting: boolean;
};
}
// 初始状态
const initialState: StaffManagementState = {
tabs: {
[StaffType.ALL]: {
data: [],
total: 0,
loading: false,
error: null,
pagination: { current: 1, pageSize: 20 },
filters: { keyword: '', department: [], title: [] },
sorter: { field: null, order: null },
},
[StaffType.DOCTOR]: {
data: [],
total: 0,
loading: false,
error: null,
pagination: { current: 1, pageSize: 20 },
filters: { keyword: '', department: [], title: [] },
sorter: { field: null, order: null },
},
[StaffType.TECHNICIAN]: {
data: [],
total: 0,
loading: false,
error: null,
pagination: { current: 1, pageSize: 20 },
filters: { keyword: '', department: [], title: [] },
sorter: { field: null, order: null },
},
},
activeTab: StaffType.ALL,
selection: {
selectedRowKeys: [],
selectedRows: [],
},
modals: {
createStaff: false,
editStaff: false,
},
currentStaff: null,
operations: {
creating: false,
updating: false,
deleting: false,
},
};
// 异步 Thunks
export const fetchStaffList = createAsyncThunk(
'staffManagement/fetchStaffList',
async ({ type, params }: { type: StaffType; params: FetchStaffParams }) => {
const response = await getStaffList(type, params);
return { type, data: response };
}
);
export const createStaff = createAsyncThunk(
'staffManagement/createStaff',
async (data: CreateStaffForm) => {
const response = await createStaffAPI(data);
return response;
}
);
export const updateStaff = createAsyncThunk(
'staffManagement/updateStaff',
async ({ id, data }: { id: string; data: UpdateStaffForm }) => {
const response = await updateStaffAPI(id, data);
return response;
}
);
export const deleteStaff = createAsyncThunk(
'staffManagement/deleteStaff',
async (ids: string[]) => {
await deleteStaffAPI(ids);
return ids;
}
);
// Slice 定义
const staffManagementSlice = createSlice({
name: 'staffManagement',
initialState,
reducers: {
// 切换 Tab
setActiveTab: (state, action: PayloadAction<StaffType>) => {
state.activeTab = action.payload;
// 清空选择状态
state.selection = { selectedRowKeys: [], selectedRows: [] };
},
// 更新分页
updatePagination: (state, action: PayloadAction<{ type: StaffType; pagination: Partial<PaginationState> }>) => {
const { type, pagination } = action.payload;
state.tabs[type].pagination = {
...state.tabs[type].pagination,
...pagination,
};
},
// 更新筛选
updateFilters: (state, action: PayloadAction<{ type: StaffType; filters: Partial<typeof initialState.tabs[StaffType]['filters']> }>) => {
const { type, filters } = action.payload;
state.tabs[type].filters = {
...state.tabs[type].filters,
...filters,
};
},
// 更新排序
updateSorter: (state, action: PayloadAction<{ type: StaffType; sorter: typeof initialState.tabs[StaffType]['sorter'] }>) => {
const { type, sorter } = action.payload;
state.tabs[type].sorter = sorter;
},
// 更新选择
setSelection: (state, action: PayloadAction<{ selectedRowKeys: string[]; selectedRows: StaffMember[] }>) => {
state.selection = action.payload;
},
// 打开/关闭模态框
openModal: (state, action: PayloadAction<keyof typeof initialState.modals>) => {
state.modals[action.payload] = true;
},
closeModal: (state, action: PayloadAction<keyof typeof initialState.modals>) => {
state.modals[action.payload] = false;
},
// 设置当前编辑的人员
setCurrentStaff: (state, action: PayloadAction<StaffMember | null>) => {
state.currentStaff = action.payload;
},
},
extraReducers: (builder) => {
// 加载列表
builder.addCase(fetchStaffList.pending, (state, action) => {
const { type } = action.meta.arg;
state.tabs[type].loading = true;
state.tabs[type].error = null;
});
builder.addCase(fetchStaffList.fulfilled, (state, action) => {
const { type, data } = action.payload;
state.tabs[type].loading = false;
state.tabs[type].data = data.data;
state.tabs[type].total = data.total;
});
builder.addCase(fetchStaffList.rejected, (state, action) => {
const { type } = action.meta.arg;
state.tabs[type].loading = false;
state.tabs[type].error = action.error.message || '加载失败';
});
// 创建人员
builder.addCase(createStaff.pending, (state) => {
state.operations.creating = true;
});
builder.addCase(createStaff.fulfilled, (state) => {
state.operations.creating = false;
state.modals.createStaff = false;
});
builder.addCase(createStaff.rejected, (state) => {
state.operations.creating = false;
});
// 更新人员
builder.addCase(updateStaff.pending, (state) => {
state.operations.updating = true;
});
builder.addCase(updateStaff.fulfilled, (state) => {
state.operations.updating = false;
state.modals.editStaff = false;
state.currentStaff = null;
});
builder.addCase(updateStaff.rejected, (state) => {
state.operations.updating = false;
});
// 删除人员
builder.addCase(deleteStaff.pending, (state) => {
state.operations.deleting = true;
});
builder.addCase(deleteStaff.fulfilled, (state) => {
state.operations.deleting = false;
state.selection = { selectedRowKeys: [], selectedRows: [] };
});
builder.addCase(deleteStaff.rejected, (state) => {
state.operations.deleting = false;
});
},
});
export const {
setActiveTab,
updatePagination,
updateFilters,
updateSorter,
setSelection,
openModal,
closeModal,
setCurrentStaff,
} = staffManagementSlice.actions;
export default staffManagementSlice.reducer;
// API/staff.ts
// 获取人员列表参数
export interface FetchStaffParams {
page: number;
pageSize: number;
keyword?: string;
department?: string[];
title?: string[];
sortField?: string;
sortOrder?: 'asc' | 'desc';
}
// 获取人员列表
export const getStaffList = (
type: StaffType,
params: FetchStaffParams
): Promise<StaffListResponse> => {
return request.get('/api/system/staff', {
params: {
...params,
type: type === StaffType.ALL ? undefined : type,
},
});
};
// 创建人员
export const createStaffAPI = (data: CreateStaffForm): Promise<StaffMember> => {
return request.post('/api/system/staff', data);
};
// 更新人员
export interface UpdateStaffForm {
name?: string;
department?: string;
title?: string;
phone?: string;
remark?: string;
}
export const updateStaffAPI = (id: string, data: UpdateStaffForm): Promise<StaffMember> => {
return request.put(`/api/system/staff/${id}`, data);
};
// 删除人员
export const deleteStaffAPI = (ids: string[]): Promise<void> => {
return request.delete('/api/system/staff', { data: { ids } });
};
// 检查工号是否存在
export const checkStaffNumberExists = (staffNumber: string): Promise<boolean> => {
return request.get('/api/system/staff/check-staff-number', {
params: { staffNumber },
});
};
// 获取科室列表
export const getDepartmentList = (): Promise<string[]> => {
return request.get('/api/system/staff/departments');
};
// 获取职称列表
export const getTitleList = (): Promise<string[]> => {
return request.get('/api/system/staff/titles');
};
// validation/staffRules.ts
import { z } from 'zod';
export const createStaffSchema = z.object({
name: z.string()
.min(2, '姓名至少2个字符')
.max(20, '姓名最多20个字符')
.regex(/^[\u4e00-\u9fa5a-zA-Z\s]+$/, '姓名只能包含中文、英文和空格'),
type: z.enum([StaffType.DOCTOR, StaffType.TECHNICIAN], {
errorMap: () => ({ message: '请选择人员类型' }),
}),
staffNumber: z.string()
.min(3, '工号至少3个字符')
.max(20, '工号最多20个字符')
.regex(/^[A-Z0-9]+$/, '工号只能包含大写字母和数字'),
department: z.string()
.min(2, '科室名称至少2个字符')
.max(50, '科室名称最多50个字符'),
title: z.string()
.max(20, '职称最多20个字符')
.optional(),
phone: z.string()
.regex(/^1[3-9]\d{9}$/, '请输入正确的手机号')
.optional()
.or(z.literal('')),
remark: z.string()
.max(200, '备注最多200个字符')
.optional(),
});
export const updateStaffSchema = createStaffSchema.omit({
type: true,
staffNumber: true,
});
// Ant Design Form 验证规则
export const staffFormRules = {
name: [
{ required: true, message: '请输入姓名' },
{ min: 2, max: 20, message: '姓名长度为2-20个字符' },
{ pattern: /^[\u4e00-\u9fa5a-zA-Z\s]+$/, message: '姓名只能包含中文、英文和空格' },
],
type: [
{ required: true, message: '请选择人员类型' },
],
staffNumber: [
{ required: true, message: '请输入工号' },
{ min: 3, max: 20, message: '工号长度为3-20个字符' },
{ pattern: /^[A-Z0-9]+$/, message: '工号只能包含大写字母和数字' },
// 异步验证唯一性
{
validator: async (_, value: string) => {
if (!value) return Promise.resolve();
const exists = await checkStaffNumberExists(value);
if (exists) {
return Promise.reject(new Error('该工号已存在'));
}
return Promise.resolve();
},
},
],
department: [
{ required: true, message: '请输入科室' },
{ min: 2, max: 50, message: '科室名称长度为2-50个字符' },
],
title: [
{ max: 20, message: '职称最多20个字符' },
],
phone: [
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号' },
],
remark: [
{ max: 200, message: '备注最多200个字符' },
],
};
// sections/SystemHome/DoctorsAndTechnicians.tsx
import React, { useEffect } from 'react';
import { Typography, Tabs, Table, Button, Space, Empty, Modal, Form, Input, Select, message, Popconfirm } from 'antd';
import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons';
import { useAppDispatch, useAppSelector } from '@/states/store';
import {
fetchStaffList,
createStaff,
updateStaff,
deleteStaff,
setActiveTab,
updatePagination,
setSelection,
openModal,
closeModal,
setCurrentStaff,
} from '@/states/staffManagementSlice';
import { StaffType } from '@/types/staff';
import { staffFormRules } from '@/validation/staffRules';
const DoctorsAndTechnicians: React.FC = () => {
const dispatch = useAppDispatch();
const [createForm] = Form.useForm();
const [editForm] = Form.useForm();
const {
tabs,
activeTab,
selection,
modals,
currentStaff,
operations,
} = useAppSelector(state => state.staffManagement);
const currentTabData = tabs[activeTab];
// 初始加载
useEffect(() => {
loadData();
}, [activeTab]);
// 加载数据
const loadData = () => {
dispatch(fetchStaffList({
type: activeTab,
params: {
page: currentTabData.pagination.current,
pageSize: currentTabData.pagination.pageSize,
...currentTabData.filters,
sortField: currentTabData.sorter.field || undefined,
sortOrder: currentTabData.sorter.order === 'ascend' ? 'asc' : currentTabData.sorter.order === 'descend' ? 'desc' : undefined,
},
}));
};
// Tab 切换
const handleTabChange = (key: string) => {
dispatch(setActiveTab(key as StaffType));
};
// 表格列定义
const columns = [
{
title: '姓名',
dataIndex: 'name',
key: 'name',
width: 120,
sorter: true,
},
{
title: '工号',
dataIndex: 'staffNumber',
key: 'staffNumber',
width: 120,
sorter: true,
},
{
title: '科室',
dataIndex: 'department',
key: 'department',
width: 150,
sorter: true,
},
{
title: '职称',
dataIndex: 'title',
key: 'title',
width: 120,
sorter: true,
},
{
title: '联系电话',
dataIndex: 'phone',
key: 'phone',
width: 130,
},
{
title: '备注',
dataIndex: 'remark',
key: 'remark',
ellipsis: true,
},
];
// 表格配置
const tableConfig = {
rowKey: 'id',
columns,
dataSource: currentTabData.data,
loading: currentTabData.loading,
rowSelection: {
selectedRowKeys: selection.selectedRowKeys,
onChange: (selectedRowKeys: React.Key[], selectedRows: StaffMember[]) => {
dispatch(setSelection({
selectedRowKeys: selectedRowKeys as string[],
selectedRows,
}));
},
},
pagination: {
current: currentTabData.pagination.current,
pageSize: currentTabData.pagination.pageSize,
total: currentTabData.total,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total: number) => `共 ${total} 条`,
pageSizeOptions: ['20', '50', '100'],
onChange: (page: number, pageSize: number) => {
dispatch(updatePagination({
type: activeTab,
pagination: { current: page, pageSize },
}));
},
},
locale: {
emptyText: <Empty description="暂无数据" />,
},
};
// 新建人员
const handleCreate = () => {
createForm.resetFields();
dispatch(openModal('createStaff'));
};
const handleCreateSubmit = async () => {
try {
const values = await createForm.validateFields();
await dispatch(createStaff(values)).unwrap();
message.success('创建成功');
loadData();
} catch (error) {
message.error('创建失败');
}
};
// 编辑人员
const handleEdit = () => {
if (selection.selectedRows.length !== 1) return;
const staff = selection.selectedRows[0];
dispatch(setCurrentStaff(staff));
editForm.setFieldsValue(staff);
dispatch(openModal('editStaff'));
};
const handleEditSubmit = async () => {
try {
const values = await editForm.validateFields();
await dispatch(updateStaff({
id: currentStaff!.id,
data: values,
})).unwrap();
message.success('更新成功');
loadData();
} catch (error) {
message.error('更新失败');
}
};
// 删除人员
const handleDelete = async () => {
try {
await dispatch(deleteStaff(selection.selectedRowKeys)).unwrap();
message.success('删除成功');
loadData();
} catch (error) {
message.error('删除失败');
}
};
return (
<div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
{/* 头部 */}
<div style={{
padding: '24px 24px 16px 24px',
borderBottom: '1px solid #f0f0f0',
}}>
<Typography.Title level={4} style={{ marginBottom: 16 }}>
转诊/执行医生和技师名单
</Typography.Title>
<Tabs
activeKey={activeTab}
onChange={handleTabChange}
items={[
{ key: StaffType.ALL, label: '姓名' },
{ key: StaffType.DOCTOR, label: '医生' },
{ key: StaffType.TECHNICIAN, label: '技师' },
]}
/>
</div>
{/* 内容区 */}
<div style={{ flex: 1, overflow: 'auto', padding: '24px 24px 0 24px' }}>
<Table {...tableConfig} />
</div>
{/* 底部操作栏 */}
<div style={{
height: 72,
borderTop: '1px solid #f0f0f0',
padding: '16px 24px',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
}}>
<Space>
<Popconfirm
title={`确定要删除选中的 ${selection.selectedRowKeys.length} 个人员吗?`}
description="删除后将无法恢复。"
onConfirm={handleDelete}
disabled={selection.selectedRowKeys.length === 0}
>
<Button
icon={<DeleteOutlined />}
disabled={selection.selectedRowKeys.length === 0}
loading={operations.deleting}
>
删除
</Button>
</Popconfirm>
<Button
icon={<EditOutlined />}
onClick={handleEdit}
disabled={selection.selectedRowKeys.length !== 1}
>
编辑
</Button>
</Space>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={handleCreate}
>
新建
</Button>
</div>
{/* 新建Modal */}
<Modal
title="新建人员"
open={modals.createStaff}
onOk={handleCreateSubmit}
onCancel={() => dispatch(closeModal('createStaff'))}
confirmLoading={operations.creating}
width={600}
>
<Form
form={createForm}
layout="vertical"
>
<Form.Item name="name" label="姓名" rules={staffFormRules.name}>
<Input placeholder="请输入姓名" />
</Form.Item>
<Form.Item name="type" label="人员类型" rules={staffFormRules.type}>
<Select placeholder="请选择人员类型">
<Select.Option value={StaffType.DOCTOR}>医生</Select.Option>
<Select.Option value={StaffType.TECHNICIAN}>技师</Select.Option>
</Select>
</Form.Item>
<Form.Item name="staffNumber" label="工号" rules={staffFormRules.staffNumber}>
<Input placeholder="请输入工号" />
</Form.Item>
<Form.Item name="department" label="科室" rules={staffFormRules.department}>
<Input placeholder="请输入科室" />
</Form.Item>
<Form.Item name="title" label="职称" rules={staffFormRules.title}>
<Input placeholder="请输入职称(可选)" />
</Form.Item>
<Form.Item name="phone" label="联系电话" rules={staffFormRules.phone}>
<Input placeholder="请输入联系电话(可选)" />
</Form.Item>
<Form.Item name="remark" label="备注" rules={staffFormRules.remark}>
<Input.TextArea rows={3} placeholder="请输入备注(可选)" />
</Form.Item>
</Form>
</Modal>
{/* 编辑Modal */}
<Modal
title="编辑人员"
open={modals.editStaff}
onOk={handleEditSubmit}
onCancel={() => dispatch(closeModal('editStaff'))}
confirmLoading={operations.updating}
width={600}
>
<Form
form={editForm}
layout="vertical"
>
<Form.Item name="name" label="姓名" rules={staffFormRules.name}>
<Input placeholder="请输入姓名" />
</Form.Item>
<Form.Item name="department" label="科室" rules={staffFormRules.department}>
<Input placeholder="请输入科室" />
</Form.Item>
<Form.Item name="title" label="职称" rules={staffFormRules.title}>
<Input placeholder="请输入职称(可选)" />
</Form.Item>
<Form.Item name="phone" label="联系电话" rules={staffFormRules.phone}>
<Input placeholder="请输入联系电话(可选)" />
</Form.Item>
<Form.Item name="remark" label="备注" rules={staffFormRules.remark}>
<Input.TextArea rows={3} placeholder="请输入备注(可选)" />
</Form.Item>
</Form>
</Modal>
</div>
);
};
export default DoctorsAndTechnicians;
// assets/i18n/zh-CN.json
{
"systemSettings": {
"staff": {
"title": "转诊/执行医生和技师名单",
"tabs": {
"all": "姓名",
"doctor": "医生",
"technician": "技师"
},
"columns": {
"name": "姓名",
"staffNumber": "工号",
"department": "科室",
"title": "职称",
"phone": "联系电话",
"remark": "备注"
},
"actions": {
"create": "新建",
"edit": "编辑",
"delete": "删除"
},
"form": {
"name": "姓名",
"type": "人员类型",
"staffNumber": "工号",
"department": "科室",
"title": "职称",
"phone": "联系电话",
"remark": "备注"
},
"messages": {
"createSuccess": "创建成功",
"createFailed": "创建失败",
"updateSuccess": "更新成功",
"updateFailed": "更新失败",
"deleteSuccess": "删除成功",
"deleteFailed": "删除失败",
"deleteConfirm": "确定要删除选中的 {count} 个人员吗?",
"deleteWarning": "删除后将无法恢复。"
}
}
}
}
// cypress/e2e/system/staff-management.cy.ts
describe('医生和技师管理', () => {
beforeEach(() => {
cy.login();
cy.visit('/system/settings?section=doctors-technicians');
});
it('应该能够加载人员列表', () => {
cy.get('[data-testid="staff-table"]').should('be.visible');
cy.get('[data-testid="staff-table"] tbody tr').should('have.length.gt', 0);
});
it('应该能够切换Tab', () => {
cy.contains('医生').click();
cy.url().should('include', 'tab=doctor');
cy.contains('技师').click();
cy.url().should('include', 'tab=technician');
});
it('应该能够新建人员', () => {
cy.contains('button', '新建').click();
cy.get('[data-testid="create-staff-modal"]').should('be.visible');
cy.get('input[name="name"]').type('张三');
cy.get('select[name="type"]').select('医生');
cy.get('input[name="staffNumber"]').type('DOC001');
cy.get('input[name="department"]').type('放射科');
cy.contains('button', '确定').click();
cy.contains('创建成功').should('be.visible');
});
it('应该能够编辑人员', () => {
// 选中第一行
cy.get('[data-testid="staff-table"] tbody tr').first().click();
cy.contains('button', '编辑').click();
cy.get('[data-testid="edit-staff-modal"]').should('be.visible');
cy.get('input[name="department"]').clear().type('影像科');
cy.contains('button', '确定').click();
cy.contains('更新成功').should('be.visible');
});
it('应该能够删除人员', () => {
// 选中第一行
cy.get('[data-testid="staff-table"] tbody tr').first().click();
cy.contains('button', '删除').click();
cy.get('[data-testid="delete-confirm"]').should('be.visible');
cy.contains('button', '确定').click();
cy.contains('删除成功').should('be.visible');
});
});
数据缓存
虚拟滚动
防抖和节流
懒加载
优化重渲染
输入验证
权限控制
数据安全
审计日志
用户打开页面
↓
加载姓名Tab数据
↓
显示列表
↓
用户操作(切换Tab/新建/编辑/删除)
↓
执行对应操作
↓
操作成功 → 更新列表 → 显示成功提示
↓
操作失败 → 显示错误提示 → 允许重试
组件 → Redux Action → Async Thunk → API → 后端
↓
组件 ← Redux State ← Redux Reducer ← Response
为什么使用Tab而不是独立页面?
为什么工号不可修改?
为什么支持同名人员?
为什么采用软删除?
医生和技师管理页面是系统之家模块的重要组成部分,主要用于管理医疗系统中的医生和技师信息。通过合理的布局设计、清晰的交互流程和完善的功能实现,为用户提供高效便捷的人员管理体验。
功能增强
性能优化
用户体验
系统集成