This commit is contained in:
2026-08-10 16:29:26 +08:00
parent e364c46ce4
commit d1e8eb7316
18 changed files with 1335 additions and 819 deletions
+60
View File
@@ -497,6 +497,66 @@ export async function getInvoiceDetail(id: string): Promise<any> {
return api.get(`/invoices/${id}`);
}
// ── Invoice Headers ──────────────────────────────────────
export async function getInvoiceHeaders(): Promise<{ items: any[] }> {
return api.get('/invoice-headers');
}
export async function createInvoiceHeader(data: {
type: 'personal' | 'company';
name: string;
taxNo?: string;
registerAddress?: string;
registerPhone?: string;
bankName?: string;
bankAccount?: string;
email?: string;
isDefault?: boolean;
}): Promise<any> {
return api.post('/invoice-headers', {
type: data.type,
name: data.name,
tax_no: data.taxNo,
register_address: data.registerAddress,
register_phone: data.registerPhone,
bank_name: data.bankName,
bank_account: data.bankAccount,
email: data.email,
is_default: data.isDefault ?? false,
});
}
export async function updateInvoiceHeader(id: string, data: {
name?: string;
taxNo?: string;
registerAddress?: string;
registerPhone?: string;
bankName?: string;
bankAccount?: string;
email?: string;
isDefault?: boolean;
}): Promise<any> {
return api.put(`/invoice-headers/${id}`, {
name: data.name,
tax_no: data.taxNo,
register_address: data.registerAddress,
register_phone: data.registerPhone,
bank_name: data.bankName,
bank_account: data.bankAccount,
email: data.email,
is_default: data.isDefault,
});
}
export async function deleteInvoiceHeader(id: string): Promise<void> {
await api.delete(`/invoice-headers/${id}`);
}
export async function setDefaultInvoiceHeader(id: string): Promise<any> {
return api.put(`/invoice-headers/${id}/set-default`);
}
export async function getCreditRatios(): Promise<any[]> {
return api.get('/credits/ratios');
}
+63 -52
View File
@@ -1,8 +1,10 @@
import React, { useEffect, useState } from 'react';
import { Table, Tag, Empty, Spin, Pagination, Button, Typography, Modal, Form, Input, Radio, message, Space, DatePicker } from 'antd';
import { FileTextOutlined, PlusOutlined, CloseOutlined, AlipayCircleOutlined, WechatOutlined } from '@ant-design/icons';
import { getPaymentOrders, createInvoice, getInvoices, getInvoiceDetail } from '../api';
import dayjs from 'dayjs';
import {
getPaymentOrders, createInvoice, getInvoices, getInvoiceDetail,
getInvoiceHeaders, createInvoiceHeader, updateInvoiceHeader, deleteInvoiceHeader, setDefaultInvoiceHeader,
} from '../api';
interface InvoiceRecord {
id: string;
@@ -33,13 +35,6 @@ interface InvoiceHeader {
isDefault: boolean;
}
const mockHeaders: InvoiceHeader[] = [
{ id: 'h1', name: '北京云图科技有限公司', type: 'company', taxNo: '91110108MA01XXXXX1', registerAddress: '北京市海淀区中关村大街1号', bankName: '中国工商银行北京海淀支行', registerPhone: '010-88888888', bankAccount: '0200 0000 0000 0001', isDefault: true },
{ id: 'h2', name: '上海智能图像有限公司', type: 'company', taxNo: '91310115MA1KXXXXX2', registerAddress: '上海市浦东新区张江高科技园区', bankName: '招商银行上海张江支行', registerPhone: '021-66666666', bankAccount: '1212 0000 0000 0002', isDefault: false },
{ id: 'h3', name: '深圳创新科技有限公司', type: 'company', taxNo: '91440300MA5XXXXX3', registerAddress: '深圳市南山区科技园', bankName: '建设银行深圳南山支行', registerPhone: '0755-99999999', bankAccount: '4400 0000 0000 0003', isDefault: false },
{ id: 'h4', name: '张三', type: 'personal', isDefault: false },
];
const InvoicePage: React.FC = () => {
const [records, setRecords] = useState<InvoiceRecord[]>([]);
const [recordsLoading, setRecordsLoading] = useState(false);
@@ -63,14 +58,9 @@ const InvoicePage: React.FC = () => {
const [orderPage, setOrderPage] = useState(1);
const [orderPageSize] = useState(10);
const [occupiedOrderIds, setOccupiedOrderIds] = useState<Set<string>>(new Set());
// 默认日期范围:当前月份
const [orderDateFilter, setOrderDateFilter] = useState<[string | null, string | null]>(() => {
const now = new Date();
const start = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-01`;
const lastDay = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
const end = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(lastDay).padStart(2, '0')}`;
return [start, end];
});
// 日期筛选:默认不选,展示全部订单
const [orderDateFilter, setOrderDateFilter] = useState<[string | null, string | null]>([null, null]);
const [orderDateRange, setOrderDateRange] = useState<any>([null, null]);
// 说明弹窗
const [noticeModalOpen, setNoticeModalOpen] = useState(false);
@@ -173,6 +163,7 @@ const InvoicePage: React.FC = () => {
useEffect(() => {
loadInvoices();
loadOccupiedOrderIds();
loadHeaders();
}, []);
useEffect(() => {
@@ -418,46 +409,57 @@ const InvoicePage: React.FC = () => {
},
];
// 选择弹窗中的抬头列表(合并 mockHeaders 和用户添加的)
const allHeadersForSelect = React.useMemo(() => {
const merged = [...mockHeaders];
headers.forEach(h => {
const idx = merged.findIndex(m => m.id === h.id);
if (idx >= 0) {
merged[idx] = h;
} else {
merged.push(h);
}
});
return merged;
}, [headers]);
// 选择弹窗中的抬头列表
const allHeadersForSelect = headers;
// 加载发票抬头列表
const loadHeaders = async () => {
try {
const data = await getInvoiceHeaders();
const items: InvoiceHeader[] = (data.items || []).map((h: any) => ({
id: h.id,
name: h.name,
type: h.type,
taxNo: h.taxNo,
registerAddress: h.registerAddress,
bankName: h.bankName,
registerPhone: h.registerPhone,
bankAccount: h.bankAccount,
email: h.email,
isDefault: h.isDefault,
}));
setHeaders(items);
} catch {
// 静默失败
}
};
const handleAddHeader = async () => {
try {
const values = await headerForm.validateFields();
const isCompany = values.type === 'company';
const newHeader: InvoiceHeader = {
id: Date.now().toString(),
name: values.name,
const isDefault = values.isDefault === 'yes' || (headers.length === 0 && !headers.some(h => h.isDefault));
await createInvoiceHeader({
type: values.type,
name: values.name,
taxNo: isCompany ? values.taxNo : undefined,
registerAddress: isCompany ? values.registerAddress : undefined,
bankName: isCompany ? values.bankName : undefined,
registerPhone: isCompany ? values.registerPhone : undefined,
bankAccount: isCompany ? values.bankAccount : undefined,
email: values.email,
isDefault: values.isDefault === 'yes' || (headers.length === 0 && !allHeadersForSelect.some(h => h.isDefault)),
};
if (newHeader.isDefault) {
setHeaders(prev => prev.map(h => ({ ...h, isDefault: false })));
}
setHeaders(prev => [...prev, newHeader]);
isDefault,
});
headerForm.resetFields();
setAddModalOpen(false);
message.success('发票抬头添加成功');
loadHeaders();
} catch (e: any) {
if (e?.errorFields) {
message.warning('请完善表单信息');
} else {
const msg = e?.message || e?.response?.data?.detail || '添加失败';
message.error(msg);
}
}
};
@@ -468,16 +470,26 @@ const InvoicePage: React.FC = () => {
content: '确定要删除该发票抬头吗?',
okText: '确定',
cancelText: '取消',
onOk: () => {
setHeaders(prev => prev.filter(h => h.id !== id));
message.success('删除成功');
onOk: async () => {
try {
await deleteInvoiceHeader(id);
message.success('删除成功');
loadHeaders();
} catch (e: any) {
message.error(e?.message || '删除失败');
}
},
});
};
const handleSetDefault = (id: string) => {
setHeaders(prev => prev.map(h => ({ ...h, isDefault: h.id === id })));
message.success('已设为默认');
const handleSetDefault = async (id: string) => {
try {
await setDefaultInvoiceHeader(id);
message.success('已设为默认');
loadHeaders();
} catch (e: any) {
message.error(e?.message || '设置失败');
}
};
// 开票流程:选择订单后确认 → 打开抬头选择弹窗
@@ -838,23 +850,22 @@ const InvoicePage: React.FC = () => {
<span style={{ color: '#94a3b8', fontSize: 13 }}></span>
<DatePicker.RangePicker
size="small"
value={orderDateFilter[0] ? [dayjs(orderDateFilter[0]), orderDateFilter[1] ? dayjs(orderDateFilter[1]) : null] : [null, null]}
value={orderDateRange}
onChange={(dates) => {
setOrderDateRange(dates);
if (dates && dates[0] && dates[1]) {
const start = dates[0].format('YYYY-MM-DD');
const end = dates[1].format('YYYY-MM-DD');
setOrderDateFilter([start, end]);
} else {
// 清除时恢复默认当前月份
const now = new Date();
const start = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-01`;
const lastDay = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
const end = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(lastDay).padStart(2, '0')}`;
setOrderDateFilter([start, end]);
// 清除时展示全部
setOrderDateFilter([null, null]);
}
setOrderPage(1);
setTimeout(() => loadOrderData(1), 0);
}}
allowClear
placeholder={['开始日期', '结束日期']}
style={{ width: 280 }}
/>
</div>