1、增加订单开发票功能和发票抬头添加功能

2、一个订单只能在一个开票里,不允许多开
This commit is contained in:
2026-08-10 15:39:00 +08:00
parent e77d78db1e
commit 6d30347cdb
14 changed files with 1425 additions and 35 deletions
+37
View File
@@ -460,6 +460,43 @@ export async function cancelPaymentOrder(orderNo: string): Promise<void> {
return api.post(`/payments/orders/${orderNo}/cancel`);
}
// ── Invoices ──────────────────────────────────────────────
export async function createInvoice(data: {
headerType: 'personal' | 'company';
headerName: string;
headerTaxNo?: string;
headerRegisterAddress?: string;
headerRegisterPhone?: string;
headerBankName?: string;
headerBankAccount?: string;
email: string;
orderIds: string[];
}): Promise<any> {
return api.post('/invoices', {
header_type: data.headerType,
header_name: data.headerName,
header_tax_no: data.headerTaxNo,
header_register_address: data.headerRegisterAddress,
header_register_phone: data.headerRegisterPhone,
header_bank_name: data.headerBankName,
header_bank_account: data.headerBankAccount,
email: data.email,
order_ids: data.orderIds,
});
}
export async function getInvoices(params?: { page?: number; pageSize?: number }): Promise<{ items: any[]; total: number }> {
const qs = new URLSearchParams();
if (params?.page) qs.set('page', String(params.page));
if (params?.pageSize) qs.set('page_size', String(params.pageSize));
return api.get(`/invoices?${qs.toString()}`);
}
export async function getInvoiceDetail(id: string): Promise<any> {
return api.get(`/invoices/${id}`);
}
export async function getCreditRatios(): Promise<any[]> {
return api.get('/credits/ratios');
}
+110 -30
View File
@@ -1,7 +1,7 @@
import React, { useEffect, useState } from 'react';
import { Table, Tag, Empty, Spin, Pagination, Button, Typography, Modal, Form, Input, Radio, message, Space } from 'antd';
import { FileTextOutlined, PlusOutlined, CloseOutlined, AlipayCircleOutlined, WechatOutlined } from '@ant-design/icons';
import { getPaymentOrders } from '../api';
import { getPaymentOrders, createInvoice, getInvoices, getInvoiceDetail } from '../api';
interface InvoiceRecord {
id: string;
@@ -37,6 +37,7 @@ const mockHeaders: InvoiceHeader[] = [
const InvoicePage: React.FC = () => {
const [records, setRecords] = useState<InvoiceRecord[]>([]);
const [recordsLoading, setRecordsLoading] = useState(false);
const [headers, setHeaders] = useState<InvoiceHeader[]>([]);
const [headerModalOpen, setHeaderModalOpen] = useState(false);
const [addModalOpen, setAddModalOpen] = useState(false);
@@ -47,6 +48,8 @@ const InvoicePage: React.FC = () => {
const [selectedOrdersMap, setSelectedOrdersMap] = useState<Map<string, any>>(new Map());
const [selectHeaderModalOpen, setSelectHeaderModalOpen] = useState(false);
const [selectedHeaderId, setSelectedHeaderId] = useState<string | null>(null);
const [email, setEmail] = useState<string>('');
const [submitting, setSubmitting] = useState(false);
// 订单列表(从接口获取)
const [orderLoading, setOrderLoading] = useState(false);
@@ -87,6 +90,37 @@ const InvoicePage: React.FC = () => {
setOrderLoading(false);
};
// 加载发票记录
const loadInvoices = async () => {
setRecordsLoading(true);
try {
const data = await getInvoices({ page: 1, pageSize: 50 });
const items = (data.items || []).map((item: any) => ({
id: item.id,
orderNo: item.invoiceNo,
orderType: item.headerType === 'company' ? '企业' : '个人',
amount: item.totalAmount,
createdAt: item.createdAt,
// 后端状态映射:processing→processing, success→issued, failed→rejected
status: item.status === 'success' ? 'issued' : item.status === 'failed' ? 'rejected' : 'processing',
headerName: item.headerName,
headerType: item.headerType,
headerTaxNo: item.headerTaxNo,
selectedOrders: item.orders || [],
email: item.email,
failureReason: item.failureReason,
}));
setRecords(items);
} catch {
// 静默失败,保留空列表
}
setRecordsLoading(false);
};
useEffect(() => {
loadInvoices();
}, []);
useEffect(() => {
if (issueModalOpen) {
loadOrderData(1);
@@ -125,10 +159,10 @@ const InvoicePage: React.FC = () => {
},
{
title: '开具时间',
dataIndex: 'createTime',
key: 'createTime',
dataIndex: 'createdAt',
key: 'createdAt',
width: 180,
render: (text: string) => <span style={{ color: '#64748b' }}>{text}</span>,
render: (text: string) => text ? <span style={{ color: '#64748b' }}>{new Date(text).toLocaleString('zh-CN')}</span> : '-',
},
{
title: '发票状态',
@@ -395,33 +429,50 @@ const InvoicePage: React.FC = () => {
setSelectHeaderModalOpen(true);
};
// 开票流程:选择抬头后确认 → 计算总金额,添加到开票记录
const handleHeaderConfirm = () => {
// 开票流程:选择抬头后确认 → 调用后端 API 提交开票
const handleHeaderConfirm = async () => {
if (!selectedHeaderId) {
message.warning('请选择一个发票抬头');
return;
}
if (!email.trim()) {
message.warning('请输入电子邮箱');
return;
}
const emailRegex = /^[\w.\-]+@[\w.\-]+\.\w+$/;
if (!emailRegex.test(email.trim())) {
message.warning('请输入正确的邮箱格式');
return;
}
const selectedOrders = Array.from(selectedOrdersMap.values());
const totalAmount = selectedOrders.reduce((sum, o) => sum + (o.amount || o.total_amount || 0), 0);
const now = new Date().toLocaleString('zh-CN');
const selectedHeader = allHeadersForSelect.find(h => h.id === selectedHeaderId);
const newRecord: InvoiceRecord = {
id: Date.now().toString(),
orderNo: generateRandomNo(),
orderType: selectedOrders.length > 1 ? `${selectedOrders.length}笔订单合并` : (selectedOrders[0].type || '订单'),
amount: totalAmount,
createTime: now,
status: 'pending',
headerName: selectedHeader?.name,
headerType: selectedHeader?.type,
headerTaxNo: selectedHeader?.taxNo,
selectedOrders,
};
setRecords(prev => [newRecord, ...prev]);
setSelectHeaderModalOpen(false);
setSelectedOrdersMap(new Map());
setSelectedHeaderId(null);
message.success(`开票申请已提交,合计金额 ¥${totalAmount.toFixed(2)}`);
const orderIds = selectedOrders.map((o: any) => o.id || o.order_no || o.orderNo);
setSubmitting(true);
try {
await createInvoice({
headerType: selectedHeader?.type as 'personal' | 'company',
headerName: selectedHeader?.name || '',
headerTaxNo: selectedHeader?.taxNo,
headerRegisterAddress: selectedHeader?.registerAddress,
headerRegisterPhone: selectedHeader?.registerPhone,
headerBankName: selectedHeader?.bankName,
headerBankAccount: selectedHeader?.bankAccount,
email: email.trim(),
orderIds,
});
message.success('开票申请已提交,请等待审核');
setSelectHeaderModalOpen(false);
setSelectedOrdersMap(new Map());
setSelectedHeaderId(null);
setEmail('');
// 刷新发票列表
loadInvoices();
} catch (err: any) {
const msg = err?.message || err?.response?.data?.detail || '提交失败,请稍后重试';
message.error(msg);
}
setSubmitting(false);
};
const orderRowSelection = {
@@ -526,7 +577,9 @@ const InvoicePage: React.FC = () => {
overflow: 'hidden',
}}
>
{records.length === 0 ? (
{recordsLoading ? (
<div style={{ textAlign: 'center', padding: 60 }}>...</div>
) : records.length === 0 ? (
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description={<span style={{ color: '#94a3b8' }}></span>}
@@ -603,6 +656,18 @@ const InvoicePage: React.FC = () => {
currentDetail.status === 'issued' ? '已开具' : '已驳回'}
</Tag>
</span>
{currentDetail.email && (
<>
<span style={{ color: '#94a3b8' }}></span>
<span style={{ color: '#1e293b' }}>{currentDetail.email}</span>
</>
)}
{currentDetail.failureReason && (
<>
<span style={{ color: '#94a3b8' }}></span>
<span style={{ color: '#ef4444' }}>{currentDetail.failureReason}</span>
</>
)}
</div>
</div>
</div>
@@ -644,7 +709,7 @@ const InvoicePage: React.FC = () => {
key: 'orderNo',
render: (_: any, r: any) => (
<span style={{ fontFamily: 'monospace', fontSize: 12, color: '#64748b' }}>
{r.order_no || r.orderNo || r.id}
{r.order_no || r.orderNo || r.orderNo || r.id}
</span>
),
},
@@ -746,13 +811,13 @@ const InvoicePage: React.FC = () => {
</div>
}
open={selectHeaderModalOpen}
onCancel={() => { setSelectHeaderModalOpen(false); setSelectedHeaderId(null); setSelectedOrdersMap(new Map()); }}
onCancel={() => { setSelectHeaderModalOpen(false); setSelectedHeaderId(null); setSelectedOrdersMap(new Map()); setEmail(''); }}
width={820}
footer={[
<Button key="cancel" onClick={() => { setSelectHeaderModalOpen(false); setSelectedHeaderId(null); setSelectedOrdersMap(new Map()); }}>
<Button key="cancel" onClick={() => { setSelectHeaderModalOpen(false); setSelectedHeaderId(null); setSelectedOrdersMap(new Map()); setEmail(''); }}>
</Button>,
<Button key="confirm" type="primary" onClick={handleHeaderConfirm}>
<Button key="confirm" type="primary" loading={submitting} onClick={handleHeaderConfirm}>
</Button>
]}
@@ -761,6 +826,21 @@ const InvoicePage: React.FC = () => {
closeIcon={<CloseOutlined style={{ fontSize: 16, color: '#94a3b8' }} />}
>
<div style={{ padding: '0 24px 24px' }}>
{/* 邮箱输入 */}
<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={email}
onChange={(e) => setEmail(e.target.value)}
style={{ maxWidth: 360 }}
allowClear
/>
</div>
</div>
<Table
dataSource={allHeadersForSelect}
columns={[