使用 AuthButton 声明式组件包装健康模块全部操作按钮: - health.patient.manage: PatientList/PatientDetail/PatientTagManage - health.appointment.manage: AppointmentList - health.doctor.manage: DoctorList/DoctorSchedule - health.follow-up.manage: FollowUpTaskList - health.consultation.manage: ConsultationList/ConsultationDetail - health.points.manage: OfflineEventList/PointsProductList/PointsOrderList/PointsRuleList
294 lines
7.9 KiB
TypeScript
294 lines
7.9 KiB
TypeScript
import { useEffect, useState, useCallback } from 'react';
|
|
import {
|
|
Table,
|
|
Button,
|
|
Space,
|
|
Modal,
|
|
Form,
|
|
Input,
|
|
Select,
|
|
Badge,
|
|
message,
|
|
Card,
|
|
Row,
|
|
Col,
|
|
Tag,
|
|
} from 'antd';
|
|
import {
|
|
CheckCircleOutlined,
|
|
} from '@ant-design/icons';
|
|
import dayjs from 'dayjs';
|
|
import {
|
|
pointsApi,
|
|
type PointsOrder,
|
|
} from '../../api/health/points';
|
|
import { patientApi } from '../../api/health/patients';
|
|
import { AuthButton } from '../../components/AuthButton';
|
|
|
|
/** 订单状态映射 */
|
|
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;
|
|
}
|
|
|
|
export default function PointsOrderList() {
|
|
const [data, setData] = useState<PointsOrder[]>([]);
|
|
const [total, setTotal] = useState(0);
|
|
const [page, setPage] = useState(1);
|
|
const [pageSize, setPageSize] = useState(20);
|
|
const [loading, setLoading] = useState(false);
|
|
const [statusFilter, setStatusFilter] = useState<string | undefined>(undefined);
|
|
const [verifyModalOpen, setVerifyModalOpen] = useState(false);
|
|
const [verifyForm] = Form.useForm();
|
|
const [verifying, setVerifying] = useState(false);
|
|
|
|
// 名称缓存
|
|
const [nameCache, setNameCache] = useState<Record<string, string>>({});
|
|
|
|
// ---- 数据获取 ----
|
|
const fetchData = useCallback(async (p = page, ps = pageSize) => {
|
|
setLoading(true);
|
|
try {
|
|
const result = await pointsApi.listOrders({
|
|
page: p,
|
|
page_size: ps,
|
|
status: statusFilter || undefined,
|
|
});
|
|
setData(result.data);
|
|
setTotal(result.total);
|
|
|
|
// 批量解析患者名称
|
|
const patientIds = [...new Set(result.data.map((o) => o.patient_id))];
|
|
const missingIds = patientIds.filter((id) => !nameCache[id]);
|
|
if (missingIds.length > 0) {
|
|
const newNames: Record<string, string> = {};
|
|
await Promise.all(
|
|
missingIds.map(async (id) => {
|
|
try {
|
|
const detail = await patientApi.get(id);
|
|
newNames[id] = detail.name;
|
|
} catch {
|
|
newNames[id] = id.slice(0, 8);
|
|
}
|
|
}),
|
|
);
|
|
setNameCache((prev) => ({ ...prev, ...newNames }));
|
|
}
|
|
} catch {
|
|
message.error('加载订单列表失败');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [page, pageSize, statusFilter, nameCache]);
|
|
|
|
useEffect(() => {
|
|
fetchData();
|
|
}, [fetchData]);
|
|
|
|
// ---- 核销 ----
|
|
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();
|
|
fetchData(page, pageSize);
|
|
} catch {
|
|
message.error('核销失败,请检查二维码是否正确');
|
|
} finally {
|
|
setVerifying(false);
|
|
}
|
|
};
|
|
|
|
// ---- 列定义 ----
|
|
const columns = [
|
|
{
|
|
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) => nameCache[id] || id.slice(0, 8),
|
|
},
|
|
{
|
|
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) => (val ? dayjs(val).format('YYYY-MM-DD HH:mm') : '-'),
|
|
},
|
|
{
|
|
title: '核销时间',
|
|
dataIndex: 'verified_at',
|
|
key: 'verified_at',
|
|
width: 170,
|
|
render: (val: string) => (val ? dayjs(val).format('YYYY-MM-DD HH:mm') : '-'),
|
|
},
|
|
{
|
|
title: '核销人',
|
|
dataIndex: 'verified_by',
|
|
key: 'verified_by',
|
|
width: 100,
|
|
render: (val: string | null) => val ? <Tag color="blue">{nameCache[val] || val.slice(0, 8)}</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 }}>
|
|
{dayjs(val).format('YYYY-MM-DD HH:mm')}
|
|
</span>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
title: '备注',
|
|
dataIndex: 'notes',
|
|
key: 'notes',
|
|
width: 150,
|
|
ellipsis: true,
|
|
render: (val: string | null) => val || '-',
|
|
},
|
|
];
|
|
|
|
return (
|
|
<Card>
|
|
{/* 筛选栏 */}
|
|
<Row gutter={16} style={{ marginBottom: 16 }} align="middle">
|
|
<Col flex="auto">
|
|
<Space>
|
|
<Select
|
|
placeholder="筛选状态"
|
|
value={statusFilter}
|
|
onChange={(val) => {
|
|
setStatusFilter(val);
|
|
setPage(1);
|
|
}}
|
|
options={STATUS_OPTIONS}
|
|
allowClear
|
|
style={{ width: 140 }}
|
|
/>
|
|
</Space>
|
|
</Col>
|
|
<Col>
|
|
<AuthButton code="health.points.manage">
|
|
<Button
|
|
type="primary"
|
|
icon={<CheckCircleOutlined />}
|
|
onClick={openVerifyModal}
|
|
>
|
|
核销订单
|
|
</Button>
|
|
</AuthButton>
|
|
</Col>
|
|
</Row>
|
|
|
|
{/* 数据表格 */}
|
|
<Table
|
|
rowKey="id"
|
|
columns={columns}
|
|
dataSource={data}
|
|
loading={loading}
|
|
scroll={{ x: 1200 }}
|
|
pagination={{
|
|
current: page,
|
|
pageSize,
|
|
total,
|
|
showSizeChanger: true,
|
|
showTotal: (t) => `共 ${t} 条`,
|
|
onChange: (p, ps) => {
|
|
setPage(p);
|
|
setPageSize(ps);
|
|
},
|
|
}}
|
|
/>
|
|
|
|
{/* 核销弹窗 */}
|
|
<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>
|
|
</Card>
|
|
);
|
|
}
|