feat: 新增多模块功能与服务实现
新增广告计划、用户资产、B2B交易、合规规则等核心模型 实现爬虫工作器、贸易服务、现金流预测等业务服务 添加RBAC权限测试、压力测试等测试用例 完善扩展程序的消息处理与内容脚本功能 重构应用入口与文档生成器 更新项目规则与业务闭环分析文档
This commit is contained in:
544
dashboard/src/pages/Orders/OrderList.tsx
Normal file
544
dashboard/src/pages/Orders/OrderList.tsx
Normal file
@@ -0,0 +1,544 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Card,
|
||||
Table,
|
||||
Tag,
|
||||
Button,
|
||||
Space,
|
||||
Modal,
|
||||
Form,
|
||||
Input,
|
||||
Select,
|
||||
DatePicker,
|
||||
message,
|
||||
Tooltip,
|
||||
Row,
|
||||
Col,
|
||||
Statistic,
|
||||
Badge,
|
||||
Dropdown,
|
||||
Menu,
|
||||
Tabs,
|
||||
} from 'antd';
|
||||
import {
|
||||
EyeOutlined,
|
||||
SyncOutlined,
|
||||
DownloadOutlined,
|
||||
ReloadOutlined,
|
||||
FilterOutlined,
|
||||
CheckCircleOutlined,
|
||||
ClockCircleOutlined,
|
||||
TruckOutlined,
|
||||
CloseCircleOutlined,
|
||||
MoreOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
|
||||
const { Option } = Select;
|
||||
const { RangePicker } = DatePicker;
|
||||
const { TabPane } = Tabs;
|
||||
|
||||
interface Order {
|
||||
id: string;
|
||||
orderId: string;
|
||||
platform: 'AMAZON' | 'EBAY' | 'SHOPIFY' | 'SHOPEE' | 'LAZADA';
|
||||
customerName: string;
|
||||
customerEmail: string;
|
||||
totalAmount: number;
|
||||
currency: string;
|
||||
status: 'PULLED' | 'PENDING_REVIEW' | 'CONFIRMED' | 'ALLOCATED' | 'READY_TO_SHIP' | 'SHIPPED' | 'DELIVERED' | 'CANCELLED';
|
||||
paymentStatus: 'PENDING' | 'PAID' | 'REFUNDED';
|
||||
itemCount: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
shopId: string;
|
||||
shopName: string;
|
||||
}
|
||||
|
||||
const ORDER_STATUS_MAP: Record<string, { color: string; text: string; icon: React.ReactNode }> = {
|
||||
PULLED: { color: 'default', text: 'Pulled', icon: <ClockCircleOutlined /> },
|
||||
PENDING_REVIEW: { color: 'processing', text: 'Pending Review', icon: <SyncOutlined spin /> },
|
||||
CONFIRMED: { color: 'blue', text: 'Confirmed', icon: <CheckCircleOutlined /> },
|
||||
ALLOCATED: { color: 'cyan', text: 'Allocated', icon: <CheckCircleOutlined /> },
|
||||
READY_TO_SHIP: { color: 'geekblue', text: 'Ready to Ship', icon: <TruckOutlined /> },
|
||||
SHIPPED: { color: 'purple', text: 'Shipped', icon: <TruckOutlined /> },
|
||||
DELIVERED: { color: 'success', text: 'Delivered', icon: <CheckCircleOutlined /> },
|
||||
CANCELLED: { color: 'error', text: 'Cancelled', icon: <CloseCircleOutlined /> },
|
||||
};
|
||||
|
||||
const PAYMENT_STATUS_MAP: Record<string, { color: string; text: string }> = {
|
||||
PENDING: { color: 'warning', text: 'Pending' },
|
||||
PAID: { color: 'success', text: 'Paid' },
|
||||
REFUNDED: { color: 'default', text: 'Refunded' },
|
||||
};
|
||||
|
||||
const PLATFORM_MAP: Record<string, { color: string; text: string }> = {
|
||||
AMAZON: { color: 'orange', text: 'Amazon' },
|
||||
EBAY: { color: 'blue', text: 'eBay' },
|
||||
SHOPIFY: { color: 'green', text: 'Shopify' },
|
||||
SHOPEE: { color: 'red', text: 'Shopee' },
|
||||
LAZADA: { color: 'purple', text: 'Lazada' },
|
||||
};
|
||||
|
||||
export const OrderList: React.FC = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [orders, setOrders] = useState<Order[]>([]);
|
||||
const [selectedOrder, setSelectedOrder] = useState<Order | null>(null);
|
||||
const [detailModalVisible, setDetailModalVisible] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState('all');
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
||||
const [stats, setStats] = useState({
|
||||
total: 0,
|
||||
pending: 0,
|
||||
processing: 0,
|
||||
shipped: 0,
|
||||
totalAmount: 0,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
fetchOrders();
|
||||
}, []);
|
||||
|
||||
const fetchOrders = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const mockOrders: Order[] = [
|
||||
{
|
||||
id: '1',
|
||||
orderId: 'ORD-2026-001',
|
||||
platform: 'AMAZON',
|
||||
customerName: 'John Smith',
|
||||
customerEmail: 'john.smith@email.com',
|
||||
totalAmount: 159.99,
|
||||
currency: 'USD',
|
||||
status: 'SHIPPED',
|
||||
paymentStatus: 'PAID',
|
||||
itemCount: 3,
|
||||
createdAt: '2026-03-18 10:30:00',
|
||||
updatedAt: '2026-03-18 14:00:00',
|
||||
shopId: 'SHOP_001',
|
||||
shopName: 'Main Store',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
orderId: 'ORD-2026-002',
|
||||
platform: 'EBAY',
|
||||
customerName: 'Emma Wilson',
|
||||
customerEmail: 'emma.wilson@email.com',
|
||||
totalAmount: 89.50,
|
||||
currency: 'USD',
|
||||
status: 'PENDING_REVIEW',
|
||||
paymentStatus: 'PAID',
|
||||
itemCount: 2,
|
||||
createdAt: '2026-03-18 09:15:00',
|
||||
updatedAt: '2026-03-18 09:15:00',
|
||||
shopId: 'SHOP_002',
|
||||
shopName: 'eBay Outlet',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
orderId: 'ORD-2026-003',
|
||||
platform: 'SHOPIFY',
|
||||
customerName: 'Michael Brown',
|
||||
customerEmail: 'michael.brown@email.com',
|
||||
totalAmount: 245.00,
|
||||
currency: 'USD',
|
||||
status: 'CONFIRMED',
|
||||
paymentStatus: 'PAID',
|
||||
itemCount: 5,
|
||||
createdAt: '2026-03-17 16:45:00',
|
||||
updatedAt: '2026-03-18 08:00:00',
|
||||
shopId: 'SHOP_003',
|
||||
shopName: 'Brand Store',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
orderId: 'ORD-2026-004',
|
||||
platform: 'SHOPEE',
|
||||
customerName: 'Sarah Davis',
|
||||
customerEmail: 'sarah.davis@email.com',
|
||||
totalAmount: 78.00,
|
||||
currency: 'USD',
|
||||
status: 'DELIVERED',
|
||||
paymentStatus: 'PAID',
|
||||
itemCount: 1,
|
||||
createdAt: '2026-03-15 11:20:00',
|
||||
updatedAt: '2026-03-17 15:30:00',
|
||||
shopId: 'SHOP_004',
|
||||
shopName: 'Shopee Mall',
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
orderId: 'ORD-2026-005',
|
||||
platform: 'AMAZON',
|
||||
customerName: 'David Lee',
|
||||
customerEmail: 'david.lee@email.com',
|
||||
totalAmount: 320.00,
|
||||
currency: 'USD',
|
||||
status: 'PULLED',
|
||||
paymentStatus: 'PENDING',
|
||||
itemCount: 4,
|
||||
createdAt: '2026-03-18 12:00:00',
|
||||
updatedAt: '2026-03-18 12:00:00',
|
||||
shopId: 'SHOP_001',
|
||||
shopName: 'Main Store',
|
||||
},
|
||||
];
|
||||
setOrders(mockOrders);
|
||||
calculateStats(mockOrders);
|
||||
} catch (error) {
|
||||
message.error('Failed to load orders');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const calculateStats = (orderList: Order[]) => {
|
||||
setStats({
|
||||
total: orderList.length,
|
||||
pending: orderList.filter((o) => o.status === 'PENDING_REVIEW' || o.status === 'PULLED').length,
|
||||
processing: orderList.filter((o) => ['CONFIRMED', 'ALLOCATED', 'READY_TO_SHIP'].includes(o.status)).length,
|
||||
shipped: orderList.filter((o) => o.status === 'SHIPPED').length,
|
||||
totalAmount: orderList.reduce((sum, o) => sum + o.totalAmount, 0),
|
||||
});
|
||||
};
|
||||
|
||||
const handleViewDetail = (order: Order) => {
|
||||
setSelectedOrder(order);
|
||||
setDetailModalVisible(true);
|
||||
};
|
||||
|
||||
const handleStatusChange = async (orderId: string, newStatus: string) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
console.log('Updating order status:', { orderId, newStatus });
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
message.success('Status updated successfully');
|
||||
fetchOrders();
|
||||
} catch (error) {
|
||||
message.error('Failed to update status');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBatchAction = async (action: string) => {
|
||||
if (selectedRowKeys.length === 0) {
|
||||
message.warning('Please select orders first');
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
try {
|
||||
console.log('Batch action:', { action, orderIds: selectedRowKeys });
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
message.success(`Batch ${action} completed for ${selectedRowKeys.length} orders`);
|
||||
setSelectedRowKeys([]);
|
||||
fetchOrders();
|
||||
} catch (error) {
|
||||
message.error('Batch action failed');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleExport = () => {
|
||||
message.info('Exporting orders...');
|
||||
};
|
||||
|
||||
const getFilteredOrders = () => {
|
||||
if (activeTab === 'all') return orders;
|
||||
if (activeTab === 'pending') return orders.filter((o) => ['PULLED', 'PENDING_REVIEW'].includes(o.status));
|
||||
if (activeTab === 'processing') return orders.filter((o) => ['CONFIRMED', 'ALLOCATED', 'READY_TO_SHIP'].includes(o.status));
|
||||
if (activeTab === 'shipped') return orders.filter((o) => o.status === 'SHIPPED');
|
||||
if (activeTab === 'delivered') return orders.filter((o) => o.status === 'DELIVERED');
|
||||
return orders;
|
||||
};
|
||||
|
||||
const columns: ColumnsType<Order> = [
|
||||
{
|
||||
title: 'Order ID',
|
||||
dataIndex: 'orderId',
|
||||
key: 'orderId',
|
||||
width: 140,
|
||||
render: (text: string, record) => (
|
||||
<a onClick={() => handleViewDetail(record)}>{text}</a>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Platform',
|
||||
dataIndex: 'platform',
|
||||
key: 'platform',
|
||||
width: 100,
|
||||
render: (platform: string) => {
|
||||
const config = PLATFORM_MAP[platform];
|
||||
return <Tag color={config.color}>{config.text}</Tag>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Customer',
|
||||
dataIndex: 'customerName',
|
||||
key: 'customerName',
|
||||
width: 140,
|
||||
},
|
||||
{
|
||||
title: 'Items',
|
||||
dataIndex: 'itemCount',
|
||||
key: 'itemCount',
|
||||
width: 70,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: 'Amount',
|
||||
dataIndex: 'totalAmount',
|
||||
key: 'totalAmount',
|
||||
width: 100,
|
||||
render: (amount: number) => (
|
||||
<span style={{ fontWeight: 'bold' }}>${amount.toFixed(2)}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Payment',
|
||||
dataIndex: 'paymentStatus',
|
||||
key: 'paymentStatus',
|
||||
width: 90,
|
||||
render: (status: string) => {
|
||||
const config = PAYMENT_STATUS_MAP[status];
|
||||
return <Tag color={config.color}>{config.text}</Tag>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Status',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 130,
|
||||
render: (status: string) => {
|
||||
const config = ORDER_STATUS_MAP[status];
|
||||
return <Tag icon={config.icon} color={config.color}>{config.text}</Tag>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Shop',
|
||||
dataIndex: 'shopName',
|
||||
key: 'shopName',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: 'Created',
|
||||
dataIndex: 'createdAt',
|
||||
key: 'createdAt',
|
||||
width: 160,
|
||||
},
|
||||
{
|
||||
title: 'Actions',
|
||||
key: 'action',
|
||||
width: 120,
|
||||
fixed: 'right',
|
||||
render: (_, record) => (
|
||||
<Space>
|
||||
<Tooltip title="View Details">
|
||||
<Button type="link" icon={<EyeOutlined />} onClick={() => handleViewDetail(record)} />
|
||||
</Tooltip>
|
||||
<Dropdown
|
||||
overlay={
|
||||
<Menu>
|
||||
{record.status === 'PULLED' && (
|
||||
<Menu.Item key="review" onClick={() => handleStatusChange(record.orderId, 'PENDING_REVIEW')}>
|
||||
Submit for Review
|
||||
</Menu.Item>
|
||||
)}
|
||||
{record.status === 'PENDING_REVIEW' && (
|
||||
<Menu.Item key="confirm" onClick={() => handleStatusChange(record.orderId, 'CONFIRMED')}>
|
||||
Confirm Order
|
||||
</Menu.Item>
|
||||
)}
|
||||
{record.status === 'CONFIRMED' && (
|
||||
<Menu.Item key="allocate" onClick={() => handleStatusChange(record.orderId, 'ALLOCATED')}>
|
||||
Allocate Stock
|
||||
</Menu.Item>
|
||||
)}
|
||||
<Menu.Item key="cancel" onClick={() => handleStatusChange(record.orderId, 'CANCELLED')}>
|
||||
Cancel Order
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
}
|
||||
>
|
||||
<Button type="link" icon={<MoreOutlined />} />
|
||||
</Dropdown>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const rowSelection = {
|
||||
selectedRowKeys,
|
||||
onChange: setSelectedRowKeys,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="order-list-page">
|
||||
<Row gutter={16} style={{ marginBottom: 24 }}>
|
||||
<Col span={4}>
|
||||
<Card>
|
||||
<Statistic title="Total Orders" value={stats.total} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="Pending"
|
||||
value={stats.pending}
|
||||
valueStyle={{ color: '#faad14' }}
|
||||
prefix={<ClockCircleOutlined />}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="Processing"
|
||||
value={stats.processing}
|
||||
valueStyle={{ color: '#1890ff' }}
|
||||
prefix={<SyncOutlined spin />}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="Shipped"
|
||||
value={stats.shipped}
|
||||
valueStyle={{ color: '#722ed1' }}
|
||||
prefix={<TruckOutlined />}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="Total Amount"
|
||||
value={stats.totalAmount}
|
||||
precision={2}
|
||||
prefix="$"
|
||||
valueStyle={{ color: '#52c41a' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Card
|
||||
title="Order Management"
|
||||
extra={
|
||||
<Space>
|
||||
<Button icon={<ReloadOutlined />} onClick={fetchOrders}>
|
||||
Refresh
|
||||
</Button>
|
||||
<Button icon={<DownloadOutlined />} onClick={handleExport}>
|
||||
Export
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Tabs activeKey={activeTab} onChange={setActiveTab}>
|
||||
<TabPane tab="All Orders" key="all" />
|
||||
<TabPane tab={<Badge count={stats.pending} offset={[10, 0]} size="small">Pending</Badge>} key="pending" />
|
||||
<TabPane tab="Processing" key="processing" />
|
||||
<TabPane tab="Shipped" key="shipped" />
|
||||
<TabPane tab="Delivered" key="delivered" />
|
||||
</Tabs>
|
||||
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Space>
|
||||
<Button
|
||||
type="primary"
|
||||
disabled={selectedRowKeys.length === 0}
|
||||
onClick={() => handleBatchAction('confirm')}
|
||||
>
|
||||
Batch Confirm ({selectedRowKeys.length})
|
||||
</Button>
|
||||
<Button
|
||||
disabled={selectedRowKeys.length === 0}
|
||||
onClick={() => handleBatchAction('allocate')}
|
||||
>
|
||||
Batch Allocate
|
||||
</Button>
|
||||
<Button
|
||||
danger
|
||||
disabled={selectedRowKeys.length === 0}
|
||||
onClick={() => handleBatchAction('cancel')}
|
||||
>
|
||||
Batch Cancel
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<Table
|
||||
rowSelection={rowSelection}
|
||||
columns={columns}
|
||||
dataSource={getFilteredOrders()}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
scroll={{ x: 1300 }}
|
||||
pagination={{
|
||||
showSizeChanger: true,
|
||||
showTotal: (total) => `Total ${total} orders`,
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Modal
|
||||
title="Order Details"
|
||||
open={detailModalVisible}
|
||||
onCancel={() => setDetailModalVisible(false)}
|
||||
footer={[
|
||||
<Button key="close" onClick={() => setDetailModalVisible(false)}>
|
||||
Close
|
||||
</Button>,
|
||||
]}
|
||||
width={700}
|
||||
>
|
||||
{selectedOrder && (
|
||||
<>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Card size="small" title="Order Info">
|
||||
<p><strong>Order ID:</strong> {selectedOrder.orderId}</p>
|
||||
<p><strong>Platform:</strong> <Tag color={PLATFORM_MAP[selectedOrder.platform].color}>{PLATFORM_MAP[selectedOrder.platform].text}</Tag></p>
|
||||
<p><strong>Status:</strong> <Tag color={ORDER_STATUS_MAP[selectedOrder.status].color}>{ORDER_STATUS_MAP[selectedOrder.status].text}</Tag></p>
|
||||
<p><strong>Payment:</strong> <Tag color={PAYMENT_STATUS_MAP[selectedOrder.paymentStatus].color}>{PAYMENT_STATUS_MAP[selectedOrder.paymentStatus].text}</Tag></p>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Card size="small" title="Customer Info">
|
||||
<p><strong>Name:</strong> {selectedOrder.customerName}</p>
|
||||
<p><strong>Email:</strong> {selectedOrder.customerEmail}</p>
|
||||
<p><strong>Shop:</strong> {selectedOrder.shopName}</p>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
<Card size="small" title="Amount" style={{ marginTop: 16 }}>
|
||||
<Row>
|
||||
<Col span={12}>
|
||||
<Statistic
|
||||
title="Total Amount"
|
||||
value={selectedOrder.totalAmount}
|
||||
precision={2}
|
||||
prefix={selectedOrder.currency === 'USD' ? '$' : selectedOrder.currency}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Statistic title="Items" value={selectedOrder.itemCount} />
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
<Card size="small" title="Timestamps" style={{ marginTop: 16 }}>
|
||||
<p><strong>Created:</strong> {selectedOrder.createdAt}</p>
|
||||
<p><strong>Updated:</strong> {selectedOrder.updatedAt}</p>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OrderList;
|
||||
Reference in New Issue
Block a user