feat: 新增售后逆向流程和物流策略闭环页面

refactor: 移除不再使用的提示文件和文档副本

docs: 更新开发进度文档,补充前端任务完成情况
This commit is contained in:
2026-03-18 21:02:23 +08:00
parent 6d0d2b6157
commit 96373deb2f
17 changed files with 29574 additions and 901 deletions

View File

@@ -0,0 +1,508 @@
import React, { useState, useEffect } from 'react';
import {
Card,
Table,
Button,
Modal,
Form,
Input,
Select,
TextArea,
message,
Spin,
Row,
Col,
Typography,
Divider,
Space,
Tag,
Descriptions,
Badge,
} from 'antd';
import {
MessageOutlined,
PhoneOutlined,
MailOutlined,
UserOutlined,
ClockCircleOutlined,
CheckCircleOutlined,
CloseCircleOutlined,
QuestionCircleOutlined,
MessageSquareOutlined,
} from '@ant-design/icons';
const { Option } = Select;
const { Title, Text, Paragraph } = Typography;
// 模拟客户服务数据
const mockCustomerServices = [
{
id: 'CS-20260318-001',
customerName: '张三',
customerId: 'CUST-001',
contactInfo: '13800138001',
serviceType: 'PRODUCT_ISSUE',
subject: '商品质量问题',
description: '购买的iPhone 15 Pro屏幕出现闪烁问题',
status: 'PENDING',
priority: 'HIGH',
createdAt: '2026-03-18T10:00:00Z',
updatedAt: '2026-03-18T10:00:00Z',
assignedTo: '客服专员A',
messages: [
{
id: 'MSG-001',
sender: 'CUSTOMER',
content: '购买的iPhone 15 Pro屏幕出现闪烁问题',
timestamp: '2026-03-18T10:00:00Z',
},
],
},
{
id: 'CS-20260318-002',
customerName: '李四',
customerId: 'CUST-002',
contactInfo: '13900139002',
serviceType: 'ORDER_ISSUE',
subject: '订单发货问题',
description: '订单已经付款但一直未发货',
status: 'PROCESSING',
priority: 'MEDIUM',
createdAt: '2026-03-17T15:30:00Z',
updatedAt: '2026-03-18T09:00:00Z',
assignedTo: '客服专员B',
messages: [
{
id: 'MSG-002',
sender: 'CUSTOMER',
content: '订单已经付款但一直未发货',
timestamp: '2026-03-17T15:30:00Z',
},
{
id: 'MSG-003',
sender: 'SUPPORT',
content: '您好,我们正在核实您的订单情况,稍后会给您回复',
timestamp: '2026-03-18T09:00:00Z',
},
],
},
{
id: 'CS-20260318-003',
customerName: '王五',
customerId: 'CUST-003',
contactInfo: '13700137003',
serviceType: 'REFUND_ISSUE',
subject: '退款未到账',
description: '退款申请已经通过但钱还没到账',
status: 'COMPLETED',
priority: 'HIGH',
createdAt: '2026-03-16T14:00:00Z',
updatedAt: '2026-03-17T10:00:00Z',
assignedTo: '客服专员A',
messages: [
{
id: 'MSG-004',
sender: 'CUSTOMER',
content: '退款申请已经通过但钱还没到账',
timestamp: '2026-03-16T14:00:00Z',
},
{
id: 'MSG-005',
sender: 'SUPPORT',
content: '您好退款通常需要1-3个工作日到账请耐心等待',
timestamp: '2026-03-16T15:00:00Z',
},
{
id: 'MSG-006',
sender: 'CUSTOMER',
content: '已经收到退款了,谢谢',
timestamp: '2026-03-17T10:00:00Z',
},
],
},
];
// 服务类型选项
const serviceTypes = [
{ value: 'PRODUCT_ISSUE', label: '商品问题' },
{ value: 'ORDER_ISSUE', label: '订单问题' },
{ value: 'REFUND_ISSUE', label: '退款问题' },
{ value: 'SHIPPING_ISSUE', label: '物流问题' },
{ value: 'ACCOUNT_ISSUE', label: '账户问题' },
{ value: 'OTHER', label: '其他问题' },
];
// 优先级选项
const priorities = [
{ value: 'LOW', label: '低', color: 'blue' },
{ value: 'MEDIUM', label: '中', color: 'orange' },
{ value: 'HIGH', label: '高', color: 'red' },
];
// 状态选项
const statuses = [
{ value: 'PENDING', label: '待处理' },
{ value: 'PROCESSING', label: '处理中' },
{ value: 'COMPLETED', label: '已完成' },
{ value: 'CLOSED', label: '已关闭' },
];
const CustomerService: React.FC = () => {
const [services, setServices] = useState(mockCustomerServices);
const [selectedService, setSelectedService] = useState<any>(null);
const [isModalVisible, setIsModalVisible] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [form] = Form.useForm();
const [messageForm] = Form.useForm();
const [newMessage, setNewMessage] = useState('');
// 处理服务详情查看
const handleServiceView = (service: any) => {
setSelectedService(service);
setIsModalVisible(true);
};
// 处理消息发送
const handleMessageSend = async () => {
if (!newMessage.trim()) return;
setIsLoading(true);
try {
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 500));
const updatedServices = services.map(service => {
if (service.id === selectedService.id) {
const updatedService = {
...service,
messages: [
...service.messages,
{
id: `MSG-${Date.now()}`,
sender: 'SUPPORT',
content: newMessage,
timestamp: new Date().toISOString(),
},
],
updatedAt: new Date().toISOString(),
};
return updatedService;
}
return service;
});
setServices(updatedServices);
setSelectedService(updatedServices.find(s => s.id === selectedService.id));
setNewMessage('');
message.success('消息发送成功');
} catch (error) {
message.error('消息发送失败');
} finally {
setIsLoading(false);
}
};
// 处理服务状态更新
const handleStatusUpdate = async (values: any) => {
setIsLoading(true);
try {
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 1000));
const updatedServices = services.map(service => {
if (service.id === selectedService.id) {
return {
...service,
status: values.status,
priority: values.priority,
updatedAt: new Date().toISOString(),
};
}
return service;
});
setServices(updatedServices);
setSelectedService(updatedServices.find(s => s.id === selectedService.id));
message.success('服务状态更新成功');
} catch (error) {
message.error('服务状态更新失败');
} finally {
setIsLoading(false);
}
};
// 获取服务类型标签
const getServiceTypeTag = (type: string) => {
const serviceType = serviceTypes.find(t => t.value === type);
return serviceType ? <Tag>{serviceType.label}</Tag> : <Tag>{type}</Tag>;
};
// 获取优先级标签
const getPriorityTag = (priority: string) => {
const p = priorities.find(p => p.value === priority);
return p ? <Tag color={p.color}>{p.label}</Tag> : <Tag>{priority}</Tag>;
};
// 获取状态标签
const getStatusTag = (status: string) => {
switch (status) {
case 'PENDING':
return <Tag color="processing"><ClockCircleOutlined /> </Tag>;
case 'PROCESSING':
return <Tag color="processing"><ClockCircleOutlined /> </Tag>;
case 'COMPLETED':
return <Tag color="success"><CheckCircleOutlined /> </Tag>;
case 'CLOSED':
return <Tag color="default"><CloseCircleOutlined /> </Tag>;
default:
return <Tag>{status}</Tag>;
}
};
// 服务表格列
const serviceColumns = [
{
title: '服务ID',
dataIndex: 'id',
key: 'id',
},
{
title: '客户信息',
key: 'customer',
render: (_, record: any) => (
<div>
<div>{record.customerName}</div>
<div style={{ fontSize: '12px', color: '#999' }}>{record.contactInfo}</div>
</div>
),
},
{
title: '服务类型',
dataIndex: 'serviceType',
key: 'serviceType',
render: (type: string) => getServiceTypeTag(type),
},
{
title: '主题',
dataIndex: 'subject',
key: 'subject',
},
{
title: '优先级',
dataIndex: 'priority',
key: 'priority',
render: (priority: string) => getPriorityTag(priority),
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
render: (status: string) => getStatusTag(status),
},
{
title: '创建时间',
dataIndex: 'createdAt',
key: 'createdAt',
render: (date: string) => new Date(date).toLocaleString(),
},
{
title: '操作',
key: 'action',
render: (_: any, record: any) => (
<Button type="primary" onClick={() => handleServiceView(record)}>
</Button>
),
},
];
return (
<div style={{ padding: '24px' }}>
<Card
title={
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Title level={4} style={{ margin: 0 }}></Title>
<Text type="secondary"></Text>
</div>
}
extra={
<Button type="primary">
</Button>
}
>
<Divider orientation="left"></Divider>
<Table
columns={serviceColumns}
dataSource={services}
rowKey="id"
pagination={{ pageSize: 10 }}
/>
</Card>
{/* 客户服务详情模态框 */}
{selectedService && (
<Modal
title="客户服务详情"
open={isModalVisible}
onCancel={() => setIsModalVisible(false)}
footer={null}
width={1000}
>
<Row gutter={16}>
<Col span={16}>
<Descriptions bordered column={1}>
<Descriptions.Item label="服务ID">{selectedService.id}</Descriptions.Item>
<Descriptions.Item label="客户姓名">{selectedService.customerName}</Descriptions.Item>
<Descriptions.Item label="客户ID">{selectedService.customerId}</Descriptions.Item>
<Descriptions.Item label="联系方式">{selectedService.contactInfo}</Descriptions.Item>
<Descriptions.Item label="服务类型">{getServiceTypeTag(selectedService.serviceType)}</Descriptions.Item>
<Descriptions.Item label="主题">{selectedService.subject}</Descriptions.Item>
<Descriptions.Item label="描述">{selectedService.description}</Descriptions.Item>
<Descriptions.Item label="优先级">{getPriorityTag(selectedService.priority)}</Descriptions.Item>
<Descriptions.Item label="状态">{getStatusTag(selectedService.status)}</Descriptions.Item>
<Descriptions.Item label="创建时间">
{new Date(selectedService.createdAt).toLocaleString()}
</Descriptions.Item>
<Descriptions.Item label="更新时间">
{new Date(selectedService.updatedAt).toLocaleString()}
</Descriptions.Item>
<Descriptions.Item label="分配给">{selectedService.assignedTo}</Descriptions.Item>
</Descriptions>
</Col>
<Col span={8}>
<Card title="状态管理">
<Form
form={form}
layout="vertical"
onFinish={handleStatusUpdate}
initialValues={{
status: selectedService.status,
priority: selectedService.priority,
}}
>
<Form.Item
name="status"
label="服务状态"
rules={[{ required: true, message: '请选择服务状态' }]}
>
<Select placeholder="请选择服务状态">
{statuses.map(status => (
<Option key={status.value} value={status.value}>
{status.label}
</Option>
))}
</Select>
</Form.Item>
<Form.Item
name="priority"
label="优先级"
rules={[{ required: true, message: '请选择优先级' }]}
>
<Select placeholder="请选择优先级">
{priorities.map(priority => (
<Option key={priority.value} value={priority.value}>
{priority.label}
</Option>
))}
</Select>
</Form.Item>
<Form.Item
name="remark"
label="处理备注"
>
<TextArea rows={3} placeholder="请输入处理备注" />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" loading={isLoading}>
</Button>
</Form.Item>
</Form>
</Card>
</Col>
</Row>
<Divider orientation="left"></Divider>
<div style={{
height: 400,
border: '1px solid #f0f0f0',
borderRadius: 4,
padding: 16,
overflow: 'auto',
marginBottom: 16,
}}>
{selectedService.messages.map((message: any, index: number) => (
<div
key={message.id}
style={{
marginBottom: 16,
display: 'flex',
flexDirection: message.sender === 'CUSTOMER' ? 'row' : 'row-reverse',
}}
>
<div
style={{
width: '32px',
height: '32px',
borderRadius: '50%',
backgroundColor: message.sender === 'CUSTOMER' ? '#1890ff' : '#52c41a',
color: 'white',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
margin: '0 8px',
}}
>
{message.sender === 'CUSTOMER' ? (
<UserOutlined />
) : (
<MessageSquareOutlined />
)}
</div>
<div
style={{
maxWidth: '70%',
padding: 12,
borderRadius: 8,
backgroundColor: message.sender === 'CUSTOMER' ? '#f0f0f0' : '#e6f7ff',
}}
>
<div style={{ marginBottom: 4, fontSize: '12px', color: '#999' }}>
{message.sender === 'CUSTOMER' ? '客户' : '客服'}
{' '}|
{' '}{new Date(message.timestamp).toLocaleString()}
</div>
<div>{message.content}</div>
</div>
</div>
))}
</div>
<div style={{ display: 'flex' }}>
<TextArea
value={newMessage}
onChange={(e) => setNewMessage(e.target.value)}
placeholder="输入回复消息"
rows={3}
style={{ flex: 1, marginRight: 8 }}
/>
<Button
type="primary"
onClick={handleMessageSend}
loading={isLoading}
disabled={!newMessage.trim()}
>
</Button>
</div>
</Modal>
)}
</div>
);
};
export default CustomerService;

View File

@@ -0,0 +1,390 @@
import React, { useState, useEffect } from 'react';
import {
Card,
Table,
Button,
Modal,
Form,
Input,
Select,
DatePicker,
message,
Spin,
Row,
Col,
Typography,
Divider,
Space,
Tag,
Descriptions,
Timeline,
} from 'antd';
import {
CheckCircleOutlined,
ClockCircleOutlined,
CloseCircleOutlined,
DollarOutlined,
BankOutlined,
AlipayOutlined,
WechatOutlined,
} from '@ant-design/icons';
const { Option } = Select;
const { Title, Text, Paragraph } = Typography;
const { RangePicker } = DatePicker;
// 模拟退款数据
const mockRefunds = [
{
id: 'REF-20260318-001',
orderId: 'ORD-20260318-001',
returnId: 'RET-123456',
customerName: '张三',
productName: 'Apple iPhone 15 Pro',
refundAmount: 9999,
paymentMethod: 'ALIPAY',
status: 'PENDING',
createdAt: '2026-03-18T10:00:00Z',
updatedAt: '2026-03-18T10:00:00Z',
timeline: [
{
time: '2026-03-18 10:00',
status: 'PENDING',
description: '退款申请提交成功',
},
],
},
{
id: 'REF-20260318-002',
orderId: 'ORD-20260318-002',
returnId: 'RET-123457',
customerName: '李四',
productName: 'MacBook Pro 14',
refundAmount: 14999,
paymentMethod: 'WECHAT',
status: 'PROCESSING',
createdAt: '2026-03-17T15:30:00Z',
updatedAt: '2026-03-18T09:00:00Z',
timeline: [
{
time: '2026-03-17 15:30',
status: 'PENDING',
description: '退款申请提交成功',
},
{
time: '2026-03-18 09:00',
status: 'PROCESSING',
description: '退款处理中',
},
],
},
{
id: 'REF-20260318-003',
orderId: 'ORD-20260318-003',
returnId: 'RET-123458',
customerName: '王五',
productName: 'AirPods Pro 2',
refundAmount: 1899,
paymentMethod: 'BANK',
status: 'COMPLETED',
createdAt: '2026-03-16T14:00:00Z',
updatedAt: '2026-03-17T10:00:00Z',
timeline: [
{
time: '2026-03-16 14:00',
status: 'PENDING',
description: '退款申请提交成功',
},
{
time: '2026-03-16 16:00',
status: 'PROCESSING',
description: '退款处理中',
},
{
time: '2026-03-17 10:00',
status: 'COMPLETED',
description: '退款成功',
},
],
},
];
// 退款状态选项
const refundStatuses = [
{ value: 'PENDING', label: '待处理' },
{ value: 'PROCESSING', label: '处理中' },
{ value: 'COMPLETED', label: '已完成' },
{ value: 'REJECTED', label: '已拒绝' },
];
// 支付方式选项
const paymentMethods = [
{ value: 'ALIPAY', label: '支付宝', icon: <AlipayOutlined /> },
{ value: 'WECHAT', label: '微信支付', icon: <WechatOutlined /> },
{ value: 'BANK', label: '银行卡', icon: <BankOutlined /> },
];
const RefundProcess: React.FC = () => {
const [refunds, setRefunds] = useState(mockRefunds);
const [selectedRefund, setSelectedRefund] = useState<any>(null);
const [isModalVisible, setIsModalVisible] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [form] = Form.useForm();
// 处理退款详情查看
const handleRefundView = (refund: any) => {
setSelectedRefund(refund);
setIsModalVisible(true);
};
// 处理退款状态更新
const handleStatusUpdate = async (values: any) => {
setIsLoading(true);
try {
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 1000));
const updatedRefunds = refunds.map(refund => {
if (refund.id === selectedRefund.id) {
const updatedRefund = {
...refund,
status: values.status,
updatedAt: new Date().toISOString(),
timeline: [
...refund.timeline,
{
time: new Date().toLocaleString(),
status: values.status,
description: getStatusDescription(values.status),
},
],
};
return updatedRefund;
}
return refund;
});
setRefunds(updatedRefunds);
setSelectedRefund(updatedRefunds.find(r => r.id === selectedRefund.id));
message.success('退款状态更新成功');
} catch (error) {
message.error('退款状态更新失败');
} finally {
setIsLoading(false);
}
};
// 获取状态描述
const getStatusDescription = (status: string) => {
switch (status) {
case 'PENDING':
return '退款申请提交成功';
case 'PROCESSING':
return '退款处理中';
case 'COMPLETED':
return '退款成功';
case 'REJECTED':
return '退款被拒绝';
default:
return status;
}
};
// 退款状态标签
const getStatusTag = (status: string) => {
switch (status) {
case 'PENDING':
return <Tag color="processing"><ClockCircleOutlined /> </Tag>;
case 'PROCESSING':
return <Tag color="processing"><ClockCircleOutlined /> </Tag>;
case 'COMPLETED':
return <Tag color="success"><CheckCircleOutlined /> </Tag>;
case 'REJECTED':
return <Tag color="error"><CloseCircleOutlined /> </Tag>;
default:
return <Tag>{status}</Tag>;
}
};
// 支付方式标签
const getPaymentTag = (paymentMethod: string) => {
const method = paymentMethods.find(m => m.value === paymentMethod);
return method ? (
<Tag icon={method.icon}>{method.label}</Tag>
) : (
<Tag>{paymentMethod}</Tag>
);
};
// 退款表格列
const refundColumns = [
{
title: '退款ID',
dataIndex: 'id',
key: 'id',
},
{
title: '订单ID',
dataIndex: 'orderId',
key: 'orderId',
},
{
title: '客户姓名',
dataIndex: 'customerName',
key: 'customerName',
},
{
title: '商品名称',
dataIndex: 'productName',
key: 'productName',
},
{
title: '退款金额',
dataIndex: 'refundAmount',
key: 'refundAmount',
render: (amount: number) => `¥${amount}`,
},
{
title: '支付方式',
dataIndex: 'paymentMethod',
key: 'paymentMethod',
render: (method: string) => getPaymentTag(method),
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
render: (status: string) => getStatusTag(status),
},
{
title: '申请时间',
dataIndex: 'createdAt',
key: 'createdAt',
render: (date: string) => new Date(date).toLocaleString(),
},
{
title: '操作',
key: 'action',
render: (_: any, record: any) => (
<Button type="primary" onClick={() => handleRefundView(record)}>
</Button>
),
},
];
return (
<div style={{ padding: '24px' }}>
<Card
title={
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Title level={4} style={{ margin: 0 }}>退</Title>
<Text type="secondary"></Text>
</div>
}
extra={
<Button type="primary" ghost>
退
</Button>
}
>
<Divider orientation="left">退</Divider>
<Table
columns={refundColumns}
dataSource={refunds}
rowKey="id"
pagination={{ pageSize: 10 }}
/>
</Card>
{/* 退款详情模态框 */}
{selectedRefund && (
<Modal
title="退款详情"
open={isModalVisible}
onCancel={() => setIsModalVisible(false)}
footer={null}
width={900}
>
<Descriptions bordered column={2}>
<Descriptions.Item label="退款ID">{selectedRefund.id}</Descriptions.Item>
<Descriptions.Item label="订单ID">{selectedRefund.orderId}</Descriptions.Item>
<Descriptions.Item label="退货申请ID">{selectedRefund.returnId}</Descriptions.Item>
<Descriptions.Item label="客户姓名">{selectedRefund.customerName}</Descriptions.Item>
<Descriptions.Item label="商品名称">{selectedRefund.productName}</Descriptions.Item>
<Descriptions.Item label="退款金额">¥{selectedRefund.refundAmount}</Descriptions.Item>
<Descriptions.Item label="支付方式">{getPaymentTag(selectedRefund.paymentMethod)}</Descriptions.Item>
<Descriptions.Item label="当前状态">{getStatusTag(selectedRefund.status)}</Descriptions.Item>
<Descriptions.Item label="申请时间" span={2}>
{new Date(selectedRefund.createdAt).toLocaleString()}
</Descriptions.Item>
<Descriptions.Item label="更新时间" span={2}>
{new Date(selectedRefund.updatedAt).toLocaleString()}
</Descriptions.Item>
</Descriptions>
<Divider orientation="left"></Divider>
<Timeline>
{selectedRefund.timeline.map((item: any, index: number) => (
<Timeline.Item
key={index}
color={
item.status === 'COMPLETED' ? 'green' :
item.status === 'REJECTED' ? 'red' :
'blue'
}
>
<div>
<Text strong>{item.time}</Text>
<p>{item.description}</p>
</div>
</Timeline.Item>
))}
</Timeline>
<Divider orientation="left"></Divider>
<Form
form={form}
layout="vertical"
onFinish={handleStatusUpdate}
initialValues={{ status: selectedRefund.status }}
>
<Form.Item
name="status"
label="退款状态"
rules={[{ required: true, message: '请选择退款状态' }]}
>
<Select placeholder="请选择退款状态">
{refundStatuses.map(status => (
<Option key={status.value} value={status.value}>
{status.label}
</Option>
))}
</Select>
</Form.Item>
<Form.Item
name="remark"
label="处理备注"
>
<Input.TextArea rows={3} placeholder="请输入处理备注" />
</Form.Item>
<Form.Item>
<Space style={{ width: '100%', justifyContent: 'flex-end' }}>
<Button onClick={() => setIsModalVisible(false)}>
</Button>
<Button type="primary" htmlType="submit" loading={isLoading}>
</Button>
</Space>
</Form.Item>
</Form>
</Modal>
)}
</div>
);
};
export default RefundProcess;

View File

@@ -0,0 +1,399 @@
import React, { useState, useEffect } from 'react';
import {
Card,
Form,
Input,
Select,
DatePicker,
Upload,
Button,
Table,
Modal,
message,
Spin,
Row,
Col,
Typography,
Divider,
Space,
Tag,
} from 'antd';
import {
FileOutlined,
ExclamationCircleOutlined,
CheckCircleOutlined,
ClockCircleOutlined,
CloseCircleOutlined,
} from '@ant-design/icons';
const { Option } = Select;
const { Title, Text, Paragraph } = Typography;
const { RangePicker } = DatePicker;
// 模拟订单数据
const mockOrders = [
{
id: 'ORD-20260318-001',
customerName: '张三',
productName: 'Apple iPhone 15 Pro',
quantity: 1,
price: 9999,
orderDate: '2026-03-15',
status: 'DELIVERED',
},
{
id: 'ORD-20260318-002',
customerName: '李四',
productName: 'MacBook Pro 14',
quantity: 1,
price: 14999,
orderDate: '2026-03-10',
status: 'DELIVERED',
},
{
id: 'ORD-20260318-003',
customerName: '王五',
productName: 'AirPods Pro 2',
quantity: 2,
price: 1899,
orderDate: '2026-03-05',
status: 'DELIVERED',
},
];
// 退货原因选项
const returnReasons = [
{ value: 'QUALITY_ISSUE', label: '商品质量问题' },
{ value: 'WRONG_ITEM', label: '发错商品' },
{ value: 'DAMAGED', label: '商品损坏' },
{ value: 'NOT_AS_DESCRIBED', label: '与描述不符' },
{ value: 'CHANGE_MIND', label: '买家改变主意' },
{ value: 'OTHER', label: '其他原因' },
];
const ReturnApply: React.FC = () => {
const [form] = Form.useForm();
const [orders, setOrders] = useState(mockOrders);
const [selectedOrder, setSelectedOrder] = useState<any>(null);
const [isModalVisible, setIsModalVisible] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [returnApplications, setReturnApplications] = useState<any[]>([]);
// 处理订单选择
const handleOrderSelect = (order: any) => {
setSelectedOrder(order);
form.setFieldsValue({
orderId: order.id,
productName: order.productName,
quantity: order.quantity,
purchaseAmount: order.price,
});
setIsModalVisible(true);
};
// 处理退货申请提交
const handleSubmit = async (values: any) => {
setIsLoading(true);
try {
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 1000));
const newApplication = {
id: `RET-${Date.now()}`,
orderId: values.orderId,
productName: values.productName,
quantity: values.quantity,
returnReason: values.returnReason,
description: values.description,
refundAmount: values.refundAmount,
status: 'PENDING',
createdAt: new Date().toISOString(),
};
setReturnApplications([...returnApplications, newApplication]);
message.success('退货申请提交成功');
setIsModalVisible(false);
form.resetFields();
} catch (error) {
message.error('退货申请提交失败');
} finally {
setIsLoading(false);
}
};
// 订单状态标签
const getStatusTag = (status: string) => {
switch (status) {
case 'PENDING':
return <Tag color="processing"><ClockCircleOutlined /> </Tag>;
case 'APPROVED':
return <Tag color="success"><CheckCircleOutlined /> </Tag>;
case 'REJECTED':
return <Tag color="error"><CloseCircleOutlined /> </Tag>;
case 'DELIVERED':
return <Tag color="success"></Tag>;
default:
return <Tag>{status}</Tag>;
}
};
// 订单表格列
const orderColumns = [
{
title: '订单ID',
dataIndex: 'id',
key: 'id',
},
{
title: '客户姓名',
dataIndex: 'customerName',
key: 'customerName',
},
{
title: '商品名称',
dataIndex: 'productName',
key: 'productName',
},
{
title: '数量',
dataIndex: 'quantity',
key: 'quantity',
},
{
title: '金额',
dataIndex: 'price',
key: 'price',
render: (price: number) => `¥${price}`,
},
{
title: '下单日期',
dataIndex: 'orderDate',
key: 'orderDate',
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
render: (status: string) => getStatusTag(status),
},
{
title: '操作',
key: 'action',
render: (_: any, record: any) => (
<Button type="primary" onClick={() => handleOrderSelect(record)}>
退
</Button>
),
},
];
// 退货申请表格列
const applicationColumns = [
{
title: '申请ID',
dataIndex: 'id',
key: 'id',
},
{
title: '订单ID',
dataIndex: 'orderId',
key: 'orderId',
},
{
title: '商品名称',
dataIndex: 'productName',
key: 'productName',
},
{
title: '数量',
dataIndex: 'quantity',
key: 'quantity',
},
{
title: '退货原因',
dataIndex: 'returnReason',
key: 'returnReason',
render: (reason: string) => {
const reasonObj = returnReasons.find(r => r.value === reason);
return reasonObj ? reasonObj.label : reason;
},
},
{
title: '退款金额',
dataIndex: 'refundAmount',
key: 'refundAmount',
render: (amount: number) => `¥${amount}`,
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
render: (status: string) => getStatusTag(status),
},
{
title: '申请时间',
dataIndex: 'createdAt',
key: 'createdAt',
render: (date: string) => new Date(date).toLocaleString(),
},
];
return (
<div style={{ padding: '24px' }}>
<Card
title={
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Title level={4} style={{ margin: 0 }}>退</Title>
<Text type="secondary"></Text>
</div>
}
extra={
<Button type="primary" ghost>
退
</Button>
}
>
<Divider orientation="left">退</Divider>
<Table
columns={orderColumns}
dataSource={orders}
rowKey="id"
pagination={{ pageSize: 10 }}
style={{ marginBottom: '24px' }}
/>
<Divider orientation="left">退</Divider>
<Table
columns={applicationColumns}
dataSource={returnApplications}
rowKey="id"
pagination={{ pageSize: 10 }}
/>
</Card>
{/* 退货申请模态框 */}
<Modal
title="提交退货申请"
open={isModalVisible}
onCancel={() => setIsModalVisible(false)}
footer={null}
width={800}
>
<Form
form={form}
layout="vertical"
onFinish={handleSubmit}
>
<Row gutter={16}>
<Col span={12}>
<Form.Item
name="orderId"
label="订单ID"
rules={[{ required: true, message: '请选择订单' }]}
>
<Input disabled />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="productName"
label="商品名称"
rules={[{ required: true, message: '商品名称不能为空' }]}
>
<Input disabled />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item
name="quantity"
label="数量"
rules={[{ required: true, message: '数量不能为空' }]}
>
<Input type="number" disabled />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="purchaseAmount"
label="购买金额"
rules={[{ required: true, message: '购买金额不能为空' }]}
>
<Input prefix="¥" disabled />
</Form.Item>
</Col>
</Row>
<Form.Item
name="returnReason"
label="退货原因"
rules={[{ required: true, message: '请选择退货原因' }]}
>
<Select placeholder="请选择退货原因">
{returnReasons.map(reason => (
<Option key={reason.value} value={reason.value}>
{reason.label}
</Option>
))}
</Select>
</Form.Item>
<Form.Item
name="description"
label="详细描述"
rules={[{ required: true, message: '请详细描述退货原因' }]}
>
<Input.TextArea rows={4} placeholder="请详细描述退货原因,如有商品问题请提供具体情况" />
</Form.Item>
<Form.Item
name="refundAmount"
label="申请退款金额"
rules={[{ required: true, message: '请填写退款金额' }]}
>
<Input prefix="¥" />
</Form.Item>
<Form.Item
name="evidence"
label="上传凭证"
>
<Upload
name="evidence"
action="https://www.mocky.io/v2/5cc8019d300000980a055e76"
listType="picture"
>
<Button icon={<FileOutlined />}></Button>
</Upload>
<Text type="secondary" style={{ display: 'block', marginTop: 8 }}>
5
</Text>
</Form.Item>
<Form.Item
name="contactInfo"
label="联系方式"
rules={[{ required: true, message: '请填写联系方式' }]}
>
<Input placeholder="请填写手机号码或邮箱" />
</Form.Item>
<Form.Item>
<Space style={{ width: '100%', justifyContent: 'flex-end' }}>
<Button onClick={() => setIsModalVisible(false)}>
</Button>
<Button type="primary" htmlType="submit" loading={isLoading}>
</Button>
</Space>
</Form.Item>
</Form>
</Modal>
</div>
);
};
export default ReturnApply;

View File

@@ -0,0 +1,470 @@
import React, { useState, useEffect } from 'react';
import {
Card,
Table,
Button,
Modal,
Form,
Input,
Select,
DatePicker,
message,
Spin,
Row,
Col,
Typography,
Divider,
Space,
Tag,
Popconfirm,
Alert,
} from 'antd';
import {
PlusOutlined,
EditOutlined,
DeleteOutlined,
SearchOutlined,
WarningOutlined,
UserOutlined,
EnvironmentOutlined,
PhoneOutlined,
MailOutlined,
} from '@ant-design/icons';
const { Option } = Select;
const { Title, Text, Paragraph } = Typography;
const { RangePicker } = DatePicker;
// 模拟黑名单数据
const mockBlacklist = [
{
id: 'BL-20260318-001',
customerName: '张三',
customerId: 'CUST-001',
phone: '13800138001',
email: 'zhangsan@example.com',
address: '北京市朝阳区',
reason: '恶意退款',
level: 'HIGH',
addedBy: '管理员A',
addedAt: '2026-03-18T10:00:00Z',
expireAt: '2026-06-18T00:00:00Z',
},
{
id: 'BL-20260318-002',
customerName: '李四',
customerId: 'CUST-002',
phone: '13900139002',
email: 'lisi@example.com',
address: '上海市浦东新区',
reason: '虚假交易',
level: 'MEDIUM',
addedBy: '管理员B',
addedAt: '2026-03-17T15:30:00Z',
expireAt: '2026-05-17T00:00:00Z',
},
{
id: 'BL-20260318-003',
customerName: '王五',
customerId: 'CUST-003',
phone: '13700137003',
email: 'wangwu@example.com',
address: '广州市天河区',
reason: '恶意差评',
level: 'LOW',
addedBy: '管理员A',
addedAt: '2026-03-16T14:00:00Z',
expireAt: '2026-04-16T00:00:00Z',
},
];
// 黑名单级别选项
const blacklistLevels = [
{ value: 'LOW', label: '低', color: 'blue' },
{ value: 'MEDIUM', label: '中', color: 'orange' },
{ value: 'HIGH', label: '高', color: 'red' },
];
// 黑名单原因选项
const blacklistReasons = [
{ value: 'MALICIOUS_REFUND', label: '恶意退款' },
{ value: 'FAKE_TRANSACTION', label: '虚假交易' },
{ value: 'MALICIOUS_NEGATIVE', label: '恶意差评' },
{ value: 'FRAUD', label: '欺诈行为' },
{ value: 'VIOLATION', label: '违规行为' },
{ value: 'OTHER', label: '其他原因' },
];
const BlacklistManage: React.FC = () => {
const [form] = Form.useForm();
const [blacklist, setBlacklist] = useState(mockBlacklist);
const [isModalVisible, setIsModalVisible] = useState(false);
const [isEditing, setIsEditing] = useState(false);
const [currentItem, setCurrentItem] = useState<any>(null);
const [isLoading, setIsLoading] = useState(false);
const [searchText, setSearchText] = useState('');
// 处理添加黑名单
const handleAdd = () => {
setIsEditing(false);
setCurrentItem(null);
form.resetFields();
setIsModalVisible(true);
};
// 处理编辑黑名单
const handleEdit = (item: any) => {
setIsEditing(true);
setCurrentItem(item);
form.setFieldsValue({
customerName: item.customerName,
customerId: item.customerId,
phone: item.phone,
email: item.email,
address: item.address,
reason: item.reason,
level: item.level,
expireAt: item.expireAt ? new Date(item.expireAt) : null,
});
setIsModalVisible(true);
};
// 处理删除黑名单
const handleDelete = (id: string) => {
setIsLoading(true);
try {
// 模拟API调用
setTimeout(() => {
const updatedBlacklist = blacklist.filter(item => item.id !== id);
setBlacklist(updatedBlacklist);
message.success('删除成功');
setIsLoading(false);
}, 500);
} catch (error) {
message.error('删除失败');
setIsLoading(false);
}
};
// 处理表单提交
const handleSubmit = async (values: any) => {
setIsLoading(true);
try {
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 1000));
if (isEditing && currentItem) {
// 编辑模式
const updatedBlacklist = blacklist.map(item => {
if (item.id === currentItem.id) {
return {
...item,
...values,
expireAt: values.expireAt ? values.expireAt.toISOString() : null,
};
}
return item;
});
setBlacklist(updatedBlacklist);
message.success('更新成功');
} else {
// 添加模式
const newItem = {
id: `BL-${Date.now()}`,
...values,
addedBy: '当前用户',
addedAt: new Date().toISOString(),
expireAt: values.expireAt ? values.expireAt.toISOString() : null,
};
setBlacklist([newItem, ...blacklist]);
message.success('添加成功');
}
setIsModalVisible(false);
form.resetFields();
} catch (error) {
message.error('操作失败');
} finally {
setIsLoading(false);
}
};
// 获取级别标签
const getLevelTag = (level: string) => {
const levelObj = blacklistLevels.find(l => l.value === level);
return levelObj ? <Tag color={levelObj.color}>{levelObj.label}</Tag> : <Tag>{level}</Tag>;
};
// 获取原因标签
const getReasonTag = (reason: string) => {
const reasonObj = blacklistReasons.find(r => r.value === reason);
return reasonObj ? <Tag>{reasonObj.label}</Tag> : <Tag>{reason}</Tag>;
};
// 过滤黑名单
const filteredBlacklist = blacklist.filter(item =>
item.customerName.includes(searchText) ||
item.customerId.includes(searchText) ||
item.phone.includes(searchText) ||
item.email.includes(searchText)
);
// 黑名单表格列
const columns = [
{
title: '黑名单ID',
dataIndex: 'id',
key: 'id',
},
{
title: '客户信息',
key: 'customer',
render: (_, record: any) => (
<div>
<div style={{ fontWeight: 'bold' }}>{record.customerName}</div>
<div style={{ fontSize: '12px', color: '#999' }}>ID: {record.customerId}</div>
<div style={{ fontSize: '12px', color: '#999' }}><PhoneOutlined style={{ marginRight: 4 }} />{record.phone}</div>
<div style={{ fontSize: '12px', color: '#999' }}><MailOutlined style={{ marginRight: 4 }} />{record.email}</div>
</div>
),
},
{
title: '地址',
dataIndex: 'address',
key: 'address',
render: (address: string) => (
<div><EnvironmentOutlined style={{ marginRight: 4 }} />{address}</div>
),
},
{
title: '原因',
dataIndex: 'reason',
key: 'reason',
render: (reason: string) => getReasonTag(reason),
},
{
title: '级别',
dataIndex: 'level',
key: 'level',
render: (level: string) => getLevelTag(level),
},
{
title: '添加时间',
dataIndex: 'addedAt',
key: 'addedAt',
render: (date: string) => new Date(date).toLocaleString(),
},
{
title: '过期时间',
dataIndex: 'expireAt',
key: 'expireAt',
render: (date: string) => date ? new Date(date).toLocaleString() : '永久',
},
{
title: '操作',
key: 'action',
render: (_: any, record: any) => (
<Space>
<Button
type="primary"
icon={<EditOutlined />}
onClick={() => handleEdit(record)}
>
</Button>
<Popconfirm
title="确定要删除吗?"
onConfirm={() => handleDelete(record.id)}
okText="确定"
cancelText="取消"
>
<Button
danger
icon={<DeleteOutlined />}
loading={isLoading}
>
</Button>
</Popconfirm>
</Space>
),
},
];
return (
<div style={{ padding: '24px' }}>
<Card
title={
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Title level={4} style={{ margin: 0 }}></Title>
<Text type="secondary"></Text>
</div>
}
extra={
<Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>
</Button>
}
>
<Alert
message="黑名单管理提醒"
description="黑名单用户将被限制购买和退款权限,请谨慎操作"
type="warning"
showIcon
style={{ marginBottom: 16 }}
/>
<Row gutter={16} style={{ marginBottom: 16 }}>
<Col span={12}>
<Input
placeholder="搜索客户姓名、ID、电话或邮箱"
prefix={<SearchOutlined />}
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
/>
</Col>
<Col span={12} style={{ textAlign: 'right' }}>
<Space>
<Button type="default"></Button>
<Button type="default"></Button>
</Space>
</Col>
</Row>
<Table
columns={columns}
dataSource={filteredBlacklist}
rowKey="id"
pagination={{ pageSize: 10 }}
/>
</Card>
{/* 黑名单编辑模态框 */}
<Modal
title={isEditing ? "编辑黑名单" : "添加黑名单"}
open={isModalVisible}
onCancel={() => setIsModalVisible(false)}
footer={null}
width={700}
>
<Form
form={form}
layout="vertical"
onFinish={handleSubmit}
>
<Row gutter={16}>
<Col span={12}>
<Form.Item
name="customerName"
label="客户姓名"
rules={[{ required: true, message: '请输入客户姓名' }]}
>
<Input placeholder="请输入客户姓名" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="customerId"
label="客户ID"
rules={[{ required: true, message: '请输入客户ID' }]}
>
<Input placeholder="请输入客户ID" />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item
name="phone"
label="电话"
rules={[{ required: true, message: '请输入电话' }]}
>
<Input placeholder="请输入电话" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="email"
label="邮箱"
rules={[{ required: true, message: '请输入邮箱' }]}
>
<Input placeholder="请输入邮箱" />
</Form.Item>
</Col>
</Row>
<Form.Item
name="address"
label="地址"
rules={[{ required: true, message: '请输入地址' }]}
>
<Input placeholder="请输入地址" />
</Form.Item>
<Row gutter={16}>
<Col span={12}>
<Form.Item
name="reason"
label="黑名单原因"
rules={[{ required: true, message: '请选择黑名单原因' }]}
>
<Select placeholder="请选择黑名单原因">
{blacklistReasons.map(reason => (
<Option key={reason.value} value={reason.value}>
{reason.label}
</Option>
))}
</Select>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="level"
label="风险级别"
rules={[{ required: true, message: '请选择风险级别' }]}
>
<Select placeholder="请选择风险级别">
{blacklistLevels.map(level => (
<Option key={level.value} value={level.value}>
{level.label}
</Option>
))}
</Select>
</Form.Item>
</Col>
</Row>
<Form.Item
name="expireAt"
label="过期时间"
>
<DatePicker
style={{ width: '100%' }}
placeholder="选择过期时间(不选则为永久)"
/>
</Form.Item>
<Form.Item
name="remark"
label="备注"
>
<Input.TextArea rows={3} placeholder="请输入备注信息" />
</Form.Item>
<Form.Item>
<Space style={{ width: '100%', justifyContent: 'flex-end' }}>
<Button onClick={() => setIsModalVisible(false)}>
</Button>
<Button type="primary" htmlType="submit" loading={isLoading}>
{isEditing ? '更新' : '添加'}
</Button>
</Space>
</Form.Item>
</Form>
</Modal>
</div>
);
};
export default BlacklistManage;

View File

@@ -0,0 +1,475 @@
import React, { useState, useEffect } from 'react';
import {
Card,
Table,
Button,
Modal,
Form,
Input,
Select,
DatePicker,
message,
Spin,
Row,
Col,
Typography,
Divider,
Space,
Tag,
Alert,
Statistic,
Progress,
Badge,
} from 'antd';
import {
WarningOutlined,
SearchOutlined,
EyeOutlined,
AlertOutlined,
LineChartOutlined,
ShieldOutlined,
} from '@ant-design/icons';
const { Option } = Select;
const { Title, Text, Paragraph } = Typography;
const { RangePicker } = DatePicker;
// 模拟风险数据
const mockRisks = [
{
id: 'RISK-20260318-001',
customerId: 'CUST-001',
customerName: '张三',
riskType: 'MALICIOUS_REFUND',
riskLevel: 'HIGH',
score: 95,
description: '短时间内多次申请退款',
status: 'PENDING',
detectedAt: '2026-03-18T10:00:00Z',
lastUpdated: '2026-03-18T10:00:00Z',
},
{
id: 'RISK-20260318-002',
customerId: 'CUST-002',
customerName: '李四',
riskType: 'FAKE_TRANSACTION',
riskLevel: 'MEDIUM',
score: 75,
description: '同一IP地址多次下单',
status: 'PROCESSING',
detectedAt: '2026-03-17T15:30:00Z',
lastUpdated: '2026-03-18T09:00:00Z',
},
{
id: 'RISK-20260318-003',
customerId: 'CUST-003',
customerName: '王五',
riskType: 'MALICIOUS_NEGATIVE',
riskLevel: 'LOW',
score: 45,
description: '多次给出恶意差评',
status: 'RESOLVED',
detectedAt: '2026-03-16T14:00:00Z',
lastUpdated: '2026-03-17T10:00:00Z',
},
];
// 风险类型选项
const riskTypes = [
{ value: 'MALICIOUS_REFUND', label: '恶意退款' },
{ value: 'FAKE_TRANSACTION', label: '虚假交易' },
{ value: 'MALICIOUS_NEGATIVE', label: '恶意差评' },
{ value: 'FRAUD', label: '欺诈行为' },
{ value: 'VIOLATION', label: '违规行为' },
{ value: 'OTHER', label: '其他风险' },
];
// 风险级别选项
const riskLevels = [
{ value: 'LOW', label: '低', color: 'blue' },
{ value: 'MEDIUM', label: '中', color: 'orange' },
{ value: 'HIGH', label: '高', color: 'red' },
];
// 风险状态选项
const riskStatuses = [
{ value: 'PENDING', label: '待处理' },
{ value: 'PROCESSING', label: '处理中' },
{ value: 'RESOLVED', label: '已解决' },
{ value: 'IGNORED', label: '已忽略' },
];
const RiskMonitor: React.FC = () => {
const [form] = Form.useForm();
const [risks, setRisks] = useState(mockRisks);
const [isModalVisible, setIsModalVisible] = useState(false);
const [currentRisk, setCurrentRisk] = useState<any>(null);
const [isLoading, setIsLoading] = useState(false);
const [searchText, setSearchText] = useState('');
const [riskStats, setRiskStats] = useState({
total: 100,
high: 30,
medium: 40,
low: 30,
});
// 处理风险详情查看
const handleRiskView = (risk: any) => {
setCurrentRisk(risk);
setIsModalVisible(true);
};
// 处理风险状态更新
const handleStatusUpdate = async (values: any) => {
setIsLoading(true);
try {
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 1000));
const updatedRisks = risks.map(risk => {
if (risk.id === currentRisk.id) {
return {
...risk,
status: values.status,
lastUpdated: new Date().toISOString(),
};
}
return risk;
});
setRisks(updatedRisks);
setCurrentRisk(updatedRisks.find(r => r.id === currentRisk.id));
message.success('状态更新成功');
} catch (error) {
message.error('状态更新失败');
} finally {
setIsLoading(false);
}
};
// 获取风险级别标签
const getLevelTag = (level: string) => {
const levelObj = riskLevels.find(l => l.value === level);
return levelObj ? <Tag color={levelObj.color}>{levelObj.label}</Tag> : <Tag>{level}</Tag>;
};
// 获取风险类型标签
const getTypeTag = (type: string) => {
const typeObj = riskTypes.find(t => t.value === type);
return typeObj ? <Tag>{typeObj.label}</Tag> : <Tag>{type}</Tag>;
};
// 获取风险状态标签
const getStatusTag = (status: string) => {
const statusObj = riskStatuses.find(s => s.value === status);
switch (status) {
case 'PENDING':
return <Tag color="blue">{statusObj?.label}</Tag>;
case 'PROCESSING':
return <Tag color="processing">{statusObj?.label}</Tag>;
case 'RESOLVED':
return <Tag color="success">{statusObj?.label}</Tag>;
case 'IGNORED':
return <Tag color="default">{statusObj?.label}</Tag>;
default:
return <Tag>{statusObj?.label || status}</Tag>;
}
};
// 过滤风险
const filteredRisks = risks.filter(risk =>
risk.customerName.includes(searchText) ||
risk.customerId.includes(searchText) ||
risk.id.includes(searchText)
);
// 风险表格列
const columns = [
{
title: '风险ID',
dataIndex: 'id',
key: 'id',
},
{
title: '客户信息',
key: 'customer',
render: (_, record: any) => (
<div>
<div style={{ fontWeight: 'bold' }}>{record.customerName}</div>
<div style={{ fontSize: '12px', color: '#999' }}>ID: {record.customerId}</div>
</div>
),
},
{
title: '风险类型',
dataIndex: 'riskType',
key: 'riskType',
render: (type: string) => getTypeTag(type),
},
{
title: '风险级别',
dataIndex: 'riskLevel',
key: 'riskLevel',
render: (level: string) => getLevelTag(level),
},
{
title: '风险评分',
dataIndex: 'score',
key: 'score',
render: (score: number) => (
<div>
<Progress
percent={score}
status={score > 70 ? 'exception' : score > 40 ? 'normal' : 'success'}
size="small"
/>
<Text style={{ display: 'block', textAlign: 'center', marginTop: 4 }}>
{score}
</Text>
</div>
),
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
render: (status: string) => getStatusTag(status),
},
{
title: '检测时间',
dataIndex: 'detectedAt',
key: 'detectedAt',
render: (date: string) => new Date(date).toLocaleString(),
},
{
title: '操作',
key: 'action',
render: (_: any, record: any) => (
<Button
type="primary"
icon={<EyeOutlined />}
onClick={() => handleRiskView(record)}
>
</Button>
),
},
];
return (
<div style={{ padding: '24px' }}>
<Card
title={
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Title level={4} style={{ margin: 0 }}></Title>
<Text type="secondary"></Text>
</div>
}
extra={
<Button type="primary" ghost>
</Button>
}
>
<Alert
message="风险监控提醒"
description="系统会自动检测潜在风险,高风险用户将被自动加入黑名单"
type="info"
showIcon
style={{ marginBottom: 16 }}
/>
<Row gutter={16} style={{ marginBottom: 16 }}>
<Col span={6}>
<Card>
<Statistic
title="总风险数"
value={riskStats.total}
prefix={<AlertOutlined />}
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="高风险"
value={riskStats.high}
prefix={<WarningOutlined />}
valueStyle={{ color: '#ff4d4f' }}
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="中风险"
value={riskStats.medium}
prefix={<AlertOutlined />}
valueStyle={{ color: '#faad14' }}
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="低风险"
value={riskStats.low}
prefix={<ShieldOutlined />}
valueStyle={{ color: '#52c41a' }}
/>
</Card>
</Col>
</Row>
<Row gutter={16} style={{ marginBottom: 16 }}>
<Col span={12}>
<Input
placeholder="搜索客户姓名、ID或风险ID"
prefix={<SearchOutlined />}
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
/>
</Col>
<Col span={12} style={{ textAlign: 'right' }}>
<Space>
<Button type="default"></Button>
<Button type="default"></Button>
</Space>
</Col>
</Row>
<Table
columns={columns}
dataSource={filteredRisks}
rowKey="id"
pagination={{ pageSize: 10 }}
/>
</Card>
{/* 风险详情模态框 */}
{currentRisk && (
<Modal
title="风险详情"
open={isModalVisible}
onCancel={() => setIsModalVisible(false)}
footer={null}
width={800}
>
<Row gutter={16}>
<Col span={12}>
<Statistic
title="风险评分"
value={currentRisk.score}
valueStyle={{ color: currentRisk.score > 70 ? '#ff4d4f' : currentRisk.score > 40 ? '#faad14' : '#52c41a' }}
prefix={<WarningOutlined />}
/>
<div style={{ marginTop: 16 }}>
<Title level={5}></Title>
<div style={{ marginBottom: 8 }}>
<Text strong>ID:</Text> {currentRisk.id}
</div>
<div style={{ marginBottom: 8 }}>
<Text strong>:</Text> {currentRisk.customerName}
</div>
<div style={{ marginBottom: 8 }}>
<Text strong>ID:</Text> {currentRisk.customerId}
</div>
<div style={{ marginBottom: 8 }}>
<Text strong>:</Text> {getTypeTag(currentRisk.riskType)}
</div>
<div style={{ marginBottom: 8 }}>
<Text strong>:</Text> {getLevelTag(currentRisk.riskLevel)}
</div>
<div style={{ marginBottom: 8 }}>
<Text strong>:</Text> {getStatusTag(currentRisk.status)}
</div>
<div style={{ marginBottom: 8 }}>
<Text strong>:</Text> {new Date(currentRisk.detectedAt).toLocaleString()}
</div>
<div style={{ marginBottom: 8 }}>
<Text strong>:</Text> {new Date(currentRisk.lastUpdated).toLocaleString()}
</div>
</div>
</Col>
<Col span={12}>
<div style={{ marginBottom: 16 }}>
<Title level={5}></Title>
<div style={{ padding: 16, backgroundColor: '#f0f0f0', borderRadius: 4 }}>
{currentRisk.description}
</div>
</div>
<div>
<Title level={5}></Title>
<div style={{ padding: 16, backgroundColor: '#f6ffed', borderRadius: 4, border: '1px solid #b7eb8f' }}>
{currentRisk.riskLevel === 'HIGH' && (
<>
<p>1. </p>
<p>2. </p>
<p>3. </p>
</>
)}
{currentRisk.riskLevel === 'MEDIUM' && (
<>
<p>1. </p>
<p>2. </p>
<p>3. </p>
</>
)}
{currentRisk.riskLevel === 'LOW' && (
<>
<p>1. </p>
<p>2. </p>
<p>3. </p>
</>
)}
</div>
</div>
</Col>
</Row>
<Divider orientation="left"></Divider>
<Form
form={form}
layout="vertical"
onFinish={handleStatusUpdate}
initialValues={{ status: currentRisk.status }}
>
<Form.Item
name="status"
label="处理状态"
rules={[{ required: true, message: '请选择处理状态' }]}
>
<Select placeholder="请选择处理状态">
{riskStatuses.map(status => (
<Option key={status.value} value={status.value}>
{status.label}
</Option>
))}
</Select>
</Form.Item>
<Form.Item
name="remark"
label="处理备注"
>
<Input.TextArea rows={3} placeholder="请输入处理备注" />
</Form.Item>
<Form.Item>
<Space style={{ width: '100%', justifyContent: 'flex-end' }}>
<Button onClick={() => setIsModalVisible(false)}>
</Button>
<Button type="primary" htmlType="submit" loading={isLoading}>
</Button>
</Space>
</Form.Item>
</Form>
</Modal>
)}
</div>
);
};
export default RiskMonitor;

View File

@@ -0,0 +1,485 @@
import React, { useState, useEffect } from 'react';
import {
Card,
Form,
Input,
Select,
Button,
Table,
Modal,
message,
Spin,
Row,
Col,
Typography,
Divider,
Space,
Tag,
Radio,
Descriptions,
} from 'antd';
import {
TruckOutlined,
PlaneOutlined,
ShipOutlined,
ClockCircleOutlined,
DollarOutlined,
CheckCircleOutlined,
} from '@ant-design/icons';
const { Option } = Select;
const { Title, Text, Paragraph } = Typography;
// 模拟订单数据
const mockOrders = [
{
id: 'ORD-20260318-001',
customerName: '张三',
productName: 'Apple iPhone 15 Pro',
quantity: 1,
weight: 0.5,
volume: 0.01,
destination: '北京市朝阳区',
status: 'PENDING',
},
{
id: 'ORD-20260318-002',
customerName: '李四',
productName: 'MacBook Pro 14',
quantity: 1,
weight: 2.0,
volume: 0.05,
destination: '上海市浦东新区',
status: 'PENDING',
},
{
id: 'ORD-20260318-003',
customerName: '王五',
productName: 'AirPods Pro 2',
quantity: 2,
weight: 0.2,
volume: 0.005,
destination: '广州市天河区',
status: 'PENDING',
},
];
// 模拟物流选项
const mockLogisticsOptions = [
{
id: 'LOG-001',
name: '顺丰速运',
type: 'EXPRESS',
icon: <TruckOutlined />,
estimatedDays: 1-2,
cost: 23.00,
reliability: 98,
features: ['次日达', '上门取件', '保价服务'],
},
{
id: 'LOG-002',
name: '中通快递',
type: 'STANDARD',
icon: <TruckOutlined />,
estimatedDays: 2-3,
cost: 12.00,
reliability: 95,
features: ['上门取件', '短信通知'],
},
{
id: 'LOG-003',
name: 'EMS',
type: 'EXPRESS',
icon: <PlaneOutlined />,
estimatedDays: 1-2,
cost: 25.00,
reliability: 99,
features: ['次日达', '上门取件', '全球配送'],
},
{
id: 'LOG-004',
name: '韵达快递',
type: 'STANDARD',
icon: <TruckOutlined />,
estimatedDays: 3-4,
cost: 10.00,
reliability: 90,
features: ['上门取件'],
},
];
// 国家/地区选项
const countries = [
{ value: 'CN', label: '中国' },
{ value: 'US', label: '美国' },
{ value: 'JP', label: '日本' },
{ value: 'GB', label: '英国' },
{ value: 'DE', label: '德国' },
{ value: 'FR', label: '法国' },
];
const LogisticsSelect: React.FC = () => {
const [form] = Form.useForm();
const [orders, setOrders] = useState(mockOrders);
const [selectedOrder, setSelectedOrder] = useState<any>(null);
const [isModalVisible, setIsModalVisible] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [logisticsOptions, setLogisticsOptions] = useState(mockLogisticsOptions);
const [selectedOption, setSelectedOption] = useState<string>('');
const [showResults, setShowResults] = useState(false);
// 处理订单选择
const handleOrderSelect = (order: any) => {
setSelectedOrder(order);
form.setFieldsValue({
orderId: order.id,
productName: order.productName,
quantity: order.quantity,
weight: order.weight,
volume: order.volume,
destination: order.destination,
});
setIsModalVisible(true);
};
// 处理物流选项查询
const handleQuery = async (values: any) => {
setIsLoading(true);
try {
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 1000));
// 根据目的地和重量过滤物流选项
const filteredOptions = mockLogisticsOptions.map(option => {
// 模拟根据重量计算价格
const calculatedCost = option.cost * (values.weight || 1);
return {
...option,
cost: calculatedCost,
};
});
setLogisticsOptions(filteredOptions);
setShowResults(true);
message.success('物流选项查询成功');
} catch (error) {
message.error('物流选项查询失败');
} finally {
setIsLoading(false);
}
};
// 处理物流方式选择
const handleSelect = async (optionId: string) => {
setIsLoading(true);
try {
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 1000));
setSelectedOption(optionId);
message.success('物流方式选择成功');
// 更新订单状态
const updatedOrders = orders.map(order => {
if (order.id === selectedOrder.id) {
return {
...order,
status: 'SHIPPING',
};
}
return order;
});
setOrders(updatedOrders);
setIsModalVisible(false);
form.resetFields();
setShowResults(false);
} catch (error) {
message.error('物流方式选择失败');
} finally {
setIsLoading(false);
}
};
// 物流类型标签
const getTypeTag = (type: string) => {
switch (type) {
case 'EXPRESS':
return <Tag color="red"></Tag>;
case 'STANDARD':
return <Tag color="blue"></Tag>;
case 'ECONOMY':
return <Tag color="green"></Tag>;
default:
return <Tag>{type}</Tag>;
}
};
// 订单状态标签
const getStatusTag = (status: string) => {
switch (status) {
case 'PENDING':
return <Tag color="processing"></Tag>;
case 'SHIPPING':
return <Tag color="success"></Tag>;
case 'DELIVERED':
return <Tag color="success"></Tag>;
default:
return <Tag>{status}</Tag>;
}
};
// 订单表格列
const orderColumns = [
{
title: '订单ID',
dataIndex: 'id',
key: 'id',
},
{
title: '客户姓名',
dataIndex: 'customerName',
key: 'customerName',
},
{
title: '商品名称',
dataIndex: 'productName',
key: 'productName',
},
{
title: '数量',
dataIndex: 'quantity',
key: 'quantity',
},
{
title: '重量(kg)',
dataIndex: 'weight',
key: 'weight',
},
{
title: '目的地',
dataIndex: 'destination',
key: 'destination',
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
render: (status: string) => getStatusTag(status),
},
{
title: '操作',
key: 'action',
render: (_: any, record: any) => (
<Button type="primary" onClick={() => handleOrderSelect(record)}>
</Button>
),
},
];
return (
<div style={{ padding: '24px' }}>
<Card
title={
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Title level={4} style={{ margin: 0 }}></Title>
<Text type="secondary"></Text>
</div>
}
extra={
<Button type="primary" ghost>
</Button>
}
>
<Divider orientation="left"></Divider>
<Table
columns={orderColumns}
dataSource={orders}
rowKey="id"
pagination={{ pageSize: 10 }}
/>
</Card>
{/* 物流选择模态框 */}
<Modal
title="选择物流方式"
open={isModalVisible}
onCancel={() => setIsModalVisible(false)}
footer={null}
width={1000}
>
<Form
form={form}
layout="vertical"
onFinish={handleQuery}
>
<Row gutter={16}>
<Col span={12}>
<Form.Item
name="orderId"
label="订单ID"
rules={[{ required: true, message: '请选择订单' }]}
>
<Input disabled />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="productName"
label="商品名称"
rules={[{ required: true, message: '商品名称不能为空' }]}
>
<Input disabled />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={8}>
<Form.Item
name="quantity"
label="数量"
rules={[{ required: true, message: '数量不能为空' }]}
>
<Input type="number" disabled />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item
name="weight"
label="重量(kg)"
rules={[{ required: true, message: '重量不能为空' }]}
>
<Input type="number" step="0.1" />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item
name="volume"
label="体积(m³)"
rules={[{ required: true, message: '体积不能为空' }]}
>
<Input type="number" step="0.001" />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item
name="country"
label="国家/地区"
rules={[{ required: true, message: '请选择国家/地区' }]}
>
<Select placeholder="请选择国家/地区">
{countries.map(country => (
<Option key={country.value} value={country.value}>
{country.label}
</Option>
))}
</Select>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="destination"
label="目的地"
rules={[{ required: true, message: '请输入目的地' }]}
>
<Input />
</Form.Item>
</Col>
</Row>
<Form.Item
name="logisticsType"
label="物流类型"
rules={[{ required: true, message: '请选择物流类型' }]}
>
<Radio.Group>
<Radio value="EXPRESS"></Radio>
<Radio value="STANDARD"></Radio>
<Radio value="ECONOMY"></Radio>
</Radio.Group>
</Form.Item>
<Form.Item>
<Space style={{ width: '100%', justifyContent: 'flex-end' }}>
<Button onClick={() => setIsModalVisible(false)}>
</Button>
<Button type="primary" htmlType="submit" loading={isLoading}>
</Button>
</Space>
</Form.Item>
</Form>
{showResults && (
<>
<Divider orientation="left"></Divider>
<Row gutter={16}>
{logisticsOptions.map(option => (
<Col span={12} key={option.id}>
<Card
hoverable
style={{ marginBottom: 16 }}
bordered={selectedOption === option.id}
bodyStyle={{ padding: 16 }}
>
<div style={{ display: 'flex', alignItems: 'center', marginBottom: 12 }}>
<div style={{ marginRight: 12, fontSize: 24 }}>
{option.icon}
</div>
<div>
<div style={{ fontWeight: 'bold', fontSize: 16 }}>{option.name}</div>
{getTypeTag(option.type)}
</div>
</div>
<Descriptions column={2} size="small">
<Descriptions.Item label="预计时效">
<ClockCircleOutlined style={{ marginRight: 4 }} />
{option.estimatedDays}
</Descriptions.Item>
<Descriptions.Item label="运费">
<DollarOutlined style={{ marginRight: 4 }} />
¥{option.cost.toFixed(2)}
</Descriptions.Item>
<Descriptions.Item label="可靠性">
{option.reliability}%
</Descriptions.Item>
<Descriptions.Item label="特色服务">
{option.features.join(', ')}
</Descriptions.Item>
</Descriptions>
<Button
type={selectedOption === option.id ? 'primary' : 'default'}
style={{ marginTop: 16, width: '100%' }}
onClick={() => handleSelect(option.id)}
loading={isLoading}
>
{selectedOption === option.id ? (
<>
<CheckCircleOutlined style={{ marginRight: 4 }} />
</>
) : (
'选择'
)}
</Button>
</Card>
</Col>
))}
</Row>
</>
)}
</Modal>
</div>
);
};
export default LogisticsSelect;

View File

@@ -0,0 +1,441 @@
import React, { useState, useEffect } from 'react';
import {
Card,
Form,
Input,
Select,
Button,
Table,
Modal,
message,
Spin,
Row,
Col,
Typography,
Divider,
Space,
Tag,
Descriptions,
Timeline,
Alert,
} from 'antd';
import {
SearchOutlined,
TruckOutlined,
ClockCircleOutlined,
CheckCircleOutlined,
ExclamationCircleOutlined,
HomeOutlined,
FieldTimeOutlined,
EnvironmentOutlined,
} from '@ant-design/icons';
const { Option } = Select;
const { Title, Text, Paragraph } = Typography;
// 模拟物流商数据
const logisticsProviders = [
{ value: 'SF', label: '顺丰速运' },
{ value: 'ZTO', label: '中通快递' },
{ value: 'EMS', label: 'EMS' },
{ value: 'YUNDA', label: '韵达快递' },
{ value: 'DEPPON', label: '德邦物流' },
{ value: 'JD', label: '京东物流' },
{ value: 'STO', label: '申通快递' },
{ value: 'YTO', label: '圆通快递' },
];
// 模拟物流追踪数据
const mockTrackingData = {
'SF1234567890': {
trackingNumber: 'SF1234567890',
provider: '顺丰速运',
status: 'IN_TRANSIT',
estimatedDelivery: '2026-03-20',
sender: '深圳市南山区',
recipient: '北京市朝阳区',
timeline: [
{
time: '2026-03-18 10:30:00',
status: 'IN_TRANSIT',
location: '北京市朝阳区',
description: '快递正在派送中派送员张三电话13800138000',
},
{
time: '2026-03-18 08:00:00',
status: 'IN_TRANSIT',
location: '北京市朝阳区营业点',
description: '快递已到达营业点',
},
{
time: '2026-03-17 18:00:00',
status: 'IN_TRANSIT',
location: '北京市',
description: '快递已到达北京市',
},
{
time: '2026-03-17 09:00:00',
status: 'IN_TRANSIT',
location: '上海市',
description: '快递已离开上海市,发往北京市',
},
{
time: '2026-03-16 18:00:00',
status: 'PICKED_UP',
location: '深圳市南山区',
description: '快递已揽收',
},
{
time: '2026-03-16 16:00:00',
status: 'CREATED',
location: '深圳市',
description: '快递已下单',
},
],
},
'ZTO9876543210': {
trackingNumber: 'ZTO9876543210',
provider: '中通快递',
status: 'DELIVERED',
estimatedDelivery: '2026-03-17',
actualDelivery: '2026-03-17',
sender: '上海市浦东新区',
recipient: '广州市天河区',
timeline: [
{
time: '2026-03-17 14:30:00',
status: 'DELIVERED',
location: '广州市天河区',
description: '快递已签收,签收人:李四',
},
{
time: '2026-03-17 10:00:00',
status: 'IN_TRANSIT',
location: '广州市天河区营业点',
description: '快递正在派送中',
},
{
time: '2026-03-17 08:00:00',
status: 'IN_TRANSIT',
location: '广州市',
description: '快递已到达广州市',
},
{
time: '2026-03-16 18:00:00',
status: 'IN_TRANSIT',
location: '上海市',
description: '快递已离开上海市,发往广州市',
},
{
time: '2026-03-16 10:00:00',
status: 'PICKED_UP',
location: '上海市浦东新区',
description: '快递已揽收',
},
],
},
};
// 模拟历史追踪记录
const mockTrackingHistory = [
{
id: 'TRACK-20260318-001',
trackingNumber: 'SF1234567890',
provider: '顺丰速运',
status: 'IN_TRANSIT',
lastChecked: '2026-03-18T10:30:00Z',
},
{
id: 'TRACK-20260318-002',
trackingNumber: 'ZTO9876543210',
provider: '中通快递',
status: 'DELIVERED',
lastChecked: '2026-03-17T14:30:00Z',
},
];
const LogisticsTracking: React.FC = () => {
const [form] = Form.useForm();
const [isLoading, setIsLoading] = useState(false);
const [trackingData, setTrackingData] = useState<any>(null);
const [trackingHistory, setTrackingHistory] = useState(mockTrackingHistory);
const [showTracking, setShowTracking] = useState(false);
// 处理物流追踪查询
const handleTrack = async (values: any) => {
setIsLoading(true);
try {
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 1000));
const data = mockTrackingData[values.trackingNumber];
if (data) {
setTrackingData(data);
setShowTracking(true);
message.success('追踪成功');
// 更新历史记录
const existingRecord = trackingHistory.find(record => record.trackingNumber === values.trackingNumber);
if (existingRecord) {
const updatedHistory = trackingHistory.map(record => {
if (record.trackingNumber === values.trackingNumber) {
return {
...record,
status: data.status,
lastChecked: new Date().toISOString(),
};
}
return record;
});
setTrackingHistory(updatedHistory);
} else {
const newRecord = {
id: `TRACK-${Date.now()}`,
trackingNumber: values.trackingNumber,
provider: data.provider,
status: data.status,
lastChecked: new Date().toISOString(),
};
setTrackingHistory([newRecord, ...trackingHistory]);
}
} else {
message.error('未找到物流信息');
setShowTracking(false);
}
} catch (error) {
message.error('追踪失败');
} finally {
setIsLoading(false);
}
};
// 获取状态标签
const getStatusTag = (status: string) => {
switch (status) {
case 'CREATED':
return <Tag color="blue"></Tag>;
case 'PICKED_UP':
return <Tag color="processing"></Tag>;
case 'IN_TRANSIT':
return <Tag color="processing"></Tag>;
case 'OUT_FOR_DELIVERY':
return <Tag color="processing"></Tag>;
case 'DELIVERED':
return <Tag color="success"></Tag>;
case 'FAILED':
return <Tag color="error"></Tag>;
default:
return <Tag>{status}</Tag>;
}
};
// 获取状态图标
const getStatusIcon = (status: string) => {
switch (status) {
case 'CREATED':
return <ClockCircleOutlined />;
case 'PICKED_UP':
return <TruckOutlined />;
case 'IN_TRANSIT':
return <TruckOutlined />;
case 'OUT_FOR_DELIVERY':
return <TruckOutlined />;
case 'DELIVERED':
return <CheckCircleOutlined />;
case 'FAILED':
return <ExclamationCircleOutlined />;
default:
return <ClockCircleOutlined />;
}
};
// 历史追踪表格列
const historyColumns = [
{
title: '物流单号',
dataIndex: 'trackingNumber',
key: 'trackingNumber',
},
{
title: '物流商',
dataIndex: 'provider',
key: 'provider',
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
render: (status: string) => getStatusTag(status),
},
{
title: '最后查询时间',
dataIndex: 'lastChecked',
key: 'lastChecked',
render: (date: string) => new Date(date).toLocaleString(),
},
{
title: '操作',
key: 'action',
render: (_: any, record: any) => (
<Button
type="primary"
onClick={() => {
form.setFieldsValue({
trackingNumber: record.trackingNumber,
provider: record.provider,
});
handleTrack({ trackingNumber: record.trackingNumber, provider: record.provider });
}}
>
</Button>
),
},
];
return (
<div style={{ padding: '24px' }}>
<Card
title={
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Title level={4} style={{ margin: 0 }}></Title>
<Text type="secondary"></Text>
</div>
}
extra={
<Button type="primary" ghost>
</Button>
}
>
<Divider orientation="left"></Divider>
<Form
form={form}
layout="vertical"
onFinish={handleTrack}
>
<Row gutter={16}>
<Col span={16}>
<Form.Item
name="trackingNumber"
label="物流单号"
rules={[{ required: true, message: '请输入物流单号' }]}
>
<Input placeholder="请输入物流单号" />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item
name="provider"
label="物流商"
rules={[{ required: true, message: '请选择物流商' }]}
>
<Select placeholder="请选择物流商">
{logisticsProviders.map(provider => (
<Option key={provider.value} value={provider.value}>
{provider.label}
</Option>
))}
</Select>
</Form.Item>
</Col>
</Row>
<Form.Item>
<Space style={{ width: '100%', justifyContent: 'flex-end' }}>
<Button onClick={() => form.resetFields()}>
</Button>
<Button type="primary" htmlType="submit" loading={isLoading}>
<SearchOutlined style={{ marginRight: 4 }} />
</Button>
</Space>
</Form.Item>
</Form>
{showTracking && trackingData && (
<>
<Divider orientation="left"></Divider>
<Card>
<Row gutter={16}>
<Col span={12}>
<Descriptions column={1}>
<Descriptions.Item label="物流单号">{trackingData.trackingNumber}</Descriptions.Item>
<Descriptions.Item label="物流商">{trackingData.provider}</Descriptions.Item>
<Descriptions.Item label="当前状态">{getStatusTag(trackingData.status)}</Descriptions.Item>
<Descriptions.Item label="预计送达时间">{trackingData.estimatedDelivery}</Descriptions.Item>
{trackingData.actualDelivery && (
<Descriptions.Item label="实际送达时间">{trackingData.actualDelivery}</Descriptions.Item>
)}
<Descriptions.Item label="寄件地">{trackingData.sender}</Descriptions.Item>
<Descriptions.Item label="收件地">{trackingData.recipient}</Descriptions.Item>
</Descriptions>
</Col>
<Col span={12}>
<div style={{ marginBottom: 16 }}>
<Text strong></Text>
<div style={{ marginTop: 8, padding: 16, backgroundColor: '#f0f0f0', borderRadius: 4 }}>
{trackingData.status === 'DELIVERED' ? (
<Alert
message="快递已送达"
type="success"
showIcon
/>
) : trackingData.status === 'IN_TRANSIT' ? (
<Alert
message="快递运输中"
type="info"
showIcon
/>
) : trackingData.status === 'PICKED_UP' ? (
<Alert
message="快递已揽收"
type="info"
showIcon
/>
) : (
<Alert
message="快递已下单"
type="info"
showIcon
/>
)}
</div>
</div>
</Col>
</Row>
<Divider orientation="left"></Divider>
<Timeline
items={trackingData.timeline.map((item: any, index: number) => ({
color: item.status === 'DELIVERED' ? 'green' : 'blue',
children: (
<div>
<Text strong>{item.time}</Text>
<div style={{ marginTop: 4 }}>
<Text type="secondary">{item.location}</Text>
</div>
<div style={{ marginTop: 4 }}>{item.description}</div>
</div>
),
}))}
/>
</Card>
</>
)}
<Divider orientation="left"></Divider>
<Table
columns={historyColumns}
dataSource={trackingHistory}
rowKey="id"
pagination={{ pageSize: 10 }}
/>
</Card>
</div>
);
};
export default LogisticsTracking;

View File

@@ -0,0 +1,384 @@
import React, { useState, useEffect } from 'react';
import {
Card,
Form,
Input,
Select,
Button,
Table,
Modal,
message,
Spin,
Row,
Col,
Typography,
Divider,
Space,
Tag,
Descriptions,
Statistic,
} from 'antd';
import {
CalculatorOutlined,
TruckOutlined,
PlaneOutlined,
ShipOutlined,
DollarOutlined,
ArrowRightOutlined,
} from '@ant-design/icons';
const { Option } = Select;
const { Title, Text, Paragraph } = Typography;
// 模拟物流商数据
const logisticsProviders = [
{
id: 'SF',
name: '顺丰速运',
icon: <TruckOutlined />,
basePrice: 12.00,
pricePerKg: 2.00,
minDays: 1,
maxDays: 2,
},
{
id: 'ZTO',
name: '中通快递',
icon: <TruckOutlined />,
basePrice: 8.00,
pricePerKg: 1.50,
minDays: 2,
maxDays: 3,
},
{
id: 'EMS',
name: 'EMS',
icon: <PlaneOutlined />,
basePrice: 15.00,
pricePerKg: 3.00,
minDays: 1,
maxDays: 2,
},
{
id: 'YUNDA',
name: '韵达快递',
icon: <TruckOutlined />,
basePrice: 7.00,
pricePerKg: 1.20,
minDays: 3,
maxDays: 4,
},
{
id: 'DEPPON',
name: '德邦物流',
icon: <TruckOutlined />,
basePrice: 10.00,
pricePerKg: 1.80,
minDays: 2,
maxDays: 3,
},
];
// 国家/地区选项
const countries = [
{ value: 'CN', label: '中国', rate: 1.00 },
{ value: 'US', label: '美国', rate: 7.00 },
{ value: 'JP', label: '日本', rate: 0.05 },
{ value: 'GB', label: '英国', rate: 8.50 },
{ value: 'DE', label: '德国', rate: 7.50 },
{ value: 'FR', label: '法国', rate: 7.20 },
];
// 模拟历史计算记录
const mockCalculationHistory = [
{
id: 'CALC-20260318-001',
orderId: 'ORD-20260318-001',
productName: 'Apple iPhone 15 Pro',
weight: 0.5,
destination: '北京市朝阳区',
provider: '顺丰速运',
cost: 13.00,
createdAt: '2026-03-18T10:00:00Z',
},
{
id: 'CALC-20260318-002',
orderId: 'ORD-20260318-002',
productName: 'MacBook Pro 14',
weight: 2.0,
destination: '上海市浦东新区',
provider: '中通快递',
cost: 11.00,
createdAt: '2026-03-18T09:30:00Z',
},
];
const ShippingCostCalculator: React.FC = () => {
const [form] = Form.useForm();
const [isLoading, setIsLoading] = useState(false);
const [calculationResults, setCalculationResults] = useState<any[]>([]);
const [showResults, setShowResults] = useState(false);
const [calculationHistory, setCalculationHistory] = useState(mockCalculationHistory);
const [selectedCountry, setSelectedCountry] = useState('CN');
const [exchangeRate, setExchangeRate] = useState(1.00);
// 处理国家选择
const handleCountryChange = (value: string) => {
const country = countries.find(c => c.value === value);
if (country) {
setSelectedCountry(value);
setExchangeRate(country.rate);
}
};
// 处理运费计算
const handleCalculate = async (values: any) => {
setIsLoading(true);
try {
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 1000));
// 计算各物流商的运费
const results = logisticsProviders.map(provider => {
// 基础运费 + 重量 * 每公斤价格
const baseCost = provider.basePrice + (values.weight * provider.pricePerKg);
// 转换为目标国家货币
const convertedCost = baseCost * exchangeRate;
return {
...provider,
cost: convertedCost,
convertedCost: convertedCost,
};
});
// 按价格排序
results.sort((a, b) => a.cost - b.cost);
setCalculationResults(results);
setShowResults(true);
message.success('运费计算成功');
// 添加到历史记录
const newHistory = {
id: `CALC-${Date.now()}`,
orderId: values.orderId || `ORD-${Date.now()}`,
productName: values.productName,
weight: values.weight,
destination: values.destination,
provider: results[0].name,
cost: results[0].cost,
createdAt: new Date().toISOString(),
};
setCalculationHistory([newHistory, ...calculationHistory]);
} catch (error) {
message.error('运费计算失败');
} finally {
setIsLoading(false);
}
};
// 计算历史表格列
const historyColumns = [
{
title: '计算ID',
dataIndex: 'id',
key: 'id',
},
{
title: '订单ID',
dataIndex: 'orderId',
key: 'orderId',
},
{
title: '商品名称',
dataIndex: 'productName',
key: 'productName',
},
{
title: '重量(kg)',
dataIndex: 'weight',
key: 'weight',
},
{
title: '目的地',
dataIndex: 'destination',
key: 'destination',
},
{
title: '物流商',
dataIndex: 'provider',
key: 'provider',
},
{
title: '运费',
dataIndex: 'cost',
key: 'cost',
render: (cost: number) => `¥${cost.toFixed(2)}`,
},
{
title: '计算时间',
dataIndex: 'createdAt',
key: 'createdAt',
render: (date: string) => new Date(date).toLocaleString(),
},
];
return (
<div style={{ padding: '24px' }}>
<Card
title={
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Title level={4} style={{ margin: 0 }}></Title>
<Text type="secondary"></Text>
</div>
}
extra={
<Button type="primary" ghost>
</Button>
}
>
<Divider orientation="left"></Divider>
<Form
form={form}
layout="vertical"
onFinish={handleCalculate}
>
<Row gutter={16}>
<Col span={12}>
<Form.Item
name="orderId"
label="订单ID"
>
<Input placeholder="请输入订单ID" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="productName"
label="商品名称"
rules={[{ required: true, message: '请输入商品名称' }]}
>
<Input placeholder="请输入商品名称" />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={8}>
<Form.Item
name="weight"
label="重量(kg)"
rules={[{ required: true, message: '请输入重量' }]}
>
<Input type="number" step="0.1" placeholder="请输入重量" />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item
name="volume"
label="体积(m³)"
rules={[{ required: true, message: '请输入体积' }]}
>
<Input type="number" step="0.001" placeholder="请输入体积" />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item
name="country"
label="国家/地区"
rules={[{ required: true, message: '请选择国家/地区' }]}
>
<Select
placeholder="请选择国家/地区"
onChange={handleCountryChange}
defaultValue="CN"
>
{countries.map(country => (
<Option key={country.value} value={country.value}>
{country.label}
</Option>
))}
</Select>
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={24}>
<Form.Item
name="destination"
label="目的地"
rules={[{ required: true, message: '请输入目的地' }]}
>
<Input placeholder="请输入详细地址" />
</Form.Item>
</Col>
</Row>
<Form.Item>
<Space style={{ width: '100%', justifyContent: 'flex-end' }}>
<Button onClick={() => form.resetFields()}>
</Button>
<Button type="primary" htmlType="submit" loading={isLoading}>
<CalculatorOutlined style={{ marginRight: 4 }} />
</Button>
</Space>
</Form.Item>
</Form>
{showResults && (
<>
<Divider orientation="left"></Divider>
<Row gutter={16}>
{calculationResults.map((result, index) => (
<Col span={8} key={result.id}>
<Card
hoverable
style={{ marginBottom: 16 }}
bodyStyle={{ padding: 16 }}
title={
<div style={{ display: 'flex', alignItems: 'center' }}>
<span style={{ marginRight: 8 }}>{result.icon}</span>
<span>{result.name}</span>
{index === 0 && <Tag color="green" style={{ marginLeft: 8 }}></Tag>}
</div>
}
>
<Descriptions column={1} size="small">
<Descriptions.Item label="预计时效">
{result.minDays}-{result.maxDays}
</Descriptions.Item>
<Descriptions.Item label="运费">
<Statistic
value={result.convertedCost}
prefix="¥"
precision={2}
valueStyle={{ color: index === 0 ? '#52c41a' : '#1890ff' }}
/>
</Descriptions.Item>
</Descriptions>
</Card>
</Col>
))}
</Row>
</>
)}
<Divider orientation="left"></Divider>
<Table
columns={historyColumns}
dataSource={calculationHistory}
rowKey="id"
pagination={{ pageSize: 10 }}
/>
</Card>
</div>
);
};
export default ShippingCostCalculator;