Compare commits
1 Commits
feature_bu
...
feature_20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c44389f4fe |
472
src/components/Invoice/InvoiceApplyForm.tsx
Normal file
472
src/components/Invoice/InvoiceApplyForm.tsx
Normal file
@@ -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<void>;
|
||||
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<string, string> = {
|
||||
pro: 'Pro 专业版',
|
||||
max: 'Max 旗舰版',
|
||||
};
|
||||
|
||||
const billingCycleMap: Record<string, string> = {
|
||||
monthly: '月付',
|
||||
quarterly: '季付',
|
||||
semiannual: '半年付',
|
||||
yearly: '年付',
|
||||
};
|
||||
|
||||
export default function InvoiceApplyForm({
|
||||
order,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
loading = false,
|
||||
}: InvoiceApplyFormProps) {
|
||||
const [formData, setFormData] = useState<FormData>({
|
||||
invoiceType: 'electronic',
|
||||
titleType: 'personal',
|
||||
title: '',
|
||||
taxNumber: '',
|
||||
companyAddress: '',
|
||||
companyPhone: '',
|
||||
bankName: '',
|
||||
bankAccount: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
mailingAddress: '',
|
||||
recipientName: '',
|
||||
recipientPhone: '',
|
||||
remark: '',
|
||||
saveTemplate: false,
|
||||
});
|
||||
|
||||
const [errors, setErrors] = useState<FormErrors>({});
|
||||
const [showNewTitleForm, setShowNewTitleForm] = useState(false);
|
||||
|
||||
const borderColor = useColorModeValue('gray.200', 'gray.600');
|
||||
const bgCard = useColorModeValue('gray.50', 'gray.700');
|
||||
|
||||
// 更新表单字段
|
||||
const updateField = <K extends keyof FormData>(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 (
|
||||
<VStack align="stretch" spacing={6}>
|
||||
{/* 订单信息 */}
|
||||
<Box p={4} borderRadius="lg" bg={bgCard}>
|
||||
<Text fontSize="sm" fontWeight="600" mb={3}>
|
||||
开票订单
|
||||
</Text>
|
||||
<VStack align="stretch" spacing={2}>
|
||||
<HStack justify="space-between">
|
||||
<Text fontSize="sm" color="gray.500">
|
||||
订单号
|
||||
</Text>
|
||||
<Text fontSize="sm">{order.orderNo}</Text>
|
||||
</HStack>
|
||||
<HStack justify="space-between">
|
||||
<Text fontSize="sm" color="gray.500">
|
||||
套餐
|
||||
</Text>
|
||||
<Text fontSize="sm">
|
||||
{planNameMap[order.planName] || order.planName} ·{' '}
|
||||
{billingCycleMap[order.billingCycle] || order.billingCycle}
|
||||
</Text>
|
||||
</HStack>
|
||||
<HStack justify="space-between">
|
||||
<Text fontSize="sm" color="gray.500">
|
||||
开票金额
|
||||
</Text>
|
||||
<Text fontSize="sm" fontWeight="600" color="blue.500">
|
||||
¥{order.amount.toFixed(2)}
|
||||
</Text>
|
||||
</HStack>
|
||||
</VStack>
|
||||
</Box>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* 发票类型 */}
|
||||
<FormControl>
|
||||
<FormLabel fontSize="sm" fontWeight="600">
|
||||
发票类型
|
||||
</FormLabel>
|
||||
<InvoiceTypeSelector
|
||||
value={formData.invoiceType}
|
||||
onChange={(type) => updateField('invoiceType', type)}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* 抬头类型 */}
|
||||
<FormControl>
|
||||
<FormLabel fontSize="sm" fontWeight="600">
|
||||
抬头类型
|
||||
</FormLabel>
|
||||
<RadioGroup
|
||||
value={formData.titleType}
|
||||
onChange={(value: InvoiceTitleType) => updateField('titleType', value)}
|
||||
>
|
||||
<Stack direction="row" spacing={6}>
|
||||
<Radio value="personal">个人</Radio>
|
||||
<Radio value="company">企业</Radio>
|
||||
</Stack>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
|
||||
{/* 发票抬头选择 */}
|
||||
<FormControl>
|
||||
<FormLabel fontSize="sm" fontWeight="600">
|
||||
发票抬头
|
||||
</FormLabel>
|
||||
<InvoiceTitleSelector
|
||||
titleType={formData.titleType}
|
||||
onSelect={handleSelectTemplate}
|
||||
onAddNew={() => setShowNewTitleForm(true)}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
{/* 新抬头表单 */}
|
||||
<Collapse in={showNewTitleForm} animateOpacity>
|
||||
<VStack
|
||||
align="stretch"
|
||||
spacing={4}
|
||||
p={4}
|
||||
borderRadius="lg"
|
||||
border="1px solid"
|
||||
borderColor={borderColor}
|
||||
>
|
||||
<FormControl isRequired isInvalid={!!errors.title}>
|
||||
<FormLabel fontSize="sm">发票抬头</FormLabel>
|
||||
<Input
|
||||
placeholder={
|
||||
formData.titleType === 'company'
|
||||
? '请输入公司名称'
|
||||
: '请输入个人姓名'
|
||||
}
|
||||
value={formData.title}
|
||||
onChange={(e) => updateField('title', e.target.value)}
|
||||
/>
|
||||
<FormErrorMessage>{errors.title}</FormErrorMessage>
|
||||
</FormControl>
|
||||
|
||||
{formData.titleType === 'company' && (
|
||||
<>
|
||||
<FormControl isRequired isInvalid={!!errors.taxNumber}>
|
||||
<FormLabel fontSize="sm">税号</FormLabel>
|
||||
<Input
|
||||
placeholder="请输入纳税人识别号"
|
||||
value={formData.taxNumber}
|
||||
onChange={(e) => updateField('taxNumber', e.target.value)}
|
||||
/>
|
||||
<FormErrorMessage>{errors.taxNumber}</FormErrorMessage>
|
||||
</FormControl>
|
||||
|
||||
<Alert status="info" fontSize="sm" borderRadius="md">
|
||||
<AlertIcon />
|
||||
以下为选填信息,可用于开具增值税专用发票
|
||||
</Alert>
|
||||
|
||||
<FormControl>
|
||||
<FormLabel fontSize="sm">公司地址</FormLabel>
|
||||
<Input
|
||||
placeholder="请输入公司地址(选填)"
|
||||
value={formData.companyAddress}
|
||||
onChange={(e) => updateField('companyAddress', e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl>
|
||||
<FormLabel fontSize="sm">公司电话</FormLabel>
|
||||
<Input
|
||||
placeholder="请输入公司电话(选填)"
|
||||
value={formData.companyPhone}
|
||||
onChange={(e) => updateField('companyPhone', e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<HStack spacing={4}>
|
||||
<FormControl>
|
||||
<FormLabel fontSize="sm">开户银行</FormLabel>
|
||||
<Input
|
||||
placeholder="开户银行(选填)"
|
||||
value={formData.bankName}
|
||||
onChange={(e) => updateField('bankName', e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<FormLabel fontSize="sm">银行账号</FormLabel>
|
||||
<Input
|
||||
placeholder="银行账号(选填)"
|
||||
value={formData.bankAccount}
|
||||
onChange={(e) => updateField('bankAccount', e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
</HStack>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Checkbox
|
||||
isChecked={formData.saveTemplate}
|
||||
onChange={(e) => updateField('saveTemplate', e.target.checked)}
|
||||
>
|
||||
<Text fontSize="sm">保存为常用抬头</Text>
|
||||
</Checkbox>
|
||||
</VStack>
|
||||
</Collapse>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* 接收信息 */}
|
||||
<FormControl isRequired isInvalid={!!errors.email}>
|
||||
<FormLabel fontSize="sm" fontWeight="600">
|
||||
接收邮箱
|
||||
</FormLabel>
|
||||
<Input
|
||||
type="email"
|
||||
placeholder="发票将发送至此邮箱"
|
||||
value={formData.email}
|
||||
onChange={(e) => updateField('email', e.target.value)}
|
||||
/>
|
||||
<FormErrorMessage>{errors.email}</FormErrorMessage>
|
||||
</FormControl>
|
||||
|
||||
<FormControl>
|
||||
<FormLabel fontSize="sm">联系电话(选填)</FormLabel>
|
||||
<Input
|
||||
placeholder="方便开票人员联系"
|
||||
value={formData.phone}
|
||||
onChange={(e) => updateField('phone', e.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
{/* 纸质发票邮寄信息 */}
|
||||
<Collapse in={formData.invoiceType === 'paper'} animateOpacity>
|
||||
<VStack align="stretch" spacing={4}>
|
||||
<Divider />
|
||||
<Text fontSize="sm" fontWeight="600">
|
||||
邮寄信息
|
||||
</Text>
|
||||
|
||||
<FormControl isRequired isInvalid={!!errors.mailingAddress}>
|
||||
<FormLabel fontSize="sm">邮寄地址</FormLabel>
|
||||
<Textarea
|
||||
placeholder="请输入详细的邮寄地址"
|
||||
value={formData.mailingAddress}
|
||||
onChange={(e) => updateField('mailingAddress', e.target.value)}
|
||||
rows={2}
|
||||
/>
|
||||
<FormErrorMessage>{errors.mailingAddress}</FormErrorMessage>
|
||||
</FormControl>
|
||||
|
||||
<HStack spacing={4}>
|
||||
<FormControl isRequired isInvalid={!!errors.recipientName}>
|
||||
<FormLabel fontSize="sm">收件人</FormLabel>
|
||||
<Input
|
||||
placeholder="收件人姓名"
|
||||
value={formData.recipientName}
|
||||
onChange={(e) => updateField('recipientName', e.target.value)}
|
||||
/>
|
||||
<FormErrorMessage>{errors.recipientName}</FormErrorMessage>
|
||||
</FormControl>
|
||||
<FormControl isRequired isInvalid={!!errors.recipientPhone}>
|
||||
<FormLabel fontSize="sm">联系电话</FormLabel>
|
||||
<Input
|
||||
placeholder="收件人电话"
|
||||
value={formData.recipientPhone}
|
||||
onChange={(e) => updateField('recipientPhone', e.target.value)}
|
||||
/>
|
||||
<FormErrorMessage>{errors.recipientPhone}</FormErrorMessage>
|
||||
</FormControl>
|
||||
</HStack>
|
||||
</VStack>
|
||||
</Collapse>
|
||||
|
||||
{/* 备注 */}
|
||||
<FormControl>
|
||||
<FormLabel fontSize="sm">备注(选填)</FormLabel>
|
||||
<Textarea
|
||||
placeholder="如有特殊要求请在此说明"
|
||||
value={formData.remark}
|
||||
onChange={(e) => updateField('remark', e.target.value)}
|
||||
rows={2}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<HStack justify="flex-end" spacing={3} pt={4}>
|
||||
<Button variant="ghost" onClick={onCancel} isDisabled={loading}>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
colorScheme="blue"
|
||||
onClick={handleSubmit}
|
||||
isLoading={loading}
|
||||
loadingText="提交中..."
|
||||
>
|
||||
提交申请
|
||||
</Button>
|
||||
</HStack>
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
222
src/components/Invoice/InvoiceApplyModal.tsx
Normal file
222
src/components/Invoice/InvoiceApplyModal.tsx
Normal file
@@ -0,0 +1,222 @@
|
||||
/**
|
||||
* 发票申请弹窗
|
||||
*/
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
useToast,
|
||||
Spinner,
|
||||
Center,
|
||||
Text,
|
||||
VStack,
|
||||
Box,
|
||||
HStack,
|
||||
Button,
|
||||
useColorModeValue,
|
||||
} from '@chakra-ui/react';
|
||||
import { FileText } from 'lucide-react';
|
||||
import InvoiceApplyForm from './InvoiceApplyForm';
|
||||
import { getAvailableOrders, applyInvoice } from '@/services/invoiceService';
|
||||
import type { InvoiceableOrder, CreateInvoiceRequest } from '@/types/invoice';
|
||||
|
||||
interface InvoiceApplyModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
orderId?: string; // 可选:指定订单ID
|
||||
onSuccess?: () => void;
|
||||
}
|
||||
|
||||
const planNameMap: Record<string, string> = {
|
||||
pro: 'Pro 专业版',
|
||||
max: 'Max 旗舰版',
|
||||
};
|
||||
|
||||
const billingCycleMap: Record<string, string> = {
|
||||
monthly: '月付',
|
||||
quarterly: '季付',
|
||||
semiannual: '半年付',
|
||||
yearly: '年付',
|
||||
};
|
||||
|
||||
export default function InvoiceApplyModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
orderId,
|
||||
onSuccess,
|
||||
}: InvoiceApplyModalProps) {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [orders, setOrders] = useState<InvoiceableOrder[]>([]);
|
||||
const [selectedOrder, setSelectedOrder] = useState<InvoiceableOrder | null>(null);
|
||||
|
||||
const toast = useToast();
|
||||
const borderColor = useColorModeValue('gray.200', 'gray.600');
|
||||
const bgHover = useColorModeValue('gray.50', 'gray.700');
|
||||
const bgSelected = useColorModeValue('blue.50', 'blue.900');
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
loadOrders();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
const loadOrders = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const res = await getAvailableOrders();
|
||||
if (res.code === 200 && res.data) {
|
||||
setOrders(res.data);
|
||||
// 如果指定了订单ID,直接选中
|
||||
if (orderId) {
|
||||
const order = res.data.find((o) => o.id === orderId);
|
||||
if (order) {
|
||||
setSelectedOrder(order);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载可开票订单失败:', error);
|
||||
toast({
|
||||
title: '加载失败',
|
||||
description: '无法获取可开票订单列表',
|
||||
status: 'error',
|
||||
duration: 3000,
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (data: CreateInvoiceRequest) => {
|
||||
try {
|
||||
setSubmitting(true);
|
||||
const res = await applyInvoice(data);
|
||||
if (res.code === 200) {
|
||||
toast({
|
||||
title: '申请成功',
|
||||
description: res.message || '开票申请已提交,预计1-3个工作日内处理',
|
||||
status: 'success',
|
||||
duration: 5000,
|
||||
});
|
||||
onClose();
|
||||
onSuccess?.();
|
||||
} else {
|
||||
toast({
|
||||
title: '申请失败',
|
||||
description: res.message || '开票申请提交失败',
|
||||
status: 'error',
|
||||
duration: 3000,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('提交开票申请失败:', error);
|
||||
toast({
|
||||
title: '申请失败',
|
||||
description: '网络错误,请稍后重试',
|
||||
status: 'error',
|
||||
duration: 3000,
|
||||
});
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setSelectedOrder(null);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={handleClose}
|
||||
size="xl"
|
||||
scrollBehavior="inside"
|
||||
isCentered
|
||||
>
|
||||
<ModalOverlay />
|
||||
<ModalContent maxH="85vh">
|
||||
<ModalHeader>
|
||||
<HStack>
|
||||
<FileText size={20} />
|
||||
<Text>申请开票</Text>
|
||||
</HStack>
|
||||
</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody pb={6}>
|
||||
{loading ? (
|
||||
<Center py={10}>
|
||||
<VStack spacing={4}>
|
||||
<Spinner size="lg" />
|
||||
<Text color="gray.500">加载中...</Text>
|
||||
</VStack>
|
||||
</Center>
|
||||
) : orders.length === 0 ? (
|
||||
<Center py={10}>
|
||||
<VStack spacing={4}>
|
||||
<Text fontSize="lg">暂无可开票订单</Text>
|
||||
<Text color="gray.500" fontSize="sm">
|
||||
仅已支付且未开票的订单可申请开票
|
||||
</Text>
|
||||
</VStack>
|
||||
</Center>
|
||||
) : selectedOrder ? (
|
||||
<InvoiceApplyForm
|
||||
order={selectedOrder}
|
||||
onSubmit={handleSubmit}
|
||||
onCancel={() => setSelectedOrder(null)}
|
||||
loading={submitting}
|
||||
/>
|
||||
) : (
|
||||
<VStack align="stretch" spacing={4}>
|
||||
<Text fontSize="sm" color="gray.500">
|
||||
请选择要开票的订单
|
||||
</Text>
|
||||
{orders.map((order) => (
|
||||
<Box
|
||||
key={order.id}
|
||||
p={4}
|
||||
borderRadius="lg"
|
||||
border="1px solid"
|
||||
borderColor={borderColor}
|
||||
cursor="pointer"
|
||||
transition="all 0.2s"
|
||||
onClick={() => setSelectedOrder(order)}
|
||||
_hover={{ bg: bgHover, borderColor: 'blue.400' }}
|
||||
>
|
||||
<HStack justify="space-between">
|
||||
<VStack align="flex-start" spacing={1}>
|
||||
<Text fontWeight="600">
|
||||
{planNameMap[order.planName] || order.planName}
|
||||
</Text>
|
||||
<Text fontSize="sm" color="gray.500">
|
||||
{order.orderNo} ·{' '}
|
||||
{billingCycleMap[order.billingCycle] || order.billingCycle}
|
||||
</Text>
|
||||
<Text fontSize="xs" color="gray.400">
|
||||
支付时间: {new Date(order.paidAt).toLocaleString('zh-CN')}
|
||||
</Text>
|
||||
</VStack>
|
||||
<VStack align="flex-end" spacing={1}>
|
||||
<Text fontWeight="600" color="blue.500">
|
||||
¥{order.amount.toFixed(2)}
|
||||
</Text>
|
||||
<Button size="sm" colorScheme="blue" variant="outline">
|
||||
申请开票
|
||||
</Button>
|
||||
</VStack>
|
||||
</HStack>
|
||||
</Box>
|
||||
))}
|
||||
</VStack>
|
||||
)}
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
175
src/components/Invoice/InvoiceCard.tsx
Normal file
175
src/components/Invoice/InvoiceCard.tsx
Normal file
@@ -0,0 +1,175 @@
|
||||
/**
|
||||
* 发票卡片组件
|
||||
* 用于展示发票信息
|
||||
*/
|
||||
import React from 'react';
|
||||
import {
|
||||
Box,
|
||||
VStack,
|
||||
HStack,
|
||||
Text,
|
||||
Button,
|
||||
Icon,
|
||||
useColorModeValue,
|
||||
Divider,
|
||||
} from '@chakra-ui/react';
|
||||
import { Download, Eye, X, FileText } from 'lucide-react';
|
||||
import InvoiceStatusBadge from './InvoiceStatusBadge';
|
||||
import type { InvoiceInfo } from '@/types/invoice';
|
||||
|
||||
interface InvoiceCardProps {
|
||||
invoice: InvoiceInfo;
|
||||
onView?: () => void;
|
||||
onDownload?: () => void;
|
||||
onCancel?: () => void;
|
||||
}
|
||||
|
||||
const invoiceTypeMap: Record<string, string> = {
|
||||
electronic: '电子发票',
|
||||
paper: '纸质发票',
|
||||
};
|
||||
|
||||
const titleTypeMap: Record<string, string> = {
|
||||
personal: '个人',
|
||||
company: '企业',
|
||||
};
|
||||
|
||||
function InvoiceCard({
|
||||
invoice,
|
||||
onView,
|
||||
onDownload,
|
||||
onCancel,
|
||||
}: InvoiceCardProps) {
|
||||
const bgCard = useColorModeValue('white', 'gray.800');
|
||||
const borderColor = useColorModeValue('gray.200', 'gray.600');
|
||||
const headerBg = useColorModeValue('gray.50', 'gray.700');
|
||||
|
||||
const canDownload = invoice.status === 'completed' && invoice.invoiceType === 'electronic';
|
||||
const canCancel = invoice.status === 'pending';
|
||||
|
||||
return (
|
||||
<Box
|
||||
bg={bgCard}
|
||||
borderRadius="lg"
|
||||
border="1px solid"
|
||||
borderColor={borderColor}
|
||||
overflow="hidden"
|
||||
transition="all 0.2s"
|
||||
_hover={{ shadow: 'md' }}
|
||||
>
|
||||
{/* 头部 */}
|
||||
<HStack justify="space-between" p={4} bg={headerBg}>
|
||||
<HStack spacing={3}>
|
||||
<Icon as={FileText} boxSize={5} color="blue.500" />
|
||||
<VStack align="flex-start" spacing={0}>
|
||||
<Text fontWeight="600" fontSize="sm">
|
||||
{invoice.title}
|
||||
</Text>
|
||||
<Text fontSize="xs" color="gray.500">
|
||||
{invoiceTypeMap[invoice.invoiceType]} · {titleTypeMap[invoice.titleType]}
|
||||
</Text>
|
||||
</VStack>
|
||||
</HStack>
|
||||
<InvoiceStatusBadge status={invoice.status} />
|
||||
</HStack>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* 内容 */}
|
||||
<VStack align="stretch" spacing={3} p={4}>
|
||||
<HStack justify="space-between">
|
||||
<Text fontSize="sm" color="gray.500">
|
||||
订单号
|
||||
</Text>
|
||||
<Text fontSize="sm">{invoice.orderNo}</Text>
|
||||
</HStack>
|
||||
|
||||
<HStack justify="space-between">
|
||||
<Text fontSize="sm" color="gray.500">
|
||||
开票金额
|
||||
</Text>
|
||||
<Text fontSize="sm" fontWeight="600" color="blue.500">
|
||||
¥{invoice.amount.toFixed(2)}
|
||||
</Text>
|
||||
</HStack>
|
||||
|
||||
{invoice.invoiceNo && (
|
||||
<HStack justify="space-between">
|
||||
<Text fontSize="sm" color="gray.500">
|
||||
发票号码
|
||||
</Text>
|
||||
<Text fontSize="sm">{invoice.invoiceNo}</Text>
|
||||
</HStack>
|
||||
)}
|
||||
|
||||
<HStack justify="space-between">
|
||||
<Text fontSize="sm" color="gray.500">
|
||||
申请时间
|
||||
</Text>
|
||||
<Text fontSize="sm">
|
||||
{new Date(invoice.createdAt).toLocaleString('zh-CN')}
|
||||
</Text>
|
||||
</HStack>
|
||||
|
||||
{invoice.completedAt && (
|
||||
<HStack justify="space-between">
|
||||
<Text fontSize="sm" color="gray.500">
|
||||
开具时间
|
||||
</Text>
|
||||
<Text fontSize="sm">
|
||||
{new Date(invoice.completedAt).toLocaleString('zh-CN')}
|
||||
</Text>
|
||||
</HStack>
|
||||
)}
|
||||
|
||||
{invoice.rejectReason && (
|
||||
<Box p={2} bg="red.50" borderRadius="md">
|
||||
<Text fontSize="xs" color="red.600">
|
||||
拒绝原因: {invoice.rejectReason}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</VStack>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<HStack justify="flex-end" spacing={2} p={3}>
|
||||
{onView && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
leftIcon={<Icon as={Eye} boxSize={4} />}
|
||||
onClick={onView}
|
||||
>
|
||||
查看详情
|
||||
</Button>
|
||||
)}
|
||||
{canDownload && onDownload && (
|
||||
<Button
|
||||
size="sm"
|
||||
colorScheme="blue"
|
||||
variant="outline"
|
||||
leftIcon={<Icon as={Download} boxSize={4} />}
|
||||
onClick={onDownload}
|
||||
>
|
||||
下载发票
|
||||
</Button>
|
||||
)}
|
||||
{canCancel && onCancel && (
|
||||
<Button
|
||||
size="sm"
|
||||
colorScheme="red"
|
||||
variant="ghost"
|
||||
leftIcon={<Icon as={X} boxSize={4} />}
|
||||
onClick={onCancel}
|
||||
>
|
||||
取消申请
|
||||
</Button>
|
||||
)}
|
||||
</HStack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(InvoiceCard);
|
||||
37
src/components/Invoice/InvoiceStatusBadge.tsx
Normal file
37
src/components/Invoice/InvoiceStatusBadge.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* 发票状态徽章组件
|
||||
*/
|
||||
import React from 'react';
|
||||
import { Badge } from '@chakra-ui/react';
|
||||
import type { InvoiceStatus } from '@/types/invoice';
|
||||
|
||||
interface InvoiceStatusBadgeProps {
|
||||
status: InvoiceStatus;
|
||||
}
|
||||
|
||||
const statusConfig: Record<
|
||||
InvoiceStatus,
|
||||
{ label: string; colorScheme: string }
|
||||
> = {
|
||||
pending: { label: '待处理', colorScheme: 'yellow' },
|
||||
processing: { label: '处理中', colorScheme: 'blue' },
|
||||
completed: { label: '已开具', colorScheme: 'green' },
|
||||
rejected: { label: '已拒绝', colorScheme: 'red' },
|
||||
cancelled: { label: '已取消', colorScheme: 'gray' },
|
||||
};
|
||||
|
||||
export default function InvoiceStatusBadge({ status }: InvoiceStatusBadgeProps) {
|
||||
const config = statusConfig[status] || { label: '未知', colorScheme: 'gray' };
|
||||
|
||||
return (
|
||||
<Badge
|
||||
colorScheme={config.colorScheme}
|
||||
px={2}
|
||||
py={0.5}
|
||||
borderRadius="md"
|
||||
fontSize="xs"
|
||||
>
|
||||
{config.label}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
179
src/components/Invoice/InvoiceTitleSelector.tsx
Normal file
179
src/components/Invoice/InvoiceTitleSelector.tsx
Normal file
@@ -0,0 +1,179 @@
|
||||
/**
|
||||
* 发票抬头选择器
|
||||
* 支持选择常用抬头或新增抬头
|
||||
*/
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
VStack,
|
||||
HStack,
|
||||
Text,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
Button,
|
||||
Icon,
|
||||
Spinner,
|
||||
useColorModeValue,
|
||||
Badge,
|
||||
} from '@chakra-ui/react';
|
||||
import { Plus, Building2, User } from 'lucide-react';
|
||||
import { getTitleTemplates } from '@/services/invoiceService';
|
||||
import type { InvoiceTitleTemplate, InvoiceTitleType } from '@/types/invoice';
|
||||
|
||||
interface InvoiceTitleSelectorProps {
|
||||
titleType: InvoiceTitleType;
|
||||
onSelect: (template: InvoiceTitleTemplate | null) => void;
|
||||
onAddNew: () => void;
|
||||
}
|
||||
|
||||
export default function InvoiceTitleSelector({
|
||||
titleType,
|
||||
onSelect,
|
||||
onAddNew,
|
||||
}: InvoiceTitleSelectorProps) {
|
||||
const [templates, setTemplates] = useState<InvoiceTitleTemplate[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [selectedId, setSelectedId] = useState<string>('');
|
||||
|
||||
const bgHover = useColorModeValue('gray.50', 'gray.700');
|
||||
const borderColor = useColorModeValue('gray.200', 'gray.600');
|
||||
|
||||
useEffect(() => {
|
||||
loadTemplates();
|
||||
}, []);
|
||||
|
||||
const loadTemplates = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const res = await getTitleTemplates();
|
||||
if (res.code === 200 && res.data) {
|
||||
setTemplates(res.data);
|
||||
// 自动选中默认模板
|
||||
const defaultTemplate = res.data.find(
|
||||
(t) => t.isDefault && t.titleType === titleType
|
||||
);
|
||||
if (defaultTemplate) {
|
||||
setSelectedId(defaultTemplate.id);
|
||||
onSelect(defaultTemplate);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载发票抬头失败:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 根据抬头类型筛选模板
|
||||
const filteredTemplates = templates.filter((t) => t.titleType === titleType);
|
||||
|
||||
const handleSelect = (id: string) => {
|
||||
setSelectedId(id);
|
||||
if (id === 'new') {
|
||||
onSelect(null);
|
||||
onAddNew();
|
||||
} else {
|
||||
const template = templates.find((t) => t.id === id);
|
||||
onSelect(template || null);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Box py={4} textAlign="center">
|
||||
<Spinner size="sm" />
|
||||
<Text fontSize="sm" color="gray.500" mt={2}>
|
||||
加载常用抬头...
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<VStack align="stretch" spacing={2}>
|
||||
{filteredTemplates.length > 0 && (
|
||||
<Text fontSize="sm" color="gray.500" mb={1}>
|
||||
选择常用抬头
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<RadioGroup value={selectedId} onChange={handleSelect}>
|
||||
<VStack align="stretch" spacing={2}>
|
||||
{filteredTemplates.map((template) => (
|
||||
<Box
|
||||
key={template.id}
|
||||
p={3}
|
||||
borderRadius="md"
|
||||
border="1px solid"
|
||||
borderColor={selectedId === template.id ? 'blue.500' : borderColor}
|
||||
cursor="pointer"
|
||||
transition="all 0.2s"
|
||||
onClick={() => handleSelect(template.id)}
|
||||
_hover={{ bg: bgHover }}
|
||||
>
|
||||
<HStack justify="space-between">
|
||||
<HStack spacing={3}>
|
||||
<Radio value={template.id} />
|
||||
<Icon
|
||||
as={template.titleType === 'company' ? Building2 : User}
|
||||
boxSize={4}
|
||||
color="gray.500"
|
||||
/>
|
||||
<VStack align="flex-start" spacing={0}>
|
||||
<HStack>
|
||||
<Text fontSize="sm" fontWeight="500">
|
||||
{template.title}
|
||||
</Text>
|
||||
{template.isDefault && (
|
||||
<Badge colorScheme="blue" fontSize="xs">
|
||||
默认
|
||||
</Badge>
|
||||
)}
|
||||
</HStack>
|
||||
{template.taxNumber && (
|
||||
<Text fontSize="xs" color="gray.500">
|
||||
税号: {template.taxNumber}
|
||||
</Text>
|
||||
)}
|
||||
</VStack>
|
||||
</HStack>
|
||||
</HStack>
|
||||
</Box>
|
||||
))}
|
||||
|
||||
{/* 新增抬头选项 */}
|
||||
<Box
|
||||
p={3}
|
||||
borderRadius="md"
|
||||
border="1px dashed"
|
||||
borderColor={borderColor}
|
||||
cursor="pointer"
|
||||
transition="all 0.2s"
|
||||
onClick={() => handleSelect('new')}
|
||||
_hover={{ bg: bgHover, borderColor: 'blue.400' }}
|
||||
>
|
||||
<HStack spacing={3}>
|
||||
<Radio value="new" />
|
||||
<Icon as={Plus} boxSize={4} color="blue.500" />
|
||||
<Text fontSize="sm" color="blue.500">
|
||||
使用新的发票抬头
|
||||
</Text>
|
||||
</HStack>
|
||||
</Box>
|
||||
</VStack>
|
||||
</RadioGroup>
|
||||
|
||||
{filteredTemplates.length === 0 && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
leftIcon={<Icon as={Plus} />}
|
||||
onClick={onAddNew}
|
||||
colorScheme="blue"
|
||||
>
|
||||
添加发票抬头
|
||||
</Button>
|
||||
)}
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
95
src/components/Invoice/InvoiceTypeSelector.tsx
Normal file
95
src/components/Invoice/InvoiceTypeSelector.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* 发票类型选择器
|
||||
* 支持选择电子发票或纸质发票
|
||||
*/
|
||||
import React from 'react';
|
||||
import {
|
||||
Box,
|
||||
HStack,
|
||||
Text,
|
||||
Icon,
|
||||
useColorModeValue,
|
||||
VStack,
|
||||
} from '@chakra-ui/react';
|
||||
import { FileText, Truck } from 'lucide-react';
|
||||
import type { InvoiceType } from '@/types/invoice';
|
||||
|
||||
interface InvoiceTypeSelectorProps {
|
||||
value: InvoiceType;
|
||||
onChange: (type: InvoiceType) => void;
|
||||
}
|
||||
|
||||
interface TypeOption {
|
||||
type: InvoiceType;
|
||||
label: string;
|
||||
description: string;
|
||||
icon: typeof FileText;
|
||||
}
|
||||
|
||||
const typeOptions: TypeOption[] = [
|
||||
{
|
||||
type: 'electronic',
|
||||
label: '电子发票',
|
||||
description: '即时开具,发送至邮箱',
|
||||
icon: FileText,
|
||||
},
|
||||
{
|
||||
type: 'paper',
|
||||
label: '纸质发票',
|
||||
description: '需填写邮寄地址,3-5个工作日',
|
||||
icon: Truck,
|
||||
},
|
||||
];
|
||||
|
||||
export default function InvoiceTypeSelector({
|
||||
value,
|
||||
onChange,
|
||||
}: InvoiceTypeSelectorProps) {
|
||||
const bgSelected = useColorModeValue('blue.50', 'blue.900');
|
||||
const borderSelected = useColorModeValue('blue.500', 'blue.400');
|
||||
const bgHover = useColorModeValue('gray.50', 'gray.700');
|
||||
const borderDefault = useColorModeValue('gray.200', 'gray.600');
|
||||
|
||||
return (
|
||||
<HStack spacing={4} w="100%">
|
||||
{typeOptions.map((option) => {
|
||||
const isSelected = value === option.type;
|
||||
|
||||
return (
|
||||
<Box
|
||||
key={option.type}
|
||||
flex={1}
|
||||
p={4}
|
||||
borderRadius="lg"
|
||||
border="2px solid"
|
||||
borderColor={isSelected ? borderSelected : borderDefault}
|
||||
bg={isSelected ? bgSelected : 'transparent'}
|
||||
cursor="pointer"
|
||||
transition="all 0.2s"
|
||||
onClick={() => onChange(option.type)}
|
||||
_hover={{
|
||||
bg: isSelected ? bgSelected : bgHover,
|
||||
borderColor: isSelected ? borderSelected : 'gray.300',
|
||||
}}
|
||||
>
|
||||
<VStack align="flex-start" spacing={1}>
|
||||
<HStack>
|
||||
<Icon
|
||||
as={option.icon}
|
||||
boxSize={5}
|
||||
color={isSelected ? 'blue.500' : 'gray.500'}
|
||||
/>
|
||||
<Text fontWeight="600" fontSize="sm">
|
||||
{option.label}
|
||||
</Text>
|
||||
</HStack>
|
||||
<Text fontSize="xs" color="gray.500">
|
||||
{option.description}
|
||||
</Text>
|
||||
</VStack>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</HStack>
|
||||
);
|
||||
}
|
||||
10
src/components/Invoice/index.ts
Normal file
10
src/components/Invoice/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* 发票组件统一导出
|
||||
*/
|
||||
|
||||
export { default as InvoiceStatusBadge } from './InvoiceStatusBadge';
|
||||
export { default as InvoiceTypeSelector } from './InvoiceTypeSelector';
|
||||
export { default as InvoiceTitleSelector } from './InvoiceTitleSelector';
|
||||
export { default as InvoiceApplyForm } from './InvoiceApplyForm';
|
||||
export { default as InvoiceApplyModal } from './InvoiceApplyModal';
|
||||
export { default as InvoiceCard } from './InvoiceCard';
|
||||
@@ -231,9 +231,9 @@ export default function SubscriptionContentNew() {
|
||||
if (data.success && (data.data?.status === 'paid' || data.payment_success)) {
|
||||
toast({
|
||||
title: '支付成功!',
|
||||
description: '您的订阅已激活',
|
||||
description: '您的订阅已激活。如需发票,请前往「发票管理」申请',
|
||||
status: 'success',
|
||||
duration: 5000,
|
||||
duration: 6000,
|
||||
isClosable: true,
|
||||
});
|
||||
|
||||
@@ -683,9 +683,9 @@ export default function SubscriptionContentNew() {
|
||||
|
||||
toast({
|
||||
title: '支付成功!',
|
||||
description: '您的订阅已激活',
|
||||
description: '您的订阅已激活。如需发票,请前往「发票管理」申请',
|
||||
status: 'success',
|
||||
duration: 5000,
|
||||
duration: 6000,
|
||||
isClosable: true,
|
||||
});
|
||||
|
||||
@@ -721,9 +721,9 @@ export default function SubscriptionContentNew() {
|
||||
if (data.success && (data.data.status === 'paid' || data.payment_success)) {
|
||||
toast({
|
||||
title: '支付成功!',
|
||||
description: '您的订阅已激活',
|
||||
description: '您的订阅已激活。如需发票,请前往「发票管理」申请',
|
||||
status: 'success',
|
||||
duration: 5000,
|
||||
duration: 6000,
|
||||
isClosable: true,
|
||||
});
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import { agentHandlers } from './agent';
|
||||
import { bytedeskHandlers } from './bytedesk';
|
||||
import { predictionHandlers } from './prediction';
|
||||
import { forumHandlers } from './forum';
|
||||
import { invoiceHandlers } from './invoice';
|
||||
|
||||
// 可以在这里添加更多的 handlers
|
||||
// import { userHandlers } from './user';
|
||||
@@ -42,5 +43,6 @@ export const handlers = [
|
||||
...bytedeskHandlers, // ⚡ Bytedesk 客服 Widget passthrough
|
||||
...predictionHandlers, // 预测市场
|
||||
...forumHandlers, // 价值论坛帖子 (ES)
|
||||
...invoiceHandlers, // 发票管理
|
||||
// ...userHandlers,
|
||||
];
|
||||
|
||||
920
src/mocks/handlers/invoice.js
Normal file
920
src/mocks/handlers/invoice.js
Normal file
@@ -0,0 +1,920 @@
|
||||
// src/mocks/handlers/invoice.js
|
||||
import { http, HttpResponse, delay } from 'msw';
|
||||
import { getCurrentUser } from '../data/users';
|
||||
|
||||
// 模拟网络延迟(毫秒)
|
||||
const NETWORK_DELAY = 500;
|
||||
|
||||
// 模拟发票数据存储
|
||||
const mockInvoices = new Map();
|
||||
const mockTitleTemplates = new Map();
|
||||
let invoiceIdCounter = 1000;
|
||||
let templateIdCounter = 100;
|
||||
|
||||
// 模拟可开票订单数据
|
||||
const mockInvoiceableOrders = [
|
||||
{
|
||||
id: 'ORDER_1001_1703001600000',
|
||||
orderNo: 'VF20241220001',
|
||||
planName: 'pro',
|
||||
billingCycle: 'yearly',
|
||||
amount: 2699,
|
||||
paidAt: '2024-12-20T10:00:00Z',
|
||||
invoiceApplied: false,
|
||||
},
|
||||
{
|
||||
id: 'ORDER_1002_1703088000000',
|
||||
orderNo: 'VF20241221001',
|
||||
planName: 'max',
|
||||
billingCycle: 'monthly',
|
||||
amount: 599,
|
||||
paidAt: '2024-12-21T10:00:00Z',
|
||||
invoiceApplied: false,
|
||||
},
|
||||
];
|
||||
|
||||
// 为每个用户生成模拟发票数据
|
||||
const initMockInvoices = () => {
|
||||
// 为用户 ID 1-4 都生成一些发票数据
|
||||
const userInvoiceData = [
|
||||
// 用户1 (免费用户) - 无发票
|
||||
// 用户2 (Pro会员) - 有多张发票
|
||||
{
|
||||
id: 'INV_001',
|
||||
orderId: 'ORDER_999_1702396800000',
|
||||
orderNo: 'VF20241213001',
|
||||
userId: 2,
|
||||
invoiceType: 'electronic',
|
||||
titleType: 'company',
|
||||
title: '北京价值前沿科技有限公司',
|
||||
taxNumber: '91110108MA01XXXXX',
|
||||
amount: 2699,
|
||||
email: 'pro@example.com',
|
||||
status: 'completed',
|
||||
invoiceNo: 'E20241213001',
|
||||
invoiceCode: '011001900111',
|
||||
invoiceUrl: 'https://example.com/invoices/E20241213001.pdf',
|
||||
createdAt: '2024-12-13T10:00:00Z',
|
||||
updatedAt: '2024-12-14T15:30:00Z',
|
||||
completedAt: '2024-12-14T15:30:00Z',
|
||||
},
|
||||
{
|
||||
id: 'INV_002',
|
||||
orderId: 'ORDER_998_1701792000000',
|
||||
orderNo: 'VF20241206001',
|
||||
userId: 2,
|
||||
invoiceType: 'electronic',
|
||||
titleType: 'personal',
|
||||
title: '张三',
|
||||
amount: 599,
|
||||
email: 'pro@example.com',
|
||||
status: 'processing',
|
||||
createdAt: '2024-12-06T10:00:00Z',
|
||||
updatedAt: '2024-12-06T10:00:00Z',
|
||||
},
|
||||
{
|
||||
id: 'INV_003',
|
||||
orderId: 'ORDER_997_1700000000000',
|
||||
orderNo: 'VF20241115001',
|
||||
userId: 2,
|
||||
invoiceType: 'electronic',
|
||||
titleType: 'personal',
|
||||
title: '李四',
|
||||
amount: 299,
|
||||
email: 'pro@example.com',
|
||||
status: 'pending',
|
||||
createdAt: '2024-12-24T10:00:00Z',
|
||||
updatedAt: '2024-12-24T10:00:00Z',
|
||||
},
|
||||
// 用户3 (Max会员) - 有发票
|
||||
{
|
||||
id: 'INV_004',
|
||||
orderId: 'ORDER_996_1703000000000',
|
||||
orderNo: 'VF20241220002',
|
||||
userId: 3,
|
||||
invoiceType: 'electronic',
|
||||
titleType: 'company',
|
||||
title: '上海科技发展有限公司',
|
||||
taxNumber: '91310115MA01YYYYY',
|
||||
amount: 5999,
|
||||
email: 'max@example.com',
|
||||
status: 'completed',
|
||||
invoiceNo: 'E20241220001',
|
||||
invoiceCode: '011001900222',
|
||||
invoiceUrl: 'https://example.com/invoices/E20241220001.pdf',
|
||||
createdAt: '2024-12-20T10:00:00Z',
|
||||
updatedAt: '2024-12-21T09:00:00Z',
|
||||
completedAt: '2024-12-21T09:00:00Z',
|
||||
},
|
||||
{
|
||||
id: 'INV_005',
|
||||
orderId: 'ORDER_995_1702500000000',
|
||||
orderNo: 'VF20241214001',
|
||||
userId: 3,
|
||||
invoiceType: 'paper',
|
||||
titleType: 'company',
|
||||
title: '上海科技发展有限公司',
|
||||
taxNumber: '91310115MA01YYYYY',
|
||||
amount: 2699,
|
||||
email: 'max@example.com',
|
||||
status: 'processing',
|
||||
createdAt: '2024-12-14T10:00:00Z',
|
||||
updatedAt: '2024-12-15T10:00:00Z',
|
||||
},
|
||||
// 用户1 (测试用户) - 也添加一些发票方便测试
|
||||
{
|
||||
id: 'INV_006',
|
||||
orderId: 'ORDER_994_1703100000000',
|
||||
orderNo: 'VF20241222001',
|
||||
userId: 1,
|
||||
invoiceType: 'electronic',
|
||||
titleType: 'personal',
|
||||
title: '测试用户',
|
||||
amount: 299,
|
||||
email: 'test@example.com',
|
||||
status: 'completed',
|
||||
invoiceNo: 'E20241222001',
|
||||
invoiceCode: '011001900333',
|
||||
invoiceUrl: 'https://example.com/invoices/E20241222001.pdf',
|
||||
createdAt: '2024-12-22T10:00:00Z',
|
||||
updatedAt: '2024-12-23T10:00:00Z',
|
||||
completedAt: '2024-12-23T10:00:00Z',
|
||||
},
|
||||
{
|
||||
id: 'INV_007',
|
||||
orderId: 'ORDER_993_1703200000000',
|
||||
orderNo: 'VF20241223001',
|
||||
userId: 1,
|
||||
invoiceType: 'electronic',
|
||||
titleType: 'company',
|
||||
title: '测试科技有限公司',
|
||||
taxNumber: '91110108MA01ZZZZZ',
|
||||
amount: 599,
|
||||
email: 'test@example.com',
|
||||
status: 'processing',
|
||||
createdAt: '2024-12-23T14:00:00Z',
|
||||
updatedAt: '2024-12-23T14:00:00Z',
|
||||
},
|
||||
{
|
||||
id: 'INV_008',
|
||||
orderId: 'ORDER_992_1703250000000',
|
||||
orderNo: 'VF20241225001',
|
||||
userId: 1,
|
||||
invoiceType: 'electronic',
|
||||
titleType: 'personal',
|
||||
title: '王五',
|
||||
amount: 199,
|
||||
email: 'test@example.com',
|
||||
status: 'pending',
|
||||
createdAt: '2024-12-25T10:00:00Z',
|
||||
updatedAt: '2024-12-25T10:00:00Z',
|
||||
},
|
||||
{
|
||||
id: 'INV_009',
|
||||
orderId: 'ORDER_991_1702000000000',
|
||||
orderNo: 'VF20241208001',
|
||||
userId: 1,
|
||||
invoiceType: 'electronic',
|
||||
titleType: 'personal',
|
||||
title: '赵六',
|
||||
amount: 99,
|
||||
email: 'test@example.com',
|
||||
status: 'cancelled',
|
||||
createdAt: '2024-12-08T10:00:00Z',
|
||||
updatedAt: '2024-12-09T10:00:00Z',
|
||||
},
|
||||
];
|
||||
|
||||
userInvoiceData.forEach((invoice) => {
|
||||
mockInvoices.set(invoice.id, invoice);
|
||||
});
|
||||
};
|
||||
|
||||
// 初始化模拟抬头模板 - 为每个用户生成
|
||||
const initMockTemplates = () => {
|
||||
const sampleTemplates = [
|
||||
// 用户1 (测试用户) 的模板
|
||||
{
|
||||
id: 'TPL_001',
|
||||
userId: 1,
|
||||
isDefault: true,
|
||||
titleType: 'company',
|
||||
title: '测试科技有限公司',
|
||||
taxNumber: '91110108MA01ZZZZZ',
|
||||
companyAddress: '北京市朝阳区建国路1号',
|
||||
companyPhone: '010-88888888',
|
||||
bankName: '中国建设银行北京分行',
|
||||
bankAccount: '1100001234567890123',
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
},
|
||||
{
|
||||
id: 'TPL_002',
|
||||
userId: 1,
|
||||
isDefault: false,
|
||||
titleType: 'personal',
|
||||
title: '测试用户',
|
||||
createdAt: '2024-06-01T00:00:00Z',
|
||||
},
|
||||
// 用户2 (Pro会员) 的模板
|
||||
{
|
||||
id: 'TPL_003',
|
||||
userId: 2,
|
||||
isDefault: true,
|
||||
titleType: 'company',
|
||||
title: '北京价值前沿科技有限公司',
|
||||
taxNumber: '91110108MA01XXXXX',
|
||||
companyAddress: '北京市海淀区中关村大街1号',
|
||||
companyPhone: '010-12345678',
|
||||
bankName: '中国工商银行北京分行',
|
||||
bankAccount: '0200001234567890123',
|
||||
createdAt: '2024-01-01T00:00:00Z',
|
||||
},
|
||||
{
|
||||
id: 'TPL_004',
|
||||
userId: 2,
|
||||
isDefault: false,
|
||||
titleType: 'personal',
|
||||
title: '张三',
|
||||
createdAt: '2024-06-01T00:00:00Z',
|
||||
},
|
||||
// 用户3 (Max会员) 的模板
|
||||
{
|
||||
id: 'TPL_005',
|
||||
userId: 3,
|
||||
isDefault: true,
|
||||
titleType: 'company',
|
||||
title: '上海科技发展有限公司',
|
||||
taxNumber: '91310115MA01YYYYY',
|
||||
companyAddress: '上海市浦东新区陆家嘴金融中心',
|
||||
companyPhone: '021-66666666',
|
||||
bankName: '中国银行上海分行',
|
||||
bankAccount: '4400001234567890123',
|
||||
createdAt: '2024-02-01T00:00:00Z',
|
||||
},
|
||||
];
|
||||
|
||||
sampleTemplates.forEach((template) => {
|
||||
mockTitleTemplates.set(template.id, template);
|
||||
});
|
||||
};
|
||||
|
||||
// 初始化数据
|
||||
initMockInvoices();
|
||||
initMockTemplates();
|
||||
|
||||
export const invoiceHandlers = [
|
||||
// ==================== 发票申请管理 ====================
|
||||
|
||||
// 1. 获取可开票订单列表
|
||||
http.get('/api/invoice/available-orders', async () => {
|
||||
await delay(NETWORK_DELAY);
|
||||
|
||||
const currentUser = getCurrentUser();
|
||||
if (!currentUser) {
|
||||
return HttpResponse.json(
|
||||
{
|
||||
code: 401,
|
||||
message: '未登录',
|
||||
data: null,
|
||||
},
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
// 返回未申请开票的订单
|
||||
const availableOrders = mockInvoiceableOrders.filter((order) => !order.invoiceApplied);
|
||||
|
||||
console.log('[Mock] 获取可开票订单:', { count: availableOrders.length });
|
||||
|
||||
return HttpResponse.json({
|
||||
code: 200,
|
||||
message: 'success',
|
||||
data: availableOrders,
|
||||
});
|
||||
}),
|
||||
|
||||
// 2. 申请开票
|
||||
http.post('/api/invoice/apply', async ({ request }) => {
|
||||
await delay(NETWORK_DELAY);
|
||||
|
||||
const currentUser = getCurrentUser();
|
||||
if (!currentUser) {
|
||||
return HttpResponse.json(
|
||||
{
|
||||
code: 401,
|
||||
message: '未登录',
|
||||
data: null,
|
||||
},
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { orderId, invoiceType, titleType, title, taxNumber, email, phone, remark } = body;
|
||||
|
||||
console.log('[Mock] 申请开票:', { orderId, invoiceType, titleType, title });
|
||||
|
||||
// 验证订单
|
||||
const order = mockInvoiceableOrders.find((o) => o.id === orderId);
|
||||
if (!order) {
|
||||
return HttpResponse.json(
|
||||
{
|
||||
code: 404,
|
||||
message: '订单不存在',
|
||||
data: null,
|
||||
},
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
if (order.invoiceApplied) {
|
||||
return HttpResponse.json(
|
||||
{
|
||||
code: 400,
|
||||
message: '该订单已申请开票',
|
||||
data: null,
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// 企业开票必须有税号
|
||||
if (titleType === 'company' && !taxNumber) {
|
||||
return HttpResponse.json(
|
||||
{
|
||||
code: 400,
|
||||
message: '企业开票必须填写税号',
|
||||
data: null,
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// 创建发票申请
|
||||
const invoiceId = `INV_${invoiceIdCounter++}`;
|
||||
const invoice = {
|
||||
id: invoiceId,
|
||||
orderId: order.id,
|
||||
orderNo: order.orderNo,
|
||||
userId: currentUser.id,
|
||||
invoiceType,
|
||||
titleType,
|
||||
title,
|
||||
taxNumber: taxNumber || null,
|
||||
companyAddress: body.companyAddress || null,
|
||||
companyPhone: body.companyPhone || null,
|
||||
bankName: body.bankName || null,
|
||||
bankAccount: body.bankAccount || null,
|
||||
amount: order.amount,
|
||||
email,
|
||||
phone: phone || null,
|
||||
mailingAddress: body.mailingAddress || null,
|
||||
recipientName: body.recipientName || null,
|
||||
recipientPhone: body.recipientPhone || null,
|
||||
status: 'pending',
|
||||
remark: remark || null,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
mockInvoices.set(invoiceId, invoice);
|
||||
order.invoiceApplied = true;
|
||||
|
||||
console.log('[Mock] 发票申请创建成功:', invoice);
|
||||
|
||||
// 模拟3秒后自动变为处理中
|
||||
setTimeout(() => {
|
||||
const existingInvoice = mockInvoices.get(invoiceId);
|
||||
if (existingInvoice && existingInvoice.status === 'pending') {
|
||||
existingInvoice.status = 'processing';
|
||||
existingInvoice.updatedAt = new Date().toISOString();
|
||||
console.log(`[Mock] 发票开始处理: ${invoiceId}`);
|
||||
}
|
||||
}, 3000);
|
||||
|
||||
// 模拟10秒后自动开具完成(电子发票)
|
||||
if (invoiceType === 'electronic') {
|
||||
setTimeout(() => {
|
||||
const existingInvoice = mockInvoices.get(invoiceId);
|
||||
if (existingInvoice && existingInvoice.status === 'processing') {
|
||||
existingInvoice.status = 'completed';
|
||||
existingInvoice.invoiceNo = `E${Date.now()}`;
|
||||
existingInvoice.invoiceCode = '011001900111';
|
||||
existingInvoice.invoiceUrl = `https://example.com/invoices/${existingInvoice.invoiceNo}.pdf`;
|
||||
existingInvoice.completedAt = new Date().toISOString();
|
||||
existingInvoice.updatedAt = new Date().toISOString();
|
||||
console.log(`[Mock] 电子发票开具完成: ${invoiceId}`);
|
||||
}
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
return HttpResponse.json({
|
||||
code: 200,
|
||||
message: '开票申请已提交,预计1-3个工作日内处理',
|
||||
data: invoice,
|
||||
});
|
||||
}),
|
||||
|
||||
// 3. 获取发票列表
|
||||
http.get('/api/invoice/list', async ({ request }) => {
|
||||
await delay(NETWORK_DELAY);
|
||||
|
||||
const currentUser = getCurrentUser();
|
||||
if (!currentUser) {
|
||||
return HttpResponse.json(
|
||||
{
|
||||
code: 401,
|
||||
message: '未登录',
|
||||
data: null,
|
||||
},
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
const url = new URL(request.url);
|
||||
const page = parseInt(url.searchParams.get('page') || '1', 10);
|
||||
const pageSize = parseInt(url.searchParams.get('pageSize') || '10', 10);
|
||||
const statusFilter = url.searchParams.get('status');
|
||||
|
||||
// 获取用户的发票
|
||||
let userInvoices = Array.from(mockInvoices.values())
|
||||
.filter((invoice) => invoice.userId === currentUser.id)
|
||||
.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
|
||||
|
||||
// 状态筛选
|
||||
if (statusFilter) {
|
||||
userInvoices = userInvoices.filter((invoice) => invoice.status === statusFilter);
|
||||
}
|
||||
|
||||
// 分页
|
||||
const total = userInvoices.length;
|
||||
const startIndex = (page - 1) * pageSize;
|
||||
const paginatedInvoices = userInvoices.slice(startIndex, startIndex + pageSize);
|
||||
|
||||
console.log('[Mock] 获取发票列表:', { total, page, pageSize });
|
||||
|
||||
return HttpResponse.json({
|
||||
code: 200,
|
||||
message: 'success',
|
||||
data: {
|
||||
list: paginatedInvoices,
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
totalPages: Math.ceil(total / pageSize),
|
||||
},
|
||||
});
|
||||
}),
|
||||
|
||||
// 4. 获取发票详情
|
||||
http.get('/api/invoice/:invoiceId', async ({ params }) => {
|
||||
await delay(NETWORK_DELAY);
|
||||
|
||||
const currentUser = getCurrentUser();
|
||||
if (!currentUser) {
|
||||
return HttpResponse.json(
|
||||
{
|
||||
code: 401,
|
||||
message: '未登录',
|
||||
data: null,
|
||||
},
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
const { invoiceId } = params;
|
||||
const invoice = mockInvoices.get(invoiceId);
|
||||
|
||||
if (!invoice) {
|
||||
return HttpResponse.json(
|
||||
{
|
||||
code: 404,
|
||||
message: '发票不存在',
|
||||
data: null,
|
||||
},
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
if (invoice.userId !== currentUser.id) {
|
||||
return HttpResponse.json(
|
||||
{
|
||||
code: 403,
|
||||
message: '无权访问此发票',
|
||||
data: null,
|
||||
},
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
console.log('[Mock] 获取发票详情:', { invoiceId });
|
||||
|
||||
return HttpResponse.json({
|
||||
code: 200,
|
||||
message: 'success',
|
||||
data: invoice,
|
||||
});
|
||||
}),
|
||||
|
||||
// 5. 取消发票申请
|
||||
http.post('/api/invoice/:invoiceId/cancel', async ({ params }) => {
|
||||
await delay(NETWORK_DELAY);
|
||||
|
||||
const currentUser = getCurrentUser();
|
||||
if (!currentUser) {
|
||||
return HttpResponse.json(
|
||||
{
|
||||
code: 401,
|
||||
message: '未登录',
|
||||
data: null,
|
||||
},
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
const { invoiceId } = params;
|
||||
const invoice = mockInvoices.get(invoiceId);
|
||||
|
||||
if (!invoice) {
|
||||
return HttpResponse.json(
|
||||
{
|
||||
code: 404,
|
||||
message: '发票不存在',
|
||||
data: null,
|
||||
},
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
if (invoice.userId !== currentUser.id) {
|
||||
return HttpResponse.json(
|
||||
{
|
||||
code: 403,
|
||||
message: '无权操作此发票',
|
||||
data: null,
|
||||
},
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
if (invoice.status !== 'pending') {
|
||||
return HttpResponse.json(
|
||||
{
|
||||
code: 400,
|
||||
message: '只能取消待处理的发票申请',
|
||||
data: null,
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
invoice.status = 'cancelled';
|
||||
invoice.updatedAt = new Date().toISOString();
|
||||
|
||||
// 恢复订单的开票状态
|
||||
const order = mockInvoiceableOrders.find((o) => o.id === invoice.orderId);
|
||||
if (order) {
|
||||
order.invoiceApplied = false;
|
||||
}
|
||||
|
||||
console.log('[Mock] 发票申请已取消:', invoiceId);
|
||||
|
||||
return HttpResponse.json({
|
||||
code: 200,
|
||||
message: '发票申请已取消',
|
||||
data: null,
|
||||
});
|
||||
}),
|
||||
|
||||
// 6. 下载电子发票
|
||||
http.get('/api/invoice/:invoiceId/download', async ({ params }) => {
|
||||
await delay(NETWORK_DELAY);
|
||||
|
||||
const currentUser = getCurrentUser();
|
||||
if (!currentUser) {
|
||||
return HttpResponse.json(
|
||||
{
|
||||
code: 401,
|
||||
message: '未登录',
|
||||
data: null,
|
||||
},
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
const { invoiceId } = params;
|
||||
const invoice = mockInvoices.get(invoiceId);
|
||||
|
||||
if (!invoice) {
|
||||
return HttpResponse.json(
|
||||
{
|
||||
code: 404,
|
||||
message: '发票不存在',
|
||||
data: null,
|
||||
},
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
if (invoice.userId !== currentUser.id) {
|
||||
return HttpResponse.json(
|
||||
{
|
||||
code: 403,
|
||||
message: '无权下载此发票',
|
||||
data: null,
|
||||
},
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
if (invoice.status !== 'completed') {
|
||||
return HttpResponse.json(
|
||||
{
|
||||
code: 400,
|
||||
message: '发票尚未开具完成',
|
||||
data: null,
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
console.log('[Mock] 下载电子发票:', invoiceId);
|
||||
|
||||
// 返回模拟的 PDF 内容
|
||||
const pdfContent = `%PDF-1.4
|
||||
1 0 obj
|
||||
<< /Type /Catalog /Pages 2 0 R >>
|
||||
endobj
|
||||
2 0 obj
|
||||
<< /Type /Pages /Kids [3 0 R] /Count 1 >>
|
||||
endobj
|
||||
3 0 obj
|
||||
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>
|
||||
endobj
|
||||
trailer
|
||||
<< /Root 1 0 R >>
|
||||
%%EOF`;
|
||||
|
||||
return new HttpResponse(pdfContent, {
|
||||
headers: {
|
||||
'Content-Type': 'application/pdf',
|
||||
'Content-Disposition': `attachment; filename="invoice_${invoice.invoiceNo}.pdf"`,
|
||||
},
|
||||
});
|
||||
}),
|
||||
|
||||
// 7. 获取发票统计
|
||||
http.get('/api/invoice/stats', async () => {
|
||||
await delay(NETWORK_DELAY);
|
||||
|
||||
const currentUser = getCurrentUser();
|
||||
if (!currentUser) {
|
||||
return HttpResponse.json(
|
||||
{
|
||||
code: 401,
|
||||
message: '未登录',
|
||||
data: null,
|
||||
},
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
const userInvoices = Array.from(mockInvoices.values()).filter(
|
||||
(invoice) => invoice.userId === currentUser.id
|
||||
);
|
||||
|
||||
// 计算可开票金额(未申请开票的订单)
|
||||
const availableOrders = mockInvoiceableOrders.filter((order) => !order.invoiceApplied);
|
||||
const availableAmount = availableOrders.reduce((sum, order) => sum + order.amount, 0);
|
||||
|
||||
// 计算已开票金额
|
||||
const invoicedAmount = userInvoices
|
||||
.filter((i) => i.status === 'completed')
|
||||
.reduce((sum, invoice) => sum + invoice.amount, 0);
|
||||
|
||||
// 计算处理中金额
|
||||
const processingAmount = userInvoices
|
||||
.filter((i) => i.status === 'processing' || i.status === 'pending')
|
||||
.reduce((sum, invoice) => sum + invoice.amount, 0);
|
||||
|
||||
const stats = {
|
||||
total: userInvoices.length,
|
||||
pending: userInvoices.filter((i) => i.status === 'pending').length,
|
||||
processing: userInvoices.filter((i) => i.status === 'processing').length,
|
||||
completed: userInvoices.filter((i) => i.status === 'completed').length,
|
||||
cancelled: userInvoices.filter((i) => i.status === 'cancelled').length,
|
||||
availableAmount,
|
||||
invoicedAmount,
|
||||
processingAmount,
|
||||
};
|
||||
|
||||
console.log('[Mock] 获取发票统计:', stats);
|
||||
|
||||
return HttpResponse.json({
|
||||
code: 200,
|
||||
message: 'success',
|
||||
data: stats,
|
||||
});
|
||||
}),
|
||||
|
||||
// ==================== 发票抬头模板管理 ====================
|
||||
|
||||
// 8. 获取发票抬头模板列表
|
||||
http.get('/api/invoice/title-templates', async () => {
|
||||
await delay(NETWORK_DELAY);
|
||||
|
||||
const currentUser = getCurrentUser();
|
||||
if (!currentUser) {
|
||||
return HttpResponse.json(
|
||||
{
|
||||
code: 401,
|
||||
message: '未登录',
|
||||
data: null,
|
||||
},
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
const userTemplates = Array.from(mockTitleTemplates.values())
|
||||
.filter((template) => template.userId === currentUser.id)
|
||||
.sort((a, b) => {
|
||||
// 默认的排在前面
|
||||
if (a.isDefault !== b.isDefault) {
|
||||
return b.isDefault ? 1 : -1;
|
||||
}
|
||||
return new Date(b.createdAt) - new Date(a.createdAt);
|
||||
});
|
||||
|
||||
console.log('[Mock] 获取抬头模板列表:', { count: userTemplates.length });
|
||||
|
||||
return HttpResponse.json({
|
||||
code: 200,
|
||||
message: 'success',
|
||||
data: userTemplates,
|
||||
});
|
||||
}),
|
||||
|
||||
// 9. 保存发票抬头模板
|
||||
http.post('/api/invoice/title-template', async ({ request }) => {
|
||||
await delay(NETWORK_DELAY);
|
||||
|
||||
const currentUser = getCurrentUser();
|
||||
if (!currentUser) {
|
||||
return HttpResponse.json(
|
||||
{
|
||||
code: 401,
|
||||
message: '未登录',
|
||||
data: null,
|
||||
},
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
|
||||
const templateId = `TPL_${templateIdCounter++}`;
|
||||
const template = {
|
||||
id: templateId,
|
||||
userId: currentUser.id,
|
||||
isDefault: body.isDefault || false,
|
||||
titleType: body.titleType,
|
||||
title: body.title,
|
||||
taxNumber: body.taxNumber || null,
|
||||
companyAddress: body.companyAddress || null,
|
||||
companyPhone: body.companyPhone || null,
|
||||
bankName: body.bankName || null,
|
||||
bankAccount: body.bankAccount || null,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// 如果设为默认,取消其他模板的默认状态
|
||||
if (template.isDefault) {
|
||||
mockTitleTemplates.forEach((t) => {
|
||||
if (t.userId === currentUser.id) {
|
||||
t.isDefault = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
mockTitleTemplates.set(templateId, template);
|
||||
|
||||
console.log('[Mock] 保存抬头模板:', template);
|
||||
|
||||
return HttpResponse.json({
|
||||
code: 200,
|
||||
message: '保存成功',
|
||||
data: template,
|
||||
});
|
||||
}),
|
||||
|
||||
// 10. 删除发票抬头模板
|
||||
http.delete('/api/invoice/title-template/:templateId', async ({ params }) => {
|
||||
await delay(NETWORK_DELAY);
|
||||
|
||||
const currentUser = getCurrentUser();
|
||||
if (!currentUser) {
|
||||
return HttpResponse.json(
|
||||
{
|
||||
code: 401,
|
||||
message: '未登录',
|
||||
data: null,
|
||||
},
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
const { templateId } = params;
|
||||
const template = mockTitleTemplates.get(templateId);
|
||||
|
||||
if (!template) {
|
||||
return HttpResponse.json(
|
||||
{
|
||||
code: 404,
|
||||
message: '模板不存在',
|
||||
data: null,
|
||||
},
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
if (template.userId !== currentUser.id) {
|
||||
return HttpResponse.json(
|
||||
{
|
||||
code: 403,
|
||||
message: '无权删除此模板',
|
||||
data: null,
|
||||
},
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
mockTitleTemplates.delete(templateId);
|
||||
|
||||
console.log('[Mock] 删除抬头模板:', templateId);
|
||||
|
||||
return HttpResponse.json({
|
||||
code: 200,
|
||||
message: '删除成功',
|
||||
data: null,
|
||||
});
|
||||
}),
|
||||
|
||||
// 11. 设置默认发票抬头
|
||||
http.post('/api/invoice/title-template/:templateId/default', async ({ params }) => {
|
||||
await delay(NETWORK_DELAY);
|
||||
|
||||
const currentUser = getCurrentUser();
|
||||
if (!currentUser) {
|
||||
return HttpResponse.json(
|
||||
{
|
||||
code: 401,
|
||||
message: '未登录',
|
||||
data: null,
|
||||
},
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
const { templateId } = params;
|
||||
const template = mockTitleTemplates.get(templateId);
|
||||
|
||||
if (!template) {
|
||||
return HttpResponse.json(
|
||||
{
|
||||
code: 404,
|
||||
message: '模板不存在',
|
||||
data: null,
|
||||
},
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
if (template.userId !== currentUser.id) {
|
||||
return HttpResponse.json(
|
||||
{
|
||||
code: 403,
|
||||
message: '无权操作此模板',
|
||||
data: null,
|
||||
},
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
// 取消其他模板的默认状态
|
||||
mockTitleTemplates.forEach((t) => {
|
||||
if (t.userId === currentUser.id) {
|
||||
t.isDefault = false;
|
||||
}
|
||||
});
|
||||
|
||||
template.isDefault = true;
|
||||
|
||||
console.log('[Mock] 设置默认抬头:', templateId);
|
||||
|
||||
return HttpResponse.json({
|
||||
code: 200,
|
||||
message: '设置成功',
|
||||
data: null,
|
||||
});
|
||||
}),
|
||||
];
|
||||
@@ -69,6 +69,17 @@ export const homeRoutes = [
|
||||
}
|
||||
},
|
||||
|
||||
// 发票管理 - /home/pages/account/invoice
|
||||
{
|
||||
path: 'pages/account/invoice',
|
||||
component: lazyComponents.Invoice,
|
||||
protection: PROTECTION_MODES.REDIRECT,
|
||||
meta: {
|
||||
title: '发票管理',
|
||||
description: '发票申请与管理'
|
||||
}
|
||||
},
|
||||
|
||||
// 隐私政策 - /home/privacy-policy
|
||||
{
|
||||
path: 'privacy-policy',
|
||||
|
||||
@@ -17,6 +17,7 @@ export const lazyComponents = {
|
||||
ForumMyPoints: React.lazy(() => import('@views/Profile')),
|
||||
SettingsPage: React.lazy(() => import('@views/Settings/SettingsPage')),
|
||||
Subscription: React.lazy(() => import('@views/Pages/Account/Subscription')),
|
||||
Invoice: React.lazy(() => import('@views/Pages/Account/Invoice')),
|
||||
PrivacyPolicy: React.lazy(() => import('@views/Pages/PrivacyPolicy')),
|
||||
UserAgreement: React.lazy(() => import('@views/Pages/UserAgreement')),
|
||||
WechatCallback: React.lazy(() => import('@views/Pages/WechatCallback')),
|
||||
@@ -61,6 +62,7 @@ export const {
|
||||
ForumMyPoints,
|
||||
SettingsPage,
|
||||
Subscription,
|
||||
Invoice,
|
||||
PrivacyPolicy,
|
||||
UserAgreement,
|
||||
WechatCallback,
|
||||
|
||||
251
src/services/invoiceService.ts
Normal file
251
src/services/invoiceService.ts
Normal file
@@ -0,0 +1,251 @@
|
||||
/**
|
||||
* 发票服务
|
||||
* 处理发票申请、查询、下载等操作
|
||||
*/
|
||||
|
||||
import { getApiBase } from '@utils/apiConfig';
|
||||
import type {
|
||||
InvoiceInfo,
|
||||
CreateInvoiceRequest,
|
||||
InvoiceTitleTemplate,
|
||||
InvoiceableOrder,
|
||||
InvoiceListParams,
|
||||
InvoiceStats,
|
||||
} from '@/types/invoice';
|
||||
import type { ApiResponse } from '@/types/api';
|
||||
|
||||
/** 发票列表分页响应 */
|
||||
interface InvoiceListResponse {
|
||||
code: number;
|
||||
message: string;
|
||||
data: {
|
||||
list: InvoiceInfo[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
totalPages: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取可开票订单列表
|
||||
* 返回已支付且未申请开票的订单
|
||||
*/
|
||||
export const getAvailableOrders = async (): Promise<ApiResponse<InvoiceableOrder[]>> => {
|
||||
const response = await fetch(`${getApiBase()}/api/invoice/available-orders`, {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
};
|
||||
|
||||
/**
|
||||
* 申请开票
|
||||
*/
|
||||
export const applyInvoice = async (
|
||||
data: CreateInvoiceRequest
|
||||
): Promise<ApiResponse<InvoiceInfo>> => {
|
||||
const response = await fetch(`${getApiBase()}/api/invoice/apply`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取发票列表
|
||||
*/
|
||||
export const getInvoiceList = async (
|
||||
params?: InvoiceListParams
|
||||
): Promise<InvoiceListResponse> => {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params?.page) searchParams.set('page', params.page.toString());
|
||||
if (params?.pageSize) searchParams.set('pageSize', params.pageSize.toString());
|
||||
if (params?.status) searchParams.set('status', params.status);
|
||||
|
||||
const queryString = searchParams.toString();
|
||||
const url = `${getApiBase()}/api/invoice/list${queryString ? `?${queryString}` : ''}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取发票详情
|
||||
*/
|
||||
export const getInvoiceDetail = async (invoiceId: string): Promise<ApiResponse<InvoiceInfo>> => {
|
||||
const response = await fetch(`${getApiBase()}/api/invoice/${invoiceId}`, {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
};
|
||||
|
||||
/**
|
||||
* 取消发票申请
|
||||
*/
|
||||
export const cancelInvoice = async (invoiceId: string): Promise<ApiResponse<null>> => {
|
||||
const response = await fetch(`${getApiBase()}/api/invoice/${invoiceId}/cancel`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
};
|
||||
|
||||
/**
|
||||
* 下载电子发票
|
||||
*/
|
||||
export const downloadInvoice = async (invoiceId: string): Promise<Blob> => {
|
||||
const response = await fetch(`${getApiBase()}/api/invoice/${invoiceId}/download`, {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
return response.blob();
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取发票统计信息
|
||||
*/
|
||||
export const getInvoiceStats = async (): Promise<ApiResponse<InvoiceStats>> => {
|
||||
const response = await fetch(`${getApiBase()}/api/invoice/stats`, {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取发票抬头模板列表
|
||||
*/
|
||||
export const getTitleTemplates = async (): Promise<ApiResponse<InvoiceTitleTemplate[]>> => {
|
||||
const response = await fetch(`${getApiBase()}/api/invoice/title-templates`, {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
};
|
||||
|
||||
/**
|
||||
* 保存发票抬头模板
|
||||
*/
|
||||
export const saveTitleTemplate = async (
|
||||
data: Omit<InvoiceTitleTemplate, 'id' | 'userId' | 'createdAt'>
|
||||
): Promise<ApiResponse<InvoiceTitleTemplate>> => {
|
||||
const response = await fetch(`${getApiBase()}/api/invoice/title-template`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
};
|
||||
|
||||
/**
|
||||
* 删除发票抬头模板
|
||||
*/
|
||||
export const deleteTitleTemplate = async (templateId: string): Promise<ApiResponse<null>> => {
|
||||
const response = await fetch(`${getApiBase()}/api/invoice/title-template/${templateId}`, {
|
||||
method: 'DELETE',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
};
|
||||
|
||||
/**
|
||||
* 设置默认发票抬头
|
||||
*/
|
||||
export const setDefaultTemplate = async (templateId: string): Promise<ApiResponse<null>> => {
|
||||
const response = await fetch(`${getApiBase()}/api/invoice/title-template/${templateId}/default`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
};
|
||||
124
src/types/invoice.ts
Normal file
124
src/types/invoice.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* 发票相关类型定义
|
||||
*/
|
||||
|
||||
/** 发票类型 */
|
||||
export type InvoiceType = 'electronic' | 'paper';
|
||||
|
||||
/** 发票抬头类型 */
|
||||
export type InvoiceTitleType = 'personal' | 'company';
|
||||
|
||||
/** 发票状态 */
|
||||
export type InvoiceStatus =
|
||||
| 'pending' // 待处理
|
||||
| 'processing' // 处理中
|
||||
| 'completed' // 已开具
|
||||
| 'rejected' // 已拒绝
|
||||
| 'cancelled'; // 已取消
|
||||
|
||||
/** 发票信息 */
|
||||
export interface InvoiceInfo {
|
||||
id: string;
|
||||
orderId: string; // 关联订单ID
|
||||
orderNo: string; // 订单号
|
||||
userId: number;
|
||||
|
||||
// 发票基本信息
|
||||
invoiceType: InvoiceType; // 电子/纸质
|
||||
titleType: InvoiceTitleType; // 个人/企业
|
||||
title: string; // 发票抬头
|
||||
|
||||
// 企业开票信息
|
||||
taxNumber?: string; // 税号(企业必填)
|
||||
companyAddress?: string; // 公司地址
|
||||
companyPhone?: string; // 公司电话
|
||||
bankName?: string; // 开户银行
|
||||
bankAccount?: string; // 银行账号
|
||||
|
||||
// 发票金额
|
||||
amount: number; // 开票金额
|
||||
|
||||
// 接收信息
|
||||
email: string; // 接收邮箱
|
||||
phone?: string; // 联系电话
|
||||
|
||||
// 纸质发票邮寄信息
|
||||
mailingAddress?: string; // 邮寄地址
|
||||
recipientName?: string; // 收件人姓名
|
||||
recipientPhone?: string; // 收件人电话
|
||||
|
||||
// 状态信息
|
||||
status: InvoiceStatus;
|
||||
invoiceNo?: string; // 发票号码
|
||||
invoiceCode?: string; // 发票代码
|
||||
invoiceUrl?: string; // 电子发票下载链接
|
||||
|
||||
// 时间戳
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
completedAt?: string; // 开具完成时间
|
||||
|
||||
// 备注
|
||||
remark?: string; // 用户备注
|
||||
rejectReason?: string; // 拒绝原因
|
||||
}
|
||||
|
||||
/** 创建发票申请请求 */
|
||||
export interface CreateInvoiceRequest {
|
||||
orderId: string;
|
||||
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;
|
||||
}
|
||||
|
||||
/** 发票抬头模板(用户保存的常用抬头) */
|
||||
export interface InvoiceTitleTemplate {
|
||||
id: string;
|
||||
userId: number;
|
||||
isDefault: boolean;
|
||||
titleType: InvoiceTitleType;
|
||||
title: string;
|
||||
taxNumber?: string;
|
||||
companyAddress?: string;
|
||||
companyPhone?: string;
|
||||
bankName?: string;
|
||||
bankAccount?: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
/** 可开票订单 */
|
||||
export interface InvoiceableOrder {
|
||||
id: string;
|
||||
orderNo: string;
|
||||
planName: string; // 套餐名称
|
||||
billingCycle: string; // 计费周期
|
||||
amount: number; // 订单金额
|
||||
paidAt: string; // 支付时间
|
||||
invoiceApplied: boolean; // 是否已申请开票
|
||||
}
|
||||
|
||||
/** 发票列表查询参数 */
|
||||
export interface InvoiceListParams {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
status?: InvoiceStatus;
|
||||
}
|
||||
|
||||
/** 发票统计信息 */
|
||||
export interface InvoiceStats {
|
||||
total: number; // 总申请数
|
||||
pending: number; // 待处理
|
||||
processing: number; // 处理中
|
||||
completed: number; // 已完成
|
||||
}
|
||||
@@ -36,7 +36,6 @@ import IconBox from 'components/Icons/IconBox';
|
||||
import { MastercardIcon, VisaIcon } from 'components/Icons/Icons';
|
||||
import { HSeparator } from 'components/Separator/Separator';
|
||||
import BillingRow from 'components/Tables/BillingRow';
|
||||
import InvoicesRow from 'components/Tables/InvoicesRow';
|
||||
import TransactionRow from 'components/Tables/TransactionRow';
|
||||
import React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
@@ -46,10 +45,10 @@ import {
|
||||
Calendar,
|
||||
Gem,
|
||||
CreditCard,
|
||||
FileText,
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
billingData,
|
||||
invoicesData,
|
||||
newestTransactions,
|
||||
olderTransactions,
|
||||
} from 'variables/general';
|
||||
@@ -70,7 +69,7 @@ function Billing() {
|
||||
templateColumns={{
|
||||
sm: '1fr',
|
||||
md: '1fr 1fr',
|
||||
xl: '1fr 1fr 1fr 1fr',
|
||||
xl: '1fr 1fr 1fr 1fr 1fr',
|
||||
}}
|
||||
templateRows={{ sm: 'auto auto auto', md: '1fr auto', xl: '1fr' }}
|
||||
gap='26px'
|
||||
@@ -97,7 +96,7 @@ function Billing() {
|
||||
>
|
||||
<Flex justify='space-between' align='center'>
|
||||
<Text fontSize='md' fontWeight='bold'>
|
||||
Argon x Chakra
|
||||
价值前沿
|
||||
</Text>
|
||||
<Icon
|
||||
as={CreditCard}
|
||||
@@ -110,20 +109,20 @@ function Billing() {
|
||||
<Flex direction='column'>
|
||||
<Box>
|
||||
<Text fontSize='2xl' letterSpacing='2px' fontWeight='bold'>
|
||||
7812 2139 0823 XXXX
|
||||
**** **** **** 1234
|
||||
</Text>
|
||||
</Box>
|
||||
<Flex mt='14px'>
|
||||
<Flex direction='column' me='34px'>
|
||||
<Text fontSize='xs'>VALID THRU</Text>
|
||||
<Text fontSize='xs'>有效期</Text>
|
||||
<Text fontSize='xs' fontWeight='bold'>
|
||||
05/24
|
||||
12/26
|
||||
</Text>
|
||||
</Flex>
|
||||
<Flex direction='column'>
|
||||
<Text fontSize='xs'>CVV</Text>
|
||||
<Text fontSize='xs' fontWeight='bold'>
|
||||
09X
|
||||
***
|
||||
</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
@@ -144,7 +143,7 @@ function Billing() {
|
||||
w='100%'
|
||||
>
|
||||
<Text fontSize='md' color={textColor} fontWeight='bold'>
|
||||
Salary
|
||||
账户余额
|
||||
</Text>
|
||||
<Text
|
||||
mb='24px'
|
||||
@@ -152,12 +151,12 @@ function Billing() {
|
||||
color='gray.400'
|
||||
fontWeight='semibold'
|
||||
>
|
||||
Belong Interactive
|
||||
可用于支付
|
||||
</Text>
|
||||
<HSeparator />
|
||||
</Flex>
|
||||
<Text fontSize='lg' color={textColor} fontWeight='bold'>
|
||||
+$2000
|
||||
¥0.00
|
||||
</Text>
|
||||
</Flex>
|
||||
</Card>
|
||||
@@ -193,15 +192,56 @@ function Billing() {
|
||||
</Text>
|
||||
<HSeparator />
|
||||
</Flex>
|
||||
<Button
|
||||
size='sm'
|
||||
colorScheme='purple'
|
||||
<Button
|
||||
size='sm'
|
||||
colorScheme='purple'
|
||||
onClick={() => navigate('/home/pages/account/subscription')}
|
||||
>
|
||||
管理订阅
|
||||
</Button>
|
||||
</Flex>
|
||||
</Card>
|
||||
<Card p='16px' display='flex' align='center' justify='center'>
|
||||
<Flex
|
||||
direction='column'
|
||||
align='center'
|
||||
justify='center'
|
||||
w='100%'
|
||||
py='14px'
|
||||
>
|
||||
<IconBox h={'60px'} w={'60px'} bg='teal.500'>
|
||||
<Icon h={'24px'} w={'24px'} color='white' as={FileText} />
|
||||
</IconBox>
|
||||
<Flex
|
||||
direction='column'
|
||||
m='14px'
|
||||
justify='center'
|
||||
textAlign='center'
|
||||
align='center'
|
||||
w='100%'
|
||||
>
|
||||
<Text fontSize='md' color={textColor} fontWeight='bold'>
|
||||
发票管理
|
||||
</Text>
|
||||
<Text
|
||||
mb='24px'
|
||||
fontSize='xs'
|
||||
color='gray.400'
|
||||
fontWeight='semibold'
|
||||
>
|
||||
申请与下载发票
|
||||
</Text>
|
||||
<HSeparator />
|
||||
</Flex>
|
||||
<Button
|
||||
size='sm'
|
||||
colorScheme='teal'
|
||||
onClick={() => navigate('/home/pages/account/invoice')}
|
||||
>
|
||||
管理发票
|
||||
</Button>
|
||||
</Flex>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Card p='16px' mt='24px'>
|
||||
<CardHeader>
|
||||
@@ -212,10 +252,10 @@ function Billing() {
|
||||
w='100%'
|
||||
>
|
||||
<Text fontSize='lg' color={textColor} fontWeight='bold'>
|
||||
Payment Method
|
||||
支付方式
|
||||
</Text>
|
||||
<Button variant={colorMode === 'dark' ? 'primary' : 'dark'}>
|
||||
ADD A NEW CARD
|
||||
添加新卡
|
||||
</Button>
|
||||
</Flex>
|
||||
</CardHeader>
|
||||
@@ -292,7 +332,7 @@ function Billing() {
|
||||
<CardHeader>
|
||||
<Flex justify='space-between' align='center' mb='1rem' w='100%'>
|
||||
<Text fontSize='lg' color={textColor} fontWeight='bold'>
|
||||
Invoices
|
||||
发票记录
|
||||
</Text>
|
||||
<Button
|
||||
variant='outlined'
|
||||
@@ -301,26 +341,25 @@ function Billing() {
|
||||
_hover={colorMode === 'dark' && 'none'}
|
||||
minW='110px'
|
||||
maxH='35px'
|
||||
onClick={() => navigate('/home/pages/account/invoice')}
|
||||
>
|
||||
VIEW ALL
|
||||
查看全部
|
||||
</Button>
|
||||
</Flex>
|
||||
</CardHeader>
|
||||
|
||||
<Flex direction='column' w='100%'>
|
||||
{invoicesData.map((row, index) => {
|
||||
return (
|
||||
<React.Fragment key={index}>
|
||||
<InvoicesRow
|
||||
date={row.date}
|
||||
code={row.code}
|
||||
price={row.price}
|
||||
logo={row.logo}
|
||||
format={row.format}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
<Flex direction='column' w='100%' align='center' py='20px'>
|
||||
<Icon as={FileText} w='40px' h='40px' color='gray.300' mb='10px' />
|
||||
<Text color='gray.400' fontSize='sm' mb='15px'>
|
||||
暂无发票记录
|
||||
</Text>
|
||||
<Button
|
||||
size='sm'
|
||||
colorScheme='blue'
|
||||
onClick={() => navigate('/home/pages/account/invoice')}
|
||||
>
|
||||
申请开票
|
||||
</Button>
|
||||
</Flex>
|
||||
</Card>
|
||||
</Grid>
|
||||
@@ -329,7 +368,7 @@ function Billing() {
|
||||
<Flex direction='column'>
|
||||
<CardHeader py='12px'>
|
||||
<Text color={textColor} fontSize='lg' fontWeight='bold'>
|
||||
Billing Information
|
||||
账单信息
|
||||
</Text>
|
||||
</CardHeader>
|
||||
|
||||
@@ -364,7 +403,7 @@ function Billing() {
|
||||
fontSize={{ sm: 'lg', md: 'xl', lg: 'lg' }}
|
||||
fontWeight='bold'
|
||||
>
|
||||
Your Transactions
|
||||
交易记录
|
||||
</Text>
|
||||
<Flex align='center'>
|
||||
<Icon
|
||||
@@ -388,7 +427,7 @@ function Billing() {
|
||||
fontWeight='semibold'
|
||||
my='12px'
|
||||
>
|
||||
NEWEST
|
||||
最近
|
||||
</Text>
|
||||
{newestTransactions.map((row, index) => {
|
||||
return (
|
||||
@@ -408,7 +447,7 @@ function Billing() {
|
||||
fontWeight='semibold'
|
||||
my='12px'
|
||||
>
|
||||
OLDER
|
||||
更早
|
||||
</Text>
|
||||
{olderTransactions.map((row, index) => {
|
||||
return (
|
||||
|
||||
358
src/views/Pages/Account/Invoice/index.tsx
Normal file
358
src/views/Pages/Account/Invoice/index.tsx
Normal file
@@ -0,0 +1,358 @@
|
||||
/**
|
||||
* 发票管理页面
|
||||
*/
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
VStack,
|
||||
HStack,
|
||||
Text,
|
||||
Button,
|
||||
Icon,
|
||||
Tabs,
|
||||
TabList,
|
||||
Tab,
|
||||
TabPanels,
|
||||
TabPanel,
|
||||
SimpleGrid,
|
||||
Spinner,
|
||||
Center,
|
||||
useColorModeValue,
|
||||
useToast,
|
||||
useDisclosure,
|
||||
AlertDialog,
|
||||
AlertDialogOverlay,
|
||||
AlertDialogContent,
|
||||
AlertDialogHeader,
|
||||
AlertDialogBody,
|
||||
AlertDialogFooter,
|
||||
Stat,
|
||||
StatLabel,
|
||||
StatNumber,
|
||||
StatHelpText,
|
||||
} from '@chakra-ui/react';
|
||||
import { FileText, Plus, RefreshCw, Clock, CheckCircle, AlertCircle } from 'lucide-react';
|
||||
import Card from '@components/Card/Card';
|
||||
import CardHeader from '@components/Card/CardHeader';
|
||||
import { InvoiceCard, InvoiceApplyModal } from '@components/Invoice';
|
||||
import {
|
||||
getInvoiceList,
|
||||
getInvoiceStats,
|
||||
cancelInvoice,
|
||||
downloadInvoice,
|
||||
} from '@/services/invoiceService';
|
||||
import type { InvoiceInfo, InvoiceStatus, InvoiceStats } from '@/types/invoice';
|
||||
|
||||
type TabType = 'all' | 'pending' | 'processing' | 'completed';
|
||||
|
||||
const tabConfig: { key: TabType; label: string; status?: InvoiceStatus }[] = [
|
||||
{ key: 'all', label: '全部' },
|
||||
{ key: 'pending', label: '待处理', status: 'pending' },
|
||||
{ key: 'processing', label: '处理中', status: 'processing' },
|
||||
{ key: 'completed', label: '已完成', status: 'completed' },
|
||||
];
|
||||
|
||||
export default function InvoicePage() {
|
||||
const [invoices, setInvoices] = useState<InvoiceInfo[]>([]);
|
||||
const [stats, setStats] = useState<InvoiceStats | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [activeTab, setActiveTab] = useState<TabType>('all');
|
||||
const [cancelingId, setCancelingId] = useState<string | null>(null);
|
||||
|
||||
const toast = useToast();
|
||||
const textColor = useColorModeValue('gray.700', 'white');
|
||||
const bgCard = useColorModeValue('white', 'gray.800');
|
||||
const cancelDialogRef = React.useRef<HTMLButtonElement>(null);
|
||||
|
||||
const {
|
||||
isOpen: isApplyOpen,
|
||||
onOpen: onApplyOpen,
|
||||
onClose: onApplyClose,
|
||||
} = useDisclosure();
|
||||
|
||||
const {
|
||||
isOpen: isCancelOpen,
|
||||
onOpen: onCancelOpen,
|
||||
onClose: onCancelClose,
|
||||
} = useDisclosure();
|
||||
|
||||
// 加载发票列表
|
||||
const loadInvoices = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const status = tabConfig.find((t) => t.key === activeTab)?.status;
|
||||
const res = await getInvoiceList({ status, pageSize: 50 });
|
||||
if (res.code === 200 && res.data) {
|
||||
setInvoices(res.data.list || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载发票列表失败:', error);
|
||||
toast({
|
||||
title: '加载失败',
|
||||
description: '无法获取发票列表',
|
||||
status: 'error',
|
||||
duration: 3000,
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [activeTab, toast]);
|
||||
|
||||
// 加载统计信息
|
||||
const loadStats = useCallback(async () => {
|
||||
try {
|
||||
const res = await getInvoiceStats();
|
||||
if (res.code === 200 && res.data) {
|
||||
setStats(res.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载发票统计失败:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadInvoices();
|
||||
loadStats();
|
||||
}, [loadInvoices, loadStats]);
|
||||
|
||||
// 取消发票申请
|
||||
const handleCancel = async () => {
|
||||
if (!cancelingId) return;
|
||||
|
||||
try {
|
||||
const res = await cancelInvoice(cancelingId);
|
||||
if (res.code === 200) {
|
||||
toast({
|
||||
title: '取消成功',
|
||||
status: 'success',
|
||||
duration: 2000,
|
||||
});
|
||||
loadInvoices();
|
||||
loadStats();
|
||||
} else {
|
||||
toast({
|
||||
title: '取消失败',
|
||||
description: res.message,
|
||||
status: 'error',
|
||||
duration: 3000,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: '取消失败',
|
||||
description: '网络错误',
|
||||
status: 'error',
|
||||
duration: 3000,
|
||||
});
|
||||
} finally {
|
||||
setCancelingId(null);
|
||||
onCancelClose();
|
||||
}
|
||||
};
|
||||
|
||||
// 下载发票
|
||||
const handleDownload = async (invoice: InvoiceInfo) => {
|
||||
try {
|
||||
const blob = await downloadInvoice(invoice.id);
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `发票_${invoice.invoiceNo || invoice.id}.pdf`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
window.URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: '下载失败',
|
||||
description: '无法下载发票文件',
|
||||
status: 'error',
|
||||
duration: 3000,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 开始取消流程
|
||||
const startCancel = (invoiceId: string) => {
|
||||
setCancelingId(invoiceId);
|
||||
onCancelOpen();
|
||||
};
|
||||
|
||||
// 申请成功回调
|
||||
const handleApplySuccess = () => {
|
||||
loadInvoices();
|
||||
loadStats();
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex direction="column" pt={{ base: '120px', md: '75px' }}>
|
||||
{/* 统计卡片 */}
|
||||
{stats && (
|
||||
<SimpleGrid columns={{ base: 2, md: 4 }} spacing={4} mb={6}>
|
||||
<Card p={4}>
|
||||
<Stat>
|
||||
<StatLabel color="gray.500">全部申请</StatLabel>
|
||||
<StatNumber color={textColor}>{stats.total}</StatNumber>
|
||||
<StatHelpText>
|
||||
<Icon as={FileText} boxSize={3} mr={1} />
|
||||
累计
|
||||
</StatHelpText>
|
||||
</Stat>
|
||||
</Card>
|
||||
<Card p={4}>
|
||||
<Stat>
|
||||
<StatLabel color="gray.500">待处理</StatLabel>
|
||||
<StatNumber color="yellow.500">{stats.pending}</StatNumber>
|
||||
<StatHelpText>
|
||||
<Icon as={Clock} boxSize={3} mr={1} />
|
||||
等待中
|
||||
</StatHelpText>
|
||||
</Stat>
|
||||
</Card>
|
||||
<Card p={4}>
|
||||
<Stat>
|
||||
<StatLabel color="gray.500">处理中</StatLabel>
|
||||
<StatNumber color="blue.500">{stats.processing}</StatNumber>
|
||||
<StatHelpText>
|
||||
<Icon as={AlertCircle} boxSize={3} mr={1} />
|
||||
开具中
|
||||
</StatHelpText>
|
||||
</Stat>
|
||||
</Card>
|
||||
<Card p={4}>
|
||||
<Stat>
|
||||
<StatLabel color="gray.500">已完成</StatLabel>
|
||||
<StatNumber color="green.500">{stats.completed}</StatNumber>
|
||||
<StatHelpText>
|
||||
<Icon as={CheckCircle} boxSize={3} mr={1} />
|
||||
已开具
|
||||
</StatHelpText>
|
||||
</Stat>
|
||||
</Card>
|
||||
</SimpleGrid>
|
||||
)}
|
||||
|
||||
{/* 主内容区 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Flex justify="space-between" align="center" w="100%" mb={4}>
|
||||
<HStack>
|
||||
<Icon as={FileText} boxSize={6} color="blue.500" />
|
||||
<Text fontSize="xl" fontWeight="bold" color={textColor}>
|
||||
发票管理
|
||||
</Text>
|
||||
</HStack>
|
||||
<HStack spacing={3}>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
leftIcon={<Icon as={RefreshCw} />}
|
||||
onClick={() => {
|
||||
loadInvoices();
|
||||
loadStats();
|
||||
}}
|
||||
isLoading={loading}
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
colorScheme="blue"
|
||||
leftIcon={<Icon as={Plus} />}
|
||||
onClick={onApplyOpen}
|
||||
>
|
||||
申请开票
|
||||
</Button>
|
||||
</HStack>
|
||||
</Flex>
|
||||
</CardHeader>
|
||||
|
||||
<Tabs
|
||||
index={tabConfig.findIndex((t) => t.key === activeTab)}
|
||||
onChange={(index) => setActiveTab(tabConfig[index].key)}
|
||||
>
|
||||
<TabList>
|
||||
{tabConfig.map((tab) => (
|
||||
<Tab key={tab.key}>{tab.label}</Tab>
|
||||
))}
|
||||
</TabList>
|
||||
|
||||
<TabPanels>
|
||||
{tabConfig.map((tab) => (
|
||||
<TabPanel key={tab.key} px={0}>
|
||||
{loading ? (
|
||||
<Center py={10}>
|
||||
<VStack spacing={4}>
|
||||
<Spinner size="lg" />
|
||||
<Text color="gray.500">加载中...</Text>
|
||||
</VStack>
|
||||
</Center>
|
||||
) : invoices.length === 0 ? (
|
||||
<Center py={10}>
|
||||
<VStack spacing={4}>
|
||||
<Icon as={FileText} boxSize={12} color="gray.300" />
|
||||
<Text color="gray.500">暂无发票记录</Text>
|
||||
<Button
|
||||
size="sm"
|
||||
colorScheme="blue"
|
||||
leftIcon={<Icon as={Plus} />}
|
||||
onClick={onApplyOpen}
|
||||
>
|
||||
申请开票
|
||||
</Button>
|
||||
</VStack>
|
||||
</Center>
|
||||
) : (
|
||||
<SimpleGrid columns={{ base: 1, md: 2, lg: 3 }} spacing={4}>
|
||||
{invoices.map((invoice) => (
|
||||
<InvoiceCard
|
||||
key={invoice.id}
|
||||
invoice={invoice}
|
||||
onDownload={() => handleDownload(invoice)}
|
||||
onCancel={() => startCancel(invoice.id)}
|
||||
/>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
)}
|
||||
</TabPanel>
|
||||
))}
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</Card>
|
||||
|
||||
{/* 申请开票弹窗 */}
|
||||
<InvoiceApplyModal
|
||||
isOpen={isApplyOpen}
|
||||
onClose={onApplyClose}
|
||||
onSuccess={handleApplySuccess}
|
||||
/>
|
||||
|
||||
{/* 取消确认对话框 */}
|
||||
<AlertDialog
|
||||
isOpen={isCancelOpen}
|
||||
leastDestructiveRef={cancelDialogRef}
|
||||
onClose={onCancelClose}
|
||||
>
|
||||
<AlertDialogOverlay>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader fontSize="lg" fontWeight="bold">
|
||||
取消开票申请
|
||||
</AlertDialogHeader>
|
||||
|
||||
<AlertDialogBody>确定要取消这个开票申请吗?取消后可重新申请。</AlertDialogBody>
|
||||
|
||||
<AlertDialogFooter>
|
||||
<Button ref={cancelDialogRef} onClick={onCancelClose}>
|
||||
返回
|
||||
</Button>
|
||||
<Button colorScheme="red" onClick={handleCancel} ml={3}>
|
||||
确认取消
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialogOverlay>
|
||||
</AlertDialog>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
Box,
|
||||
Button,
|
||||
Flex,
|
||||
Image,
|
||||
Stack,
|
||||
Table,
|
||||
Tbody,
|
||||
@@ -77,10 +76,10 @@ class ComponentToPrint extends React.Component {
|
||||
fontSize="lg"
|
||||
mb="12px"
|
||||
>
|
||||
St. Independence Embankment, 050105 Bucharest, Romania
|
||||
北京市海淀区中关村大街1号
|
||||
</Text>
|
||||
<Text color="gray.400" fontWeight="normal" fontSize="md">
|
||||
tel: +4 (074) 1090873
|
||||
电话: 010-12345678
|
||||
</Text>
|
||||
</Flex>
|
||||
<Flex
|
||||
@@ -94,10 +93,10 @@ class ComponentToPrint extends React.Component {
|
||||
fontSize="lg"
|
||||
mb="12px"
|
||||
>
|
||||
Billed to: John Doe
|
||||
购买方: 张三
|
||||
</Text>
|
||||
<Text color="gray.400" fontWeight="normal" fontSize="md">
|
||||
4006 Locust View Drive San Francisco CA California
|
||||
上海市浦东新区陆家嘴金融中心
|
||||
</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
@@ -118,7 +117,7 @@ class ComponentToPrint extends React.Component {
|
||||
fontSize="md"
|
||||
mb="8px"
|
||||
>
|
||||
Invoice no
|
||||
发票号码
|
||||
</Text>
|
||||
<Text color={secondaryColor} fontWeight="bold" fontSize="lg">
|
||||
#0453119
|
||||
@@ -127,18 +126,18 @@ class ComponentToPrint extends React.Component {
|
||||
<Flex direction="column">
|
||||
<Stack direction="row" mb="8px" justify={{ md: "end" }}>
|
||||
<Text color="gray.400" fontWeight="normal" fontSize="md">
|
||||
Invoice date:{" "}
|
||||
开票日期:{" "}
|
||||
</Text>
|
||||
<Text color={secondaryColor} fontWeight="bold" fontSize="lg">
|
||||
06/03/2022
|
||||
2024/03/06
|
||||
</Text>
|
||||
</Stack>
|
||||
<Stack direction="row" justify={{ md: "end" }}>
|
||||
<Text color="gray.400" fontWeight="normal" fontSize="md">
|
||||
Due date:{" "}
|
||||
有效期至:{" "}
|
||||
</Text>
|
||||
<Text color={secondaryColor} fontWeight="bold" fontSize="lg">
|
||||
29/07/2022
|
||||
2024/07/29
|
||||
</Text>
|
||||
</Stack>
|
||||
</Flex>
|
||||
@@ -154,7 +153,7 @@ class ComponentToPrint extends React.Component {
|
||||
fontWeight="normal"
|
||||
ps="0px"
|
||||
>
|
||||
Item
|
||||
项目
|
||||
</Th>
|
||||
<Th
|
||||
borderColor={borderColor}
|
||||
@@ -162,7 +161,7 @@ class ComponentToPrint extends React.Component {
|
||||
fontSize="sm"
|
||||
fontWeight="normal"
|
||||
>
|
||||
Quantity
|
||||
数量
|
||||
</Th>
|
||||
<Th
|
||||
borderColor={borderColor}
|
||||
@@ -170,7 +169,7 @@ class ComponentToPrint extends React.Component {
|
||||
fontSize="sm"
|
||||
fontWeight="normal"
|
||||
>
|
||||
Rate
|
||||
单价
|
||||
</Th>
|
||||
<Th
|
||||
borderColor={borderColor}
|
||||
@@ -178,7 +177,7 @@ class ComponentToPrint extends React.Component {
|
||||
fontSize="sm"
|
||||
fontWeight="normal"
|
||||
>
|
||||
Amount
|
||||
金额
|
||||
</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
@@ -194,7 +193,7 @@ class ComponentToPrint extends React.Component {
|
||||
fontWeight="normal"
|
||||
fontSize="md"
|
||||
>
|
||||
Premium Support
|
||||
Pro 专业版会员服务
|
||||
</Text>
|
||||
</Td>
|
||||
<Td borderColor={borderColor}>
|
||||
@@ -216,7 +215,7 @@ class ComponentToPrint extends React.Component {
|
||||
fontWeight="normal"
|
||||
fontSize="md"
|
||||
>
|
||||
$ 9.00
|
||||
¥ 2699.00
|
||||
</Text>
|
||||
</Td>
|
||||
<Td borderColor={borderColor}>
|
||||
@@ -225,49 +224,7 @@ class ComponentToPrint extends React.Component {
|
||||
fontWeight="normal"
|
||||
fontSize="md"
|
||||
>
|
||||
$ 9.00
|
||||
</Text>
|
||||
</Td>
|
||||
</Tr>
|
||||
<Tr>
|
||||
<Td
|
||||
borderColor={borderColor}
|
||||
ps="0px"
|
||||
minW={{ sm: "300px" }}
|
||||
>
|
||||
<Text
|
||||
color={secondaryColor}
|
||||
fontWeight="normal"
|
||||
fontSize="md"
|
||||
>
|
||||
Chakra UI - Dashboard PRO
|
||||
</Text>
|
||||
</Td>
|
||||
<Td borderColor={borderColor}>
|
||||
<Text
|
||||
color={secondaryColor}
|
||||
fontWeight="normal"
|
||||
fontSize="md"
|
||||
>
|
||||
3
|
||||
</Text>
|
||||
</Td>
|
||||
<Td borderColor={borderColor}>
|
||||
<Text
|
||||
color={secondaryColor}
|
||||
fontWeight="normal"
|
||||
fontSize="md"
|
||||
>
|
||||
$ 99.00
|
||||
</Text>
|
||||
</Td>
|
||||
<Td borderColor={borderColor}>
|
||||
<Text
|
||||
color={secondaryColor}
|
||||
fontWeight="normal"
|
||||
fontSize="md"
|
||||
>
|
||||
$ 297.00
|
||||
¥ 2699.00
|
||||
</Text>
|
||||
</Td>
|
||||
</Tr>
|
||||
@@ -278,40 +235,12 @@ class ComponentToPrint extends React.Component {
|
||||
minW={{ sm: "300px" }}
|
||||
border="none"
|
||||
>
|
||||
<Text
|
||||
color={secondaryColor}
|
||||
fontWeight="normal"
|
||||
fontSize="md"
|
||||
>
|
||||
Parts for Service
|
||||
</Text>
|
||||
</Td>
|
||||
<Td borderColor={borderColor} border="none">
|
||||
<Text
|
||||
color={secondaryColor}
|
||||
fontWeight="normal"
|
||||
fontSize="md"
|
||||
>
|
||||
1
|
||||
</Text>
|
||||
</Td>
|
||||
<Td borderColor={borderColor} border="none">
|
||||
<Text
|
||||
color={secondaryColor}
|
||||
fontWeight="normal"
|
||||
fontSize="md"
|
||||
>
|
||||
$ 89.00
|
||||
</Text>
|
||||
</Td>
|
||||
<Td borderColor={borderColor} border="none">
|
||||
<Text
|
||||
color={secondaryColor}
|
||||
fontWeight="normal"
|
||||
fontSize="md"
|
||||
>
|
||||
$ 89.00
|
||||
</Text>
|
||||
</Td>
|
||||
</Tr>
|
||||
<Tr>
|
||||
@@ -323,12 +252,12 @@ class ComponentToPrint extends React.Component {
|
||||
<Td borderColor={borderColor}></Td>
|
||||
<Td borderColor={borderColor}>
|
||||
<Text color={textColor} fontWeight="bold" fontSize="xl">
|
||||
Total
|
||||
合计
|
||||
</Text>
|
||||
</Td>
|
||||
<Td borderColor={borderColor}>
|
||||
<Text color={textColor} fontWeight="bold" fontSize="xl">
|
||||
$ 9.00
|
||||
¥ 2699.00
|
||||
</Text>
|
||||
</Td>
|
||||
</Tr>
|
||||
@@ -341,7 +270,7 @@ class ComponentToPrint extends React.Component {
|
||||
>
|
||||
<Flex direction="column" maxW="270px">
|
||||
<Text color={secondaryColor} fontWeight="bold" fontSize="xl">
|
||||
Thank You!
|
||||
感谢您的支持!
|
||||
</Text>
|
||||
<Text
|
||||
color="gray.400"
|
||||
@@ -350,13 +279,12 @@ class ComponentToPrint extends React.Component {
|
||||
mt="6px"
|
||||
mb="30px"
|
||||
>
|
||||
If you encounter any issues related to the invoice you can
|
||||
contact us at:
|
||||
如果您对发票有任何问题,请联系我们:
|
||||
</Text>
|
||||
<Text color="gray.400" fontWeight="normal" fontSize="md">
|
||||
email:{" "}
|
||||
邮箱:{" "}
|
||||
<Text as="span" color={secondaryColor} fontWeight="bold">
|
||||
support@creative-tim.com
|
||||
support@valuefrontier.cn
|
||||
</Text>
|
||||
</Text>
|
||||
</Flex>
|
||||
@@ -368,7 +296,7 @@ class ComponentToPrint extends React.Component {
|
||||
alignSelf={{ sm: "flex-start", md: "flex-end" }}
|
||||
mt={{ sm: "16px", md: "0px" }}
|
||||
>
|
||||
PRINT
|
||||
打印
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
@@ -391,7 +319,12 @@ function Invoice() {
|
||||
});
|
||||
|
||||
return (
|
||||
<Flex direction="column" pt={{ sm: "100px", lg: "50px" }}>
|
||||
<Flex
|
||||
direction="column"
|
||||
minH="100vh"
|
||||
justify="center"
|
||||
align="center"
|
||||
>
|
||||
<ComponentToPrint
|
||||
ref={componentRef}
|
||||
handlePrint={handlePrint}
|
||||
@@ -31,8 +31,9 @@ import {
|
||||
PinInput,
|
||||
PinInputField
|
||||
} from '@chakra-ui/react';
|
||||
import { Link2, Trash2, Pencil, Smartphone, Mail } from 'lucide-react';
|
||||
import { Link2, Trash2, Pencil, Smartphone, Mail, FileText, CreditCard } from 'lucide-react';
|
||||
import { WechatOutlined } from '@ant-design/icons';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import { getApiBase } from '../../utils/apiConfig';
|
||||
import { logger } from '../../utils/logger';
|
||||
@@ -41,6 +42,7 @@ import { useProfileEvents } from '../../hooks/useProfileEvents';
|
||||
export default function SettingsPage() {
|
||||
const { user, updateUser } = useAuth();
|
||||
const toast = useToast();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// 深色模式固定颜色(Settings 页面始终使用深色主题)
|
||||
const headingColor = 'white';
|
||||
@@ -222,6 +224,7 @@ export default function SettingsPage() {
|
||||
<Tabs variant="enclosed" colorScheme="blue">
|
||||
<TabList>
|
||||
<Tab color={textColor} _selected={{ color: 'blue.500', borderColor: 'blue.500' }}>账户绑定</Tab>
|
||||
<Tab color={textColor} _selected={{ color: 'blue.500', borderColor: 'blue.500' }}>账单与发票</Tab>
|
||||
</TabList>
|
||||
|
||||
<TabPanels>
|
||||
@@ -403,6 +406,71 @@ export default function SettingsPage() {
|
||||
|
||||
</VStack>
|
||||
</TabPanel>
|
||||
|
||||
{/* 账单与发票 */}
|
||||
<TabPanel>
|
||||
<VStack spacing={6} align="stretch">
|
||||
{/* 订阅管理 */}
|
||||
<Card bg={cardBg} borderColor={borderColor}>
|
||||
<CardHeader>
|
||||
<Heading size="md" color={headingColor}>订阅管理</Heading>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<HStack justify="space-between">
|
||||
<VStack align="start" spacing={1}>
|
||||
<HStack>
|
||||
<CreditCard size={20} />
|
||||
<Text fontWeight="medium" color={textColor}>
|
||||
{user?.subscription_type === 'max' ? 'Max 会员' :
|
||||
user?.subscription_type === 'pro' ? 'Pro 会员' : '免费版'}
|
||||
</Text>
|
||||
{user?.subscription_type && user?.subscription_type !== 'free' && (
|
||||
<Badge colorScheme="purple" size="sm">有效</Badge>
|
||||
)}
|
||||
</HStack>
|
||||
<Text fontSize="sm" color={subTextColor}>
|
||||
管理您的会员订阅和续费
|
||||
</Text>
|
||||
</VStack>
|
||||
<Button
|
||||
colorScheme="purple"
|
||||
onClick={() => navigate('/home/pages/account/subscription')}
|
||||
>
|
||||
管理订阅
|
||||
</Button>
|
||||
</HStack>
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
{/* 发票管理 */}
|
||||
<Card bg={cardBg} borderColor={borderColor}>
|
||||
<CardHeader>
|
||||
<Heading size="md" color={headingColor}>发票管理</Heading>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<HStack justify="space-between">
|
||||
<VStack align="start" spacing={1}>
|
||||
<HStack>
|
||||
<FileText size={20} />
|
||||
<Text fontWeight="medium" color={textColor}>
|
||||
电子发票申请与下载
|
||||
</Text>
|
||||
</HStack>
|
||||
<Text fontSize="sm" color={subTextColor}>
|
||||
已支付的订单可申请开具电子发票
|
||||
</Text>
|
||||
</VStack>
|
||||
<Button
|
||||
colorScheme="teal"
|
||||
onClick={() => navigate('/home/pages/account/invoice')}
|
||||
>
|
||||
管理发票
|
||||
</Button>
|
||||
</HStack>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</VStack>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</VStack>
|
||||
|
||||
Reference in New Issue
Block a user