2026-03-18 01:21:15 +08:00
|
|
|
|
# UI Components (Crawlful Hub)
|
|
|
|
|
|
|
|
|
|
|
|
> **定位**:Crawlful Hub 前端 UI 组件规范 - 基于 Ant Design 5.x 的组件库使用指南。
|
|
|
|
|
|
> **更新日期**: 2026-03-18
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 1. 设计系统
|
|
|
|
|
|
|
|
|
|
|
|
### 1.1 色彩规范
|
|
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
|
// 主色调
|
|
|
|
|
|
const colors = {
|
|
|
|
|
|
primary: '#1890ff', // 主色
|
|
|
|
|
|
success: '#52c41a', // 成功
|
|
|
|
|
|
warning: '#faad14', // 警告
|
|
|
|
|
|
error: '#f5222d', // 错误
|
|
|
|
|
|
info: '#1890ff', // 信息
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 中性色
|
|
|
|
|
|
const neutral = {
|
|
|
|
|
|
textPrimary: 'rgba(0, 0, 0, 0.85)',
|
|
|
|
|
|
textSecondary: 'rgba(0, 0, 0, 0.65)',
|
|
|
|
|
|
textDisabled: 'rgba(0, 0, 0, 0.25)',
|
|
|
|
|
|
border: '#d9d9d9',
|
|
|
|
|
|
background: '#f5f5f5',
|
|
|
|
|
|
};
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 1.2 字体规范
|
|
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
|
const typography = {
|
|
|
|
|
|
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial',
|
|
|
|
|
|
fontSize: {
|
|
|
|
|
|
small: '12px',
|
|
|
|
|
|
base: '14px',
|
|
|
|
|
|
medium: '16px',
|
|
|
|
|
|
large: '20px',
|
|
|
|
|
|
xlarge: '24px',
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 2. 基础组件
|
|
|
|
|
|
|
|
|
|
|
|
### 2.1 按钮 (Button)
|
|
|
|
|
|
|
|
|
|
|
|
**使用场景**
|
|
|
|
|
|
- 主操作:蓝色主按钮
|
|
|
|
|
|
- 次操作:默认按钮
|
|
|
|
|
|
- 危险操作:红色按钮
|
|
|
|
|
|
- 文字链接:链接按钮
|
|
|
|
|
|
|
|
|
|
|
|
**示例**
|
|
|
|
|
|
```tsx
|
|
|
|
|
|
import { Button, Space } from 'antd';
|
|
|
|
|
|
|
|
|
|
|
|
// 主操作
|
|
|
|
|
|
<Button type="primary">创建商品</Button>
|
|
|
|
|
|
|
|
|
|
|
|
// 次操作
|
|
|
|
|
|
<Button>取消</Button>
|
|
|
|
|
|
|
|
|
|
|
|
// 危险操作
|
|
|
|
|
|
<Button type="primary" danger>删除</Button>
|
|
|
|
|
|
|
|
|
|
|
|
// 图标按钮
|
|
|
|
|
|
<Button icon={<PlusOutlined />}>新增</Button>
|
|
|
|
|
|
|
|
|
|
|
|
// 加载状态
|
|
|
|
|
|
<Button loading>保存中</Button>
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 2.2 表单 (Form)
|
|
|
|
|
|
|
|
|
|
|
|
**使用场景**
|
|
|
|
|
|
- 商品创建/编辑
|
|
|
|
|
|
- 订单审核
|
|
|
|
|
|
- 用户设置
|
|
|
|
|
|
|
|
|
|
|
|
**示例**
|
|
|
|
|
|
```tsx
|
|
|
|
|
|
import { Form, Input, Select, Button } from 'antd';
|
|
|
|
|
|
|
|
|
|
|
|
const ProductForm: React.FC = () => {
|
|
|
|
|
|
const [form] = Form.useForm();
|
|
|
|
|
|
|
|
|
|
|
|
const onFinish = (values: any) => {
|
|
|
|
|
|
console.log(values);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Form form={form} onFinish={onFinish} layout="vertical">
|
|
|
|
|
|
<Form.Item
|
|
|
|
|
|
name="title"
|
|
|
|
|
|
label="商品名称"
|
|
|
|
|
|
rules={[{ required: true }]}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Input placeholder="请输入商品名称" />
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
|
|
|
|
|
<Form.Item name="platform" label="平台">
|
|
|
|
|
|
<Select>
|
|
|
|
|
|
<Select.Option value="AMAZON">Amazon</Select.Option>
|
|
|
|
|
|
<Select.Option value="EBAY">eBay</Select.Option>
|
|
|
|
|
|
<Select.Option value="SHOPIFY">Shopify</Select.Option>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
|
|
|
|
|
<Form.Item>
|
|
|
|
|
|
<Button type="primary" htmlType="submit">提交</Button>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Form>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 2.3 表格 (Table)
|
|
|
|
|
|
|
|
|
|
|
|
**使用场景**
|
|
|
|
|
|
- 商品列表
|
|
|
|
|
|
- 订单列表
|
|
|
|
|
|
- 交易流水
|
|
|
|
|
|
|
|
|
|
|
|
**示例**
|
|
|
|
|
|
```tsx
|
|
|
|
|
|
import { Table, Tag } from 'antd';
|
|
|
|
|
|
|
|
|
|
|
|
const ProductTable: React.FC = () => {
|
|
|
|
|
|
const columns = [
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '商品名称',
|
|
|
|
|
|
dataIndex: 'title',
|
|
|
|
|
|
key: 'title',
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '平台',
|
|
|
|
|
|
dataIndex: 'platform',
|
|
|
|
|
|
key: 'platform',
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '状态',
|
|
|
|
|
|
dataIndex: 'status',
|
|
|
|
|
|
key: 'status',
|
|
|
|
|
|
render: (status: string) => {
|
|
|
|
|
|
const colorMap: Record<string, string> = {
|
|
|
|
|
|
DRAFTED: 'default',
|
|
|
|
|
|
PENDING_REVIEW: 'processing',
|
|
|
|
|
|
APPROVED: 'success',
|
|
|
|
|
|
REJECTED: 'error',
|
|
|
|
|
|
};
|
|
|
|
|
|
return <Tag color={colorMap[status]}>{status}</Tag>;
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '售价',
|
|
|
|
|
|
dataIndex: 'sellingPrice',
|
|
|
|
|
|
key: 'sellingPrice',
|
|
|
|
|
|
render: (price: number) => `$${price.toFixed(2)}`,
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '操作',
|
|
|
|
|
|
key: 'action',
|
|
|
|
|
|
render: (_, record) => (
|
|
|
|
|
|
<Space>
|
|
|
|
|
|
<Button type="link">查看</Button>
|
|
|
|
|
|
<Button type="link">编辑</Button>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Table
|
|
|
|
|
|
columns={columns}
|
|
|
|
|
|
dataSource={products}
|
|
|
|
|
|
rowKey="id"
|
|
|
|
|
|
pagination={{ pageSize: 20 }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 2.4 卡片 (Card)
|
|
|
|
|
|
|
|
|
|
|
|
**使用场景**
|
|
|
|
|
|
- 仪表盘统计卡片
|
|
|
|
|
|
- 商品信息展示
|
|
|
|
|
|
- 订单概要
|
|
|
|
|
|
|
|
|
|
|
|
**示例**
|
|
|
|
|
|
```tsx
|
|
|
|
|
|
import { Card, Statistic } from 'antd';
|
|
|
|
|
|
import { ArrowUpOutlined } from '@ant-design/icons';
|
|
|
|
|
|
|
|
|
|
|
|
// 统计卡片
|
|
|
|
|
|
<Card>
|
|
|
|
|
|
<Statistic
|
|
|
|
|
|
title="本月营收"
|
|
|
|
|
|
value={112893}
|
|
|
|
|
|
precision={2}
|
|
|
|
|
|
valueStyle={{ color: '#3f8600' }}
|
|
|
|
|
|
prefix="$"
|
|
|
|
|
|
suffix={<ArrowUpOutlined />}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
|
|
// 带标题的卡片
|
|
|
|
|
|
<Card title="商品信息" extra={<a href="#">更多</a>}>
|
|
|
|
|
|
<p>商品名称: Product Name</p>
|
|
|
|
|
|
<p>平台: Amazon</p>
|
|
|
|
|
|
<p>状态: 已上架</p>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 3. 业务组件
|
|
|
|
|
|
|
2026-03-18 19:12:38 +08:00
|
|
|
|
### 3.1 自定义按钮组件 (CustomButton)
|
|
|
|
|
|
|
|
|
|
|
|
**组件定义**
|
|
|
|
|
|
```tsx
|
|
|
|
|
|
import React from 'react';
|
|
|
|
|
|
import { Button } from 'antd';
|
|
|
|
|
|
|
|
|
|
|
|
export const CustomButton: React.FC<{
|
|
|
|
|
|
type?: 'primary' | 'default' | 'dashed' | 'link' | 'text';
|
|
|
|
|
|
size?: 'small' | 'middle' | 'large';
|
|
|
|
|
|
onClick?: () => void;
|
|
|
|
|
|
children: React.ReactNode;
|
|
|
|
|
|
}> = ({ type = 'default', size = 'middle', onClick, children }) => {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Button type={type} size={size} onClick={onClick}>
|
|
|
|
|
|
{children}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 3.2 自定义卡片组件 (CustomCard)
|
|
|
|
|
|
|
|
|
|
|
|
**组件定义**
|
|
|
|
|
|
```tsx
|
|
|
|
|
|
import React from 'react';
|
|
|
|
|
|
import { Card } from 'antd';
|
|
|
|
|
|
|
|
|
|
|
|
export const CustomCard: React.FC<{
|
|
|
|
|
|
title?: string;
|
|
|
|
|
|
children: React.ReactNode;
|
|
|
|
|
|
extra?: React.ReactNode;
|
|
|
|
|
|
bordered?: boolean;
|
|
|
|
|
|
hoverable?: boolean;
|
|
|
|
|
|
}> = ({ title, children, extra, bordered = true, hoverable = true }) => {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Card
|
|
|
|
|
|
title={title}
|
|
|
|
|
|
extra={extra}
|
|
|
|
|
|
bordered={bordered}
|
|
|
|
|
|
hoverable={hoverable}
|
|
|
|
|
|
>
|
|
|
|
|
|
{children}
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 3.3 自定义输入框组件 (CustomInput)
|
|
|
|
|
|
|
|
|
|
|
|
**组件定义**
|
|
|
|
|
|
```tsx
|
|
|
|
|
|
import React from 'react';
|
|
|
|
|
|
import { Input } from 'antd';
|
|
|
|
|
|
|
|
|
|
|
|
export const CustomInput: React.FC<{
|
|
|
|
|
|
placeholder?: string;
|
|
|
|
|
|
value?: string;
|
|
|
|
|
|
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
|
|
|
|
|
disabled?: boolean;
|
|
|
|
|
|
size?: 'small' | 'middle' | 'large';
|
|
|
|
|
|
maxLength?: number;
|
|
|
|
|
|
showCount?: boolean;
|
|
|
|
|
|
}> = ({ placeholder, value, onChange, disabled = false, size = 'middle', maxLength, showCount }) => {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Input
|
|
|
|
|
|
placeholder={placeholder}
|
|
|
|
|
|
value={value}
|
|
|
|
|
|
onChange={onChange}
|
|
|
|
|
|
disabled={disabled}
|
|
|
|
|
|
size={size}
|
|
|
|
|
|
maxLength={maxLength}
|
|
|
|
|
|
showCount={showCount}
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 3.4 响应式布局组件 (ResponsiveLayout)
|
|
|
|
|
|
|
|
|
|
|
|
**组件定义**
|
|
|
|
|
|
```tsx
|
|
|
|
|
|
import React from 'react';
|
|
|
|
|
|
import { Layout, Row, Col, Menu, ConfigProvider } from 'antd';
|
|
|
|
|
|
import { useMediaQuery } from 'react-responsive';
|
|
|
|
|
|
|
|
|
|
|
|
const { Header, Sider, Content, Footer } = Layout;
|
|
|
|
|
|
|
|
|
|
|
|
export const ResponsiveLayout: React.FC<{
|
|
|
|
|
|
children: React.ReactNode;
|
|
|
|
|
|
menuItems: { key: string; label: string }[];
|
|
|
|
|
|
selectedKey: string;
|
|
|
|
|
|
onMenuSelect: (key: string) => void;
|
|
|
|
|
|
logo?: React.ReactNode;
|
|
|
|
|
|
headerContent?: React.ReactNode;
|
|
|
|
|
|
footerContent?: React.ReactNode;
|
|
|
|
|
|
}> = ({
|
|
|
|
|
|
children,
|
|
|
|
|
|
menuItems,
|
|
|
|
|
|
selectedKey,
|
|
|
|
|
|
onMenuSelect,
|
|
|
|
|
|
logo,
|
|
|
|
|
|
headerContent,
|
|
|
|
|
|
footerContent,
|
|
|
|
|
|
}) => {
|
|
|
|
|
|
const isMobile = useMediaQuery({ maxWidth: 768 });
|
|
|
|
|
|
const isTablet = useMediaQuery({ minWidth: 769, maxWidth: 1024 });
|
|
|
|
|
|
const isDesktop = useMediaQuery({ minWidth: 1025 });
|
|
|
|
|
|
|
|
|
|
|
|
const [collapsed, setCollapsed] = React.useState(false);
|
|
|
|
|
|
|
|
|
|
|
|
const toggleCollapse = () => {
|
|
|
|
|
|
setCollapsed(!collapsed);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<ConfigProvider
|
|
|
|
|
|
theme={{
|
|
|
|
|
|
token: {
|
|
|
|
|
|
colorPrimary: '#1890ff',
|
|
|
|
|
|
},
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Layout style={{ minHeight: '100vh' }}>
|
|
|
|
|
|
{!isMobile && (
|
|
|
|
|
|
<Sider
|
|
|
|
|
|
collapsible
|
|
|
|
|
|
collapsed={collapsed}
|
|
|
|
|
|
onCollapse={setCollapsed}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
background: '#fff',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className="logo" style={{ padding: '16px', textAlign: 'center' }}>
|
|
|
|
|
|
{logo || <div style={{ fontSize: '18px', fontWeight: 'bold' }}>Crawlful Hub</div>}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Menu
|
|
|
|
|
|
mode="inline"
|
|
|
|
|
|
selectedKeys={[selectedKey]}
|
|
|
|
|
|
onSelect={({ key }) => onMenuSelect(key)}
|
|
|
|
|
|
items={menuItems}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
height: '100%',
|
|
|
|
|
|
borderRight: 0,
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Sider>
|
|
|
|
|
|
)}
|
|
|
|
|
|
<Layout style={{ flex: 1 }}>
|
|
|
|
|
|
<Header
|
|
|
|
|
|
style={{
|
|
|
|
|
|
background: '#fff',
|
|
|
|
|
|
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
justifyContent: 'space-between',
|
|
|
|
|
|
padding: '0 24px',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{isMobile && (
|
|
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center' }}>
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={toggleCollapse}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
background: 'none',
|
|
|
|
|
|
border: 'none',
|
|
|
|
|
|
fontSize: '18px',
|
|
|
|
|
|
cursor: 'pointer',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
☰
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<div style={{ marginLeft: '16px', fontSize: '16px', fontWeight: 'bold' }}>
|
|
|
|
|
|
Crawlful Hub
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{headerContent || (
|
|
|
|
|
|
<div style={{ fontSize: '14px' }}>Welcome to Crawlful Hub</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Header>
|
|
|
|
|
|
<Content
|
|
|
|
|
|
style={{
|
|
|
|
|
|
margin: '24px',
|
|
|
|
|
|
padding: '24px',
|
|
|
|
|
|
background: '#fff',
|
|
|
|
|
|
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
|
|
|
|
|
|
borderRadius: '4px',
|
|
|
|
|
|
minHeight: 280,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{children}
|
|
|
|
|
|
</Content>
|
|
|
|
|
|
<Footer
|
|
|
|
|
|
style={{
|
|
|
|
|
|
textAlign: 'center',
|
|
|
|
|
|
background: '#fff',
|
|
|
|
|
|
borderTop: '1px solid #f0f0f0',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{footerContent || 'Crawlful Hub ©2026 Created by AI'}
|
|
|
|
|
|
</Footer>
|
|
|
|
|
|
</Layout>
|
|
|
|
|
|
</Layout>
|
|
|
|
|
|
</ConfigProvider>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 3.5 响应式网格组件 (ResponsiveGrid)
|
|
|
|
|
|
|
|
|
|
|
|
**组件定义**
|
|
|
|
|
|
```tsx
|
|
|
|
|
|
import React from 'react';
|
|
|
|
|
|
import { Row, Col } from 'antd';
|
|
|
|
|
|
|
|
|
|
|
|
export const ResponsiveGrid: React.FC<{
|
|
|
|
|
|
children: React.ReactNode;
|
|
|
|
|
|
gutter?: number;
|
|
|
|
|
|
xs?: number;
|
|
|
|
|
|
sm?: number;
|
|
|
|
|
|
md?: number;
|
|
|
|
|
|
lg?: number;
|
|
|
|
|
|
xl?: number;
|
|
|
|
|
|
}> = ({
|
|
|
|
|
|
children,
|
|
|
|
|
|
gutter = 16,
|
|
|
|
|
|
xs = 24,
|
|
|
|
|
|
sm = 12,
|
|
|
|
|
|
md = 8,
|
|
|
|
|
|
lg = 6,
|
|
|
|
|
|
xl = 4,
|
|
|
|
|
|
}) => {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Row gutter={gutter}>
|
|
|
|
|
|
<Col xs={xs} sm={sm} md={md} lg={lg} xl={xl}>
|
|
|
|
|
|
{children}
|
|
|
|
|
|
</Col>
|
|
|
|
|
|
</Row>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 3.6 响应式卡片组件 (ResponsiveCard)
|
|
|
|
|
|
|
|
|
|
|
|
**组件定义**
|
|
|
|
|
|
```tsx
|
|
|
|
|
|
import React from 'react';
|
|
|
|
|
|
import { useMediaQuery } from 'react-responsive';
|
|
|
|
|
|
|
|
|
|
|
|
export const ResponsiveCard: React.FC<{
|
|
|
|
|
|
title?: string;
|
|
|
|
|
|
children: React.ReactNode;
|
|
|
|
|
|
extra?: React.ReactNode;
|
|
|
|
|
|
className?: string;
|
|
|
|
|
|
}> = ({ title, children, extra, className }) => {
|
|
|
|
|
|
const isMobile = useMediaQuery({ maxWidth: 768 });
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div
|
|
|
|
|
|
className={className}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
background: '#fff',
|
|
|
|
|
|
borderRadius: '8px',
|
|
|
|
|
|
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
|
|
|
|
|
|
padding: isMobile ? '16px' : '24px',
|
|
|
|
|
|
marginBottom: '16px',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{title && (
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
fontSize: '16px',
|
|
|
|
|
|
fontWeight: 'bold',
|
|
|
|
|
|
marginBottom: '16px',
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
justifyContent: 'space-between',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{title}
|
|
|
|
|
|
{extra}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{children}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 3.7 响应式导航栏组件 (ResponsiveNavbar)
|
|
|
|
|
|
|
|
|
|
|
|
**组件定义**
|
|
|
|
|
|
```tsx
|
|
|
|
|
|
import React from 'react';
|
|
|
|
|
|
import { useMediaQuery } from 'react-responsive';
|
|
|
|
|
|
|
|
|
|
|
|
export const ResponsiveNavbar: React.FC<{
|
|
|
|
|
|
brand: React.ReactNode;
|
|
|
|
|
|
items: { label: string; href: string; active?: boolean }[];
|
|
|
|
|
|
onItemClick: (href: string) => void;
|
|
|
|
|
|
}> = ({ brand, items, onItemClick }) => {
|
|
|
|
|
|
const isMobile = useMediaQuery({ maxWidth: 768 });
|
|
|
|
|
|
const [mobileMenuOpen, setMobileMenuOpen] = React.useState(false);
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<nav
|
|
|
|
|
|
style={{
|
|
|
|
|
|
background: '#fff',
|
|
|
|
|
|
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
|
|
|
|
|
|
padding: '0 24px',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
maxWidth: '1200px',
|
|
|
|
|
|
margin: '0 auto',
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
justifyContent: 'space-between',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
height: '64px',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center' }}>{brand}</div>
|
|
|
|
|
|
|
|
|
|
|
|
{!isMobile ? (
|
|
|
|
|
|
<div style={{ display: 'flex', gap: '24px' }}>
|
|
|
|
|
|
{items.map((item, index) => (
|
|
|
|
|
|
<a
|
|
|
|
|
|
key={index}
|
|
|
|
|
|
href={item.href}
|
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
onItemClick(item.href);
|
|
|
|
|
|
}}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
color: item.active ? '#1890ff' : '#333',
|
|
|
|
|
|
textDecoration: 'none',
|
|
|
|
|
|
fontSize: '14px',
|
|
|
|
|
|
fontWeight: item.active ? 'bold' : 'normal',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{item.label}
|
|
|
|
|
|
</a>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
background: 'none',
|
|
|
|
|
|
border: 'none',
|
|
|
|
|
|
fontSize: '18px',
|
|
|
|
|
|
cursor: 'pointer',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
☰
|
|
|
|
|
|
</button>
|
|
|
|
|
|
{mobileMenuOpen && (
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
|
top: '64px',
|
|
|
|
|
|
right: '0',
|
|
|
|
|
|
left: '0',
|
|
|
|
|
|
background: '#fff',
|
|
|
|
|
|
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
|
|
|
|
|
|
padding: '16px',
|
|
|
|
|
|
zIndex: 1000,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{items.map((item, index) => (
|
|
|
|
|
|
<a
|
|
|
|
|
|
key={index}
|
|
|
|
|
|
href={item.href}
|
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
onItemClick(item.href);
|
|
|
|
|
|
setMobileMenuOpen(false);
|
|
|
|
|
|
}}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
display: 'block',
|
|
|
|
|
|
padding: '8px 0',
|
|
|
|
|
|
color: item.active ? '#1890ff' : '#333',
|
|
|
|
|
|
textDecoration: 'none',
|
|
|
|
|
|
fontSize: '14px',
|
|
|
|
|
|
fontWeight: item.active ? 'bold' : 'normal',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{item.label}
|
|
|
|
|
|
</a>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</nav>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 3.8 状态徽章组件 (StatusBadge)
|
2026-03-18 01:21:15 +08:00
|
|
|
|
|
|
|
|
|
|
**组件定义**
|
|
|
|
|
|
```tsx
|
|
|
|
|
|
import { Badge } from 'antd';
|
|
|
|
|
|
|
|
|
|
|
|
interface StatusBadgeProps {
|
|
|
|
|
|
status: string;
|
|
|
|
|
|
type: 'product' | 'order' | 'payment';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const statusMap: Record<string, Record<string, { color: string; text: string }>> = {
|
|
|
|
|
|
product: {
|
|
|
|
|
|
DRAFTED: { color: 'default', text: '草稿' },
|
|
|
|
|
|
PENDING_REVIEW: { color: 'processing', text: '待审核' },
|
|
|
|
|
|
APPROVED: { color: 'success', text: '已通过' },
|
|
|
|
|
|
REJECTED: { color: 'error', text: '已拒绝' },
|
|
|
|
|
|
LISTED: { color: 'success', text: '已上架' },
|
|
|
|
|
|
DELISTED: { color: 'default', text: '已下架' },
|
|
|
|
|
|
},
|
|
|
|
|
|
order: {
|
|
|
|
|
|
PULLED: { color: 'default', text: '已拉取' },
|
|
|
|
|
|
PENDING_REVIEW: { color: 'processing', text: '待审核' },
|
|
|
|
|
|
CONFIRMED: { color: 'success', text: '已确认' },
|
|
|
|
|
|
SHIPPED: { color: 'blue', text: '已发货' },
|
|
|
|
|
|
DELIVERED: { color: 'success', text: '已送达' },
|
|
|
|
|
|
},
|
|
|
|
|
|
payment: {
|
|
|
|
|
|
PENDING: { color: 'warning', text: '待支付' },
|
|
|
|
|
|
COMPLETED: { color: 'success', text: '已完成' },
|
|
|
|
|
|
FAILED: { color: 'error', text: '失败' },
|
|
|
|
|
|
REFUNDED: { color: 'default', text: '已退款' },
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export const StatusBadge: React.FC<StatusBadgeProps> = ({ status, type }) => {
|
|
|
|
|
|
const config = statusMap[type]?.[status] || { color: 'default', text: status };
|
|
|
|
|
|
return <Badge status={config.color as any} text={config.text} />;
|
|
|
|
|
|
};
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-03-18 19:12:38 +08:00
|
|
|
|
### 3.9 筛选面板组件 (FilterPanel)
|
2026-03-18 01:21:15 +08:00
|
|
|
|
|
|
|
|
|
|
**组件定义**
|
|
|
|
|
|
```tsx
|
|
|
|
|
|
import { Form, Input, Select, DatePicker, Button, Space } from 'antd';
|
|
|
|
|
|
|
|
|
|
|
|
interface FilterPanelProps {
|
|
|
|
|
|
onFilter: (values: any) => void;
|
|
|
|
|
|
onReset: () => void;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export const FilterPanel: React.FC<FilterPanelProps> = ({ onFilter, onReset }) => {
|
|
|
|
|
|
const [form] = Form.useForm();
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Form form={form} layout="inline" onFinish={onFilter}>
|
|
|
|
|
|
<Form.Item name="keyword" label="关键词">
|
|
|
|
|
|
<Input placeholder="搜索..." allowClear />
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
|
|
|
|
|
<Form.Item name="platform" label="平台">
|
|
|
|
|
|
<Select allowClear style={{ width: 120 }}>
|
|
|
|
|
|
<Select.Option value="AMAZON">Amazon</Select.Option>
|
|
|
|
|
|
<Select.Option value="EBAY">eBay</Select.Option>
|
|
|
|
|
|
<Select.Option value="SHOPIFY">Shopify</Select.Option>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
|
|
|
|
|
<Form.Item name="dateRange" label="日期">
|
|
|
|
|
|
<DatePicker.RangePicker />
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
|
|
|
|
|
<Form.Item>
|
|
|
|
|
|
<Space>
|
|
|
|
|
|
<Button type="primary" htmlType="submit">筛选</Button>
|
|
|
|
|
|
<Button onClick={() => { form.resetFields(); onReset(); }}>重置</Button>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Form>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-03-18 19:12:38 +08:00
|
|
|
|
### 3.10 数据表格组件 (DataTable)
|
2026-03-18 01:21:15 +08:00
|
|
|
|
|
|
|
|
|
|
**组件定义**
|
|
|
|
|
|
```tsx
|
|
|
|
|
|
import { Table, TableProps } from 'antd';
|
|
|
|
|
|
|
|
|
|
|
|
interface DataTableProps<T> extends TableProps<T> {
|
|
|
|
|
|
loading?: boolean;
|
|
|
|
|
|
pagination?: {
|
|
|
|
|
|
current: number;
|
|
|
|
|
|
pageSize: number;
|
|
|
|
|
|
total: number;
|
|
|
|
|
|
};
|
|
|
|
|
|
onPageChange?: (page: number, pageSize: number) => void;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function DataTable<T extends object>({
|
|
|
|
|
|
loading,
|
|
|
|
|
|
pagination,
|
|
|
|
|
|
onPageChange,
|
|
|
|
|
|
...tableProps
|
|
|
|
|
|
}: DataTableProps<T>) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Table<T>
|
|
|
|
|
|
{...tableProps}
|
|
|
|
|
|
loading={loading}
|
|
|
|
|
|
pagination={pagination ? {
|
|
|
|
|
|
...pagination,
|
|
|
|
|
|
showSizeChanger: true,
|
|
|
|
|
|
showTotal: (total) => `共 ${total} 条`,
|
|
|
|
|
|
onChange: onPageChange,
|
|
|
|
|
|
} : false}
|
|
|
|
|
|
scroll={{ x: 'max-content' }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 4. 图表组件
|
|
|
|
|
|
|
|
|
|
|
|
### 4.1 利润趋势图
|
|
|
|
|
|
|
|
|
|
|
|
**使用 Ant Design Charts**
|
|
|
|
|
|
```tsx
|
|
|
|
|
|
import { Line } from '@ant-design/charts';
|
|
|
|
|
|
|
|
|
|
|
|
const ProfitChart: React.FC = () => {
|
|
|
|
|
|
const data = [
|
|
|
|
|
|
{ date: '2026-03-01', profit: 1000 },
|
|
|
|
|
|
{ date: '2026-03-02', profit: 1200 },
|
|
|
|
|
|
{ date: '2026-03-03', profit: 900 },
|
|
|
|
|
|
// ...
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
const config = {
|
|
|
|
|
|
data,
|
|
|
|
|
|
xField: 'date',
|
|
|
|
|
|
yField: 'profit',
|
|
|
|
|
|
smooth: true,
|
|
|
|
|
|
point: {
|
|
|
|
|
|
size: 5,
|
|
|
|
|
|
shape: 'diamond',
|
|
|
|
|
|
},
|
|
|
|
|
|
label: {
|
|
|
|
|
|
style: {
|
|
|
|
|
|
fill: '#aaa',
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return <Line {...config} />;
|
|
|
|
|
|
};
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 4.2 订单分布图
|
|
|
|
|
|
|
|
|
|
|
|
```tsx
|
|
|
|
|
|
import { Pie } from '@ant-design/charts';
|
|
|
|
|
|
|
|
|
|
|
|
const OrderDistributionChart: React.FC = () => {
|
|
|
|
|
|
const data = [
|
|
|
|
|
|
{ type: 'Amazon', value: 400 },
|
|
|
|
|
|
{ type: 'eBay', value: 300 },
|
|
|
|
|
|
{ type: 'Shopify', value: 300 },
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
const config = {
|
|
|
|
|
|
data,
|
|
|
|
|
|
angleField: 'value',
|
|
|
|
|
|
colorField: 'type',
|
|
|
|
|
|
radius: 0.8,
|
|
|
|
|
|
label: {
|
|
|
|
|
|
type: 'outer',
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return <Pie {...config} />;
|
|
|
|
|
|
};
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 5. 布局组件
|
|
|
|
|
|
|
|
|
|
|
|
### 5.1 主布局 (MainLayout)
|
|
|
|
|
|
|
|
|
|
|
|
```tsx
|
|
|
|
|
|
import { Layout, Menu } from 'antd';
|
|
|
|
|
|
import { Outlet, useNavigate } from 'react-router-dom';
|
|
|
|
|
|
|
|
|
|
|
|
const { Header, Sider, Content } = Layout;
|
|
|
|
|
|
|
|
|
|
|
|
const MainLayout: React.FC = () => {
|
|
|
|
|
|
const navigate = useNavigate();
|
|
|
|
|
|
|
|
|
|
|
|
const menuItems = [
|
|
|
|
|
|
{ key: '/', label: '仪表盘', icon: <DashboardOutlined /> },
|
|
|
|
|
|
{ key: '/products', label: '商品管理', icon: <ShoppingOutlined /> },
|
|
|
|
|
|
{ key: '/orders', label: '订单管理', icon: <FileTextOutlined /> },
|
|
|
|
|
|
{ key: '/finance', label: '财务管理', icon: <DollarOutlined /> },
|
|
|
|
|
|
{ key: '/inventory', label: '库存管理', icon: <InboxOutlined /> },
|
|
|
|
|
|
{ key: '/marketing', label: '营销广告', icon: <BarChartOutlined /> },
|
|
|
|
|
|
{ key: '/suppliers', label: '供应商', icon: <TeamOutlined /> },
|
|
|
|
|
|
{ key: '/reports', label: '报表分析', icon: <PieChartOutlined /> },
|
|
|
|
|
|
{ key: '/settings', label: '系统设置', icon: <SettingOutlined /> },
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Layout style={{ minHeight: '100vh' }}>
|
|
|
|
|
|
<Sider theme="light">
|
|
|
|
|
|
<div style={{ height: 64, padding: 16 }}>Crawlful Hub</div>
|
|
|
|
|
|
<Menu
|
|
|
|
|
|
mode="inline"
|
|
|
|
|
|
items={menuItems}
|
|
|
|
|
|
onClick={({ key }) => navigate(key)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Sider>
|
|
|
|
|
|
<Layout>
|
|
|
|
|
|
<Header style={{ background: '#fff', padding: '0 24px' }}>
|
|
|
|
|
|
{/* Header content */}
|
|
|
|
|
|
</Header>
|
|
|
|
|
|
<Content style={{ margin: 24, padding: 24, background: '#fff' }}>
|
|
|
|
|
|
<Outlet />
|
|
|
|
|
|
</Content>
|
|
|
|
|
|
</Layout>
|
|
|
|
|
|
</Layout>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 6. 表单校验规则
|
|
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
|
// utils/validators.ts
|
|
|
|
|
|
export const validators = {
|
|
|
|
|
|
required: (message: string) => ({ required: true, message }),
|
|
|
|
|
|
|
|
|
|
|
|
email: { type: 'email', message: '请输入有效的邮箱地址' },
|
|
|
|
|
|
|
|
|
|
|
|
price: (min: number = 0) => ({
|
|
|
|
|
|
validator: (_: any, value: number) => {
|
|
|
|
|
|
if (value >= min) return Promise.resolve();
|
|
|
|
|
|
return Promise.reject(new Error(`价格不能低于 ${min}`));
|
|
|
|
|
|
},
|
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
|
|
url: { type: 'url', message: '请输入有效的URL' },
|
|
|
|
|
|
};
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 7. 相关文档
|
|
|
|
|
|
|
|
|
|
|
|
- [Frontend Design](./Frontend_Design.md)
|
|
|
|
|
|
- [Pages Flow](./Pages_Flow.md)
|
|
|
|
|
|
- [Ant Design 官方文档](https://ant.design/)
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
*本文档基于 Ant Design 5.x,最后更新: 2026-03-18*
|