import { useEffect, useRef, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; import { Button, Descriptions, Drawer, Form, Input, Modal, Select, Space, Table, Tag, Typography, Upload, message, } from 'antd'; import type { ColumnsType } from 'antd/es/table'; import { INVOICE_CATEGORY_LABELS, INVOICE_KIND_LABELS, INVOICE_STATUS_LABELS, INVOICE_TITLE_TYPE_LABELS, type InvoiceCategory, type InvoiceKind, type InvoiceStatus, type InvoiceTitleType, } from '@dukang/shared-types'; import { request } from '../lib/api'; import { fmtTime } from '../lib/constants'; import { useAdminList } from '../lib/useAdminList'; import { uploadFileToOss } from '../lib/upload'; type Row = { id: string; invoiceNo: string; orderNo?: string; titleType: InvoiceTitleType; invoiceKind: InvoiceKind; invoiceCategory?: InvoiceCategory; titleName: string; status: InvoiceStatus; overdue?: boolean; createdAt: string; fileUrl?: string; email?: string; phone?: string; taxNo?: string; addressPhone?: string; bankAccount?: string; payAmount?: string; userPhone?: string; remark?: string; }; type CreateFormValues = { orderNo: string; titleType: InvoiceTitleType; invoiceKind: InvoiceKind; invoiceCategory?: InvoiceCategory; titleName: string; taxNo?: string; addressPhone?: string; bankAccount?: string; email: string; phone: string; remark?: string; }; export default function InvoicesPage() { const [searchParams] = useSearchParams(); const initialStatus = searchParams.get('status')?.trim() || ''; const initialInvoiceNo = searchParams.get('invoiceNo')?.trim() || ''; const [filterForm] = Form.useForm(); const [filters, setFilters] = useState>(() => { const init: Record = {}; if (initialStatus) init.status = initialStatus; if (initialInvoiceNo) init.invoiceNo = initialInvoiceNo; return init; }); const { data, loading, page, pageSize, setPage, setPageSize, reload } = useAdminList( '/admin/invoices', () => { const qs = new URLSearchParams(); if (filters.status) qs.set('status', filters.status); if (filters.invoiceNo) qs.set('invoiceNo', filters.invoiceNo); return qs; }, [filters], ); const [detail, setDetail] = useState(null); const [drawerOpen, setDrawerOpen] = useState(false); const [uploading, setUploading] = useState(false); const [createOpen, setCreateOpen] = useState(false); const [creating, setCreating] = useState(false); const [createForm] = Form.useForm(); const invoiceKind = Form.useWatch('invoiceKind', createForm); const titleType = Form.useWatch('titleType', createForm); const deepLinkOpenedRef = useRef(false); useEffect(() => { filterForm.setFieldsValue({ status: filters.status || undefined, invoiceNo: filters.invoiceNo || undefined, }); }, [filterForm, filters.invoiceNo, filters.status]); async function openDetail(id: string) { setDetail(await request(`/admin/invoices/${id}`)); setDrawerOpen(true); } useEffect(() => { if (!initialInvoiceNo || deepLinkOpenedRef.current || loading) return; const first = data?.items?.[0]; if (first && String(first.invoiceNo) === initialInvoiceNo) { deepLinkOpenedRef.current = true; void openDetail(first.id); } }, [data, initialInvoiceNo, loading]); async function issueWithFile(file: File) { if (!detail) return false; setUploading(true); try { const uploaded = await uploadFileToOss(file, { bizType: 'invoice' }); await request(`/admin/invoices/${detail.id}/issue`, { method: 'POST', body: JSON.stringify({ fileUrl: uploaded.url }), }); message.success('已开票回传'); reload(); setDrawerOpen(false); } catch (e) { message.error(e instanceof Error ? e.message : '开票失败'); } finally { setUploading(false); } return false; } async function reject() { if (!detail) return; await request(`/admin/invoices/${detail.id}/reject`, { method: 'POST', body: JSON.stringify({ remark: '驳回' }), }); message.success('已驳回'); reload(); setDrawerOpen(false); } async function submitCreate() { const values = await createForm.validateFields(); setCreating(true); try { await request('/admin/invoices', { method: 'POST', body: JSON.stringify({ orderNo: values.orderNo.trim(), titleType: values.titleType, invoiceKind: values.invoiceKind, invoiceCategory: values.invoiceCategory, titleName: values.titleName.trim(), taxNo: values.taxNo?.trim() || undefined, addressPhone: values.addressPhone?.trim() || undefined, bankAccount: values.bankAccount?.trim() || undefined, email: values.email.trim(), phone: values.phone.trim(), remark: values.remark?.trim() || undefined, }), }); message.success('发票申请已创建'); setCreateOpen(false); createForm.resetFields(); reload(); } catch (e) { message.error(e instanceof Error ? e.message : '创建失败'); } finally { setCreating(false); } } const columns: ColumnsType = [ { title: '申请单号', dataIndex: 'invoiceNo', width: 180 }, { title: '订单号', dataIndex: 'orderNo', width: 160 }, { title: '抬头', width: 100, render: (_, r) => INVOICE_TITLE_TYPE_LABELS[r.titleType] ?? r.titleType, }, { title: '票种', width: 140, render: (_, r) => INVOICE_KIND_LABELS[r.invoiceKind] ?? r.invoiceKind, }, { title: '类型', width: 88, render: (_, r) => r.invoiceCategory ? INVOICE_CATEGORY_LABELS[r.invoiceCategory] ?? r.invoiceCategory : '—', }, { title: '名称', dataIndex: 'titleName', ellipsis: true }, { title: '状态', width: 110, render: (_, r) => ( <> {INVOICE_STATUS_LABELS[r.status] ?? r.status} {r.overdue ? 超时 : null} ), }, { title: '申请时间', dataIndex: 'createdAt', width: 160, render: fmtTime }, { title: '操作', width: 80, render: (_, row) => ( ), }, ]; return (
发票管理
{ setFilters(v); setPage(1); }} >
(r.overdue ? 'ant-table-row-overdue' : '')} pagination={{ current: page, pageSize, total: data?.total ?? 0, showSizeChanger: true, onChange: (p, ps) => { setPage(p); setPageSize(ps); }, }} /> setDrawerOpen(false)} extra={ detail?.status === 'PENDING' ? ( { void issueWithFile(file); return false; }} > ) : null } > {detail && ( {detail.invoiceNo} {detail.orderNo ?? '—'} {detail.payAmount ?? '—'} {detail.userPhone ?? '—'} {INVOICE_TITLE_TYPE_LABELS[detail.titleType]} {INVOICE_KIND_LABELS[detail.invoiceKind]} {detail.invoiceCategory ? INVOICE_CATEGORY_LABELS[detail.invoiceCategory] : '—'} {detail.titleName} {detail.taxNo ?? '—'} {detail.addressPhone ?? '—'} {detail.bankAccount ?? '—'} {detail.email} {detail.phone} {INVOICE_STATUS_LABELS[detail.status]} {detail.overdue ? '(超 2 工作日)' : ''} {detail.fileUrl ? ( 查看/下载 ) : ( '—' )} {detail.remark ?? '—'} )} setCreateOpen(false)} onOk={() => void submitCreate()} confirmLoading={creating} destroyOnClose okText="提交" width={520} >
({ value: k, label: INVOICE_CATEGORY_LABELS[k], }))} /> {(titleType === 'ENTERPRISE' || invoiceKind === 'SPECIAL') && ( )} {invoiceKind === 'SPECIAL' && ( <> )}
); }