- 新增 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)
413 lines
12 KiB
TypeScript
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>
|
|
);
|
|
}
|