- 新增 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)
334 lines
9.6 KiB
TypeScript
334 lines
9.6 KiB
TypeScript
import { useState, useCallback } from 'react';
|
|
import {
|
|
Table,
|
|
Select,
|
|
Button,
|
|
Modal,
|
|
Form,
|
|
Space,
|
|
Popconfirm,
|
|
message,
|
|
DatePicker,
|
|
} from 'antd';
|
|
import { PlusOutlined, CloseCircleOutlined } from '@ant-design/icons';
|
|
import type { ColumnsType, TablePaginationConfig } from 'antd/es/table';
|
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
|
import { consultationApi, type Session, type CreateSessionReq } from '../../api/health/consultations';
|
|
import { StatusTag } from './components/StatusTag';
|
|
import { PatientSelect } from './components/PatientSelect';
|
|
import { DoctorSelect } from './components/DoctorSelect';
|
|
import { ExportButton } from './components/ExportButton';
|
|
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';
|
|
import { useDictionary } from '../../hooks/useDictionary';
|
|
|
|
const STATUS_OPTIONS = [
|
|
{ value: 'waiting', label: '等待中' },
|
|
{ value: 'active', label: '进行中' },
|
|
{ value: 'closed', label: '已关闭' },
|
|
];
|
|
|
|
const CONSULTATION_TYPE_FALLBACK = [
|
|
{ value: 'customer_service', label: '客服咨询' },
|
|
{ value: 'medical', label: '医疗咨询' },
|
|
{ value: 'health_consultation', label: '健康咨询' },
|
|
];
|
|
|
|
const CONSULTATION_TYPE_MAP: Record<string, string> = {
|
|
customer_service: '客服咨询',
|
|
medical: '医疗咨询',
|
|
health_consultation: '健康咨询',
|
|
};
|
|
|
|
interface ConsultationFilters {
|
|
status?: string;
|
|
dateRange?: [string, string];
|
|
}
|
|
|
|
export default function ConsultationList() {
|
|
const { options: CONSULTATION_TYPE_OPTIONS } = useDictionary('health_consultation_type', CONSULTATION_TYPE_FALLBACK);
|
|
const navigate = useNavigate();
|
|
const [searchParams] = useSearchParams();
|
|
const urlPatientId = searchParams.get('patient_id');
|
|
|
|
// Close session
|
|
const [closingId, setClosingId] = useState<string | null>(null);
|
|
|
|
// Create modal
|
|
const [createOpen, setCreateOpen] = useState(false);
|
|
const [createLoading, setCreateLoading] = useState(false);
|
|
const [createForm] = Form.useForm<CreateSessionReq>();
|
|
|
|
// --- Paginated data with usePaginatedData ---
|
|
const fetchFn = useCallback(
|
|
async (page: number, pageSize: number, filters: ConsultationFilters) => {
|
|
const params: Record<string, unknown> = { page, page_size: pageSize };
|
|
if (filters.status) params.status = filters.status;
|
|
if (urlPatientId) params.patient_id = urlPatientId;
|
|
if (filters.dateRange) {
|
|
params.created_start = filters.dateRange[0];
|
|
params.created_end = filters.dateRange[1];
|
|
}
|
|
return consultationApi.listSessions(params as Parameters<typeof consultationApi.listSessions>[0]);
|
|
},
|
|
[urlPatientId],
|
|
);
|
|
|
|
const {
|
|
data: sessions,
|
|
total,
|
|
page,
|
|
loading,
|
|
filters,
|
|
setFilters,
|
|
refresh,
|
|
} = usePaginatedData<Session, ConsultationFilters>(fetchFn, {
|
|
pageSize: 20,
|
|
defaultFilters: {},
|
|
});
|
|
|
|
// --- Handlers ---
|
|
const handleTableChange = (pagination: TablePaginationConfig) => {
|
|
refresh(pagination.current ?? 1);
|
|
};
|
|
|
|
// Create session
|
|
const handleCreate = async () => {
|
|
try {
|
|
const values = await createForm.validateFields();
|
|
setCreateLoading(true);
|
|
await consultationApi.createSession(values);
|
|
message.success('咨询会话创建成功');
|
|
setCreateOpen(false);
|
|
createForm.resetFields();
|
|
refresh(page);
|
|
} catch (err: unknown) {
|
|
if (err && typeof err === 'object' && 'errorFields' in err) return;
|
|
message.error('创建咨询会话失败');
|
|
} finally {
|
|
setCreateLoading(false);
|
|
}
|
|
};
|
|
|
|
// Close session
|
|
const handleClose = async (session: Session) => {
|
|
setClosingId(session.id);
|
|
try {
|
|
await consultationApi.closeSession(session.id, { version: session.version });
|
|
message.success('会话已关闭');
|
|
refresh(page);
|
|
} catch {
|
|
message.error('关闭会话失败');
|
|
} finally {
|
|
setClosingId(null);
|
|
}
|
|
};
|
|
|
|
// Row click -> navigate to detail
|
|
const handleRowClick = (record: Session) => {
|
|
navigate(`/health/consultations/${record.id}`);
|
|
};
|
|
|
|
// Export params
|
|
const exportParams: Record<string, string> = {};
|
|
if (filters.status) exportParams.status = filters.status;
|
|
|
|
// --- Columns ---
|
|
const columns: ColumnsType<Session> = [
|
|
{
|
|
title: '患者',
|
|
dataIndex: 'patient_name',
|
|
key: 'patient_name',
|
|
width: 140,
|
|
render: (_: unknown, record: Session) => (
|
|
<EntityName name={record.patient_name} id={record.patient_id} />
|
|
),
|
|
},
|
|
{
|
|
title: '医护',
|
|
dataIndex: 'doctor_name',
|
|
key: 'doctor_name',
|
|
width: 140,
|
|
render: (_: unknown, record: Session) => (
|
|
<EntityName name={record.doctor_name} id={record.doctor_id} fallbackLabel="未分配" />
|
|
),
|
|
},
|
|
{
|
|
title: '咨询类型',
|
|
dataIndex: 'consultation_type',
|
|
key: 'consultation_type',
|
|
width: 110,
|
|
render: (v: string) => CONSULTATION_TYPE_MAP[v] || v,
|
|
},
|
|
{
|
|
title: '状态',
|
|
dataIndex: 'status',
|
|
key: 'status',
|
|
width: 100,
|
|
render: (status: string) => <StatusTag status={status} />,
|
|
},
|
|
{
|
|
title: '未读(患者/医护)',
|
|
key: 'unread',
|
|
width: 140,
|
|
render: (_: unknown, record: Session) => (
|
|
<span>
|
|
{record.unread_count_patient} / {record.unread_count_doctor}
|
|
</span>
|
|
),
|
|
},
|
|
{
|
|
title: '最后消息时间',
|
|
dataIndex: 'last_message_at',
|
|
key: 'last_message_at',
|
|
width: 160,
|
|
render: (v: string | undefined) => formatDateTime(v),
|
|
},
|
|
{
|
|
title: '创建时间',
|
|
dataIndex: 'created_at',
|
|
key: 'created_at',
|
|
width: 160,
|
|
render: (v: string) => formatDateTime(v),
|
|
},
|
|
{
|
|
title: '操作',
|
|
key: 'actions',
|
|
width: 120,
|
|
render: (_: unknown, record: Session) => (
|
|
<AuthButton code="health.consultation.manage">
|
|
<Space size={4}>
|
|
{record.status !== 'closed' && (
|
|
<Popconfirm
|
|
title="确认关闭该咨询会话?"
|
|
onConfirm={() => handleClose(record)}
|
|
okText="确认"
|
|
cancelText="取消"
|
|
>
|
|
<Button
|
|
type="link"
|
|
size="small"
|
|
danger
|
|
icon={<CloseCircleOutlined />}
|
|
loading={closingId === record.id}
|
|
>
|
|
关闭
|
|
</Button>
|
|
</Popconfirm>
|
|
)}
|
|
</Space>
|
|
</AuthButton>
|
|
),
|
|
},
|
|
];
|
|
|
|
return (
|
|
<PageContainer
|
|
title="咨询管理"
|
|
subtitle={`共 ${total} 条`}
|
|
filters={
|
|
<>
|
|
<Select
|
|
allowClear
|
|
placeholder="状态筛选"
|
|
style={{ width: 140 }}
|
|
options={STATUS_OPTIONS}
|
|
value={filters.status}
|
|
onChange={(value) => setFilters((prev) => ({ ...prev, status: value }))}
|
|
/>
|
|
<DatePicker.RangePicker
|
|
style={{ width: 240 }}
|
|
onChange={(dates) => {
|
|
if (dates && dates[0] && dates[1]) {
|
|
setFilters((prev) => ({
|
|
...prev,
|
|
dateRange: [dates[0]!.format('YYYY-MM-DD'), dates[1]!.format('YYYY-MM-DD')],
|
|
}));
|
|
} else {
|
|
setFilters((prev) => ({ ...prev, dateRange: undefined }));
|
|
}
|
|
}}
|
|
/>
|
|
</>
|
|
}
|
|
onResetFilters={() => setFilters({})}
|
|
actions={
|
|
<Space>
|
|
<AuthButton code="health.consultation.manage">
|
|
<Button
|
|
type="primary"
|
|
icon={<PlusOutlined />}
|
|
onClick={() => {
|
|
createForm.resetFields();
|
|
setCreateOpen(true);
|
|
}}
|
|
>
|
|
新建会话
|
|
</Button>
|
|
</AuthButton>
|
|
<ExportButton
|
|
fetchUrl="/health/consultation-sessions/export"
|
|
params={exportParams}
|
|
filename="咨询列表.csv"
|
|
/>
|
|
</Space>
|
|
}
|
|
>
|
|
<Table
|
|
rowKey="id"
|
|
columns={columns}
|
|
dataSource={sessions}
|
|
loading={loading}
|
|
onChange={handleTableChange}
|
|
onRow={(record) => ({
|
|
onClick: () => handleRowClick(record),
|
|
style: { cursor: 'pointer' },
|
|
})}
|
|
pagination={{
|
|
current: page,
|
|
pageSize: 20,
|
|
total,
|
|
showSizeChanger: true,
|
|
showTotal: (t) => `共 ${t} 条`,
|
|
}}
|
|
scroll={{ x: 1010 }}
|
|
/>
|
|
|
|
{/* Create Session Modal */}
|
|
<Modal
|
|
title="新建咨询会话"
|
|
open={createOpen}
|
|
onOk={handleCreate}
|
|
onCancel={() => setCreateOpen(false)}
|
|
confirmLoading={createLoading}
|
|
okText="创建"
|
|
cancelText="取消"
|
|
destroyOnClose
|
|
>
|
|
<Form form={createForm} layout="vertical" autoComplete="off">
|
|
<Form.Item
|
|
name="patient_id"
|
|
label="患者"
|
|
rules={[{ required: true, message: '请选择患者' }]}
|
|
>
|
|
<PatientSelect />
|
|
</Form.Item>
|
|
<Form.Item name="doctor_id" label="医护">
|
|
<DoctorSelect />
|
|
</Form.Item>
|
|
<Form.Item name="consultation_type" label="咨询类型">
|
|
<Select
|
|
options={CONSULTATION_TYPE_OPTIONS}
|
|
placeholder="选择咨询类型(默认客服咨询)"
|
|
allowClear
|
|
/>
|
|
</Form.Item>
|
|
</Form>
|
|
</Modal>
|
|
</PageContainer>
|
|
);
|
|
}
|