feat: 新增售后逆向流程和物流策略闭环页面
refactor: 移除不再使用的提示文件和文档副本 docs: 更新开发进度文档,补充前端任务完成情况
This commit is contained in:
508
client/src/pages/AfterSales/CustomerService.tsx
Normal file
508
client/src/pages/AfterSales/CustomerService.tsx
Normal 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;
|
||||
390
client/src/pages/AfterSales/RefundProcess.tsx
Normal file
390
client/src/pages/AfterSales/RefundProcess.tsx
Normal 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;
|
||||
399
client/src/pages/AfterSales/ReturnApply.tsx
Normal file
399
client/src/pages/AfterSales/ReturnApply.tsx
Normal 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;
|
||||
470
client/src/pages/Blacklist/BlacklistManage.tsx
Normal file
470
client/src/pages/Blacklist/BlacklistManage.tsx
Normal 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;
|
||||
475
client/src/pages/Blacklist/RiskMonitor.tsx
Normal file
475
client/src/pages/Blacklist/RiskMonitor.tsx
Normal 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;
|
||||
485
client/src/pages/Logistics/LogisticsSelect.tsx
Normal file
485
client/src/pages/Logistics/LogisticsSelect.tsx
Normal 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;
|
||||
441
client/src/pages/Logistics/LogisticsTracking.tsx
Normal file
441
client/src/pages/Logistics/LogisticsTracking.tsx
Normal 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;
|
||||
384
client/src/pages/Logistics/ShippingCostCalculator.tsx
Normal file
384
client/src/pages/Logistics/ShippingCostCalculator.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user