1、增加订单开发票功能和发票抬头添加功能
2、一个订单只能在一个开票里,不允许多开
This commit is contained in:
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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={[
|
||||
|
||||
Reference in New Issue
Block a user