From c44389f4fe896ee02aebab9c01b9ba0f3b5f222f Mon Sep 17 00:00:00 2001 From: zdl <3489966805@qq.com> Date: Fri, 26 Dec 2025 12:49:51 +0800 Subject: [PATCH] =?UTF-8?q?feat(invoice):=20=E5=AE=9E=E7=8E=B0=E5=8F=91?= =?UTF-8?q?=E7=A5=A8=E7=94=B3=E8=AF=B7=E4=B8=8E=E7=AE=A1=E7=90=86=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增功能: - 发票管理页面:支持查看发票列表、统计、Tab筛选 - 发票申请流程:支持电子/纸质发票、个人/企业抬头 - 发票状态追踪:待处理、处理中、已完成、已取消 - 发票抬头模板:支持保存和复用常用抬头 - 发票下载:已完成的电子发票可下载 组件架构: - InvoiceCard: 发票卡片展示(React.memo优化) - InvoiceApplyForm: 开票申请表单 - InvoiceApplyModal: 申请弹窗 - InvoiceStatusBadge: 状态徽章 - InvoiceTitleSelector: 抬头选择器 - InvoiceTypeSelector: 发票类型选择 入口集成: - 设置页添加"账单与发票"Tab - Billing页面添加发票管理入口 - 订阅支付成功后提示开票 Mock数据: - 多用户发票数据(不同状态) - 可开票订单 - 抬头模板 性能优化: - InvoiceCard 使用 React.memo 避免不必要重渲染 - useColorModeValue 移到组件顶层调用 - loadInvoices/loadStats 使用 useCallback 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/components/Invoice/InvoiceApplyForm.tsx | 472 +++++++++ src/components/Invoice/InvoiceApplyModal.tsx | 222 +++++ src/components/Invoice/InvoiceCard.tsx | 175 ++++ src/components/Invoice/InvoiceStatusBadge.tsx | 37 + .../Invoice/InvoiceTitleSelector.tsx | 179 ++++ .../Invoice/InvoiceTypeSelector.tsx | 95 ++ src/components/Invoice/index.ts | 10 + .../Subscription/SubscriptionContentNew.tsx | 12 +- src/mocks/handlers/index.js | 2 + src/mocks/handlers/invoice.js | 920 ++++++++++++++++++ src/routes/homeRoutes.js | 11 + src/routes/lazy-components.js | 2 + src/services/invoiceService.ts | 251 +++++ src/types/invoice.ts | 124 +++ src/views/Pages/Account/Billing.js | 111 ++- src/views/Pages/Account/Invoice/index.tsx | 358 +++++++ .../Account/{Invoice.js => InvoicePrint.js} | 125 +-- src/views/Settings/SettingsPage.js | 70 +- 18 files changed, 3037 insertions(+), 139 deletions(-) create mode 100644 src/components/Invoice/InvoiceApplyForm.tsx create mode 100644 src/components/Invoice/InvoiceApplyModal.tsx create mode 100644 src/components/Invoice/InvoiceCard.tsx create mode 100644 src/components/Invoice/InvoiceStatusBadge.tsx create mode 100644 src/components/Invoice/InvoiceTitleSelector.tsx create mode 100644 src/components/Invoice/InvoiceTypeSelector.tsx create mode 100644 src/components/Invoice/index.ts create mode 100644 src/mocks/handlers/invoice.js create mode 100644 src/services/invoiceService.ts create mode 100644 src/types/invoice.ts create mode 100644 src/views/Pages/Account/Invoice/index.tsx rename src/views/Pages/Account/{Invoice.js => InvoicePrint.js} (74%) diff --git a/src/components/Invoice/InvoiceApplyForm.tsx b/src/components/Invoice/InvoiceApplyForm.tsx new file mode 100644 index 00000000..0b3383ef --- /dev/null +++ b/src/components/Invoice/InvoiceApplyForm.tsx @@ -0,0 +1,472 @@ +/** + * 发票申请表单组件 + */ +import React, { useState, useEffect } from 'react'; +import { + Box, + VStack, + HStack, + FormControl, + FormLabel, + FormErrorMessage, + Input, + Textarea, + Button, + Radio, + RadioGroup, + Stack, + Text, + Divider, + useColorModeValue, + Checkbox, + Alert, + AlertIcon, + Collapse, +} from '@chakra-ui/react'; +import InvoiceTypeSelector from './InvoiceTypeSelector'; +import InvoiceTitleSelector from './InvoiceTitleSelector'; +import type { + InvoiceType, + InvoiceTitleType, + InvoiceTitleTemplate, + InvoiceableOrder, + CreateInvoiceRequest, +} from '@/types/invoice'; + +interface InvoiceApplyFormProps { + order: InvoiceableOrder; + onSubmit: (data: CreateInvoiceRequest) => Promise; + onCancel: () => void; + loading?: boolean; +} + +interface FormData { + invoiceType: InvoiceType; + titleType: InvoiceTitleType; + title: string; + taxNumber: string; + companyAddress: string; + companyPhone: string; + bankName: string; + bankAccount: string; + email: string; + phone: string; + mailingAddress: string; + recipientName: string; + recipientPhone: string; + remark: string; + saveTemplate: boolean; +} + +interface FormErrors { + title?: string; + taxNumber?: string; + email?: string; + mailingAddress?: string; + recipientName?: string; + recipientPhone?: string; +} + +const planNameMap: Record = { + pro: 'Pro 专业版', + max: 'Max 旗舰版', +}; + +const billingCycleMap: Record = { + monthly: '月付', + quarterly: '季付', + semiannual: '半年付', + yearly: '年付', +}; + +export default function InvoiceApplyForm({ + order, + onSubmit, + onCancel, + loading = false, +}: InvoiceApplyFormProps) { + const [formData, setFormData] = useState({ + invoiceType: 'electronic', + titleType: 'personal', + title: '', + taxNumber: '', + companyAddress: '', + companyPhone: '', + bankName: '', + bankAccount: '', + email: '', + phone: '', + mailingAddress: '', + recipientName: '', + recipientPhone: '', + remark: '', + saveTemplate: false, + }); + + const [errors, setErrors] = useState({}); + const [showNewTitleForm, setShowNewTitleForm] = useState(false); + + const borderColor = useColorModeValue('gray.200', 'gray.600'); + const bgCard = useColorModeValue('gray.50', 'gray.700'); + + // 更新表单字段 + const updateField = (field: K, value: FormData[K]) => { + setFormData((prev) => ({ ...prev, [field]: value })); + // 清除对应错误 + if (errors[field as keyof FormErrors]) { + setErrors((prev) => ({ ...prev, [field]: undefined })); + } + }; + + // 选择抬头模板 + const handleSelectTemplate = (template: InvoiceTitleTemplate | null) => { + if (template) { + setFormData((prev) => ({ + ...prev, + title: template.title, + titleType: template.titleType, + taxNumber: template.taxNumber || '', + companyAddress: template.companyAddress || '', + companyPhone: template.companyPhone || '', + bankName: template.bankName || '', + bankAccount: template.bankAccount || '', + })); + setShowNewTitleForm(false); + } + }; + + // 切换抬头类型时清空相关字段 + useEffect(() => { + if (formData.titleType === 'personal') { + setFormData((prev) => ({ + ...prev, + taxNumber: '', + companyAddress: '', + companyPhone: '', + bankName: '', + bankAccount: '', + })); + } + }, [formData.titleType]); + + // 表单验证 + const validate = (): boolean => { + const newErrors: FormErrors = {}; + + if (!formData.title.trim()) { + newErrors.title = '请输入发票抬头'; + } + + if (formData.titleType === 'company' && !formData.taxNumber.trim()) { + newErrors.taxNumber = '企业开票必须填写税号'; + } + + if (!formData.email.trim()) { + newErrors.email = '请输入接收邮箱'; + } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) { + newErrors.email = '请输入有效的邮箱地址'; + } + + // 纸质发票需要邮寄信息 + if (formData.invoiceType === 'paper') { + if (!formData.mailingAddress.trim()) { + newErrors.mailingAddress = '请输入邮寄地址'; + } + if (!formData.recipientName.trim()) { + newErrors.recipientName = '请输入收件人姓名'; + } + if (!formData.recipientPhone.trim()) { + newErrors.recipientPhone = '请输入收件人电话'; + } + } + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + // 提交表单 + const handleSubmit = async () => { + if (!validate()) return; + + const request: CreateInvoiceRequest = { + orderId: order.id, + invoiceType: formData.invoiceType, + titleType: formData.titleType, + title: formData.title, + taxNumber: formData.taxNumber || undefined, + companyAddress: formData.companyAddress || undefined, + companyPhone: formData.companyPhone || undefined, + bankName: formData.bankName || undefined, + bankAccount: formData.bankAccount || undefined, + email: formData.email, + phone: formData.phone || undefined, + mailingAddress: formData.mailingAddress || undefined, + recipientName: formData.recipientName || undefined, + recipientPhone: formData.recipientPhone || undefined, + remark: formData.remark || undefined, + }; + + await onSubmit(request); + }; + + return ( + + {/* 订单信息 */} + + + 开票订单 + + + + + 订单号 + + {order.orderNo} + + + + 套餐 + + + {planNameMap[order.planName] || order.planName} ·{' '} + {billingCycleMap[order.billingCycle] || order.billingCycle} + + + + + 开票金额 + + + ¥{order.amount.toFixed(2)} + + + + + + + + {/* 发票类型 */} + + + 发票类型 + + updateField('invoiceType', type)} + /> + + + + + {/* 抬头类型 */} + + + 抬头类型 + + updateField('titleType', value)} + > + + 个人 + 企业 + + + + + {/* 发票抬头选择 */} + + + 发票抬头 + + setShowNewTitleForm(true)} + /> + + + {/* 新抬头表单 */} + + + + 发票抬头 + updateField('title', e.target.value)} + /> + {errors.title} + + + {formData.titleType === 'company' && ( + <> + + 税号 + updateField('taxNumber', e.target.value)} + /> + {errors.taxNumber} + + + + + 以下为选填信息,可用于开具增值税专用发票 + + + + 公司地址 + updateField('companyAddress', e.target.value)} + /> + + + + 公司电话 + updateField('companyPhone', e.target.value)} + /> + + + + + 开户银行 + updateField('bankName', e.target.value)} + /> + + + 银行账号 + updateField('bankAccount', e.target.value)} + /> + + + + )} + + updateField('saveTemplate', e.target.checked)} + > + 保存为常用抬头 + + + + + + + {/* 接收信息 */} + + + 接收邮箱 + + updateField('email', e.target.value)} + /> + {errors.email} + + + + 联系电话(选填) + updateField('phone', e.target.value)} + /> + + + {/* 纸质发票邮寄信息 */} + + + + + 邮寄信息 + + + + 邮寄地址 +