feat: 添加前端页面和业务说明书
refactor(server): 重构服务层代码结构 feat(server): 添加基础设施、跨境电商、AI决策等核心服务 docs: 完善前端业务说明书和开发进度文档 style: 格式化代码和文档
This commit is contained in:
302
client/src/components/ComponentLibrary.tsx
Normal file
302
client/src/components/ComponentLibrary.tsx
Normal 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,
|
||||
};
|
||||
26
client/src/components/Layout.tsx
Normal file
26
client/src/components/Layout.tsx
Normal 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;
|
||||
89
client/src/components/MenuComponent.tsx
Normal file
89
client/src/components/MenuComponent.tsx
Normal 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
435
client/src/pages/AdPage.tsx
Normal 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;
|
||||
474
client/src/pages/B2BPage.tsx
Normal file
474
client/src/pages/B2BPage.tsx
Normal 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;
|
||||
500
client/src/pages/CompliancePage.tsx
Normal file
500
client/src/pages/CompliancePage.tsx
Normal 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;
|
||||
235
client/src/pages/DashboardPage.tsx
Normal file
235
client/src/pages/DashboardPage.tsx
Normal 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;
|
||||
436
client/src/pages/FinancePage.tsx
Normal file
436
client/src/pages/FinancePage.tsx
Normal 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;
|
||||
287
client/src/pages/IndependentSite/List.tsx
Normal file
287
client/src/pages/IndependentSite/List.tsx
Normal 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;
|
||||
370
client/src/pages/InventoryPage.tsx
Normal file
370
client/src/pages/InventoryPage.tsx
Normal 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;
|
||||
480
client/src/pages/OrdersPage.tsx
Normal file
480
client/src/pages/OrdersPage.tsx
Normal 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;
|
||||
527
client/src/pages/ProductPage.tsx
Normal file
527
client/src/pages/ProductPage.tsx
Normal 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;
|
||||
581
client/src/pages/SettingsPage.tsx
Normal file
581
client/src/pages/SettingsPage.tsx
Normal 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;
|
||||
37
client/src/routes/index.tsx
Normal file
37
client/src/routes/index.tsx
Normal 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;
|
||||
83
client/src/services/ApiService.ts
Normal file
83
client/src/services/ApiService.ts
Normal 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;
|
||||
Reference in New Issue
Block a user