# 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. 业务组件

### 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)

**组件定义**
```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} />;
};
```

### 3.9 筛选面板组件 (FilterPanel)

**组件定义**
```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>
  );
};
```

### 3.10 数据表格组件 (DataTable)

**组件定义**
```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*
