- AiPromptList/AiAnalysisList/AppointmentList 等 14 个主页面 - HealthRecordsTab/LabReportsTab 2 个 Tab 组件 - 每个 columns 依赖数组包含其引用的闭包变量(handleDelete/navigate 等)
287 lines
7.5 KiB
TypeScript
287 lines
7.5 KiB
TypeScript
import { useState, useCallback, useMemo } from 'react';
|
|
import {
|
|
Table,
|
|
Button,
|
|
|
|
Modal,
|
|
Form,
|
|
Input,
|
|
Select,
|
|
Badge,
|
|
message,
|
|
Tag,
|
|
DatePicker,
|
|
} from 'antd';
|
|
import {
|
|
CheckCircleOutlined,
|
|
} from '@ant-design/icons';
|
|
import {
|
|
pointsApi,
|
|
type PointsOrder,
|
|
} from '../../api/health/points';
|
|
import { AuthButton } from '../../components/AuthButton';
|
|
import { PageContainer } from '../../components/PageContainer';
|
|
import { usePaginatedData } from '../../hooks/usePaginatedData';
|
|
import { useHealthStore } from '../../stores/health';
|
|
import { formatDateTime } from '../../utils/format';
|
|
import type { Dayjs } from 'dayjs';
|
|
import { dayjs } from '../../utils/dayjs';
|
|
|
|
/** 订单状态映射 */
|
|
const STATUS_MAP: Record<string, { text: string; color: string }> = {
|
|
pending: { text: '待核销', color: 'orange' },
|
|
verified: { text: '已核销', color: 'green' },
|
|
cancelled: { text: '已取消', color: 'red' },
|
|
expired: { text: '已过期', color: 'default' },
|
|
};
|
|
|
|
/** 状态筛选选项 */
|
|
const STATUS_OPTIONS = Object.entries(STATUS_MAP).map(([value, { text }]) => ({
|
|
value,
|
|
label: text,
|
|
}));
|
|
|
|
/** 截断 ID 显示 */
|
|
function truncateId(id: string): string {
|
|
if (!id) return '-';
|
|
return id.length > 12 ? `${id.slice(0, 8)}...${id.slice(-4)}` : id;
|
|
}
|
|
|
|
interface OrderFilters {
|
|
status: string | undefined;
|
|
dateRange: [Dayjs, Dayjs] | undefined;
|
|
}
|
|
|
|
export default function PointsOrderList() {
|
|
const [verifyModalOpen, setVerifyModalOpen] = useState(false);
|
|
const [verifyForm] = Form.useForm();
|
|
const [verifying, setVerifying] = useState(false);
|
|
|
|
const { batchResolvePatientNames, getPatientName } = useHealthStore();
|
|
|
|
const fetchOrders = useCallback(
|
|
async (page: number, pageSize: number, filters: OrderFilters) => {
|
|
const result = await pointsApi.listOrders({
|
|
page,
|
|
page_size: pageSize,
|
|
status: filters.status || undefined,
|
|
});
|
|
const patientIds = result.data.map((o) => o.patient_id);
|
|
batchResolvePatientNames(patientIds);
|
|
return { data: result.data, total: result.total };
|
|
},
|
|
[batchResolvePatientNames],
|
|
);
|
|
|
|
const {
|
|
data,
|
|
total,
|
|
page,
|
|
loading,
|
|
filters,
|
|
setFilters,
|
|
refresh,
|
|
} = usePaginatedData<PointsOrder, OrderFilters>(
|
|
fetchOrders,
|
|
{ pageSize: 20, defaultFilters: { status: undefined, dateRange: undefined } },
|
|
);
|
|
|
|
// ---- 核销 ----
|
|
const openVerifyModal = () => {
|
|
verifyForm.resetFields();
|
|
setVerifyModalOpen(true);
|
|
};
|
|
|
|
const handleVerify = async (values: { qr_code: string }) => {
|
|
setVerifying(true);
|
|
try {
|
|
const order = await pointsApi.verifyOrder({ qr_code: values.qr_code });
|
|
message.success(`核销成功,订单 ${truncateId(order.id)} 已确认`);
|
|
setVerifyModalOpen(false);
|
|
verifyForm.resetFields();
|
|
refresh(page);
|
|
} catch {
|
|
message.error('核销失败,请检查二维码是否正确');
|
|
} finally {
|
|
setVerifying(false);
|
|
}
|
|
};
|
|
|
|
const resetFilters = () => {
|
|
setFilters({ status: undefined, dateRange: undefined });
|
|
};
|
|
|
|
// ---- 列定义 ----
|
|
const columns = useMemo(() => [
|
|
{
|
|
title: '订单号',
|
|
dataIndex: 'id',
|
|
key: 'id',
|
|
width: 140,
|
|
render: (val: string) => (
|
|
<span style={{ fontFamily: 'monospace', fontSize: 12 }}>{truncateId(val)}</span>
|
|
),
|
|
},
|
|
{
|
|
title: '患者',
|
|
dataIndex: 'patient_id',
|
|
key: 'patient_id',
|
|
width: 100,
|
|
render: (id: string) => getPatientName(id),
|
|
},
|
|
{
|
|
title: '商品',
|
|
dataIndex: 'product_name',
|
|
key: 'product_name',
|
|
width: 140,
|
|
render: (name: string | null, record: PointsOrder) =>
|
|
name || truncateId(record.product_id),
|
|
},
|
|
{
|
|
title: '积分',
|
|
dataIndex: 'points_cost',
|
|
key: 'points_cost',
|
|
width: 80,
|
|
render: (val: number) => <span style={{ fontWeight: 600, color: '#d97706' }}>{val}</span>,
|
|
},
|
|
{
|
|
title: '状态',
|
|
dataIndex: 'status',
|
|
key: 'status',
|
|
width: 100,
|
|
render: (val: string) => {
|
|
const cfg = STATUS_MAP[val] || { text: val, color: 'default' };
|
|
return <Badge status={cfg.color as 'success' | 'default' | 'processing' | 'error' | 'warning'} text={cfg.text} />;
|
|
},
|
|
},
|
|
{
|
|
title: '创建时间',
|
|
dataIndex: 'created_at',
|
|
key: 'created_at',
|
|
width: 170,
|
|
render: (val: string) => formatDateTime(val),
|
|
},
|
|
{
|
|
title: '核销时间',
|
|
dataIndex: 'verified_at',
|
|
key: 'verified_at',
|
|
width: 170,
|
|
render: (val: string) => formatDateTime(val),
|
|
},
|
|
{
|
|
title: '核销人',
|
|
dataIndex: 'verified_by',
|
|
key: 'verified_by',
|
|
width: 100,
|
|
render: (val: string | null) => val ? <Tag color="blue">{truncateId(val)}</Tag> : '-',
|
|
},
|
|
{
|
|
title: '过期时间',
|
|
dataIndex: 'expires_at',
|
|
key: 'expires_at',
|
|
width: 170,
|
|
render: (val: string | null) => {
|
|
if (!val) return '-';
|
|
const isExpired = dayjs(val).isBefore(dayjs());
|
|
return (
|
|
<span style={{ color: isExpired ? '#dc2626' : undefined }}>
|
|
{formatDateTime(val)}
|
|
</span>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
title: '备注',
|
|
dataIndex: 'notes',
|
|
key: 'notes',
|
|
width: 150,
|
|
ellipsis: true,
|
|
render: (val: string | null) => val || '-',
|
|
},
|
|
], [getPatientName]);
|
|
|
|
return (
|
|
<PageContainer
|
|
title="积分订单"
|
|
filters={
|
|
<>
|
|
<Select
|
|
placeholder="筛选状态"
|
|
value={filters.status}
|
|
onChange={(val) => setFilters((f) => ({ ...f, status: val }))}
|
|
options={STATUS_OPTIONS}
|
|
allowClear
|
|
style={{ width: 140 }}
|
|
/>
|
|
<DatePicker.RangePicker
|
|
value={filters.dateRange ?? undefined}
|
|
onChange={(dates) =>
|
|
setFilters((f) => ({
|
|
...f,
|
|
dateRange: dates as [Dayjs, Dayjs] | undefined,
|
|
}))
|
|
}
|
|
style={{ width: 260 }}
|
|
/>
|
|
</>
|
|
}
|
|
onResetFilters={resetFilters}
|
|
actions={
|
|
<AuthButton code="health.points.manage">
|
|
<Button
|
|
type="primary"
|
|
icon={<CheckCircleOutlined />}
|
|
onClick={openVerifyModal}
|
|
>
|
|
核销订单
|
|
</Button>
|
|
</AuthButton>
|
|
}
|
|
>
|
|
<Table
|
|
rowKey="id"
|
|
columns={columns}
|
|
dataSource={data}
|
|
loading={loading}
|
|
scroll={{ x: 1200 }}
|
|
pagination={{
|
|
current: page,
|
|
pageSize: 20,
|
|
total,
|
|
showSizeChanger: true,
|
|
showTotal: (t) => `共 ${t} 条`,
|
|
onChange: (p) => refresh(p),
|
|
}}
|
|
/>
|
|
|
|
{/* 核销弹窗 */}
|
|
<Modal
|
|
title="核销订单"
|
|
open={verifyModalOpen}
|
|
onCancel={() => {
|
|
setVerifyModalOpen(false);
|
|
verifyForm.resetFields();
|
|
}}
|
|
onOk={() => verifyForm.submit()}
|
|
confirmLoading={verifying}
|
|
destroyOnClose
|
|
width={440}
|
|
>
|
|
<Form form={verifyForm} layout="vertical" onFinish={handleVerify}>
|
|
<Form.Item
|
|
name="qr_code"
|
|
label="二维码内容"
|
|
rules={[{ required: true, message: '请输入或扫码获取二维码内容' }]}
|
|
>
|
|
<Input.TextArea
|
|
rows={3}
|
|
placeholder="请输入订单二维码内容,或使用扫码枪扫码"
|
|
autoFocus
|
|
/>
|
|
</Form.Item>
|
|
</Form>
|
|
</Modal>
|
|
</PageContainer>
|
|
);
|
|
}
|