This commit is contained in:
2026-08-10 17:59:24 +08:00
parent c1092f0deb
commit 7237e8715f
7 changed files with 170 additions and 166 deletions
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -28,7 +28,7 @@
} }
})(); })();
</script> </script>
<script type="module" crossorigin src="/assets/index-D9cWJZKc.js"></script> <script type="module" crossorigin src="/assets/index-D-7FxsJd.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-D3fwIbOp.css"> <link rel="stylesheet" crossorigin href="/assets/index-D3fwIbOp.css">
</head> </head>
<body> <body>
+2 -2
View File
@@ -204,7 +204,7 @@ const AdminInvoices: React.FC = () => {
{ {
title: '操作', title: '操作',
key: 'actions', key: 'actions',
width: 200, width: 260,
render: (_: unknown, record: InvoiceItem) => ( render: (_: unknown, record: InvoiceItem) => (
<Space size={4}> <Space size={4}>
<Button type="link" size="small" icon={<EyeOutlined />} onClick={() => handleViewDetail(record.id)}> <Button type="link" size="small" icon={<EyeOutlined />} onClick={() => handleViewDetail(record.id)}>
@@ -310,7 +310,7 @@ const AdminInvoices: React.FC = () => {
showSizeChanger: true, showSizeChanger: true,
showTotal: (t) => `${t} 条记录`, showTotal: (t) => `${t} 条记录`,
}} }}
scroll={{ x: 1200 }} scroll={{ x: 1400 }}
/> />
)} )}
</Card> </Card>
+31 -1
View File
@@ -332,7 +332,37 @@ async def list_orders(
for o in orders: for o in orders:
await _check_and_expire_order(db, o) await _check_and_expire_order(db, o)
return {"items": [PaymentOrderOut.model_validate(o) for o in orders], "total": total} # 开票模式:附带订单占用状态
items = []
if invoice_mode:
# 收集当前页订单ID
order_ids = [o.id for o in orders]
# 查询这些订单是否已被占用
from app.models.invoice import Invoice, InvoiceOrder
occupied_map: dict[str, str] = {}
if order_ids:
occ_result = await db.execute(
select(InvoiceOrder.order_id, Invoice.invoice_no)
.join(Invoice, InvoiceOrder.invoice_id == Invoice.id)
.where(
InvoiceOrder.order_id.in_(order_ids),
Invoice.status.in_(["processing", "success"]),
)
)
for row in occ_result.all():
occupied_map[row.order_id] = row.invoice_no
for o in orders:
item = PaymentOrderOut.model_validate(o)
item_dict = item.model_dump()
item_dict["is_occupied"] = o.id in occupied_map
item_dict["occupied_by"] = occupied_map.get(o.id)
items.append(item_dict)
else:
for o in orders:
item = PaymentOrderOut.model_validate(o)
items.append(item.model_dump())
return {"items": items, "total": total}
@router.get("/orders/{order_no}", response_model=PaymentOrderOut) @router.get("/orders/{order_no}", response_model=PaymentOrderOut)
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -27,7 +27,7 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet" /> <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
<title>民众智创</title> <title>民众智创</title>
<script type="module" crossorigin src="/assets/index-qMch-3Hx.js"></script> <script type="module" crossorigin src="/assets/index-CFnMIoCK.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DtYH0-uL.css"> <link rel="stylesheet" crossorigin href="/assets/index-DtYH0-uL.css">
</head> </head>
<body> <body>
+36 -62
View File
@@ -57,7 +57,6 @@ const InvoicePage: React.FC = () => {
const [orderTotal, setOrderTotal] = useState(0); const [orderTotal, setOrderTotal] = useState(0);
const [orderPage, setOrderPage] = useState(1); const [orderPage, setOrderPage] = useState(1);
const [orderPageSize] = useState(10); const [orderPageSize] = useState(10);
const [occupiedOrderIds, setOccupiedOrderIds] = useState<Set<string>>(new Set());
// 日期筛选:默认不选,展示全部订单 // 日期筛选:默认不选,展示全部订单
const [orderDateFilter, setOrderDateFilter] = useState<[string | null, string | null]>([null, null]); const [orderDateFilter, setOrderDateFilter] = useState<[string | null, string | null]>([null, null]);
const [orderDateRange, setOrderDateRange] = useState<any>([null, null]); const [orderDateRange, setOrderDateRange] = useState<any>([null, null]);
@@ -81,26 +80,8 @@ const InvoicePage: React.FC = () => {
}; };
// 加载已开票订单ID列表,返回 occupied Set // 加载已开票订单ID列表,返回 occupied Set
const loadOccupiedOrderIds = async (): Promise<Set<string>> => { // 加载订单数据(开票模式:后端返回已支付订单 + 占用状态)
try { const loadOrderData = async (page: number = orderPage) => {
const data = await getInvoices({ page: 1, pageSize: 100 });
const ids = new Set<string>();
(data.items || []).forEach((inv: any) => {
if (inv.status === 'processing' || inv.status === 'success') {
(inv.orders || []).forEach((o: any) => {
ids.add(o.orderId || o.order_id || o.orderNo || o.order_no);
});
}
});
setOccupiedOrderIds(ids);
return ids;
} catch {
return occupiedOrderIds;
}
};
// 加载订单数据(开票模式:后端只返回已支付订单)
const loadOrderData = async (page: number = orderPage, occupiedIds?: Set<string>) => {
setOrderLoading(true); setOrderLoading(true);
try { try {
const data = await getPaymentOrders(page, orderPageSize, { const data = await getPaymentOrders(page, orderPageSize, {
@@ -109,11 +90,11 @@ const InvoicePage: React.FC = () => {
endDate: orderDateFilter[1] || undefined, endDate: orderDateFilter[1] || undefined,
}); });
const items = data.items || []; const items = data.items || [];
// 标记已开票订单 // 后端已返回 is_occupied 和 occupied_by,直接使用
const occIds = occupiedIds || occupiedOrderIds;
const markedItems = items.map((o: any) => ({ const markedItems = items.map((o: any) => ({
...o, ...o,
_occupied: occIds.has(o.id || o.order_no || o.orderNo), _occupied: !!o.is_occupied,
_occupiedBy: o.occupied_by,
})); }));
setOrderData(markedItems); setOrderData(markedItems);
setOrderTotal(data.total || 0); setOrderTotal(data.total || 0);
@@ -124,11 +105,16 @@ const InvoicePage: React.FC = () => {
setOrderLoading(false); setOrderLoading(false);
}; };
// 发票记录分页
const [invoicePage, setInvoicePage] = useState(1);
const [invoicePageSize] = useState(10);
const [invoiceTotal, setInvoiceTotal] = useState(0);
// 加载发票记录 // 加载发票记录
const loadInvoices = async () => { const loadInvoices = async () => {
setRecordsLoading(true); setRecordsLoading(true);
try { try {
const data = await getInvoices({ page: 1, pageSize: 50 }); const data = await getInvoices({ page: invoicePage, pageSize: invoicePageSize });
const items: InvoiceRecord[] = (data.items || []).map((item: any) => ({ const items: InvoiceRecord[] = (data.items || []).map((item: any) => ({
id: item.id, id: item.id,
orderNo: item.invoiceNo, orderNo: item.invoiceNo,
@@ -145,6 +131,7 @@ const InvoicePage: React.FC = () => {
failureReason: item.failureReason, failureReason: item.failureReason,
})); }));
setRecords(items); setRecords(items);
setInvoiceTotal(data.total || 0);
} catch { } catch {
// 静默失败,保留空列表 // 静默失败,保留空列表
} }
@@ -153,17 +140,21 @@ const InvoicePage: React.FC = () => {
useEffect(() => { useEffect(() => {
loadInvoices(); loadInvoices();
loadOccupiedOrderIds();
loadHeaders(); loadHeaders();
}, []); }, []);
// 分页切换时重新加载发票记录
useEffect(() => {
if (invoicePage > 1) {
loadInvoices();
}
}, [invoicePage]);
useEffect(() => { useEffect(() => {
if (issueModalOpen) { if (issueModalOpen) {
// 先刷新已开票订单列表,再加载订单数据 // 后端已在订单列表中返回占用状态,直接加载
loadOccupiedOrderIds().then((ids) => { setOrderPage(1);
setOrderPage(1); loadOrderData(1);
loadOrderData(1, ids);
});
} }
}, [issueModalOpen]); }, [issueModalOpen]);
@@ -305,7 +296,11 @@ const InvoicePage: React.FC = () => {
render: (_: any, record: any) => { render: (_: any, record: any) => {
// 已开票/开票中标签 // 已开票/开票中标签
if (record._occupied) { if (record._occupied) {
return <Tag color="orange" style={{ fontWeight: 500, padding: '4px 12px' }}></Tag>; return (
<Tag color="orange" style={{ fontWeight: 500, padding: '4px 12px' }} title={record._occupiedBy ? `发票号:${record._occupiedBy}` : undefined}>
</Tag>
);
} }
const status = record.status || 'pending'; const status = record.status || 'pending';
const statusConfig: Record<string, { color: string; label: string; bg: string }> = { const statusConfig: Record<string, { color: string; label: string; bg: string }> = {
@@ -505,15 +500,10 @@ const InvoicePage: React.FC = () => {
return; return;
} }
const selectedHeader = allHeadersForSelect.find(h => h.id === selectedHeaderId); const selectedHeader = allHeadersForSelect.find(h => h.id === selectedHeaderId);
// 优先使用弹窗中填写的邮箱,否则使用抬头中保存的邮箱 // 直接使用选中抬头的邮箱
const finalEmail = (email || selectedHeader?.email || '').trim(); const finalEmail = (selectedHeader?.email || '').trim();
if (!finalEmail) { if (!finalEmail) {
message.warning('请输入电子邮箱'); message.warning('该发票抬头未填写邮箱,请先在抬头管理中设置邮箱');
return;
}
const emailRegex = /^[\w.\-]+@[\w.\-]+\.\w+$/;
if (!emailRegex.test(finalEmail)) {
message.warning('请输入正确的邮箱格式');
return; return;
} }
const selectedOrders = Array.from(selectedOrdersMap.values()); const selectedOrders = Array.from(selectedOrdersMap.values());
@@ -668,7 +658,12 @@ const InvoicePage: React.FC = () => {
dataSource={records} dataSource={records}
columns={columns} columns={columns}
rowKey="id" rowKey="id"
pagination={false} pagination={{
current: invoicePage,
pageSize: invoicePageSize,
total: invoiceTotal,
onChange: (p) => { setInvoicePage(p); }
}}
bordered={false} bordered={false}
/> />
)} )}
@@ -928,27 +923,6 @@ const InvoicePage: React.FC = () => {
closeIcon={<CloseOutlined style={{ fontSize: 16, color: '#94a3b8' }} />} closeIcon={<CloseOutlined style={{ fontSize: 16, color: '#94a3b8' }} />}
> >
<div style={{ padding: '0 24px 24px' }}> <div style={{ padding: '0 24px 24px' }}>
{/* 邮箱显示(自动使用抬头邮箱,可修改) */}
{(() => {
const currentHeader = allHeadersForSelect.find(h => h.id === selectedHeaderId);
const displayEmail = email || currentHeader?.email || '';
return (
<div style={{ padding: '16px 0 12px', borderBottom: '1px solid #f1f5f9', marginBottom: 12 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<span style={{ color: '#94a3b8', fontSize: 13, whiteSpace: 'nowrap' }}>
<span style={{ color: '#ef4444', marginRight: 4 }}>*</span>
</span>
<Input
placeholder="发票将发送至该邮箱"
value={displayEmail}
onChange={(e) => setEmail(e.target.value)}
style={{ maxWidth: 360 }}
allowClear
/>
</div>
</div>
);
})()}
<Table <Table
dataSource={allHeadersForSelect} dataSource={allHeadersForSelect}
columns={[ columns={[