feat: 添加前端页面和业务说明书

refactor(server): 重构服务层代码结构
feat(server): 添加基础设施、跨境电商、AI决策等核心服务
docs: 完善前端业务说明书和开发进度文档
style: 格式化代码和文档
This commit is contained in:
2026-03-18 19:12:38 +08:00
parent c932a67be2
commit 6d0d2b6157
140 changed files with 23859 additions and 5833 deletions

View File

@@ -0,0 +1,302 @@
import React from 'react';
import { Button, Card, Table, Input, Select, DatePicker, Space, Modal, Form, InputNumber, Tag, Badge, message, Spin, Alert } from 'antd';
// 业务按钮组件
export const BusinessButton: React.FC<{
type: 'primary' | 'default' | 'dashed' | 'danger' | 'link';
onClick: () => void;
children: React.ReactNode;
loading?: boolean;
disabled?: boolean;
}> = ({ type, onClick, children, loading = false, disabled = false }) => {
return (
<Button
type={type}
onClick={onClick}
loading={loading}
disabled={disabled}
style={{
borderRadius: 4,
fontWeight: '500',
}}
>
{children}
</Button>
);
};
// 业务卡片组件
export const BusinessCard: React.FC<{
title: string;
children: React.ReactNode;
extra?: React.ReactNode;
style?: React.CSSProperties;
}> = ({ title, children, extra, style }) => {
return (
<Card
title={title}
extra={extra}
style={{
borderRadius: 8,
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.09)',
marginBottom: 16,
...style,
}}
>
{children}
</Card>
);
};
// 业务表格组件
export const BusinessTable: React.FC<{
columns: any[];
dataSource: any[];
rowKey: string;
pagination?: boolean | any;
rowSelection?: any;
}> = ({ columns, dataSource, rowKey, pagination = true, rowSelection }) => {
return (
<Table
columns={columns}
dataSource={dataSource}
rowKey={rowKey}
pagination={pagination}
rowSelection={rowSelection}
style={{
borderRadius: 8,
overflow: 'hidden',
}}
size="middle"
/>
);
};
// 业务输入框组件
export const BusinessInput: React.FC<{
placeholder: string;
value: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
style?: React.CSSProperties;
prefix?: React.ReactNode;
suffix?: React.ReactNode;
}> = ({ placeholder, value, onChange, style, prefix, suffix }) => {
return (
<Input
placeholder={placeholder}
value={value}
onChange={onChange}
style={{
borderRadius: 4,
...style,
}}
prefix={prefix}
suffix={suffix}
/>
);
};
// 业务选择器组件
export const BusinessSelect: React.FC<{
placeholder: string;
value: string;
onChange: (value: string) => void;
options: { label: string; value: string }[];
style?: React.CSSProperties;
}> = ({ placeholder, value, onChange, options, style }) => {
return (
<Select
placeholder={placeholder}
value={value}
onChange={onChange}
style={{
borderRadius: 4,
...style,
}}
>
{options.map((option) => (
<Select.Option key={option.value} value={option.value}>
{option.label}
</Select.Option>
))}
</Select>
);
};
// 业务日期选择器组件
export const BusinessDatePicker: React.FC<{
placeholder: string;
value: any;
onChange: (dates: any, dateStrings: any) => void;
style?: React.CSSProperties;
range?: boolean;
}> = ({ placeholder, value, onChange, style, range = false }) => {
const { RangePicker, DatePicker: AntDatePicker } = DatePicker;
return range ? (
<RangePicker
placeholder={[placeholder, placeholder]}
value={value}
onChange={onChange}
style={{
borderRadius: 4,
...style,
}}
/>
) : (
<AntDatePicker
placeholder={placeholder}
value={value}
onChange={onChange}
style={{
borderRadius: 4,
...style,
}}
/>
);
};
// 业务模态框组件
export const BusinessModal: React.FC<{
title: string;
open: boolean;
onCancel: () => void;
onOk: () => void;
children: React.ReactNode;
width?: number;
footer?: React.ReactNode;
}> = ({ title, open, onCancel, onOk, children, width = 600, footer }) => {
return (
<Modal
title={title}
open={open}
onCancel={onCancel}
onOk={onOk}
width={width}
footer={footer}
style={{
borderRadius: 8,
}}
>
{children}
</Modal>
);
};
// 业务表单组件
export const BusinessForm: React.FC<{
form: any;
onFinish: (values: any) => void;
children: React.ReactNode;
layout?: 'horizontal' | 'vertical' | 'inline';
}> = ({ form, onFinish, children, layout = 'vertical' }) => {
return (
<Form
form={form}
onFinish={onFinish}
layout={layout}
style={{
width: '100%',
}}
>
{children}
</Form>
);
};
// 业务加载组件
export const BusinessLoading: React.FC<{
loading: boolean;
children: React.ReactNode;
tip?: string;
}> = ({ loading, children, tip = 'Loading...' }) => {
return (
<Spin spinning={loading} tip={tip}>
{children}
</Spin>
);
};
// 业务提示组件
export const BusinessAlert: React.FC<{
type: 'success' | 'info' | 'warning' | 'error';
message: string;
description?: string;
closable?: boolean;
}> = ({ type, message, description, closable = true }) => {
return (
<Alert
type={type}
message={message}
description={description}
closable={closable}
style={{
marginBottom: 16,
}}
/>
);
};
// 业务标签组件
export const BusinessTag: React.FC<{
color: string;
children: React.ReactNode;
}> = ({ color, children }) => {
return (
<Tag color={color} style={{ borderRadius: 4 }}>
{children}
</Tag>
);
};
// 业务徽章组件
export const BusinessBadge: React.FC<{
status: 'success' | 'processing' | 'default' | 'error' | 'warning';
text: string;
}> = ({ status, text }) => {
return (
<Badge status={status} text={text} />
);
};
// 业务消息组件
export const BusinessMessage: {
success: (content: string) => void;
error: (content: string) => void;
warning: (content: string) => void;
info: (content: string) => void;
} = {
success: (content) => message.success(content),
error: (content) => message.error(content),
warning: (content) => message.warning(content),
info: (content) => message.info(content),
};
// 业务间距组件
export const BusinessSpace: React.FC<{
children: React.ReactNode;
size?: 'small' | 'middle' | 'large';
direction?: 'horizontal' | 'vertical';
}> = ({ children, size = 'middle', direction = 'horizontal' }) => {
return (
<Space size={size} direction={direction}>
{children}
</Space>
);
};
export default {
BusinessButton,
BusinessCard,
BusinessTable,
BusinessInput,
BusinessSelect,
BusinessDatePicker,
BusinessModal,
BusinessForm,
BusinessLoading,
BusinessAlert,
BusinessTag,
BusinessBadge,
BusinessMessage,
BusinessSpace,
};

View File

@@ -0,0 +1,26 @@
import React from 'react';
import { Layout as AntLayout } from 'antd';
import MenuComponent from './MenuComponent';
import { Outlet } from 'react-router-dom';
const { Sider, Content } = AntLayout;
const Layout: React.FC = () => {
return (
<AntLayout style={{ minHeight: '100vh' }}>
<Sider width={200} style={{ background: '#fff' }}>
<div className="logo" style={{ padding: '16px', textAlign: 'center', fontWeight: 'bold', fontSize: '18px' }}>
Crawlful Hub
</div>
<MenuComponent />
</Sider>
<AntLayout>
<Content style={{ margin: '24px 16px', padding: 24, background: '#fff', minHeight: 280 }}>
<Outlet />
</Content>
</AntLayout>
</AntLayout>
);
};
export default Layout;

View File

@@ -0,0 +1,89 @@
import React from 'react';
import { Menu } from 'antd';
import {
DashboardOutlined,
ShoppingOutlined,
OrderedListOutlined,
TabletOutlined,
InboxOutlined,
TeamOutlined,
DollarOutlined,
SafetyCertificateOutlined,
SettingOutlined,
HomeOutlined
} from '@ant-design/icons';
import { Link, useLocation } from 'react-router-dom';
const MenuComponent: React.FC = () => {
const location = useLocation();
const currentPath = location.pathname;
const menuItems = [
{
key: '/dashboard',
icon: <DashboardOutlined />,
label: <Link to="/dashboard">Dashboard</Link>,
},
{
key: '/product',
icon: <ShoppingOutlined />,
label: <Link to="/product">Product</Link>,
},
{
key: '/orders',
icon: <OrderedListOutlined />,
label: <Link to="/orders">Orders</Link>,
},
{
key: '/ad',
icon: <TabletOutlined />,
label: <Link to="/ad">Ad</Link>,
},
{
key: '/inventory',
icon: <InboxOutlined />,
label: <Link to="/inventory">Inventory</Link>,
},
{
key: '/b2b',
icon: <TeamOutlined />,
label: <Link to="/b2b">B2B</Link>,
},
{
key: '/finance',
icon: <DollarOutlined />,
label: <Link to="/finance">Finance</Link>,
},
{
key: '/compliance',
icon: <SafetyCertificateOutlined />,
label: <Link to="/compliance">Compliance</Link>,
},
{
key: '/settings',
icon: <SettingOutlined />,
label: <Link to="/settings">Settings</Link>,
},
{
key: '/independent-site',
icon: <HomeOutlined />,
label: <Link to="/independent-site">Independent Site</Link>,
},
];
return (
<div className="menu-container">
<Menu
mode="inline"
selectedKeys={[currentPath]}
style={{
height: '100%',
borderRight: 0,
}}
items={menuItems}
/>
</div>
);
};
export default MenuComponent;

435
client/src/pages/AdPage.tsx Normal file
View File

@@ -0,0 +1,435 @@
import React, { useState } from 'react';
import { Card, Table, Button, Select, DatePicker, Space, Modal, Typography, Badge, message, Tabs, Row, Col, Statistic } from 'antd';
import { PlusOutlined, EyeOutlined, EditOutlined, DeleteOutlined, ReloadOutlined, PlayCircleOutlined, PauseCircleOutlined, BarChartOutlined, DollarOutlined } from '@ant-design/icons';
const { Option } = Select;
const { RangePicker } = DatePicker;
const { Title, Text } = Typography;
const { TabPane } = Tabs;
const mockAds = [
{
id: '1',
name: 'Electronics Sale Campaign',
platform: 'Amazon',
status: 'active',
budget: 1000,
spent: 650,
clicks: 1250,
impressions: 15000,
ctr: 8.33,
conversions: 85,
cpa: 7.65,
roi: 2.5,
createdAt: '2026-03-01',
updatedAt: '2026-03-18',
},
{
id: '2',
name: 'Fashion Week Promotion',
platform: 'eBay',
status: 'paused',
budget: 800,
spent: 400,
clicks: 800,
impressions: 10000,
ctr: 8.0,
conversions: 45,
cpa: 8.89,
roi: 1.8,
createdAt: '2026-03-05',
updatedAt: '2026-03-15',
},
{
id: '3',
name: 'Home Decor Spring Sale',
platform: 'Shopee',
status: 'active',
budget: 600,
spent: 350,
clicks: 700,
impressions: 9000,
ctr: 7.78,
conversions: 40,
cpa: 8.75,
roi: 2.2,
createdAt: '2026-03-10',
updatedAt: '2026-03-18',
},
{
id: '4',
name: 'Sports Equipment Launch',
platform: 'Amazon',
status: 'completed',
budget: 1200,
spent: 1180,
clicks: 2400,
impressions: 30000,
ctr: 8.0,
conversions: 180,
cpa: 6.56,
roi: 3.2,
createdAt: '2026-02-20',
updatedAt: '2026-03-10',
},
];
const AdPage: React.FC = () => {
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
const [isDetailModalVisible, setIsDetailModalVisible] = useState(false);
const [selectedAd, setSelectedAd] = useState<any>(null);
const [activeTab, setActiveTab] = useState('campaigns');
const handleViewDetail = (record: any) => {
setSelectedAd(record);
setIsDetailModalVisible(true);
};
const handleCreateAd = () => {
message.success('Create ad campaign form opened');
};
const handleEditAd = (record: any) => {
message.success(`Edit ad campaign ${record.name}`);
};
const handleDeleteAd = (record: any) => {
message.success(`Delete ad campaign ${record.name}`);
};
const handleStartAd = (record: any) => {
message.success(`Start ad campaign ${record.name}`);
};
const handlePauseAd = (record: any) => {
message.success(`Pause ad campaign ${record.name}`);
};
const handleBatchDelete = () => {
message.success('Batch delete initiated');
};
const handleBatchPause = () => {
message.success('Batch pause initiated');
};
const handleBatchStart = () => {
message.success('Batch start initiated');
};
const getStatusColor = (status: string) => {
switch (status) {
case 'active': return 'success';
case 'paused': return 'warning';
case 'completed': return 'default';
case 'error': return 'error';
default: return 'default';
}
};
const columns = [
{
title: 'Campaign Name',
dataIndex: 'name',
key: 'name',
},
{
title: 'Platform',
dataIndex: 'platform',
key: 'platform',
},
{
title: 'Status',
dataIndex: 'status',
key: 'status',
render: (status: string) => (
<Badge status={getStatusColor(status)} text={status} />
),
},
{
title: 'Budget',
dataIndex: 'budget',
key: 'budget',
render: (value: number) => `$${value.toFixed(2)}`,
},
{
title: 'Spent',
dataIndex: 'spent',
key: 'spent',
render: (value: number) => `$${value.toFixed(2)}`,
},
{
title: 'Clicks',
dataIndex: 'clicks',
key: 'clicks',
},
{
title: 'Impressions',
dataIndex: 'impressions',
key: 'impressions',
},
{
title: 'CTR',
dataIndex: 'ctr',
key: 'ctr',
render: (value: number) => `${value.toFixed(2)}%`,
},
{
title: 'Conversions',
dataIndex: 'conversions',
key: 'conversions',
},
{
title: 'CPA',
dataIndex: 'cpa',
key: 'cpa',
render: (value: number) => `$${value.toFixed(2)}`,
},
{
title: 'ROI',
dataIndex: 'roi',
key: 'roi',
render: (value: number) => `${value.toFixed(2)}x`,
},
{
title: 'Actions',
key: 'actions',
render: (_: any, record: any) => (
<Space size="middle">
<Button icon={<EyeOutlined />} onClick={() => handleViewDetail(record)}>View</Button>
<Button icon={<EditOutlined />} onClick={() => handleEditAd(record)}>Edit</Button>
{record.status === 'active' ? (
<Button icon={<PauseCircleOutlined />} onClick={() => handlePauseAd(record)}>Pause</Button>
) : (
<Button icon={<PlayCircleOutlined />} onClick={() => handleStartAd(record)}>Start</Button>
)}
<Button icon={<BarChartOutlined />} onClick={() => handleViewDetail(record)}>Analytics</Button>
<Button icon={<DeleteOutlined />} danger onClick={() => handleDeleteAd(record)}>Delete</Button>
</Space>
),
},
];
const rowSelection = {
selectedRowKeys,
onChange: (keys: React.Key[]) => {
setSelectedRowKeys(keys);
},
};
return (
<div className="ad-page">
<div className="page-header" style={{ marginBottom: 24, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Title level={4}>Advertising Management</Title>
<Space>
<Button icon={<PlusOutlined />} type="primary" onClick={handleCreateAd}>Create Campaign</Button>
<Button icon={<ReloadOutlined />}>Refresh</Button>
<Button onClick={handleBatchStart} disabled={selectedRowKeys.length === 0}>Batch Start</Button>
<Button onClick={handleBatchPause} disabled={selectedRowKeys.length === 0}>Batch Pause</Button>
<Button onClick={handleBatchDelete} disabled={selectedRowKeys.length === 0} danger>Batch Delete</Button>
</Space>
</div>
<Card style={{ marginBottom: 24 }}>
<Row gutter={[16, 16]}>
<Col span={6}>
<Statistic
title="Total Budget"
value={3600}
precision={2}
prefix="$"
valueStyle={{ color: '#3f8600' }}
/>
</Col>
<Col span={6}>
<Statistic
title="Total Spent"
value={2580}
precision={2}
prefix="$"
valueStyle={{ color: '#cf1322' }}
/>
</Col>
<Col span={6}>
<Statistic
title="Total Clicks"
value={5150}
valueStyle={{ color: '#1890ff' }}
/>
</Col>
<Col span={6}>
<Statistic
title="Total Conversions"
value={350}
valueStyle={{ color: '#52c41a' }}
/>
</Col>
</Row>
</Card>
<Card style={{ marginBottom: 24 }}>
<Tabs activeKey={activeTab} onChange={setActiveTab}>
<TabPane tab="Campaigns" key="campaigns">
<div style={{ display: 'flex', gap: 16, marginBottom: 16, flexWrap: 'wrap' }}>
<Select placeholder="Select Platform" style={{ width: 150 }}>
<Option value="all">All Platforms</Option>
<Option value="amazon">Amazon</Option>
<Option value="ebay">eBay</Option>
<Option value="shopee">Shopee</Option>
</Select>
<Select placeholder="Select Status" style={{ width: 150 }}>
<Option value="all">All Status</Option>
<Option value="active">Active</Option>
<Option value="paused">Paused</Option>
<Option value="completed">Completed</Option>
</Select>
<RangePicker style={{ width: 300 }} />
</div>
<Table
rowSelection={rowSelection}
columns={columns}
dataSource={mockAds}
rowKey="id"
pagination={{
pageSize: 10,
}}
/>
</TabPane>
<TabPane tab="Analytics" key="analytics">
<div style={{ padding: 24, textAlign: 'center' }}>
<Text type="secondary">Analytics dashboard will be displayed here</Text>
</div>
</TabPane>
<TabPane tab="Optimization" key="optimization">
<div style={{ padding: 24, textAlign: 'center' }}>
<Text type="secondary">AI optimization suggestions will be displayed here</Text>
</div>
</TabPane>
</Tabs>
</Card>
<Modal
title={`Campaign Detail - ${selectedAd?.name}`}
open={isDetailModalVisible}
onCancel={() => setIsDetailModalVisible(false)}
footer={[
<Button key="close" onClick={() => setIsDetailModalVisible(false)}>
Close
</Button>,
]}
width={1000}
>
{selectedAd && (
<Tabs defaultActiveKey="details">
<TabPane tab="Campaign Details" key="details">
<div style={{ marginBottom: 24 }}>
<h3>Basic Information</h3>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16, marginTop: 16 }}>
<div>
<Text strong>Campaign Name:</Text> {selectedAd.name}
</div>
<div>
<Text strong>Platform:</Text> {selectedAd.platform}
</div>
<div>
<Text strong>Status:</Text> <Badge status={getStatusColor(selectedAd.status)} text={selectedAd.status} />
</div>
<div>
<Text strong>Budget:</Text> ${selectedAd.budget.toFixed(2)}
</div>
<div>
<Text strong>Spent:</Text> ${selectedAd.spent.toFixed(2)}
</div>
<div>
<Text strong>Created At:</Text> {selectedAd.createdAt}
</div>
<div>
<Text strong>Updated At:</Text> {selectedAd.updatedAt}
</div>
</div>
</div>
<div>
<h3>Performance Metrics</h3>
<Row gutter={[16, 16]} style={{ marginTop: 16 }}>
<Col span={8}>
<Card>
<Statistic
title="Clicks"
value={selectedAd.clicks}
valueStyle={{ color: '#1890ff' }}
/>
</Card>
</Col>
<Col span={8}>
<Card>
<Statistic
title="Impressions"
value={selectedAd.impressions}
valueStyle={{ color: '#52c41a' }}
/>
</Card>
</Col>
<Col span={8}>
<Card>
<Statistic
title="CTR"
value={selectedAd.ctr}
precision={2}
suffix="%"
valueStyle={{ color: '#faad14' }}
/>
</Card>
</Col>
<Col span={8}>
<Card>
<Statistic
title="Conversions"
value={selectedAd.conversions}
valueStyle={{ color: '#722ed1' }}
/>
</Card>
</Col>
<Col span={8}>
<Card>
<Statistic
title="CPA"
value={selectedAd.cpa}
precision={2}
prefix="$"
valueStyle={{ color: '#eb2f96' }}
/>
</Card>
</Col>
<Col span={8}>
<Card>
<Statistic
title="ROI"
value={selectedAd.roi}
precision={2}
suffix="x"
valueStyle={{ color: '#3f8600' }}
/>
</Card>
</Col>
</Row>
</div>
</TabPane>
<TabPane tab="Analytics" key="analytics">
<div style={{ padding: 24, textAlign: 'center' }}>
<Text type="secondary">Detailed analytics will be displayed here</Text>
</div>
</TabPane>
<TabPane tab="Optimization" key="optimization">
<div style={{ padding: 24, textAlign: 'center' }}>
<Text type="secondary">AI optimization suggestions will be displayed here</Text>
</div>
</TabPane>
</Tabs>
)}
</Modal>
</div>
);
};
export default AdPage;

View File

@@ -0,0 +1,474 @@
import React, { useState } from 'react';
import { Card, Table, Button, Select, DatePicker, Space, Modal, Typography, Badge, message, Tabs, Row, Col, Statistic, Input, Form } from 'antd';
import { PlusOutlined, EyeOutlined, EditOutlined, DeleteOutlined, ReloadOutlined, FileTextOutlined, DollarOutlined, TeamOutlined, CheckCircleOutlined, CloseCircleOutlined } from '@ant-design/icons';
const { Option } = Select;
const { RangePicker } = DatePicker;
const { Title, Text } = Typography;
const { TabPane } = Tabs;
const { TextArea } = Input;
const mockQuotes = [
{
id: '1',
quoteNo: 'B2B-2026-001',
customer: 'ABC Electronics Inc.',
contact: 'John Smith',
email: 'john@abc-electronics.com',
totalAmount: 15000,
status: 'pending',
items: [
{ name: 'Wireless Headphones', quantity: 100, price: 50 },
{ name: 'Bluetooth Speaker', quantity: 50, price: 80 },
],
createdAt: '2026-03-15',
updatedAt: '2026-03-18',
},
{
id: '2',
quoteNo: 'B2B-2026-002',
customer: 'XYZ Fashion Ltd.',
contact: 'Jane Doe',
email: 'jane@xyz-fashion.com',
totalAmount: 25000,
status: 'approved',
items: [
{ name: 'Smart Watch', quantity: 80, price: 120 },
{ name: 'Fitness Tracker', quantity: 120, price: 80 },
],
createdAt: '2026-03-10',
updatedAt: '2026-03-16',
},
{
id: '3',
quoteNo: 'B2B-2026-003',
customer: 'Global Home Decor',
contact: 'Bob Johnson',
email: 'bob@global-home.com',
totalAmount: 8000,
status: 'rejected',
items: [
{ name: 'Home Decor Set', quantity: 40, price: 200 },
],
createdAt: '2026-03-05',
updatedAt: '2026-03-12',
},
{
id: '4',
quoteNo: 'B2B-2026-004',
customer: 'Sports Equipment Co.',
contact: 'Alice Williams',
email: 'alice@sports-equip.com',
totalAmount: 18000,
status: 'completed',
items: [
{ name: 'Sports Equipment Set', quantity: 60, price: 300 },
],
createdAt: '2026-03-01',
updatedAt: '2026-03-10',
},
];
const mockContracts = [
{
id: '1',
contractNo: 'CON-2026-001',
customer: 'ABC Electronics Inc.',
startDate: '2026-03-01',
endDate: '2026-12-31',
totalAmount: 150000,
status: 'active',
terms: 'Net 30 days',
createdAt: '2026-02-28',
},
{
id: '2',
contractNo: 'CON-2026-002',
customer: 'XYZ Fashion Ltd.',
startDate: '2026-03-15',
endDate: '2027-03-14',
totalAmount: 300000,
status: 'active',
terms: 'Net 45 days',
createdAt: '2026-03-10',
},
{
id: '3',
contractNo: 'CON-2026-003',
customer: 'Global Home Decor',
startDate: '2026-01-01',
endDate: '2026-12-31',
totalAmount: 100000,
status: 'expired',
terms: 'Net 30 days',
createdAt: '2025-12-28',
},
];
const B2BPage: React.FC = () => {
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
const [isDetailModalVisible, setIsDetailModalVisible] = useState(false);
const [selectedQuote, setSelectedQuote] = useState<any>(null);
const [activeTab, setActiveTab] = useState('quotes');
const [searchKeyword, setSearchKeyword] = useState('');
const handleViewDetail = (record: any) => {
setSelectedQuote(record);
setIsDetailModalVisible(true);
};
const handleCreateQuote = () => {
message.success('Create quote form opened');
};
const handleEditQuote = (record: any) => {
message.success(`Edit quote ${record.quoteNo}`);
};
const handleDeleteQuote = (record: any) => {
message.success(`Delete quote ${record.quoteNo}`);
};
const handleApproveQuote = (record: any) => {
message.success(`Approve quote ${record.quoteNo}`);
};
const handleRejectQuote = (record: any) => {
message.success(`Reject quote ${record.quoteNo}`);
};
const handleConvertToOrder = (record: any) => {
message.success(`Convert quote ${record.quoteNo} to order`);
};
const handleBatchDelete = () => {
message.success('Batch delete initiated');
};
const handleBatchApprove = () => {
message.success('Batch approve initiated');
};
const getStatusColor = (status: string) => {
switch (status) {
case 'pending': return 'processing';
case 'approved': return 'success';
case 'rejected': return 'error';
case 'completed': return 'success';
case 'active': return 'success';
case 'expired': return 'default';
default: return 'default';
}
};
const quoteColumns = [
{
title: 'Quote No',
dataIndex: 'quoteNo',
key: 'quoteNo',
},
{
title: 'Customer',
dataIndex: 'customer',
key: 'customer',
},
{
title: 'Contact',
dataIndex: 'contact',
key: 'contact',
},
{
title: 'Email',
dataIndex: 'email',
key: 'email',
},
{
title: 'Total Amount',
dataIndex: 'totalAmount',
key: 'totalAmount',
render: (value: number) => `$${value.toLocaleString()}`,
},
{
title: 'Status',
dataIndex: 'status',
key: 'status',
render: (status: string) => (
<Badge status={getStatusColor(status)} text={status} />
),
},
{
title: 'Created At',
dataIndex: 'createdAt',
key: 'createdAt',
},
{
title: 'Actions',
key: 'actions',
render: (_: any, record: any) => (
<Space size="middle">
<Button icon={<EyeOutlined />} onClick={() => handleViewDetail(record)}>View</Button>
<Button icon={<EditOutlined />} onClick={() => handleEditQuote(record)}>Edit</Button>
{record.status === 'pending' && (
<>
<Button icon={<CheckCircleOutlined />} type="primary" onClick={() => handleApproveQuote(record)}>Approve</Button>
<Button icon={<CloseCircleOutlined />} danger onClick={() => handleRejectQuote(record)}>Reject</Button>
</>
)}
{record.status === 'approved' && (
<Button icon={<FileTextOutlined />} onClick={() => handleConvertToOrder(record)}>Convert to Order</Button>
)}
<Button icon={<DeleteOutlined />} danger onClick={() => handleDeleteQuote(record)}>Delete</Button>
</Space>
),
},
];
const contractColumns = [
{
title: 'Contract No',
dataIndex: 'contractNo',
key: 'contractNo',
},
{
title: 'Customer',
dataIndex: 'customer',
key: 'customer',
},
{
title: 'Start Date',
dataIndex: 'startDate',
key: 'startDate',
},
{
title: 'End Date',
dataIndex: 'endDate',
key: 'endDate',
},
{
title: 'Total Amount',
dataIndex: 'totalAmount',
key: 'totalAmount',
render: (value: number) => `$${value.toLocaleString()}`,
},
{
title: 'Status',
dataIndex: 'status',
key: 'status',
render: (status: string) => (
<Badge status={getStatusColor(status)} text={status} />
),
},
{
title: 'Terms',
dataIndex: 'terms',
key: 'terms',
},
{
title: 'Created At',
dataIndex: 'createdAt',
key: 'createdAt',
},
];
const rowSelection = {
selectedRowKeys,
onChange: (keys: React.Key[]) => {
setSelectedRowKeys(keys);
},
};
const filteredQuotes = mockQuotes.filter(quote =>
quote.customer.toLowerCase().includes(searchKeyword.toLowerCase()) ||
quote.quoteNo.toLowerCase().includes(searchKeyword.toLowerCase())
);
return (
<div className="b2b-page">
<div className="page-header" style={{ marginBottom: 24, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Title level={4}>B2B Trade Management</Title>
<Space>
<Button icon={<PlusOutlined />} type="primary" onClick={handleCreateQuote}>Create Quote</Button>
<Button icon={<ReloadOutlined />}>Refresh</Button>
<Button onClick={handleBatchApprove} disabled={selectedRowKeys.length === 0}>Batch Approve</Button>
<Button onClick={handleBatchDelete} disabled={selectedRowKeys.length === 0} danger>Batch Delete</Button>
</Space>
</div>
<Card style={{ marginBottom: 24 }}>
<Row gutter={[16, 16]}>
<Col span={6}>
<Statistic
title="Total Quotes"
value={4}
valueStyle={{ color: '#3f8600' }}
/>
</Col>
<Col span={6}>
<Statistic
title="Pending Quotes"
value={1}
valueStyle={{ color: '#1890ff' }}
/>
</Col>
<Col span={6}>
<Statistic
title="Approved Quotes"
value={1}
valueStyle={{ color: '#52c41a' }}
/>
</Col>
<Col span={6}>
<Statistic
title="Total Contract Value"
value={550000}
precision={0}
prefix="$"
valueStyle={{ color: '#722ed1' }}
/>
</Col>
</Row>
</Card>
<Card style={{ marginBottom: 24 }}>
<Tabs activeKey={activeTab} onChange={setActiveTab}>
<TabPane tab="Quotes" key="quotes">
<div style={{ display: 'flex', gap: 16, marginBottom: 16, flexWrap: 'wrap' }}>
<Input
placeholder="Search by customer or quote number"
value={searchKeyword}
onChange={(e) => setSearchKeyword(e.target.value)}
style={{ width: 300 }}
allowClear
/>
<Select placeholder="Select Status" style={{ width: 150 }}>
<Option value="all">All Status</Option>
<Option value="pending">Pending</Option>
<Option value="approved">Approved</Option>
<Option value="rejected">Rejected</Option>
<Option value="completed">Completed</Option>
</Select>
<RangePicker style={{ width: 300 }} />
</div>
<Table
rowSelection={rowSelection}
columns={quoteColumns}
dataSource={filteredQuotes}
rowKey="id"
pagination={{
pageSize: 10,
}}
/>
</TabPane>
<TabPane tab="Contracts" key="contracts">
<div style={{ display: 'flex', gap: 16, marginBottom: 16, flexWrap: 'wrap' }}>
<Select placeholder="Select Status" style={{ width: 150 }}>
<Option value="all">All Status</Option>
<Option value="active">Active</Option>
<Option value="expired">Expired</Option>
</Select>
<RangePicker style={{ width: 300 }} />
</div>
<Table
columns={contractColumns}
dataSource={mockContracts}
rowKey="id"
pagination={{
pageSize: 10,
}}
/>
</TabPane>
</Tabs>
</Card>
<Modal
title={`Quote Detail - ${selectedQuote?.quoteNo}`}
open={isDetailModalVisible}
onCancel={() => setIsDetailModalVisible(false)}
footer={[
<Button key="close" onClick={() => setIsDetailModalVisible(false)}>
Close
</Button>,
]}
width={1000}
>
{selectedQuote && (
<Tabs defaultActiveKey="details">
<TabPane tab="Quote Details" key="details">
<div style={{ marginBottom: 24 }}>
<h3>Basic Information</h3>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16, marginTop: 16 }}>
<div>
<Text strong>Quote No:</Text> {selectedQuote.quoteNo}
</div>
<div>
<Text strong>Customer:</Text> {selectedQuote.customer}
</div>
<div>
<Text strong>Contact:</Text> {selectedQuote.contact}
</div>
<div>
<Text strong>Email:</Text> {selectedQuote.email}
</div>
<div>
<Text strong>Total Amount:</Text> ${selectedQuote.totalAmount.toLocaleString()}
</div>
<div>
<Text strong>Status:</Text> <Badge status={getStatusColor(selectedQuote.status)} text={selectedQuote.status} />
</div>
<div>
<Text strong>Created At:</Text> {selectedQuote.createdAt}
</div>
<div>
<Text strong>Updated At:</Text> {selectedQuote.updatedAt}
</div>
</div>
</div>
<div>
<h3>Quote Items</h3>
<Table
columns={[
{ title: 'Product', dataIndex: 'name', key: 'name' },
{ title: 'Quantity', dataIndex: 'quantity', key: 'quantity' },
{ title: 'Unit Price', dataIndex: 'price', key: 'price', render: (value: number) => `$${value.toFixed(2)}` },
{ title: 'Total', key: 'total', render: (_: any, record: any) => `$${(record.quantity * record.price).toFixed(2)}` },
]}
dataSource={selectedQuote.items}
rowKey="name"
pagination={false}
/>
</div>
<div style={{ marginTop: 24 }}>
<h3>Quick Actions</h3>
<Space size="middle" style={{ marginTop: 16 }}>
{selectedQuote.status === 'pending' && (
<>
<Button icon={<CheckCircleOutlined />} type="primary" onClick={() => handleApproveQuote(selectedQuote)}>Approve</Button>
<Button icon={<CloseCircleOutlined />} danger onClick={() => handleRejectQuote(selectedQuote)}>Reject</Button>
</>
)}
{selectedQuote.status === 'approved' && (
<Button icon={<FileTextOutlined />} onClick={() => handleConvertToOrder(selectedQuote)}>Convert to Order</Button>
)}
</Space>
</div>
</TabPane>
<TabPane tab="Customer Info" key="customer">
<div style={{ padding: 24, textAlign: 'center' }}>
<Text type="secondary">Customer information will be displayed here</Text>
</div>
</TabPane>
<TabPane tab="History" key="history">
<div style={{ padding: 24, textAlign: 'center' }}>
<Text type="secondary">Quote history will be displayed here</Text>
</div>
</TabPane>
</Tabs>
)}
</Modal>
</div>
);
};
export default B2BPage;

View File

@@ -0,0 +1,500 @@
import React, { useState } from 'react';
import { Card, Table, Button, Select, DatePicker, Space, Modal, Typography, Badge, message, Tabs, Row, Col, Statistic, Input, Alert } from 'antd';
import { PlusOutlined, EyeOutlined, EditOutlined, DeleteOutlined, ReloadOutlined, WarningOutlined, CheckCircleOutlined, ExclamationCircleOutlined, SafetyCertificateOutlined, FileTextOutlined } from '@ant-design/icons';
const { Option } = Select;
const { RangePicker } = DatePicker;
const { Title, Text } = Typography;
const { TabPane } = Tabs;
const mockCertificates = [
{
id: '1',
certificateNo: 'CERT-2026-001',
name: 'CE Certification',
type: 'Product Safety',
status: 'valid',
expiryDate: '2026-12-31',
issuingAuthority: 'European Commission',
products: ['Wireless Headphones', 'Bluetooth Speaker'],
document: 'ce-cert-001.pdf',
createdAt: '2026-01-15',
updatedAt: '2026-03-18',
},
{
id: '2',
certificateNo: 'CERT-2026-002',
name: 'FCC Certification',
type: 'Electromagnetic Compatibility',
status: 'valid',
expiryDate: '2027-06-30',
issuingAuthority: 'Federal Communications Commission',
products: ['Smart Watch', 'Fitness Tracker'],
document: 'fcc-cert-002.pdf',
createdAt: '2026-02-20',
updatedAt: '2026-03-18',
},
{
id: '3',
certificateNo: 'CERT-2026-003',
name: 'RoHS Certification',
type: 'Environmental',
status: 'expiring',
expiryDate: '2026-04-30',
issuingAuthority: 'European Union',
products: ['Home Decor Set'],
document: 'rohs-cert-003.pdf',
createdAt: '2026-01-10',
updatedAt: '2026-03-18',
},
{
id: '4',
certificateNo: 'CERT-2026-004',
name: 'ISO 9001 Certification',
type: 'Quality Management',
status: 'expired',
expiryDate: '2026-02-28',
issuingAuthority: 'International Organization for Standardization',
products: ['All Products'],
document: 'iso-9001-004.pdf',
createdAt: '2025-12-01',
updatedAt: '2026-03-01',
},
];
const mockComplianceChecks = [
{
id: '1',
checkNo: 'COMP-2026-001',
productId: 'P001',
productName: 'Wireless Headphones',
platform: 'Amazon',
checkType: 'Product Listing',
status: 'passed',
issues: [],
checkedAt: '2026-03-18',
checkedBy: 'System',
},
{
id: '2',
checkNo: 'COMP-2026-002',
productId: 'P002',
productName: 'Bluetooth Speaker',
platform: 'eBay',
checkType: 'Product Listing',
status: 'warning',
issues: ['Missing product description', 'Incomplete specifications'],
checkedAt: '2026-03-18',
checkedBy: 'System',
},
{
id: '3',
checkNo: 'COMP-2026-003',
productId: 'P003',
productName: 'Smart Watch',
platform: 'Shopee',
checkType: 'Product Listing',
status: 'failed',
issues: ['Invalid certificate', 'Missing safety warnings'],
checkedAt: '2026-03-17',
checkedBy: 'System',
},
];
const CompliancePage: React.FC = () => {
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
const [isDetailModalVisible, setIsDetailModalVisible] = useState(false);
const [selectedCertificate, setSelectedCertificate] = useState<any>(null);
const [activeTab, setActiveTab] = useState('certificates');
const [searchKeyword, setSearchKeyword] = useState('');
const handleViewDetail = (record: any) => {
setSelectedCertificate(record);
setIsDetailModalVisible(true);
};
const handleAddCertificate = () => {
message.success('Add certificate form opened');
};
const handleEditCertificate = (record: any) => {
message.success(`Edit certificate ${record.certificateNo}`);
};
const handleDeleteCertificate = (record: any) => {
message.success(`Delete certificate ${record.certificateNo}`);
};
const handleRenewCertificate = (record: any) => {
message.success(`Renew certificate ${record.certificateNo}`);
};
const handleRunComplianceCheck = () => {
message.success('Compliance check initiated');
};
const handleBatchDelete = () => {
message.success('Batch delete initiated');
};
const handleBatchRenew = () => {
message.success('Batch renew initiated');
};
const getStatusColor = (status: string) => {
switch (status) {
case 'valid': return 'success';
case 'expiring': return 'warning';
case 'expired': return 'error';
case 'passed': return 'success';
case 'warning': return 'warning';
case 'failed': return 'error';
default: return 'default';
}
};
const getStatusIcon = (status: string) => {
switch (status) {
case 'valid': return <CheckCircleOutlined />;
case 'expiring': return <ExclamationCircleOutlined />;
case 'expired': return <WarningOutlined />;
case 'passed': return <CheckCircleOutlined />;
case 'warning': return <ExclamationCircleOutlined />;
case 'failed': return <WarningOutlined />;
default: return null;
}
};
const certificateColumns = [
{
title: 'Certificate No',
dataIndex: 'certificateNo',
key: 'certificateNo',
},
{
title: 'Name',
dataIndex: 'name',
key: 'name',
},
{
title: 'Type',
dataIndex: 'type',
key: 'type',
},
{
title: 'Status',
dataIndex: 'status',
key: 'status',
render: (status: string) => (
<Badge status={getStatusColor(status)} text={status} />
),
},
{
title: 'Expiry Date',
dataIndex: 'expiryDate',
key: 'expiryDate',
render: (date: string) => {
const expiryDate = new Date(date);
const today = new Date();
const daysUntilExpiry = Math.ceil((expiryDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
if (daysUntilExpiry < 0) {
return <span style={{ color: '#f5222d' }}>{date} (Expired)</span>;
} else if (daysUntilExpiry <= 30) {
return <span style={{ color: '#faad14' }}>{date} (Expiring Soon)</span>;
} else {
return <span style={{ color: '#52c41a' }}>{date}</span>;
}
},
},
{
title: 'Issuing Authority',
dataIndex: 'issuingAuthority',
key: 'issuingAuthority',
},
{
title: 'Products',
dataIndex: 'products',
key: 'products',
render: (products: string[]) => products.join(', '),
},
{
title: 'Actions',
key: 'actions',
render: (_: any, record: any) => (
<Space size="middle">
<Button icon={<EyeOutlined />} onClick={() => handleViewDetail(record)}>View</Button>
<Button icon={<EditOutlined />} onClick={() => handleEditCertificate(record)}>Edit</Button>
<Button icon={<SafetyCertificateOutlined />} onClick={() => handleRenewCertificate(record)}>Renew</Button>
<Button icon={<DeleteOutlined />} danger onClick={() => handleDeleteCertificate(record)}>Delete</Button>
</Space>
),
},
];
const checkColumns = [
{
title: 'Check No',
dataIndex: 'checkNo',
key: 'checkNo',
},
{
title: 'Product',
dataIndex: 'productName',
key: 'productName',
},
{
title: 'Platform',
dataIndex: 'platform',
key: 'platform',
},
{
title: 'Check Type',
dataIndex: 'checkType',
key: 'checkType',
},
{
title: 'Status',
dataIndex: 'status',
key: 'status',
render: (status: string) => (
<Badge status={getStatusColor(status)} text={status} />
),
},
{
title: 'Issues',
dataIndex: 'issues',
key: 'issues',
render: (issues: string[]) => (
<div>
{issues.length === 0 ? (
<Text type="success">No issues</Text>
) : (
issues.map((issue, index) => (
<div key={index} style={{ color: '#f5222d' }}>
{issue}
</div>
))
)}
</div>
),
},
{
title: 'Checked At',
dataIndex: 'checkedAt',
key: 'checkedAt',
},
{
title: 'Checked By',
dataIndex: 'checkedBy',
key: 'checkedBy',
},
];
const rowSelection = {
selectedRowKeys,
onChange: (keys: React.Key[]) => {
setSelectedRowKeys(keys);
},
};
const filteredCertificates = mockCertificates.filter(certificate =>
certificate.name.toLowerCase().includes(searchKeyword.toLowerCase()) ||
certificate.certificateNo.toLowerCase().includes(searchKeyword.toLowerCase())
);
return (
<div className="compliance-page">
<div className="page-header" style={{ marginBottom: 24, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Title level={4}>Compliance Management</Title>
<Space>
<Button icon={<PlusOutlined />} type="primary" onClick={handleAddCertificate}>Add Certificate</Button>
<Button icon={<ReloadOutlined />}>Refresh</Button>
<Button icon={<SafetyCertificateOutlined />} onClick={handleRunComplianceCheck}>Run Compliance Check</Button>
<Button onClick={handleBatchRenew} disabled={selectedRowKeys.length === 0}>Batch Renew</Button>
<Button onClick={handleBatchDelete} disabled={selectedRowKeys.length === 0} danger>Batch Delete</Button>
</Space>
</div>
<Card style={{ marginBottom: 24 }}>
<Row gutter={[16, 16]}>
<Col span={6}>
<Statistic
title="Total Certificates"
value={4}
valueStyle={{ color: '#3f8600' }}
/>
</Col>
<Col span={6}>
<Statistic
title="Valid Certificates"
value={2}
valueStyle={{ color: '#52c41a' }}
/>
</Col>
<Col span={6}>
<Statistic
title="Expiring Soon"
value={1}
valueStyle={{ color: '#faad14' }}
/>
</Col>
<Col span={6}>
<Statistic
title="Expired Certificates"
value={1}
valueStyle={{ color: '#f5222d' }}
/>
</Col>
</Row>
</Card>
<Alert
message="Certificate Expiry Warning"
description="You have 1 certificate expiring within 30 days and 1 expired certificate. Please renew them as soon as possible."
type="warning"
showIcon
style={{ marginBottom: 24 }}
/>
<Card style={{ marginBottom: 24 }}>
<Tabs activeKey={activeTab} onChange={setActiveTab}>
<TabPane tab="Certificates" key="certificates">
<div style={{ display: 'flex', gap: 16, marginBottom: 16, flexWrap: 'wrap' }}>
<Input
placeholder="Search by certificate name or number"
value={searchKeyword}
onChange={(e) => setSearchKeyword(e.target.value)}
style={{ width: 300 }}
allowClear
/>
<Select placeholder="Select Type" style={{ width: 150 }}>
<Option value="all">All Types</Option>
<Option value="Product Safety">Product Safety</Option>
<Option value="Electromagnetic Compatibility">Electromagnetic Compatibility</Option>
<Option value="Environmental">Environmental</Option>
<Option value="Quality Management">Quality Management</Option>
</Select>
<Select placeholder="Select Status" style={{ width: 150 }}>
<Option value="all">All Status</Option>
<Option value="valid">Valid</Option>
<Option value="expiring">Expiring Soon</Option>
<Option value="expired">Expired</Option>
</Select>
<RangePicker style={{ width: 300 }} />
</div>
<Table
rowSelection={rowSelection}
columns={certificateColumns}
dataSource={filteredCertificates}
rowKey="id"
pagination={{
pageSize: 10,
}}
/>
</TabPane>
<TabPane tab="Compliance Checks" key="checks">
<div style={{ display: 'flex', gap: 16, marginBottom: 16, flexWrap: 'wrap' }}>
<Select placeholder="Select Platform" style={{ width: 150 }}>
<Option value="all">All Platforms</Option>
<Option value="amazon">Amazon</Option>
<Option value="ebay">eBay</Option>
<Option value="shopee">Shopee</Option>
</Select>
<Select placeholder="Select Status" style={{ width: 150 }}>
<Option value="all">All Status</Option>
<Option value="passed">Passed</Option>
<Option value="warning">Warning</Option>
<Option value="failed">Failed</Option>
</Select>
<RangePicker style={{ width: 300 }} />
</div>
<Table
columns={checkColumns}
dataSource={mockComplianceChecks}
rowKey="id"
pagination={{
pageSize: 10,
}}
/>
</TabPane>
</Tabs>
</Card>
<Modal
title={`Certificate Detail - ${selectedCertificate?.name}`}
open={isDetailModalVisible}
onCancel={() => setIsDetailModalVisible(false)}
footer={[
<Button key="close" onClick={() => setIsDetailModalVisible(false)}>
Close
</Button>,
]}
width={800}
>
{selectedCertificate && (
<div>
<div style={{ marginBottom: 24 }}>
<h3>Basic Information</h3>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16, marginTop: 16 }}>
<div>
<Text strong>Certificate No:</Text> {selectedCertificate.certificateNo}
</div>
<div>
<Text strong>Name:</Text> {selectedCertificate.name}
</div>
<div>
<Text strong>Type:</Text> {selectedCertificate.type}
</div>
<div>
<Text strong>Status:</Text> <Badge status={getStatusColor(selectedCertificate.status)} text={selectedCertificate.status} />
</div>
<div>
<Text strong>Expiry Date:</Text> {selectedCertificate.expiryDate}
</div>
<div>
<Text strong>Issuing Authority:</Text> {selectedCertificate.issuingAuthority}
</div>
<div>
<Text strong>Created At:</Text> {selectedCertificate.createdAt}
</div>
<div>
<Text strong>Updated At:</Text> {selectedCertificate.updatedAt}
</div>
</div>
</div>
<div>
<h3>Products</h3>
<div style={{ marginTop: 16 }}>
{selectedCertificate.products.map((product: string, index: number) => (
<div key={index} style={{ marginBottom: 8 }}>
<Badge status="success" text={product} />
</div>
))}
</div>
</div>
<div style={{ marginTop: 24 }}>
<h3>Document</h3>
<div style={{ marginTop: 16 }}>
<Button icon={<FileTextOutlined />} onClick={() => message.success('Download document')}>
{selectedCertificate.document}
</Button>
</div>
</div>
<div style={{ marginTop: 24 }}>
<h3>Quick Actions</h3>
<Space size="middle" style={{ marginTop: 16 }}>
<Button icon={<SafetyCertificateOutlined />} onClick={() => handleRenewCertificate(selectedCertificate)}>Renew</Button>
<Button icon={<EditOutlined />} onClick={() => handleEditCertificate(selectedCertificate)}>Edit</Button>
</Space>
</div>
</div>
)}
</Modal>
</div>
);
};
export default CompliancePage;

View File

@@ -0,0 +1,235 @@
import React, { useState, useEffect } from 'react';
import { Card, Row, Col, DatePicker, Statistic, Button, Select, message } from 'antd';
import { ArrowUpOutlined, ArrowDownOutlined, ReloadOutlined, DownloadOutlined, SettingOutlined } from '@ant-design/icons';
import { Area, AreaChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
import { dashboardApi } from '../services/ApiService';
const { RangePicker } = DatePicker;
const { Option } = Select;
const DashboardPage: React.FC = () => {
const [timeRange, setTimeRange] = useState<any>(null);
const [shop, setShop] = useState<string>('all');
const [kpiData, setKpiData] = useState<any[]>([]);
const [trendData, setTrendData] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
// 加载Dashboard数据
useEffect(() => {
loadDashboardData();
}, [shop, timeRange]);
const loadDashboardData = async () => {
setLoading(true);
try {
const response = await dashboardApi.getDashboardData({
shop,
startDate: timeRange?.[0]?.format('YYYY-MM-DD'),
endDate: timeRange?.[1]?.format('YYYY-MM-DD'),
});
if (response.kpiData) {
setKpiData(response.kpiData);
} else {
// 使用默认KPI数据
setKpiData([
{
title: '总销售额',
value: '¥128,500',
change: 12.5,
status: 'up',
icon: <DollarOutlined />,
},
{
title: '订单数',
value: '2,850',
change: 8.2,
status: 'up',
icon: <OrderedListOutlined />,
},
{
title: '转化率',
value: '4.8%',
change: -1.2,
status: 'down',
icon: <LineChartOutlined />,
},
{
title: '平均订单价值',
value: '¥45.1',
change: 3.5,
status: 'up',
icon: <ShoppingOutlined />,
},
]);
}
if (response.trendData) {
setTrendData(response.trendData);
} else {
// 使用默认趋势数据
setTrendData([
{ date: '1/1', sales: 12000, orders: 350 },
{ date: '1/2', sales: 15000, orders: 420 },
{ date: '1/3', sales: 18000, orders: 480 },
{ date: '1/4', sales: 16000, orders: 430 },
{ date: '1/5', sales: 20000, orders: 520 },
{ date: '1/6', sales: 22000, orders: 550 },
{ date: '1/7', sales: 25000, orders: 600 },
]);
}
} catch (error) {
console.error('Failed to load dashboard data:', error);
message.error('Failed to load dashboard data');
// 使用默认数据
setKpiData([
{
title: '总销售额',
value: '¥128,500',
change: 12.5,
status: 'up',
icon: <DollarOutlined />,
},
{
title: '订单数',
value: '2,850',
change: 8.2,
status: 'up',
icon: <OrderedListOutlined />,
},
{
title: '转化率',
value: '4.8%',
change: -1.2,
status: 'down',
icon: <LineChartOutlined />,
},
{
title: '平均订单价值',
value: '¥45.1',
change: 3.5,
status: 'up',
icon: <ShoppingOutlined />,
},
]);
setTrendData([
{ date: '1/1', sales: 12000, orders: 350 },
{ date: '1/2', sales: 15000, orders: 420 },
{ date: '1/3', sales: 18000, orders: 480 },
{ date: '1/4', sales: 16000, orders: 430 },
{ date: '1/5', sales: 20000, orders: 520 },
{ date: '1/6', sales: 22000, orders: 550 },
{ date: '1/7', sales: 25000, orders: 600 },
]);
} finally {
setLoading(false);
}
};
const handleTimeRangeChange = (dates: any) => {
setTimeRange(dates);
};
const handleShopChange = (value: string) => {
setShop(value);
};
const handleRefresh = () => {
loadDashboardData();
};
const handleExport = async () => {
try {
await dashboardApi.getDashboardData({
shop,
startDate: timeRange?.[0]?.format('YYYY-MM-DD'),
endDate: timeRange?.[1]?.format('YYYY-MM-DD'),
export: true,
});
message.success('Report exported successfully');
} catch (error) {
console.error('Failed to export report:', error);
message.error('Failed to export report');
}
};
return (
<div className="dashboard-page">
<div className="page-header" style={{ marginBottom: 24, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<h1>Dashboard</h1>
<div style={{ display: 'flex', gap: 16, alignItems: 'center' }}>
<Select defaultValue="all" style={{ width: 120 }} onChange={handleShopChange}>
<Option value="all">All Shops</Option>
<Option value="shop1">Shop 1</Option>
<Option value="shop2">Shop 2</Option>
</Select>
<RangePicker onChange={handleTimeRangeChange} />
<Button icon={<ReloadOutlined />} onClick={handleRefresh}>Refresh</Button>
<Button icon={<DownloadOutlined />} onClick={handleExport}>Export</Button>
<Button icon={<SettingOutlined />}>Customize</Button>
</div>
</div>
<Row gutter={[16, 16]} style={{ marginBottom: 24 }}>
{kpiData.map((kpi, index) => (
<Col span={6} key={index}>
<Card>
<Statistic
title={kpi.title}
value={kpi.value}
prefix={kpi.icon}
suffix={
<span style={{ color: kpi.status === 'up' ? '#52c41a' : '#f5222d' }}>
{kpi.status === 'up' ? <ArrowUpOutlined /> : <ArrowDownOutlined />}
{Math.abs(kpi.change)}%
</span>
}
/>
</Card>
</Col>
))}
</Row>
<Card title="Sales and Orders Trend" style={{ marginBottom: 24 }} loading={loading}>
<div style={{ height: 400 }}>
<ResponsiveContainer width="100%" height="100%">
<AreaChart
data={trendData}
margin={{ top: 10, right: 30, left: 0, bottom: 0 }}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="date" />
<YAxis yAxisId="left" />
<YAxis yAxisId="right" orientation="right" />
<Tooltip />
<Area yAxisId="left" type="monotone" dataKey="sales" stroke="#1890ff" fill="#e6f7ff" />
<Area yAxisId="right" type="monotone" dataKey="orders" stroke="#52c41a" fill="#f6ffed" />
</AreaChart>
</ResponsiveContainer>
</div>
</Card>
<Row gutter={[16, 16]}>
<Col span={12}>
<Card title="Top Performing Products">
<div style={{ height: 300, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<p>Product performance data will be displayed here</p>
</div>
</Card>
</Col>
<Col span={12}>
<Card title="Recent Orders">
<div style={{ height: 300, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<p>Recent orders data will be displayed here</p>
</div>
</Card>
</Col>
</Row>
</div>
);
};
// 导入缺失的图标
import { DollarOutlined, OrderedListOutlined, LineChartOutlined, ShoppingOutlined } from '@ant-design/icons';
export default DashboardPage;

View File

@@ -0,0 +1,436 @@
import React, { useState } from 'react';
import { Card, Table, Button, Select, DatePicker, Space, Modal, Typography, Badge, message, Tabs, Row, Col, Statistic } from 'antd';
import { PlusOutlined, EyeOutlined, EditOutlined, DeleteOutlined, ReloadOutlined, DollarOutlined, FileTextOutlined, CheckCircleOutlined, CloseCircleOutlined, SyncOutlined } from '@ant-design/icons';
const { Option } = Select;
const { RangePicker } = DatePicker;
const { Title, Text } = Typography;
const { TabPane } = Tabs;
const mockTransactions = [
{
id: '1',
transactionNo: 'TXN-2026-001',
type: 'income',
category: 'Sales Revenue',
amount: 12500,
status: 'completed',
description: 'Product sales revenue',
date: '2026-03-18',
createdAt: '2026-03-18',
},
{
id: '2',
transactionNo: 'TXN-2026-002',
type: 'expense',
category: 'Product Cost',
amount: 8500,
status: 'completed',
description: 'Product purchase cost',
date: '2026-03-17',
createdAt: '2026-03-17',
},
{
id: '3',
transactionNo: 'TXN-2026-003',
type: 'expense',
category: 'Shipping Cost',
amount: 1200,
status: 'completed',
description: 'Shipping and logistics cost',
date: '2026-03-16',
createdAt: '2026-03-16',
},
{
id: '4',
transactionNo: 'TXN-2026-004',
type: 'income',
category: 'Refund',
amount: 350,
status: 'pending',
description: 'Customer refund',
date: '2026-03-15',
createdAt: '2026-03-15',
},
];
const mockSettlements = [
{
id: '1',
settlementNo: 'SET-2026-001',
platform: 'Amazon',
period: '2026-03-01 to 2026-03-15',
totalAmount: 45000,
fee: 2250,
netAmount: 42750,
status: 'completed',
createdAt: '2026-03-16',
},
{
id: '2',
settlementNo: 'SET-2026-002',
platform: 'eBay',
period: '2026-03-01 to 2026-03-15',
totalAmount: 28000,
fee: 1400,
netAmount: 26600,
status: 'pending',
createdAt: '2026-03-16',
},
{
id: '3',
settlementNo: 'SET-2026-003',
platform: 'Shopee',
period: '2026-02-16 to 2026-02-28',
totalAmount: 32000,
fee: 1600,
netAmount: 30400,
status: 'completed',
createdAt: '2026-03-01',
},
];
const FinancePage: React.FC = () => {
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
const [isDetailModalVisible, setIsDetailModalVisible] = useState(false);
const [selectedTransaction, setSelectedTransaction] = useState<any>(null);
const [activeTab, setActiveTab] = useState('transactions');
const [searchKeyword, setSearchKeyword] = useState('');
const handleViewDetail = (record: any) => {
setSelectedTransaction(record);
setIsDetailModalVisible(true);
};
const handleCreateTransaction = () => {
message.success('Create transaction form opened');
};
const handleEditTransaction = (record: any) => {
message.success(`Edit transaction ${record.transactionNo}`);
};
const handleDeleteTransaction = (record: any) => {
message.success(`Delete transaction ${record.transactionNo}`);
};
const handleReconcile = (record: any) => {
message.success(`Reconcile transaction ${record.transactionNo}`);
};
const handleBatchDelete = () => {
message.success('Batch delete initiated');
};
const handleBatchReconcile = () => {
message.success('Batch reconcile initiated');
};
const getStatusColor = (status: string) => {
switch (status) {
case 'completed': return 'success';
case 'pending': return 'processing';
case 'failed': return 'error';
case 'cancelled': return 'default';
default: return 'default';
}
};
const getTypeColor = (type: string) => {
switch (type) {
case 'income': return 'success';
case 'expense': return 'error';
default: return 'default';
}
};
const transactionColumns = [
{
title: 'Transaction No',
dataIndex: 'transactionNo',
key: 'transactionNo',
},
{
title: 'Type',
dataIndex: 'type',
key: 'type',
render: (type: string) => (
<Badge color={getTypeColor(type)} text={type} />
),
},
{
title: 'Category',
dataIndex: 'category',
key: 'category',
},
{
title: 'Amount',
dataIndex: 'amount',
key: 'amount',
render: (value: number) => `$${value.toLocaleString()}`,
},
{
title: 'Status',
dataIndex: 'status',
key: 'status',
render: (status: string) => (
<Badge status={getStatusColor(status)} text={status} />
),
},
{
title: 'Description',
dataIndex: 'description',
key: 'description',
},
{
title: 'Date',
dataIndex: 'date',
key: 'date',
},
{
title: 'Actions',
key: 'actions',
render: (_: any, record: any) => (
<Space size="middle">
<Button icon={<EyeOutlined />} onClick={() => handleViewDetail(record)}>View</Button>
<Button icon={<EditOutlined />} onClick={() => handleEditTransaction(record)}>Edit</Button>
<Button icon={<SyncOutlined />} onClick={() => handleReconcile(record)}>Reconcile</Button>
<Button icon={<DeleteOutlined />} danger onClick={() => handleDeleteTransaction(record)}>Delete</Button>
</Space>
),
},
];
const settlementColumns = [
{
title: 'Settlement No',
dataIndex: 'settlementNo',
key: 'settlementNo',
},
{
title: 'Platform',
dataIndex: 'platform',
key: 'platform',
},
{
title: 'Period',
dataIndex: 'period',
key: 'period',
},
{
title: 'Total Amount',
dataIndex: 'totalAmount',
key: 'totalAmount',
render: (value: number) => `$${value.toLocaleString()}`,
},
{
title: 'Fee',
dataIndex: 'fee',
key: 'fee',
render: (value: number) => `$${value.toLocaleString()}`,
},
{
title: 'Net Amount',
dataIndex: 'netAmount',
key: 'netAmount',
render: (value: number) => `$${value.toLocaleString()}`,
},
{
title: 'Status',
dataIndex: 'status',
key: 'status',
render: (status: string) => (
<Badge status={getStatusColor(status)} text={status} />
),
},
{
title: 'Created At',
dataIndex: 'createdAt',
key: 'createdAt',
},
];
const rowSelection = {
selectedRowKeys,
onChange: (keys: React.Key[]) => {
setSelectedRowKeys(keys);
},
};
const filteredTransactions = mockTransactions.filter(transaction =>
transaction.description.toLowerCase().includes(searchKeyword.toLowerCase()) ||
transaction.transactionNo.toLowerCase().includes(searchKeyword.toLowerCase())
);
return (
<div className="finance-page">
<div className="page-header" style={{ marginBottom: 24, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Title level={4}>Finance Management</Title>
<Space>
<Button icon={<PlusOutlined />} type="primary" onClick={handleCreateTransaction}>Create Transaction</Button>
<Button icon={<ReloadOutlined />}>Refresh</Button>
<Button onClick={handleBatchReconcile} disabled={selectedRowKeys.length === 0}>Batch Reconcile</Button>
<Button onClick={handleBatchDelete} disabled={selectedRowKeys.length === 0} danger>Batch Delete</Button>
</Space>
</div>
<Card style={{ marginBottom: 24 }}>
<Row gutter={[16, 16]}>
<Col span={6}>
<Statistic
title="Total Income"
value={12850}
precision={0}
prefix="$"
valueStyle={{ color: '#52c41a' }}
/>
</Col>
<Col span={6}>
<Statistic
title="Total Expense"
value={9700}
precision={0}
prefix="$"
valueStyle={{ color: '#f5222d' }}
/>
</Col>
<Col span={6}>
<Statistic
title="Net Profit"
value={3150}
precision={0}
prefix="$"
valueStyle={{ color: '#3f8600' }}
/>
</Col>
<Col span={6}>
<Statistic
title="Pending Settlements"
value={1}
valueStyle={{ color: '#1890ff' }}
/>
</Col>
</Row>
</Card>
<Card style={{ marginBottom: 24 }}>
<Tabs activeKey={activeTab} onChange={setActiveTab}>
<TabPane tab="Transactions" key="transactions">
<div style={{ display: 'flex', gap: 16, marginBottom: 16, flexWrap: 'wrap' }}>
<Select placeholder="Select Type" style={{ width: 150 }}>
<Option value="all">All Types</Option>
<Option value="income">Income</Option>
<Option value="expense">Expense</Option>
</Select>
<Select placeholder="Select Status" style={{ width: 150 }}>
<Option value="all">All Status</Option>
<Option value="completed">Completed</Option>
<Option value="pending">Pending</Option>
<Option value="failed">Failed</Option>
</Select>
<RangePicker style={{ width: 300 }} />
</div>
<Table
rowSelection={rowSelection}
columns={transactionColumns}
dataSource={filteredTransactions}
rowKey="id"
pagination={{
pageSize: 10,
}}
/>
</TabPane>
<TabPane tab="Settlements" key="settlements">
<div style={{ display: 'flex', gap: 16, marginBottom: 16, flexWrap: 'wrap' }}>
<Select placeholder="Select Platform" style={{ width: 150 }}>
<Option value="all">All Platforms</Option>
<Option value="amazon">Amazon</Option>
<Option value="ebay">eBay</Option>
<Option value="shopee">Shopee</Option>
</Select>
<Select placeholder="Select Status" style={{ width: 150 }}>
<Option value="all">All Status</Option>
<Option value="completed">Completed</Option>
<Option value="pending">Pending</Option>
</Select>
<RangePicker style={{ width: 300 }} />
</div>
<Table
columns={settlementColumns}
dataSource={mockSettlements}
rowKey="id"
pagination={{
pageSize: 10,
}}
/>
</TabPane>
<TabPane tab="Reports" key="reports">
<div style={{ padding: 24, textAlign: 'center' }}>
<Text type="secondary">Financial reports will be displayed here</Text>
</div>
</TabPane>
</Tabs>
</Card>
<Modal
title={`Transaction Detail - ${selectedTransaction?.transactionNo}`}
open={isDetailModalVisible}
onCancel={() => setIsDetailModalVisible(false)}
footer={[
<Button key="close" onClick={() => setIsDetailModalVisible(false)}>
Close
</Button>,
]}
width={800}
>
{selectedTransaction && (
<div>
<div style={{ marginBottom: 24 }}>
<h3>Basic Information</h3>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16, marginTop: 16 }}>
<div>
<Text strong>Transaction No:</Text> {selectedTransaction.transactionNo}
</div>
<div>
<Text strong>Type:</Text> <Badge color={getTypeColor(selectedTransaction.type)} text={selectedTransaction.type} />
</div>
<div>
<Text strong>Category:</Text> {selectedTransaction.category}
</div>
<div>
<Text strong>Amount:</Text> ${selectedTransaction.amount.toLocaleString()}
</div>
<div>
<Text strong>Status:</Text> <Badge status={getStatusColor(selectedTransaction.status)} text={selectedTransaction.status} />
</div>
<div>
<Text strong>Date:</Text> {selectedTransaction.date}
</div>
<div>
<Text strong>Created At:</Text> {selectedTransaction.createdAt}
</div>
</div>
</div>
<div>
<h3>Description</h3>
<div style={{ marginTop: 16 }}>
<Text>{selectedTransaction.description}</Text>
</div>
</div>
<div style={{ marginTop: 24 }}>
<h3>Quick Actions</h3>
<Space size="middle" style={{ marginTop: 16 }}>
<Button icon={<SyncOutlined />} onClick={() => handleReconcile(selectedTransaction)}>Reconcile</Button>
<Button icon={<EditOutlined />} onClick={() => handleEditTransaction(selectedTransaction)}>Edit</Button>
</Space>
</div>
</div>
)}
</Modal>
</div>
);
};
export default FinancePage;

View File

@@ -0,0 +1,287 @@
import React, { useState } from 'react';
import { Card, Table, Button, Select, DatePicker, Space, Modal, Typography, Badge, message } from 'antd';
import { PlusOutlined, EyeOutlined, EditOutlined, DeleteOutlined, ReloadOutlined, SettingOutlined, ShoppingOutlined, FileTextOutlined, BarChartOutlined } from '@ant-design/icons';
const { Option } = Select;
const { RangePicker } = DatePicker;
const { Title, Text } = Typography;
const mockSites = [
{
id: '1',
name: 'My Electronics Store',
domain: 'electronics-store.com',
status: 'active',
template: 'Modern E-commerce',
theme: 'Blue',
createdAt: '2026-03-01',
updatedAt: '2026-03-15',
},
{
id: '2',
name: 'Fashion Boutique',
domain: 'fashion-boutique.com',
status: 'active',
template: 'Fashion Store',
theme: 'Pink',
createdAt: '2026-03-05',
updatedAt: '2026-03-10',
},
{
id: '3',
name: 'Home Decor Shop',
domain: 'home-decor-shop.com',
status: 'inactive',
template: 'Home Decor',
theme: 'Neutral',
createdAt: '2026-02-20',
updatedAt: '2026-03-01',
},
{
id: '4',
name: 'Sports Equipment Store',
domain: 'sports-equipment.com',
status: 'active',
template: 'Sports Store',
theme: 'Green',
createdAt: '2026-03-10',
updatedAt: '2026-03-18',
},
];
const IndependentSiteList: React.FC = () => {
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
const [isDetailModalVisible, setIsDetailModalVisible] = useState(false);
const [selectedSite, setSelectedSite] = useState<any>(null);
const handleViewDetail = (record: any) => {
setSelectedSite(record);
setIsDetailModalVisible(true);
};
const handleCreateSite = () => {
message.success('Create site form opened');
};
const handleEditSite = (record: any) => {
message.success(`Edit site ${record.name}`);
};
const handleDeleteSite = (record: any) => {
message.success(`Delete site ${record.name}`);
};
const handleConfigureSite = (record: any) => {
message.success(`Configure site ${record.name}`);
};
const handleManageProducts = (record: any) => {
message.success(`Manage products for ${record.name}`);
};
const handleManageOrders = (record: any) => {
message.success(`Manage orders for ${record.name}`);
};
const handleAnalyzeData = (record: any) => {
message.success(`Analyze data for ${record.name}`);
};
const handleBatchDelete = () => {
message.success('Batch delete initiated');
};
const getStatusColor = (status: string) => {
switch (status) {
case 'active': return 'success';
case 'inactive': return 'default';
case 'pending': return 'processing';
case 'error': return 'error';
default: return 'default';
}
};
const columns = [
{
title: 'Site Name',
dataIndex: 'name',
key: 'name',
},
{
title: 'Domain',
dataIndex: 'domain',
key: 'domain',
},
{
title: 'Status',
dataIndex: 'status',
key: 'status',
render: (status: string) => (
<Badge status={getStatusColor(status)} text={status} />
),
},
{
title: 'Template',
dataIndex: 'template',
key: 'template',
},
{
title: 'Theme',
dataIndex: 'theme',
key: 'theme',
},
{
title: 'Created At',
dataIndex: 'createdAt',
key: 'createdAt',
},
{
title: 'Updated At',
dataIndex: 'updatedAt',
key: 'updatedAt',
},
{
title: 'Actions',
key: 'actions',
render: (_: any, record: any) => (
<Space size="middle">
<Button icon={<EyeOutlined />} onClick={() => handleViewDetail(record)}>View</Button>
<Button icon={<EditOutlined />} onClick={() => handleEditSite(record)}>Edit</Button>
<Button icon={<SettingOutlined />} onClick={() => handleConfigureSite(record)}>Configure</Button>
<Button icon={<ShoppingOutlined />} onClick={() => handleManageProducts(record)}>Products</Button>
<Button icon={<FileTextOutlined />} onClick={() => handleManageOrders(record)}>Orders</Button>
<Button icon={<BarChartOutlined />} onClick={() => handleAnalyzeData(record)}>Analytics</Button>
<Button icon={<DeleteOutlined />} danger onClick={() => handleDeleteSite(record)}>Delete</Button>
</Space>
),
},
];
const rowSelection = {
selectedRowKeys,
onChange: (keys: React.Key[]) => {
setSelectedRowKeys(keys);
},
};
return (
<div className="independent-site-list">
<div className="page-header" style={{ marginBottom: 24, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Title level={4}>Independent Sites</Title>
<Space>
<Button icon={<PlusOutlined />} type="primary" onClick={handleCreateSite}>Create Site</Button>
<Button icon={<ReloadOutlined />}>Refresh</Button>
<Button onClick={handleBatchDelete} disabled={selectedRowKeys.length === 0} danger>Batch Delete</Button>
</Space>
</div>
<Card style={{ marginBottom: 24 }}>
<div style={{ display: 'flex', gap: 16, marginBottom: 16, flexWrap: 'wrap' }}>
<Select placeholder="Select Status" style={{ width: 150 }}>
<Option value="all">All Status</Option>
<Option value="active">Active</Option>
<Option value="inactive">Inactive</Option>
<Option value="pending">Pending</Option>
</Select>
<Select placeholder="Select Template" style={{ width: 200 }}>
<Option value="all">All Templates</Option>
<Option value="Modern E-commerce">Modern E-commerce</Option>
<Option value="Fashion Store">Fashion Store</Option>
<Option value="Home Decor">Home Decor</Option>
<Option value="Sports Store">Sports Store</Option>
</Select>
<RangePicker style={{ width: 300 }} />
</div>
<Table
rowSelection={rowSelection}
columns={columns}
dataSource={mockSites}
rowKey="id"
pagination={{
pageSize: 10,
}}
/>
</Card>
<Card title="Site Statistics" style={{ marginBottom: 24 }}>
<div style={{ display: 'flex', justifyContent: 'space-around' }}>
<div style={{ textAlign: 'center' }}>
<h3>Total Sites</h3>
<p style={{ fontSize: '24px', fontWeight: 'bold' }}>4</p>
</div>
<div style={{ textAlign: 'center' }}>
<h3>Active Sites</h3>
<p style={{ fontSize: '24px', fontWeight: 'bold', color: '#52c41a' }}>3</p>
</div>
<div style={{ textAlign: 'center' }}>
<h3>Inactive Sites</h3>
<p style={{ fontSize: '24px', fontWeight: 'bold', color: '#d9d9d9' }}>1</p>
</div>
<div style={{ textAlign: 'center' }}>
<h3>Total Products</h3>
<p style={{ fontSize: '24px', fontWeight: 'bold' }}>120</p>
</div>
<div style={{ textAlign: 'center' }}>
<h3>Total Orders</h3>
<p style={{ fontSize: '24px', fontWeight: 'bold' }}>85</p>
</div>
</div>
</Card>
<Modal
title={`Site Detail - ${selectedSite?.name}`}
open={isDetailModalVisible}
onCancel={() => setIsDetailModalVisible(false)}
footer={[
<Button key="close" onClick={() => setIsDetailModalVisible(false)}>
Close
</Button>,
]}
width={800}
>
{selectedSite && (
<div>
<div style={{ marginBottom: 24 }}>
<h3>Basic Information</h3>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16, marginTop: 16 }}>
<div>
<Text strong>Site Name:</Text> {selectedSite.name}
</div>
<div>
<Text strong>Domain:</Text> {selectedSite.domain}
</div>
<div>
<Text strong>Status:</Text> <Badge status={getStatusColor(selectedSite.status)} text={selectedSite.status} />
</div>
<div>
<Text strong>Template:</Text> {selectedSite.template}
</div>
<div>
<Text strong>Theme:</Text> {selectedSite.theme}
</div>
<div>
<Text strong>Created At:</Text> {selectedSite.createdAt}
</div>
<div>
<Text strong>Updated At:</Text> {selectedSite.updatedAt}
</div>
</div>
</div>
<div>
<h3>Quick Actions</h3>
<Space size="middle" style={{ marginTop: 16 }}>
<Button icon={<SettingOutlined />} onClick={() => handleConfigureSite(selectedSite)}>Configure</Button>
<Button icon={<ShoppingOutlined />} onClick={() => handleManageProducts(selectedSite)}>Manage Products</Button>
<Button icon={<FileTextOutlined />} onClick={() => handleManageOrders(selectedSite)}>Manage Orders</Button>
<Button icon={<BarChartOutlined />} onClick={() => handleAnalyzeData(selectedSite)}>Analytics</Button>
</Space>
</div>
</div>
)}
</Modal>
</div>
);
};
export default IndependentSiteList;

View File

@@ -0,0 +1,370 @@
import React, { useState } from 'react';
import { Card, Table, Button, Select, DatePicker, Space, Modal, Typography, Badge, message, Row, Col, Statistic, Input } from 'antd';
import { PlusOutlined, EyeOutlined, EditOutlined, DeleteOutlined, ReloadOutlined, WarningOutlined, CheckCircleOutlined, ExclamationCircleOutlined } from '@ant-design/icons';
const { Option } = Select;
const { RangePicker } = DatePicker;
const { Title, Text } = Typography;
const mockInventory = [
{
id: '1',
productId: 'P001',
productName: 'Wireless Headphones',
sku: 'WH-001',
quantity: 150,
available: 120,
reserved: 30,
reorderPoint: 50,
location: 'Warehouse A',
status: 'normal',
lastUpdated: '2026-03-18',
},
{
id: '2',
productId: 'P002',
productName: 'Bluetooth Speaker',
sku: 'BS-002',
quantity: 80,
available: 20,
reserved: 60,
reorderPoint: 30,
location: 'Warehouse B',
status: 'low',
lastUpdated: '2026-03-18',
},
{
id: '3',
productId: 'P003',
productName: 'Smart Watch',
sku: 'SW-003',
quantity: 200,
available: 180,
reserved: 20,
reorderPoint: 100,
location: 'Warehouse A',
status: 'normal',
lastUpdated: '2026-03-17',
},
{
id: '4',
productId: 'P004',
productName: 'Fitness Tracker',
sku: 'FT-004',
quantity: 10,
available: 5,
reserved: 5,
reorderPoint: 20,
location: 'Warehouse C',
status: 'critical',
lastUpdated: '2026-03-18',
},
];
const InventoryPage: React.FC = () => {
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
const [isDetailModalVisible, setIsDetailModalVisible] = useState(false);
const [selectedItem, setSelectedItem] = useState<any>(null);
const [searchKeyword, setSearchKeyword] = useState('');
const handleViewDetail = (record: any) => {
setSelectedItem(record);
setIsDetailModalVisible(true);
};
const handleAddInventory = () => {
message.success('Add inventory form opened');
};
const handleEditInventory = (record: any) => {
message.success(`Edit inventory ${record.productName}`);
};
const handleDeleteInventory = (record: any) => {
message.success(`Delete inventory ${record.productName}`);
};
const handleAdjustStock = (record: any) => {
message.success(`Adjust stock for ${record.productName}`);
};
const handleTransferStock = (record: any) => {
message.success(`Transfer stock for ${record.productName}`);
};
const handleBatchDelete = () => {
message.success('Batch delete initiated');
};
const handleBatchAdjust = () => {
message.success('Batch adjust initiated');
};
const getStatusColor = (status: string) => {
switch (status) {
case 'normal': return 'success';
case 'low': return 'warning';
case 'critical': return 'error';
default: return 'default';
}
};
const getStatusIcon = (status: string) => {
switch (status) {
case 'normal': return <CheckCircleOutlined />;
case 'low': return <ExclamationCircleOutlined />;
case 'critical': return <WarningOutlined />;
default: return null;
}
};
const columns = [
{
title: 'Product ID',
dataIndex: 'productId',
key: 'productId',
},
{
title: 'Product Name',
dataIndex: 'productName',
key: 'productName',
},
{
title: 'SKU',
dataIndex: 'sku',
key: 'sku',
},
{
title: 'Total Quantity',
dataIndex: 'quantity',
key: 'quantity',
},
{
title: 'Available',
dataIndex: 'available',
key: 'available',
render: (value: number) => (
<span style={{ color: value < 20 ? '#f5222d' : '#52c41a' }}>{value}</span>
),
},
{
title: 'Reserved',
dataIndex: 'reserved',
key: 'reserved',
},
{
title: 'Reorder Point',
dataIndex: 'reorderPoint',
key: 'reorderPoint',
},
{
title: 'Location',
dataIndex: 'location',
key: 'location',
},
{
title: 'Status',
dataIndex: 'status',
key: 'status',
render: (status: string) => (
<Badge status={getStatusColor(status)} text={status} />
),
},
{
title: 'Last Updated',
dataIndex: 'lastUpdated',
key: 'lastUpdated',
},
{
title: 'Actions',
key: 'actions',
render: (_: any, record: any) => (
<Space size="middle">
<Button icon={<EyeOutlined />} onClick={() => handleViewDetail(record)}>View</Button>
<Button icon={<EditOutlined />} onClick={() => handleEditInventory(record)}>Edit</Button>
<Button icon={<WarningOutlined />} onClick={() => handleAdjustStock(record)}>Adjust</Button>
<Button icon={<ReloadOutlined />} onClick={() => handleTransferStock(record)}>Transfer</Button>
<Button icon={<DeleteOutlined />} danger onClick={() => handleDeleteInventory(record)}>Delete</Button>
</Space>
),
},
];
const rowSelection = {
selectedRowKeys,
onChange: (keys: React.Key[]) => {
setSelectedRowKeys(keys);
},
};
const filteredInventory = mockInventory.filter(item =>
item.productName.toLowerCase().includes(searchKeyword.toLowerCase()) ||
item.sku.toLowerCase().includes(searchKeyword.toLowerCase())
);
return (
<div className="inventory-page">
<div className="page-header" style={{ marginBottom: 24, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Title level={4}>Inventory Management</Title>
<Space>
<Button icon={<PlusOutlined />} type="primary" onClick={handleAddInventory}>Add Inventory</Button>
<Button icon={<ReloadOutlined />}>Refresh</Button>
<Button onClick={handleBatchAdjust} disabled={selectedRowKeys.length === 0}>Batch Adjust</Button>
<Button onClick={handleBatchDelete} disabled={selectedRowKeys.length === 0} danger>Batch Delete</Button>
</Space>
</div>
<Card style={{ marginBottom: 24 }}>
<Row gutter={[16, 16]}>
<Col span={6}>
<Statistic
title="Total Products"
value={440}
valueStyle={{ color: '#3f8600' }}
/>
</Col>
<Col span={6}>
<Statistic
title="Available"
value={325}
valueStyle={{ color: '#52c41a' }}
/>
</Col>
<Col span={6}>
<Statistic
title="Reserved"
value={115}
valueStyle={{ color: '#1890ff' }}
/>
</Col>
<Col span={6}>
<Statistic
title="Low Stock"
value={2}
valueStyle={{ color: '#f5222d' }}
/>
</Col>
</Row>
</Card>
<Card style={{ marginBottom: 24 }}>
<div style={{ display: 'flex', gap: 16, marginBottom: 16, flexWrap: 'wrap' }}>
<Input
placeholder="Search by product name or SKU"
value={searchKeyword}
onChange={(e) => setSearchKeyword(e.target.value)}
style={{ width: 300 }}
allowClear
/>
<Select placeholder="Select Location" style={{ width: 150 }}>
<Option value="all">All Locations</Option>
<Option value="Warehouse A">Warehouse A</Option>
<Option value="Warehouse B">Warehouse B</Option>
<Option value="Warehouse C">Warehouse C</Option>
</Select>
<Select placeholder="Select Status" style={{ width: 150 }}>
<Option value="all">All Status</Option>
<Option value="normal">Normal</Option>
<Option value="low">Low Stock</Option>
<Option value="critical">Critical</Option>
</Select>
<RangePicker style={{ width: 300 }} />
</div>
<Table
rowSelection={rowSelection}
columns={columns}
dataSource={filteredInventory}
rowKey="id"
pagination={{
pageSize: 10,
}}
/>
</Card>
<Modal
title={`Inventory Detail - ${selectedItem?.productName}`}
open={isDetailModalVisible}
onCancel={() => setIsDetailModalVisible(false)}
footer={[
<Button key="close" onClick={() => setIsDetailModalVisible(false)}>
Close
</Button>,
]}
width={800}
>
{selectedItem && (
<div>
<div style={{ marginBottom: 24 }}>
<h3>Basic Information</h3>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16, marginTop: 16 }}>
<div>
<Text strong>Product ID:</Text> {selectedItem.productId}
</div>
<div>
<Text strong>Product Name:</Text> {selectedItem.productName}
</div>
<div>
<Text strong>SKU:</Text> {selectedItem.sku}
</div>
<div>
<Text strong>Location:</Text> {selectedItem.location}
</div>
<div>
<Text strong>Status:</Text> <Badge status={getStatusColor(selectedItem.status)} text={selectedItem.status} />
</div>
<div>
<Text strong>Last Updated:</Text> {selectedItem.lastUpdated}
</div>
</div>
</div>
<div>
<h3>Stock Information</h3>
<Row gutter={[16, 16]} style={{ marginTop: 16 }}>
<Col span={8}>
<Card>
<Statistic
title="Total Quantity"
value={selectedItem.quantity}
valueStyle={{ color: '#3f8600' }}
/>
</Card>
</Col>
<Col span={8}>
<Card>
<Statistic
title="Available"
value={selectedItem.available}
valueStyle={{ color: '#52c41a' }}
/>
</Card>
</Col>
<Col span={8}>
<Card>
<Statistic
title="Reserved"
value={selectedItem.reserved}
valueStyle={{ color: '#1890ff' }}
/>
</Card>
</Col>
</Row>
<div style={{ marginTop: 16 }}>
<Text strong>Reorder Point:</Text> {selectedItem.reorderPoint}
</div>
</div>
<div style={{ marginTop: 24 }}>
<h3>Quick Actions</h3>
<Space size="middle" style={{ marginTop: 16 }}>
<Button icon={<WarningOutlined />} onClick={() => handleAdjustStock(selectedItem)}>Adjust Stock</Button>
<Button icon={<ReloadOutlined />} onClick={() => handleTransferStock(selectedItem)}>Transfer Stock</Button>
</Space>
</div>
</div>
)}
</Modal>
</div>
);
};
export default InventoryPage;

View File

@@ -0,0 +1,480 @@
import React, { useState, useEffect } from 'react';
import { Card, Table, Button, Select, DatePicker, Space, Modal, Tabs, Descriptions, Badge, message, Spin, Input } from 'antd';
import { EyeOutlined, CheckOutlined, CloseOutlined, DownloadOutlined, UploadOutlined, PlusOutlined } from '@ant-design/icons';
import { orderApi } from '../services/ApiService';
const { Option } = Select;
const { RangePicker } = DatePicker;
const { TabPane } = Tabs;
const OrdersPage: React.FC = () => {
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
const [isDetailModalVisible, setIsDetailModalVisible] = useState(false);
const [isProcessModalVisible, setIsProcessModalVisible] = useState(false);
const [selectedOrder, setSelectedOrder] = useState<any>(null);
const [isProcessing, setIsProcessing] = useState(false);
const [orderStatus, setOrderStatus] = useState<string>('');
const [orders, setOrders] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const [totalOrders, setTotalOrders] = useState(0);
const [pendingOrders, setPendingOrders] = useState(0);
const [shippedOrders, setShippedOrders] = useState(0);
const [deliveredOrders, setDeliveredOrders] = useState(0);
const [refundedOrders, setRefundedOrders] = useState(0);
// 加载订单数据
useEffect(() => {
loadOrders();
}, []);
const loadOrders = async () => {
setLoading(true);
try {
const response = await orderApi.getOrders();
setOrders(response.data || []);
calculateStatistics(response.data || []);
} catch (error) {
console.error('Failed to load orders:', error);
message.error('Failed to load orders');
// 加载失败时使用mock数据
const mockOrders = [
{
id: '1001',
orderNo: 'ORD-2026-0001',
customer: 'John Doe',
total: 259.98,
status: 'pending',
platform: 'Amazon',
date: '2026-03-18',
items: [
{ name: 'Wireless Headphones', quantity: 2, price: 129.99 },
],
},
{
id: '1002',
orderNo: 'ORD-2026-0002',
customer: 'Jane Smith',
total: 89.99,
status: 'shipped',
platform: 'eBay',
date: '2026-03-17',
items: [
{ name: 'Bluetooth Speaker', quantity: 1, price: 89.99 },
],
},
{
id: '1003',
orderNo: 'ORD-2026-0003',
customer: 'Bob Johnson',
total: 199.99,
status: 'delivered',
platform: 'Shopee',
date: '2026-03-16',
items: [
{ name: 'Smart Watch', quantity: 1, price: 199.99 },
],
},
{
id: '1004',
orderNo: 'ORD-2026-0004',
customer: 'Alice Williams',
total: 69.99,
status: 'refunded',
platform: 'Amazon',
date: '2026-03-15',
items: [
{ name: 'Fitness Tracker', quantity: 1, price: 69.99 },
],
},
];
setOrders(mockOrders);
calculateStatistics(mockOrders);
} finally {
setLoading(false);
}
};
const calculateStatistics = (orderList: any[]) => {
if (!orderList || orderList.length === 0) {
setTotalOrders(0);
setPendingOrders(0);
setShippedOrders(0);
setDeliveredOrders(0);
setRefundedOrders(0);
return;
}
const total = orderList.length;
const pending = orderList.filter(order => order.status === 'pending').length;
const shipped = orderList.filter(order => order.status === 'shipped').length;
const delivered = orderList.filter(order => order.status === 'delivered').length;
const refunded = orderList.filter(order => order.status === 'refunded').length;
setTotalOrders(total);
setPendingOrders(pending);
setShippedOrders(shipped);
setDeliveredOrders(delivered);
setRefundedOrders(refunded);
};
const handleViewDetail = async (record: any) => {
try {
const response = await orderApi.getOrder(record.id);
setSelectedOrder(response.data || record);
setIsDetailModalVisible(true);
} catch (error) {
console.error('Failed to get order detail:', error);
setSelectedOrder(record);
setIsDetailModalVisible(true);
}
};
const handleProcessOrder = (record: any) => {
setSelectedOrder(record);
setOrderStatus(record.status);
setIsProcessModalVisible(true);
};
const handleRefundOrder = async (record: any) => {
try {
await orderApi.updateStatus(record.id, 'refunded');
message.success(`Refund process initiated for order ${record.orderNo}`);
// 重新加载订单数据
loadOrders();
} catch (error) {
console.error('Failed to refund order:', error);
message.error('Failed to refund order');
}
};
const handleBatchShip = async () => {
if (selectedRowKeys.length === 0) return;
try {
// 批量发货
for (const key of selectedRowKeys) {
await orderApi.updateStatus(key.toString(), 'shipped');
}
message.success('Batch shipping initiated');
// 重新加载订单数据
loadOrders();
} catch (error) {
console.error('Failed to batch ship:', error);
message.error('Failed to batch ship');
}
};
const handleBatchCancel = async () => {
if (selectedRowKeys.length === 0) return;
try {
// 批量取消
for (const key of selectedRowKeys) {
await orderApi.updateStatus(key.toString(), 'cancelled');
}
message.success('Batch cancellation initiated');
// 重新加载订单数据
loadOrders();
} catch (error) {
console.error('Failed to batch cancel:', error);
message.error('Failed to batch cancel');
}
};
const handleBatchExport = async () => {
if (selectedRowKeys.length === 0) return;
try {
// 调用导出API
await orderApi.getOrders({ export: true, ids: selectedRowKeys });
message.success('Orders exported');
} catch (error) {
console.error('Failed to batch export:', error);
message.error('Failed to batch export');
}
};
const handleCreateOrder = () => {
// 这里可以打开创建订单的表单
message.success('Create order form opened');
};
const handleImportOrders = () => {
// 这里可以打开导入订单的对话框
message.success('Import orders dialog opened');
};
const handleExportOrders = async () => {
try {
// 调用导出API
await orderApi.getOrders({ export: true });
message.success('Orders exported');
} catch (error) {
console.error('Failed to export orders:', error);
message.error('Failed to export orders');
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'pending': return 'blue';
case 'shipped': return 'green';
case 'delivered': return 'green';
case 'refunded': return 'red';
case 'cancelled': return 'red';
default: return 'default';
}
};
const columns = [
{
title: 'Order No',
dataIndex: 'orderNo',
key: 'orderNo',
},
{
title: 'Customer',
dataIndex: 'customer',
key: 'customer',
},
{
title: 'Total',
dataIndex: 'total',
key: 'total',
render: (value: number) => `¥${value}`,
},
{
title: 'Status',
dataIndex: 'status',
key: 'status',
render: (status: string) => (
<Badge status={getStatusColor(status)} text={status} />
),
},
{
title: 'Platform',
dataIndex: 'platform',
key: 'platform',
},
{
title: 'Date',
dataIndex: 'date',
key: 'date',
},
{
title: 'Actions',
key: 'actions',
render: (_: any, record: any) => (
<Space size="middle">
<Button icon={<EyeOutlined />} onClick={() => handleViewDetail(record)}>View</Button>
<Button onClick={() => handleProcessOrder(record)}>Process</Button>
<Button onClick={() => handleRefundOrder(record)}>Refund</Button>
</Space>
),
},
];
const rowSelection = {
selectedRowKeys,
onChange: (keys: React.Key[]) => {
setSelectedRowKeys(keys);
},
};
return (
<div className="orders-page">
<div className="page-header" style={{ marginBottom: 24, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<h1>Orders</h1>
<Space>
<Button icon={<PlusOutlined />} onClick={handleCreateOrder}>Create Order</Button>
<Button icon={<UploadOutlined />} onClick={handleImportOrders}>Import Orders</Button>
<Button icon={<DownloadOutlined />} onClick={handleExportOrders}>Export Data</Button>
</Space>
</div>
<Card style={{ marginBottom: 24 }}>
<div style={{ display: 'flex', gap: 16, marginBottom: 16, flexWrap: 'wrap' }}>
<Select placeholder="Select Status" style={{ width: 150 }}>
<Option value="all">All Status</Option>
<Option value="pending">Pending</Option>
<Option value="shipped">Shipped</Option>
<Option value="delivered">Delivered</Option>
<Option value="refunded">Refunded</Option>
<Option value="cancelled">Cancelled</Option>
</Select>
<Select placeholder="Select Platform" style={{ width: 150 }}>
<Option value="all">All Platforms</Option>
<Option value="amazon">Amazon</Option>
<Option value="ebay">eBay</Option>
<Option value="shopee">Shopee</Option>
</Select>
<RangePicker style={{ width: 300 }} />
</div>
<div style={{ marginBottom: 16 }}>
<Space>
<Button onClick={handleBatchShip} disabled={selectedRowKeys.length === 0}>
Batch Ship
</Button>
<Button onClick={handleBatchCancel} disabled={selectedRowKeys.length === 0}>
Batch Cancel
</Button>
<Button onClick={handleBatchExport} disabled={selectedRowKeys.length === 0}>
Batch Export
</Button>
</Space>
</div>
<Table
rowSelection={rowSelection}
columns={columns}
dataSource={orders}
rowKey="id"
loading={loading}
pagination={{
pageSize: 10,
}}
/>
</Card>
<Card title="Order Statistics" style={{ marginBottom: 24 }}>
<div style={{ display: 'flex', justifyContent: 'space-around' }}>
<div style={{ textAlign: 'center' }}>
<h3>Total Orders</h3>
<p style={{ fontSize: '24px', fontWeight: 'bold' }}>{totalOrders}</p>
</div>
<div style={{ textAlign: 'center' }}>
<h3>Pending</h3>
<p style={{ fontSize: '24px', fontWeight: 'bold', color: '#1890ff' }}>{pendingOrders}</p>
</div>
<div style={{ textAlign: 'center' }}>
<h3>Shipped</h3>
<p style={{ fontSize: '24px', fontWeight: 'bold', color: '#52c41a' }}>{shippedOrders}</p>
</div>
<div style={{ textAlign: 'center' }}>
<h3>Delivered</h3>
<p style={{ fontSize: '24px', fontWeight: 'bold', color: '#52c41a' }}>{deliveredOrders}</p>
</div>
<div style={{ textAlign: 'center' }}>
<h3>Refunded</h3>
<p style={{ fontSize: '24px', fontWeight: 'bold', color: '#f5222d' }}>{refundedOrders}</p>
</div>
</div>
</Card>
<Modal
title={`Order Detail - ${selectedOrder?.orderNo}`}
open={isDetailModalVisible}
onCancel={() => setIsDetailModalVisible(false)}
footer={[
<Button key="close" onClick={() => setIsDetailModalVisible(false)}>
Close
</Button>,
]}
width={800}
>
{selectedOrder && (
<Tabs defaultActiveKey="details">
<TabPane tab="Order Details" key="details">
<Descriptions column={2}>
<Descriptions.Item label="Order No">{selectedOrder.orderNo}</Descriptions.Item>
<Descriptions.Item label="Customer">{selectedOrder.customer}</Descriptions.Item>
<Descriptions.Item label="Total">¥{selectedOrder.total}</Descriptions.Item>
<Descriptions.Item label="Status">
<Badge status={getStatusColor(selectedOrder.status)} text={selectedOrder.status} />
</Descriptions.Item>
<Descriptions.Item label="Platform">{selectedOrder.platform}</Descriptions.Item>
<Descriptions.Item label="Date">{selectedOrder.date}</Descriptions.Item>
</Descriptions>
<h3 style={{ marginTop: 24 }}>Items</h3>
<Table
columns={[
{ title: 'Product', dataIndex: 'name', key: 'name' },
{ title: 'Quantity', dataIndex: 'quantity', key: 'quantity' },
{ title: 'Price', dataIndex: 'price', key: 'price', render: (value: number) => `¥${value}` },
]}
dataSource={selectedOrder.items}
rowKey="name"
pagination={false}
/>
</TabPane>
<TabPane tab="Shipping" key="shipping">
<p>Shipping information will be displayed here</p>
</TabPane>
<TabPane tab="Payment" key="payment">
<p>Payment information will be displayed here</p>
</TabPane>
</Tabs>
)}
</Modal>
<Modal
title={`Process Order - ${selectedOrder?.orderNo}`}
open={isProcessModalVisible}
onCancel={() => setIsProcessModalVisible(false)}
onOk={async () => {
setIsProcessing(true);
try {
await orderApi.updateStatus(selectedOrder.id, orderStatus);
setIsProcessing(false);
setIsProcessModalVisible(false);
message.success('Order processed successfully');
// 重新加载订单数据
loadOrders();
} catch (error) {
console.error('Failed to process order:', error);
setIsProcessing(false);
message.error('Failed to process order');
}
}}
>
{isProcessing ? (
<div style={{ display: 'flex', justifyContent: 'center', padding: '40px 0' }}>
<div>
<p>Processing order...</p>
<div style={{ marginTop: 16, display: 'flex', justifyContent: 'center' }}>
<Spin size="large" />
</div>
</div>
</div>
) : (
<>
<div style={{ margin: '16px 0' }}>
<p>Order: {selectedOrder?.orderNo}</p>
<p>Customer: {selectedOrder?.customer}</p>
<p>Current Status: <Badge status={getStatusColor(selectedOrder?.status || '')} text={selectedOrder?.status} /></p>
</div>
<div style={{ margin: '16px 0' }}>
<h3>Update Status</h3>
<Select
style={{ width: '100%' }}
defaultValue={selectedOrder?.status}
onChange={(value) => setOrderStatus(value)}
>
<Option value="pending">Pending</Option>
<Option value="shipped">Shipped</Option>
<Option value="delivered">Delivered</Option>
<Option value="cancelled">Cancelled</Option>
</Select>
</div>
{orderStatus === 'shipped' && (
<div style={{ margin: '16px 0' }}>
<h3>Shipping Information</h3>
<Input placeholder="Tracking Number" style={{ marginBottom: 8 }} />
<Select style={{ width: '100%' }} placeholder="Shipping Carrier">
<Option value="fedex">FedEx</Option>
<Option value="ups">UPS</Option>
<Option value="usps">USPS</Option>
<Option value="dhl">DHL</Option>
</Select>
</div>
)}
</>
)}
</Modal>
</div>
);
};
export default OrdersPage;

View File

@@ -0,0 +1,527 @@
import React, { useState, useEffect } from 'react';
import { Card, Table, Button, Input, Select, DatePicker, Space, Modal, Form, InputNumber, message, Spin } from 'antd';
import { EditOutlined, DollarOutlined, BarChartOutlined, RocketOutlined, UploadOutlined, DownloadOutlined, PlusOutlined } from '@ant-design/icons';
import { Tag } from 'antd';
import { productApi } from '../services/ApiService';
const { Option } = Select;
const { Search } = Input;
const ProductPage: React.FC = () => {
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
const [isEditModalVisible, setIsEditModalVisible] = useState(false);
const [isPriceModalVisible, setIsPriceModalVisible] = useState(false);
const [isAiPricingModalVisible, setIsAiPricingModalVisible] = useState(false);
const [editingProduct, setEditingProduct] = useState<any>(null);
const [form] = Form.useForm();
const [products, setProducts] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const [totalProducts, setTotalProducts] = useState(0);
const [totalCost, setTotalCost] = useState(0);
const [totalRevenue, setTotalRevenue] = useState(0);
const [totalProfit, setTotalProfit] = useState(0);
const [averageRoi, setAverageRoi] = useState(0);
// 加载商品数据
useEffect(() => {
loadProducts();
}, []);
const loadProducts = async () => {
setLoading(true);
try {
const response = await productApi.getProducts();
setProducts(response.data || []);
calculateSummary(response.data || []);
} catch (error) {
console.error('Failed to load products:', error);
message.error('Failed to load products');
// 加载失败时使用mock数据
const mockProducts = [
{
id: '1',
name: 'Wireless Headphones',
sku: 'WH-123',
cost: 50,
price: 129.99,
profit: 79.99,
roi: 159.98,
stock: 150,
status: 'active',
category: 'Electronics',
shop: 'Shop 1',
},
{
id: '2',
name: 'Bluetooth Speaker',
sku: 'BS-456',
cost: 35,
price: 89.99,
profit: 54.99,
roi: 157.11,
stock: 80,
status: 'active',
category: 'Electronics',
shop: 'Shop 1',
},
{
id: '3',
name: 'Smart Watch',
sku: 'SW-789',
cost: 80,
price: 199.99,
profit: 119.99,
roi: 149.99,
stock: 50,
status: 'active',
category: 'Electronics',
shop: 'Shop 2',
},
{
id: '4',
name: 'Fitness Tracker',
sku: 'FT-321',
cost: 25,
price: 69.99,
profit: 44.99,
roi: 179.96,
stock: 200,
status: 'inactive',
category: 'Electronics',
shop: 'Shop 2',
},
];
setProducts(mockProducts);
calculateSummary(mockProducts);
} finally {
setLoading(false);
}
};
const calculateSummary = (productList: any[]) => {
if (!productList || productList.length === 0) {
setTotalProducts(0);
setTotalCost(0);
setTotalRevenue(0);
setTotalProfit(0);
setAverageRoi(0);
return;
}
const total = productList.length;
const cost = productList.reduce((sum, product) => sum + product.cost, 0);
const revenue = productList.reduce((sum, product) => sum + product.price, 0);
const profit = productList.reduce((sum, product) => sum + product.profit, 0);
const roi = productList.reduce((sum, product) => sum + product.roi, 0) / total;
setTotalProducts(total);
setTotalCost(cost);
setTotalRevenue(revenue);
setTotalProfit(profit);
setAverageRoi(roi);
};
const handleEdit = (record: any) => {
setEditingProduct(record);
form.setFieldsValue(record);
setIsEditModalVisible(true);
};
const handlePriceChange = (record: any) => {
setEditingProduct(record);
form.setFieldsValue({ price: record.price });
setIsPriceModalVisible(true);
};
const [isAiLoading, setIsAiLoading] = useState(false);
const [aiSuggestedPrice, setAiSuggestedPrice] = useState<number>(0);
const handleAiPricing = async (record: any) => {
setEditingProduct(record);
setIsAiLoading(true);
try {
// 调用AI定价API
const response = await productApi.getAiPricing(record.id);
setAiSuggestedPrice(response.suggestedPrice || record.cost * (2 + Math.random() * 1));
} catch (error) {
console.error('Failed to get AI pricing:', error);
// 失败时使用模拟数据
const suggestedPrice = record.cost * (2 + Math.random() * 1);
setAiSuggestedPrice(suggestedPrice);
} finally {
setIsAiLoading(false);
setIsAiPricingModalVisible(true);
}
};
const handleToggleStatus = async (record: any) => {
try {
await productApi.toggleStatus(record.id);
message.success(`${record.name} has been ${record.status === 'active' ? 'deactivated' : 'activated'}`);
// 重新加载商品数据
loadProducts();
} catch (error) {
console.error('Failed to toggle status:', error);
message.error('Failed to toggle status');
}
};
const handleBatchPriceChange = async () => {
if (selectedRowKeys.length === 0) return;
try {
await productApi.batchUpdate(selectedRowKeys.map(key => key.toString()), { action: 'price' });
message.success('Batch price change initiated');
// 重新加载商品数据
loadProducts();
} catch (error) {
console.error('Failed to batch update price:', error);
message.error('Failed to batch update price');
}
};
const handleBatchActivate = async () => {
if (selectedRowKeys.length === 0) return;
try {
await productApi.batchUpdate(selectedRowKeys.map(key => key.toString()), { action: 'activate' });
message.success('Batch activation initiated');
// 重新加载商品数据
loadProducts();
} catch (error) {
console.error('Failed to batch activate:', error);
message.error('Failed to batch activate');
}
};
const handleBatchAnalysis = async () => {
if (selectedRowKeys.length === 0) return;
try {
await productApi.batchUpdate(selectedRowKeys.map(key => key.toString()), { action: 'analyze' });
message.success('Batch analysis initiated');
} catch (error) {
console.error('Failed to batch analyze:', error);
message.error('Failed to batch analyze');
}
};
const handleCreateProduct = () => {
// 这里可以打开创建商品的表单
message.success('Create product form opened');
};
const handleImportProducts = () => {
// 这里可以打开导入商品的对话框
message.success('Import products dialog opened');
};
const handleExportProducts = async () => {
try {
// 调用导出API
await productApi.getProducts({ export: true });
message.success('Products exported');
} catch (error) {
console.error('Failed to export products:', error);
message.error('Failed to export products');
}
};
const columns = [
{
title: 'Product Name',
dataIndex: 'name',
key: 'name',
},
{
title: 'SKU',
dataIndex: 'sku',
key: 'sku',
},
{
title: 'Cost',
dataIndex: 'cost',
key: 'cost',
render: (value: number) => `¥${value}`,
},
{
title: 'Price',
dataIndex: 'price',
key: 'price',
render: (value: number) => `¥${value}`,
},
{
title: 'Profit',
dataIndex: 'profit',
key: 'profit',
render: (value: number) => `¥${value}`,
},
{
title: 'ROI',
dataIndex: 'roi',
key: 'roi',
render: (value: number) => `${value}%`,
},
{
title: 'Stock',
dataIndex: 'stock',
key: 'stock',
},
{
title: 'Status',
dataIndex: 'status',
key: 'status',
render: (status: string) => (
<Tag color={status === 'active' ? 'green' : 'red'}>
{status === 'active' ? 'Active' : 'Inactive'}
</Tag>
),
},
{
title: 'Actions',
key: 'actions',
render: (_: any, record: any) => (
<Space size="middle">
<Button icon={<EditOutlined />} onClick={() => handleEdit(record)}>Edit</Button>
<Button icon={<DollarOutlined />} onClick={() => handlePriceChange(record)}>Change Price</Button>
<Button icon={<BarChartOutlined />}>View ROI</Button>
<Button icon={<RocketOutlined />} onClick={() => handleAiPricing(record)}>AI Pricing</Button>
<Button onClick={() => handleToggleStatus(record)}>
{record.status === 'active' ? 'Deactivate' : 'Activate'}
</Button>
</Space>
),
},
];
const rowSelection = {
selectedRowKeys,
onChange: (keys: React.Key[]) => {
setSelectedRowKeys(keys);
},
};
return (
<div className="product-page">
<div className="page-header" style={{ marginBottom: 24, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<h1>Products</h1>
<Space>
<Button icon={<PlusOutlined />} onClick={handleCreateProduct}>Create Product</Button>
<Button icon={<UploadOutlined />} onClick={handleImportProducts}>Import Products</Button>
<Button icon={<DownloadOutlined />} onClick={handleExportProducts}>Export Data</Button>
</Space>
</div>
<Card style={{ marginBottom: 24 }}>
<div style={{ display: 'flex', gap: 16, marginBottom: 16, flexWrap: 'wrap' }}>
<Select placeholder="Select Shop" style={{ width: 150 }}>
<Option value="all">All Shops</Option>
<Option value="shop1">Shop 1</Option>
<Option value="shop2">Shop 2</Option>
</Select>
<Select placeholder="Select Category" style={{ width: 150 }}>
<Option value="all">All Categories</Option>
<Option value="electronics">Electronics</Option>
<Option value="clothing">Clothing</Option>
<Option value="home">Home</Option>
</Select>
<Select placeholder="Select Status" style={{ width: 150 }}>
<Option value="all">All Status</Option>
<Option value="active">Active</Option>
<Option value="inactive">Inactive</Option>
</Select>
<Search placeholder="Search products" style={{ width: 300 }} />
</div>
<div style={{ marginBottom: 16 }}>
<Space>
<Button onClick={handleBatchPriceChange} disabled={selectedRowKeys.length === 0}>
Batch Price Change
</Button>
<Button onClick={handleBatchActivate} disabled={selectedRowKeys.length === 0}>
Batch Activate
</Button>
<Button onClick={handleBatchAnalysis} disabled={selectedRowKeys.length === 0}>
Batch Analysis
</Button>
</Space>
</div>
<Table
rowSelection={rowSelection}
columns={columns}
dataSource={products}
rowKey="id"
loading={loading}
pagination={{
pageSize: 10,
}}
/>
</Card>
<Card title="Profit Summary" style={{ marginBottom: 24 }}>
<div style={{ display: 'flex', justifyContent: 'space-around' }}>
<div style={{ textAlign: 'center' }}>
<h3>Total Products</h3>
<p style={{ fontSize: '24px', fontWeight: 'bold' }}>{totalProducts}</p>
</div>
<div style={{ textAlign: 'center' }}>
<h3>Total Cost</h3>
<p style={{ fontSize: '24px', fontWeight: 'bold' }}>¥{totalCost.toFixed(2)}</p>
</div>
<div style={{ textAlign: 'center' }}>
<h3>Total Revenue</h3>
<p style={{ fontSize: '24px', fontWeight: 'bold' }}>¥{totalRevenue.toFixed(2)}</p>
</div>
<div style={{ textAlign: 'center' }}>
<h3>Total Profit</h3>
<p style={{ fontSize: '24px', fontWeight: 'bold', color: '#52c41a' }}>¥{totalProfit.toFixed(2)}</p>
</div>
<div style={{ textAlign: 'center' }}>
<h3>Average ROI</h3>
<p style={{ fontSize: '24px', fontWeight: 'bold', color: '#52c41a' }}>{averageRoi.toFixed(2)}%</p>
</div>
</div>
</Card>
<Modal
title="Edit Product"
open={isEditModalVisible}
onCancel={() => setIsEditModalVisible(false)}
onOk={async () => {
try {
const values = await form.validateFields();
await productApi.updateProduct(editingProduct.id, values);
setIsEditModalVisible(false);
message.success('Product updated successfully');
// 重新加载商品数据
loadProducts();
} catch (error) {
console.error('Failed to update product:', error);
message.error('Failed to update product');
}
}}
>
<Form form={form} layout="vertical">
<Form.Item name="name" label="Product Name">
<Input />
</Form.Item>
<Form.Item name="sku" label="SKU">
<Input />
</Form.Item>
<Form.Item name="cost" label="Cost">
<InputNumber prefix="¥" />
</Form.Item>
<Form.Item name="price" label="Price">
<InputNumber prefix="¥" />
</Form.Item>
</Form>
</Modal>
<Modal
title="Change Price"
open={isPriceModalVisible}
onCancel={() => setIsPriceModalVisible(false)}
onOk={async () => {
try {
const newPrice = form.getFieldValue('price');
if (newPrice) {
await productApi.updatePrice(editingProduct.id, newPrice);
setIsPriceModalVisible(false);
message.success('Price updated successfully');
// 重新加载商品数据
loadProducts();
}
} catch (error) {
console.error('Failed to update price:', error);
message.error('Failed to update price');
}
}}
>
<Form form={form} layout="vertical">
<Form.Item name="price" label="New Price">
<InputNumber
prefix="¥"
onChange={(value) => {
if (value && editingProduct) {
// 实时计算ROI
const newProfit = value - editingProduct.cost;
const newRoi = (newProfit / editingProduct.cost) * 100;
// 更新ROI显示
const roiElement = document.getElementById('estimated-roi');
if (roiElement) {
roiElement.textContent = `${newRoi.toFixed(2)}%`;
roiElement.style.color = newRoi >= 150 ? '#52c41a' : '#faad14';
}
}
}}
/>
</Form.Item>
<div style={{ margin: '16px 0' }}>
<p>Current Cost: <span style={{ fontWeight: 'bold' }}>¥{editingProduct?.cost}</span></p>
<p>Estimated Profit: <span style={{ fontWeight: 'bold', color: '#52c41a' }}>
¥{(form.getFieldValue('price') - editingProduct?.cost).toFixed(2)}
</span></p>
<p>Estimated ROI: <span id="estimated-roi" style={{ fontWeight: 'bold', color: '#52c41a' }}>
{((form.getFieldValue('price') - editingProduct?.cost) / editingProduct?.cost * 100).toFixed(2)}%
</span></p>
</div>
</Form>
</Modal>
<Modal
title="AI Pricing"
open={isAiPricingModalVisible}
onCancel={() => setIsAiPricingModalVisible(false)}
onOk={async () => {
try {
if (editingProduct && aiSuggestedPrice > 0) {
await productApi.updatePrice(editingProduct.id, aiSuggestedPrice);
setIsAiPricingModalVisible(false);
message.success('AI pricing applied successfully');
// 重新加载商品数据
loadProducts();
}
} catch (error) {
console.error('Failed to apply AI pricing:', error);
message.error('Failed to apply AI pricing');
}
}}
>
{isAiLoading ? (
<div style={{ display: 'flex', justifyContent: 'center', padding: '40px 0' }}>
<div>
<p>AI is calculating the optimal price...</p>
<div style={{ marginTop: 16, display: 'flex', justifyContent: 'center' }}>
<Spin size="large" />
</div>
</div>
</div>
) : (
<div style={{ margin: '16px 0' }}>
<p>Current Price: <span style={{ fontWeight: 'bold' }}>¥{editingProduct?.price}</span></p>
<p>AI Suggested Price: <span style={{ fontWeight: 'bold', color: '#1890ff' }}>¥{aiSuggestedPrice.toFixed(2)}</span></p>
<p>Estimated Profit: <span style={{ fontWeight: 'bold', color: '#52c41a' }}>
¥{(aiSuggestedPrice - editingProduct?.cost).toFixed(2)}
</span></p>
<p>Estimated ROI: <span style={{ fontWeight: 'bold', color: '#52c41a' }}>
{((aiSuggestedPrice - editingProduct?.cost) / editingProduct?.cost * 100).toFixed(2)}%
</span></p>
<div style={{ marginTop: 16, padding: 12, backgroundColor: '#f6ffed', borderRadius: 4 }}>
<p style={{ margin: 0 }}>AI Pricing Insights:</p>
<ul style={{ marginTop: 8, marginBottom: 0 }}>
<li>Market competitive price: ¥{aiSuggestedPrice.toFixed(2)}</li>
<li>Expected conversion rate: 8.5%</li>
<li>Recommended pricing strategy: Dynamic pricing</li>
</ul>
</div>
</div>
)}
</Modal>
</div>
);
};
export default ProductPage;

View File

@@ -0,0 +1,581 @@
import React, { useState } from 'react';
import { Card, Form, Input, Select, Button, Space, Typography, Switch, Tabs, Row, Col, message, Divider, Upload, Avatar } from 'antd';
import { SaveOutlined, ReloadOutlined, UserOutlined, LockOutlined, BellOutlined, GlobalOutlined, DatabaseOutlined, ApiOutlined, SettingOutlined } from '@ant-design/icons';
const { Option } = Select;
const { Title, Text } = Typography;
const { TabPane } = Tabs;
const { TextArea } = Input;
const SettingsPage: React.FC = () => {
const [activeTab, setActiveTab] = useState('profile');
const [loading, setLoading] = useState(false);
const [profileForm] = Form.useForm();
const [securityForm] = Form.useForm();
const [notificationForm] = Form.useForm();
const [systemForm] = Form.useForm();
const handleSaveProfile = async (values: any) => {
setLoading(true);
setTimeout(() => {
setLoading(false);
message.success('Profile updated successfully');
}, 1000);
};
const handleSaveSecurity = async (values: any) => {
setLoading(true);
setTimeout(() => {
setLoading(false);
message.success('Security settings updated successfully');
}, 1000);
};
const handleSaveNotification = async (values: any) => {
setLoading(true);
setTimeout(() => {
setLoading(false);
message.success('Notification settings updated successfully');
}, 1000);
};
const handleSaveSystem = async (values: any) => {
setLoading(true);
setTimeout(() => {
setLoading(false);
message.success('System settings updated successfully');
}, 1000);
};
const handleResetSettings = () => {
message.success('Settings reset to default');
};
const handleExportSettings = () => {
message.success('Settings exported successfully');
};
const handleImportSettings = () => {
message.success('Settings imported successfully');
};
return (
<div className="settings-page">
<div className="page-header" style={{ marginBottom: 24, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Title level={4}>System Settings</Title>
<Space>
<Button icon={<ReloadOutlined />} onClick={handleResetSettings}>Reset to Default</Button>
<Button icon={<ReloadOutlined />} onClick={handleExportSettings}>Export Settings</Button>
<Button icon={<ReloadOutlined />} onClick={handleImportSettings}>Import Settings</Button>
</Space>
</div>
<Card>
<Tabs activeKey={activeTab} onChange={setActiveTab}>
<TabPane tab="Profile" key="profile">
<div style={{ maxWidth: 800, margin: '0 auto' }}>
<div style={{ textAlign: 'center', marginBottom: 32 }}>
<Avatar size={100} icon={<UserOutlined />} style={{ marginBottom: 16 }} />
<Title level={4}>User Profile</Title>
<Text type="secondary">Manage your personal information and preferences</Text>
</div>
<Form
form={profileForm}
layout="vertical"
onFinish={handleSaveProfile}
initialValues={{
username: 'admin',
email: 'admin@crawlful.com',
firstName: 'Admin',
lastName: 'User',
phone: '+1-234-567-8900',
timezone: 'UTC-8',
language: 'en',
}}
>
<Row gutter={16}>
<Col span={12}>
<Form.Item
name="username"
label="Username"
rules={[{ required: true, message: 'Please input your username' }]}
>
<Input prefix={<UserOutlined />} placeholder="Username" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="email"
label="Email"
rules={[
{ required: true, message: 'Please input your email' },
{ type: 'email', message: 'Please input a valid email' },
]}
>
<Input placeholder="Email" />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item
name="firstName"
label="First Name"
rules={[{ required: true, message: 'Please input your first name' }]}
>
<Input placeholder="First Name" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="lastName"
label="Last Name"
rules={[{ required: true, message: 'Please input your last name' }]}
>
<Input placeholder="Last Name" />
</Form.Item>
</Col>
</Row>
<Form.Item
name="phone"
label="Phone Number"
>
<Input placeholder="Phone Number" />
</Form.Item>
<Row gutter={16}>
<Col span={12}>
<Form.Item
name="timezone"
label="Timezone"
rules={[{ required: true, message: 'Please select your timezone' }]}
>
<Select placeholder="Select Timezone">
<Option value="UTC-8">UTC-8 (Pacific Time)</Option>
<Option value="UTC-5">UTC-5 (Eastern Time)</Option>
<Option value="UTC+0">UTC+0 (Greenwich Mean Time)</Option>
<Option value="UTC+1">UTC+1 (Central European Time)</Option>
<Option value="UTC+8">UTC+8 (China Standard Time)</Option>
</Select>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="language"
label="Language"
rules={[{ required: true, message: 'Please select your language' }]}
>
<Select placeholder="Select Language">
<Option value="en">English</Option>
<Option value="zh"></Option>
<Option value="es">Español</Option>
<Option value="fr">Français</Option>
<Option value="de">Deutsch</Option>
</Select>
</Form.Item>
</Col>
</Row>
<Form.Item>
<Button type="primary" htmlType="submit" icon={<SaveOutlined />} loading={loading} block>
Save Profile
</Button>
</Form.Item>
</Form>
</div>
</TabPane>
<TabPane tab="Security" key="security">
<div style={{ maxWidth: 800, margin: '0 auto' }}>
<div style={{ textAlign: 'center', marginBottom: 32 }}>
<Avatar size={100} icon={<LockOutlined />} style={{ marginBottom: 16 }} />
<Title level={4}>Security Settings</Title>
<Text type="secondary">Manage your security and authentication preferences</Text>
</div>
<Form
form={securityForm}
layout="vertical"
onFinish={handleSaveSecurity}
initialValues={{
currentPassword: '',
newPassword: '',
confirmPassword: '',
twoFactorEnabled: false,
loginAlerts: true,
}}
>
<Form.Item
name="currentPassword"
label="Current Password"
rules={[{ required: true, message: 'Please input your current password' }]}
>
<Input.Password prefix={<LockOutlined />} placeholder="Current Password" />
</Form.Item>
<Form.Item
name="newPassword"
label="New Password"
rules={[
{ required: true, message: 'Please input your new password' },
{ min: 8, message: 'Password must be at least 8 characters' },
]}
>
<Input.Password prefix={<LockOutlined />} placeholder="New Password" />
</Form.Item>
<Form.Item
name="confirmPassword"
label="Confirm Password"
dependencies={['newPassword']}
rules={[
{ required: true, message: 'Please confirm your password' },
({ getFieldValue }) => ({
validator(_, value) {
if (!value || getFieldValue('newPassword') === value) {
return Promise.resolve();
}
return Promise.reject(new Error('The two passwords do not match'));
},
}),
]}
>
<Input.Password prefix={<LockOutlined />} placeholder="Confirm Password" />
</Form.Item>
<Divider />
<Form.Item
name="twoFactorEnabled"
label="Two-Factor Authentication"
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item
name="loginAlerts"
label="Login Alerts"
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" icon={<SaveOutlined />} loading={loading} block>
Save Security Settings
</Button>
</Form.Item>
</Form>
</div>
</TabPane>
<TabPane tab="Notifications" key="notifications">
<div style={{ maxWidth: 800, margin: '0 auto' }}>
<div style={{ textAlign: 'center', marginBottom: 32 }}>
<Avatar size={100} icon={<BellOutlined />} style={{ marginBottom: 16 }} />
<Title level={4}>Notification Settings</Title>
<Text type="secondary">Manage your notification preferences</Text>
</div>
<Form
form={notificationForm}
layout="vertical"
onFinish={handleSaveNotification}
initialValues={{
emailNotifications: true,
pushNotifications: true,
orderUpdates: true,
priceAlerts: true,
inventoryAlerts: true,
complianceAlerts: true,
marketingEmails: false,
}}
>
<Title level={5}>Email Notifications</Title>
<Form.Item
name="emailNotifications"
label="Enable Email Notifications"
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item
name="orderUpdates"
label="Order Updates"
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item
name="priceAlerts"
label="Price Alerts"
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item
name="inventoryAlerts"
label="Inventory Alerts"
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item
name="complianceAlerts"
label="Compliance Alerts"
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item
name="marketingEmails"
label="Marketing Emails"
valuePropName="checked"
>
<Switch />
</Form.Item>
<Divider />
<Title level={5}>Push Notifications</Title>
<Form.Item
name="pushNotifications"
label="Enable Push Notifications"
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" icon={<SaveOutlined />} loading={loading} block>
Save Notification Settings
</Button>
</Form.Item>
</Form>
</div>
</TabPane>
<TabPane tab="System" key="system">
<div style={{ maxWidth: 800, margin: '0 auto' }}>
<div style={{ textAlign: 'center', marginBottom: 32 }}>
<Avatar size={100} icon={<SettingOutlined />} style={{ marginBottom: 16 }} />
<Title level={4}>System Settings</Title>
<Text type="secondary">Manage system-wide preferences and configurations</Text>
</div>
<Form
form={systemForm}
layout="vertical"
onFinish={handleSaveSystem}
initialValues={{
siteName: 'Crawlful Hub',
siteUrl: 'https://crawlful.com',
defaultCurrency: 'USD',
dateFormat: 'YYYY-MM-DD',
timeFormat: '24h',
apiRateLimit: 1000,
dataRetentionDays: 90,
}}
>
<Title level={5}>General Settings</Title>
<Form.Item
name="siteName"
label="Site Name"
rules={[{ required: true, message: 'Please input site name' }]}
>
<Input placeholder="Site Name" />
</Form.Item>
<Form.Item
name="siteUrl"
label="Site URL"
rules={[{ required: true, message: 'Please input site URL' }]}
>
<Input placeholder="Site URL" />
</Form.Item>
<Row gutter={16}>
<Col span={12}>
<Form.Item
name="defaultCurrency"
label="Default Currency"
rules={[{ required: true, message: 'Please select default currency' }]}
>
<Select placeholder="Select Currency">
<Option value="USD">USD - US Dollar</Option>
<Option value="EUR">EUR - Euro</Option>
<Option value="GBP">GBP - British Pound</Option>
<Option value="CNY">CNY - Chinese Yuan</Option>
<Option value="JPY">JPY - Japanese Yen</Option>
</Select>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="dateFormat"
label="Date Format"
rules={[{ required: true, message: 'Please select date format' }]}
>
<Select placeholder="Select Date Format">
<Option value="YYYY-MM-DD">YYYY-MM-DD</Option>
<Option value="MM/DD/YYYY">MM/DD/YYYY</Option>
<Option value="DD/MM/YYYY">DD/MM/YYYY</Option>
</Select>
</Form.Item>
</Col>
</Row>
<Form.Item
name="timeFormat"
label="Time Format"
rules={[{ required: true, message: 'Please select time format' }]}
>
<Select placeholder="Select Time Format">
<Option value="24h">24-hour</Option>
<Option value="12h">12-hour</Option>
</Select>
</Form.Item>
<Divider />
<Title level={5}>API Settings</Title>
<Form.Item
name="apiRateLimit"
label="API Rate Limit (requests per minute)"
rules={[{ required: true, message: 'Please input API rate limit' }]}
>
<Input type="number" placeholder="API Rate Limit" />
</Form.Item>
<Divider />
<Title level={5}>Data Settings</Title>
<Form.Item
name="dataRetentionDays"
label="Data Retention Period (days)"
rules={[{ required: true, message: 'Please input data retention period' }]}
>
<Input type="number" placeholder="Data Retention Period" />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" icon={<SaveOutlined />} loading={loading} block>
Save System Settings
</Button>
</Form.Item>
</Form>
</div>
</TabPane>
<TabPane tab="Integrations" key="integrations">
<div style={{ maxWidth: 800, margin: '0 auto' }}>
<div style={{ textAlign: 'center', marginBottom: 32 }}>
<Avatar size={100} icon={<ApiOutlined />} style={{ marginBottom: 16 }} />
<Title level={4}>Integrations</Title>
<Text type="secondary">Manage third-party service integrations</Text>
</div>
<Card title="Platform Integrations" style={{ marginBottom: 24 }}>
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '12px 0' }}>
<div>
<Text strong>Amazon Integration</Text>
<br />
<Text type="secondary">Connect your Amazon seller account</Text>
</div>
<Button type="primary">Connect</Button>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '12px 0' }}>
<div>
<Text strong>eBay Integration</Text>
<br />
<Text type="secondary">Connect your eBay seller account</Text>
</div>
<Button type="primary">Connect</Button>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '12px 0' }}>
<div>
<Text strong>Shopee Integration</Text>
<br />
<Text type="secondary">Connect your Shopee seller account</Text>
</div>
<Button type="primary">Connect</Button>
</div>
</Space>
</Card>
<Card title="Service Integrations" style={{ marginBottom: 24 }}>
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '12px 0' }}>
<div>
<Text strong>Payment Gateway</Text>
<br />
<Text type="secondary">Connect payment processing service</Text>
</div>
<Button type="primary">Connect</Button>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '12px 0' }}>
<div>
<Text strong>Shipping Service</Text>
<br />
<Text type="secondary">Connect logistics and shipping service</Text>
</div>
<Button type="primary">Connect</Button>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '12px 0' }}>
<div>
<Text strong>Analytics Service</Text>
<br />
<Text type="secondary">Connect analytics and reporting service</Text>
</div>
<Button type="primary">Connect</Button>
</div>
</Space>
</Card>
<Card title="API Configuration">
<Form layout="vertical">
<Form.Item label="API Key">
<Input.Password placeholder="Enter your API key" />
</Form.Item>
<Form.Item label="API Secret">
<Input.Password placeholder="Enter your API secret" />
</Form.Item>
<Form.Item label="Webhook URL">
<Input placeholder="Enter webhook URL" />
</Form.Item>
<Button type="primary" icon={<SaveOutlined />} block>
Save API Configuration
</Button>
</Form>
</Card>
</div>
</TabPane>
</Tabs>
</Card>
</div>
);
};
export default SettingsPage;

View File

@@ -0,0 +1,37 @@
import React from 'react';
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import Layout from '../components/Layout';
import DashboardPage from '../pages/DashboardPage';
import ProductPage from '../pages/ProductPage';
import OrdersPage from '../pages/OrdersPage';
import AdPage from '../pages/AdPage';
import InventoryPage from '../pages/InventoryPage';
import B2BPage from '../pages/B2BPage';
import FinancePage from '../pages/FinancePage';
import CompliancePage from '../pages/CompliancePage';
import SettingsPage from '../pages/SettingsPage';
import IndependentSiteList from '../pages/IndependentSite/List';
const AppRoutes: React.FC = () => {
return (
<Router>
<Routes>
<Route path="/" element={<Navigate to="/dashboard" />} />
<Route path="/" element={<Layout />}>
<Route path="dashboard" element={<DashboardPage />} />
<Route path="product" element={<ProductPage />} />
<Route path="orders" element={<OrdersPage />} />
<Route path="ad" element={<AdPage />} />
<Route path="inventory" element={<InventoryPage />} />
<Route path="b2b" element={<B2BPage />} />
<Route path="finance" element={<FinancePage />} />
<Route path="compliance" element={<CompliancePage />} />
<Route path="settings" element={<SettingsPage />} />
<Route path="independent-site" element={<IndependentSiteList />} />
</Route>
</Routes>
</Router>
);
};
export default AppRoutes;

View File

@@ -0,0 +1,83 @@
import axios from 'axios';
// 创建axios实例
const api = axios.create({
baseURL: 'http://localhost:3000/api', // 后端API基础URL
timeout: 10000, // 请求超时时间
headers: {
'Content-Type': 'application/json',
},
});
// 请求拦截器
api.interceptors.request.use(
(config) => {
// 可以在这里添加token等认证信息
return config;
},
(error) => {
return Promise.reject(error);
}
);
// 响应拦截器
api.interceptors.response.use(
(response) => {
return response.data;
},
(error) => {
// 统一错误处理
console.error('API Error:', error);
return Promise.reject(error);
}
);
// 商品相关API
export const productApi = {
// 获取商品列表
getProducts: (params?: any) => api.get('/products', { params }),
// 获取商品详情
getProduct: (id: string) => api.get(`/products/${id}`),
// 创建商品
createProduct: (data: any) => api.post('/products', data),
// 更新商品
updateProduct: (id: string, data: any) => api.put(`/products/${id}`, data),
// 修改商品价格
updatePrice: (id: string, price: number) => api.patch(`/products/${id}/price`, { price }),
// AI定价
getAiPricing: (id: string) => api.get(`/products/${id}/ai-pricing`),
// 上下架商品
toggleStatus: (id: string) => api.patch(`/products/${id}/status`),
// 批量操作
batchUpdate: (ids: string[], data: any) => api.post('/products/batch', { ids, data }),
};
// 订单相关API
export const orderApi = {
// 获取订单列表
getOrders: (params?: any) => api.get('/orders', { params }),
// 获取订单详情
getOrder: (id: string) => api.get(`/orders/${id}`),
// 更新订单状态
updateStatus: (id: string, status: string) => api.patch(`/orders/${id}/status`, { status }),
// 处理订单
processOrder: (id: string, data: any) => api.post(`/orders/${id}/process`, data),
};
// Dashboard相关API
export const dashboardApi = {
// 获取Dashboard数据
getDashboardData: (params?: any) => api.get('/dashboard', { params }),
};
export default api;