Files
hms/apps/web/src/pages/health/DoctorList.tsx
iven 63ead0c442 refactor(web): 新增 useDictionary hook + 4 个页面下拉选项改用字典 API
- 新增 useDictionary hook 支持字典 API 获取 + fallback 降级
- DoctorList 科室/职称改用 useDictionary (health_department/health_title)
- FollowUpTaskList 随访类型改用 useDictionary (health_follow_up_type)
- ConsultationList 咨询类型改用 useDictionary (health_consultation_type)
- FamilyMembersTab 家庭关系改用 useDictionary (health_relationship)
2026-05-02 11:27:11 +08:00

413 lines
12 KiB
TypeScript

import { useState, useCallback, useRef, useMemo } from 'react';
import {
Table,
Button,
Space,
Modal,
Form,
Input,
Select,
Badge,
Popconfirm,
message,
Row,
Col,
} from 'antd';
import {
PlusOutlined,
SearchOutlined,
EditOutlined,
DeleteOutlined,
} from '@ant-design/icons';
import { doctorApi, type Doctor, type CreateDoctorReq, type UpdateDoctorReq } from '../../api/health/doctors';
import { useDictionary } from '../../hooks/useDictionary';
import { AuthButton } from '../../components/AuthButton';
import { PageContainer } from '../../components/PageContainer';
import { EntityName } from '../../components/EntityName';
import { formatDateTime } from '../../utils/format';
import { usePaginatedData } from '../../hooks/usePaginatedData';
/** 科室选项 — 可后续改为从字典接口获取 */
const DEPARTMENT_FALLBACK = [
{ value: '全科', label: '全科' },
{ value: '内科', label: '内科' },
{ value: '外科', label: '外科' },
{ value: '儿科', label: '儿科' },
{ value: '妇产科', label: '妇产科' },
{ value: '骨科', label: '骨科' },
{ value: '眼科', label: '眼科' },
{ value: '口腔科', label: '口腔科' },
{ value: '皮肤科', label: '皮肤科' },
{ value: '中医科', label: '中医科' },
{ value: '体检中心', label: '体检中心' },
];
const TITLE_FALLBACK = [
{ value: '住院医师', label: '住院医师' },
{ value: '主治医师', label: '主治医师' },
{ value: '副主任医师', label: '副主任医师' },
{ value: '主任医师', label: '主任医师' },
{ value: '护士', label: '护士' },
{ value: '护师', label: '护师' },
{ value: '主管护师', label: '主管护师' },
{ value: '副主任护师', label: '副主任护师' },
{ value: '主任护师', label: '主任护师' },
];
const STATUS_OPTIONS = [
{ value: 'online', label: '在线' },
{ value: 'offline', label: '离线' },
{ value: 'busy', label: '忙碌' },
];
const ONLINE_STATUS_MAP: Record<string, { status: 'success' | 'default' | 'processing'; text: string }> = {
online: { status: 'success', text: '在线' },
offline: { status: 'default', text: '离线' },
busy: { status: 'processing', text: '忙碌' },
};
/** 筛选器类型 */
interface DoctorFilters {
search: string;
department: string | undefined;
title: string | undefined;
status: string | undefined;
}
export default function DoctorList() {
const { options: DEPARTMENT_OPTIONS } = useDictionary('health_department', DEPARTMENT_FALLBACK);
const { options: TITLE_OPTIONS } = useDictionary('health_title', TITLE_FALLBACK);
const [modalOpen, setModalOpen] = useState(false);
const [editing, setEditing] = useState<Doctor | null>(null);
const [form] = Form.useForm();
// ---- 数据获取 ----
const fetcher = useCallback(
async (page: number, pageSize: number, filters: DoctorFilters) => {
return doctorApi.list({
page,
page_size: pageSize,
search: filters.search || undefined,
department: filters.department || undefined,
title: filters.title || undefined,
});
},
[],
);
const {
data,
total,
page,
loading,
filters,
setFilters,
refresh,
} = usePaginatedData<Doctor, DoctorFilters>(fetcher, {
pageSize: 20,
defaultFilters: { search: '', department: undefined, title: undefined, status: undefined },
});
// ---- 搜索防抖 ----
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const handleSearchChange = useCallback((val: string) => {
setFilters((prev) => ({ ...prev, search: val }));
if (debounceRef.current) clearTimeout(debounceRef.current);
debounceRef.current = setTimeout(() => refresh(1), 300);
}, [setFilters, refresh]);
const handleFilterChange = useCallback(
(key: keyof DoctorFilters, value: string | undefined) => {
setFilters((prev) => ({ ...prev, [key]: value }));
refresh(1);
},
[setFilters, refresh],
);
const resetFilters = useCallback(() => {
setFilters({ search: '', department: undefined, title: undefined, status: undefined });
refresh(1);
}, [setFilters, refresh]);
// ---- 新建 / 编辑 ----
const openCreate = () => {
setEditing(null);
form.resetFields();
setModalOpen(true);
};
const openEdit = (record: Doctor) => {
setEditing(record);
form.setFieldsValue({
name: record.name,
department: record.department,
title: record.title,
specialty: record.specialty,
license_number: record.license_number,
bio: record.bio,
});
setModalOpen(true);
};
const handleSubmit = async (values: {
name: string;
department?: string;
title?: string;
specialty?: string;
license_number?: string;
bio?: string;
}) => {
try {
if (editing) {
const req: UpdateDoctorReq & { version: number } = {
name: values.name,
department: values.department,
title: values.title,
specialty: values.specialty,
license_number: values.license_number,
bio: values.bio,
version: editing.version,
};
await doctorApi.update(editing.id, req);
message.success('更新成功');
} else {
const req: CreateDoctorReq = {
name: values.name,
department: values.department,
title: values.title,
specialty: values.specialty,
license_number: values.license_number,
bio: values.bio,
};
await doctorApi.create(req);
message.success('创建成功');
}
setModalOpen(false);
form.resetFields();
refresh();
} catch {
message.error(editing ? '更新失败' : '创建失败');
}
};
// ---- 删除 ----
const handleDelete = async (id: string) => {
try {
await doctorApi.delete(id);
message.success('删除成功');
refresh();
} catch {
message.error('删除失败');
}
};
// ---- 列定义 ----
const columns = useMemo(() => [
{
title: '姓名',
dataIndex: 'name',
key: 'name',
width: 120,
fixed: 'left' as const,
},
{
title: '科室',
dataIndex: 'department',
key: 'department',
width: 120,
render: (val: string) => val || '-',
},
{
title: '职称',
dataIndex: 'title',
key: 'title',
width: 120,
render: (val: string) => val || '-',
},
{
title: '专长',
dataIndex: 'specialty',
key: 'specialty',
width: 200,
ellipsis: true,
render: (val: string) => val || '-',
},
{
title: '执业编号',
dataIndex: 'license_number',
key: 'license_number',
width: 150,
render: (val: string) => val || '-',
},
{
title: '关联用户',
dataIndex: 'user_id',
key: 'user_id',
width: 120,
render: (_: unknown, record: Doctor) =>
record.user_id ? (
<EntityName name={record.name} id={record.user_id} fallbackLabel="已关联" />
) : (
'-'
),
},
{
title: '在线状态',
dataIndex: 'online_status',
key: 'online_status',
width: 100,
render: (val: string) => {
const cfg = ONLINE_STATUS_MAP[val] || { status: 'default' as const, text: val };
return <Badge status={cfg.status} text={cfg.text} />;
},
},
{
title: '创建时间',
dataIndex: 'created_at',
key: 'created_at',
width: 180,
render: (val: string) => formatDateTime(val),
},
{
title: '操作',
key: 'action',
width: 140,
fixed: 'right' as const,
render: (_: unknown, record: Doctor) => (
<AuthButton code="health.doctor.manage">
<Space size="small">
<Button
type="link"
size="small"
icon={<EditOutlined />}
onClick={() => openEdit(record)}
>
</Button>
<Popconfirm
title="确定删除该医护?"
onConfirm={() => handleDelete(record.id)}
okText="确定"
cancelText="取消"
>
<Button type="link" size="small" danger icon={<DeleteOutlined />}>
</Button>
</Popconfirm>
</Space>
</AuthButton>
),
},
], [openEdit, handleDelete]);
return (
<PageContainer
title="医护管理"
filters={
<Space wrap>
<Input
placeholder="搜索姓名"
prefix={<SearchOutlined />}
value={filters.search}
onChange={(e) => handleSearchChange(e.target.value)}
allowClear
style={{ width: 220 }}
/>
<Select
placeholder="筛选科室"
value={filters.department}
onChange={(val) => handleFilterChange('department', val)}
options={DEPARTMENT_OPTIONS}
allowClear
style={{ width: 160 }}
/>
<Select
placeholder="筛选职称"
value={filters.title}
onChange={(val) => handleFilterChange('title', val)}
options={TITLE_OPTIONS}
allowClear
style={{ width: 160 }}
/>
<Select
placeholder="在线状态"
value={filters.status}
onChange={(val) => handleFilterChange('status', val)}
options={STATUS_OPTIONS}
allowClear
style={{ width: 120 }}
/>
</Space>
}
onResetFilters={resetFilters}
actions={
<AuthButton code="health.doctor.manage">
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
</Button>
</AuthButton>
}
>
<Table
rowKey="id"
columns={columns}
dataSource={data}
loading={loading}
scroll={{ x: 1300 }}
pagination={{
current: page,
pageSize: 20,
total,
showSizeChanger: true,
showTotal: (t) => `${t}`,
onChange: (p) => refresh(p),
}}
/>
{/* 新建 / 编辑弹窗 */}
<Modal
title={editing ? '编辑医护' : '新建医护'}
open={modalOpen}
onCancel={() => {
setModalOpen(false);
form.resetFields();
}}
onOk={() => form.submit()}
destroyOnClose
width={560}
>
<Form form={form} layout="vertical" onFinish={handleSubmit}>
<Form.Item
name="name"
label="姓名"
rules={[{ required: true, message: '请输入姓名' }]}
>
<Input placeholder="请输入医护姓名" />
</Form.Item>
<Row gutter={16}>
<Col span={12}>
<Form.Item name="department" label="科室">
<Select placeholder="选择科室" options={DEPARTMENT_OPTIONS} allowClear />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="title" label="职称">
<Select placeholder="选择职称" options={TITLE_OPTIONS} allowClear />
</Form.Item>
</Col>
</Row>
<Form.Item name="specialty" label="专长">
<Input.TextArea rows={2} placeholder="如:心血管疾病、糖尿病管理" />
</Form.Item>
<Form.Item name="license_number" label="执业编号">
<Input placeholder="请输入执业编号" />
</Form.Item>
<Form.Item name="bio" label="简介">
<Input.TextArea rows={3} placeholder="个人简介" />
</Form.Item>
</Form>
</Modal>
</PageContainer>
);
}