1
This commit is contained in:
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
+98
-98
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -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>
|
||||||
|
|||||||
@@ -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={[
|
||||||
|
|||||||
Reference in New Issue
Block a user