rtc_web/DESIGN_SPEC.md
zyc b4108961fa
All checks were successful
Build and Deploy Web / build-and-deploy (push) Successful in 1m38s
fix ui
2026-02-10 16:02:02 +08:00

18 KiB
Raw Blame History

RTC Web 前端设计规范

本文档定义 rtc_web 管理后台的 UI 设计规范。所有新页面和组件必须遵循此规范,确保视觉一致性。


1. 技术栈

技术 版本 用途
React 19.x UI 框架
TypeScript 5.9+ 类型安全
Ant Design 5.x 组件库
@ant-design/pro-components 2.x ProTable 等高级组件
@ant-design/charts 2.x 图表 (Pie, Column 等)
Zustand 5.x 状态管理
Vite 7.x 构建工具

2. 色彩体系

2.1 主题色

主题色通过 src/theme/tokens.ts 统一配置,由 Ant Design 的 ConfigProvider 全局应用。

语义 色值 用途
Primary #6366f1 主操作按钮、链接、选中态、品牌色
Success #10b981 成功状态、已绑定、完成
Warning #f59e0b 警告、未导入、待处理
Error #ef4444 错误、删除、禁用
Info #3b82f6 信息提示、只读状态

2.2 统计卡片专用色(statColors

src/theme/tokens.ts 中导入 statColors

import { statColors } from '../../theme/tokens';

// 可用颜色:
statColors.primary  // #6366f1 - 紫蓝
statColors.success  // #10b981 - 绿
statColors.warning  // #f59e0b - 橙
statColors.error    // #ef4444 - 红
statColors.info     // #3b82f6 - 蓝
statColors.purple   // #8b5cf6 - 紫
statColors.cyan     // #06b6d4 - 青
statColors.pink     // #ec4899 - 粉

2.3 中性色

用途 色值
正文标题 #1f2937
正文内容 #374151
辅助文字 #6b7280
占位/禁用 #9ca3af
边框 #e5e7eb
浅背景 #f5f5f7 (Layout 背景)
白色容器 #ffffff

2.4 CSS 变量

全局 CSS 变量定义在 src/index.css:root 中,可在任何 CSS 文件中使用:

var(--color-primary)       /* #6366f1 */
var(--color-primary-light) /* #818cf8 */
var(--color-primary-dark)  /* #4f46e5 */
var(--color-bg-base)       /* #f5f5f7 */
var(--color-bg-elevated)   /* #ffffff */
var(--color-border)        /* #e5e7eb */
var(--shadow-sm)           /* 微弱阴影 */
var(--shadow-md)           /* 中等阴影 */
var(--shadow-lg)           /* 强阴影 */
var(--transition-fast)     /* 150ms */
var(--transition-base)     /* 200ms */
var(--transition-slow)     /* 300ms */

3. 圆角规范

场景 说明
小组件 (Tag) 6px borderRadiusSM
标准组件 (Button, Input) 8px borderRadius
大容器 (Card, Modal) 12px borderRadiusLG
特殊场景 (登录卡片) 16-20px 仅限全屏独立页面
图标背景 12px 统计卡片图标容器

4. 间距规范

基于 8px 网格系统:

场景
紧凑间距 8px
小间距 12px
标准间距 16px
中等间距 24px (内容区 padding、页面级 margin)
宽松间距 32px

常见用法:

// 页面标题与内容
<Title level={4} style={{ marginBottom: 24 }}>页面标题</Title>

// Row 间距
<Row gutter={[16, 16]}>

// Card body padding
<Card styles={{ body: { padding: '20px' } }}>

// 区块间距
<Card style={{ marginBottom: 16 }}>

5. 阴影规范

轻微:  0 1px 2px rgba(0,0,0,0.03)    → 内容区、Card 默认
中等:  0 1px 3px rgba(0,0,0,0.06)    → Header
悬停:  var(--shadow-md)               → Card:hover (全局自动)
强调:  0 25px 50px rgba(99,102,241,0.25) → 登录卡片

6. 页面结构模板

6.1 列表页ProTable 页面)

这是最常见的页面类型,用于设备类型、批次、用户、管理员等管理页面。

import React, { useState, useRef } from 'react';
import { Button, message, Modal, Form, Input, Space, Tag, Popconfirm } from 'antd';
import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons';
import { ProTable } from '@ant-design/pro-components';
import type { ProColumns, ActionType } from '@ant-design/pro-components';
import { getItems, createItem, updateItem, deleteItem } from '../../api/xxx';
import type { Item } from '../../api/xxx';

const XxxPage: React.FC = () => {
    const actionRef = useRef<ActionType>(null);
    const [modalVisible, setModalVisible] = useState(false);
    const [editingRecord, setEditingRecord] = useState<Item | null>(null);
    const [form] = Form.useForm();
    const [loading, setLoading] = useState(false);

    // --- CRUD handlers ---
    const handleAdd = () => {
        setEditingRecord(null);
        form.resetFields();
        setModalVisible(true);
    };

    const handleEdit = (record: Item) => {
        setEditingRecord(record);
        form.setFieldsValue(record);
        setModalVisible(true);
    };

    const handleDelete = async (id: number) => {
        try {
            await deleteItem(id);
            message.success('删除成功');
            actionRef.current?.reload();
        } catch (error) {
            message.error(error instanceof Error ? error.message : '删除失败');
        }
    };

    const handleSubmit = async () => {
        try {
            const values = await form.validateFields();
            setLoading(true);
            if (editingRecord) {
                await updateItem(editingRecord.id, values);
                message.success('更新成功');
            } else {
                await createItem(values);
                message.success('创建成功');
            }
            setModalVisible(false);
            form.resetFields();
            actionRef.current?.reload();
        } catch (error) {
            if (error instanceof Error) {
                message.error(error.message);
            }
        } finally {
            setLoading(false);
        }
    };

    // --- Column definitions ---
    const columns: ProColumns<Item>[] = [
        {
            title: 'ID',
            dataIndex: 'id',
            width: 80,
            search: false,
        },
        {
            title: '名称',
            dataIndex: 'name',
            ellipsis: true,
        },
        {
            title: '状态',
            dataIndex: 'is_active',
            width: 80,
            search: false,
            render: (_, record) => (
                <Tag color={record.is_active ? 'green' : 'red'}>
                    {record.is_active ? '正常' : '禁用'}
                </Tag>
            ),
        },
        {
            title: '创建时间',
            dataIndex: 'created_at',
            valueType: 'dateTime',
            width: 180,
            search: false,
        },
        {
            title: '操作',
            width: 150,
            search: false,
            render: (_, record) => (
                <Space>
                    <Button type="link" size="small" icon={<EditOutlined />}
                        onClick={() => handleEdit(record)}>
                        编辑
                    </Button>
                    <Popconfirm title="确定删除吗?" onConfirm={() => handleDelete(record.id)}>
                        <Button type="link" size="small" danger icon={<DeleteOutlined />}>
                            删除
                        </Button>
                    </Popconfirm>
                </Space>
            ),
        },
    ];

    return (
        <div>
            {/* ✅ 必须加 cardBordered */}
            <ProTable<Item>
                headerTitle="Xxx 管理"
                rowKey="id"
                actionRef={actionRef}
                columns={columns}
                cardBordered
                request={async (params) => {
                    try {
                        const res = await getItems({
                            page: params.current,
                            page_size: params.pageSize,
                        });
                        return {
                            data: res.data.items,
                            total: res.data.total,
                            success: true,
                        };
                    } catch (error) {
                        message.error('获取数据失败');
                        return { data: [], total: 0, success: false };
                    }
                }}
                toolBarRender={() => [
                    <Button key="add" type="primary" icon={<PlusOutlined />} onClick={handleAdd}>
                        新增
                    </Button>,
                ]}
                pagination={{
                    defaultPageSize: 10,
                    showSizeChanger: true,
                }}
            />

            {/* Modal - 统一 layout="vertical" + marginTop 24 */}
            <Modal
                title={editingRecord ? '编辑' : '新增'}
                open={modalVisible}
                onOk={handleSubmit}
                onCancel={() => setModalVisible(false)}
                confirmLoading={loading}
                destroyOnClose
            >
                <Form form={form} layout="vertical" style={{ marginTop: 24 }}>
                    <Form.Item name="name" label="名称"
                        rules={[{ required: true, message: '请输入名称' }]}>
                        <Input placeholder="请输入名称" />
                    </Form.Item>
                </Form>
            </Modal>
        </div>
    );
};

export default XxxPage;

关键规则:

  1. ProTable 必须添加 cardBordered 属性
  2. Modal 内 Form 统一 layout="vertical" + style={{ marginTop: 24 }}
  3. 删除操作必须用 Popconfirm 确认
  4. ID 列:width: 80, search: false
  5. 时间列:valueType: 'dateTime', width: 180, search: false
  6. 操作列:search: false,按钮用 type="link" size="small"
  7. 分页统一:defaultPageSize: 10, showSizeChanger: true

6.2 详情页

import { statColors } from '../../theme/tokens';

// 页头:返回按钮 + 标题
<Space style={{ marginBottom: 24 }} align="center">
    <Button icon={<ArrowLeftOutlined />} onClick={() => navigate('/xxx')}
        type="text" style={{ color: '#6b7280' }}>
        返回
    </Button>
    <Title level={4} style={{ margin: 0, color: '#1f2937' }}>
        详情 - {record.name}
    </Title>
</Space>

// 信息卡片
<Card style={{ marginBottom: 16 }}>
    <Descriptions column={{ xs: 1, sm: 2, md: 3 }}>
        <Descriptions.Item label="字段名"></Descriptions.Item>
    </Descriptions>
</Card>

// 统计数据
<Row gutter={[16, 16]} style={{ marginBottom: 16 }}>
    <Col xs={12} sm={6}>
        <Card styles={{ body: { padding: '16px 20px' } }}>
            <Statistic title="标题" value={100}
                valueStyle={{ color: statColors.primary, fontWeight: 600 }} />
        </Card>
    </Col>
</Row>

// 子表格
<ProTable cardBordered search={false} ... />

6.3 统计卡片

Dashboard 风格的统计卡片模板:

import { statColors } from '../../theme/tokens';

<Card hoverable styles={{ body: { padding: '20px' } }}>
    <div style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
        {/* 图标容器 */}
        <div style={{
            width: 44, height: 44,
            borderRadius: 12,
            background: 'rgba(99, 102, 241, 0.08)',  // 主色 8% 透明度
            display: 'flex', alignItems: 'center', justifyContent: 'center',
            fontSize: 20,
            color: statColors.primary,
            flexShrink: 0,
        }}>
            <UserOutlined />
        </div>
        {/* 数值区 */}
        <div>
            <div style={{ fontSize: 12, color: '#6b7280', marginBottom: 2 }}>
                标题
            </div>
            <div style={{ fontSize: 24, fontWeight: 700, color: '#1f2937', lineHeight: 1.2 }}>
                {value.toLocaleString()}
            </div>
        </div>
    </div>
</Card>

图标背景色规则: 使用对应 statColor 的 8% 透明度作为背景。

rgba(99, 102, 241, 0.08)   → primary 背景
rgba(16, 185, 129, 0.08)   → success 背景
rgba(139, 92, 246, 0.08)   → purple 背景
rgba(245, 158, 11, 0.08)   → warning 背景
rgba(6, 182, 212, 0.08)    → cyan 背景
rgba(236, 72, 153, 0.08)   → pink 背景

7. 图表规范

使用 @ant-design/charts,导入方式:

import { Pie, Column, Line, Area } from '@ant-design/charts';

7.1 饼图

const pieConfig = {
    data: [{ type: '类别', value: 100 }],
    angleField: 'value',
    colorField: 'type',
    radius: 0.85,
    innerRadius: 0.6,  // 环形图
    color: [statColors.primary, statColors.success, statColors.warning],
    label: {
        text: (d) => `${d.type}\n${d.value}`,
        style: { fontSize: 12, fontWeight: 500 },
    },
    legend: {
        color: {
            position: 'bottom' as const,
            layout: { justifyContent: 'center' as const },
        },
    },
};
<Pie {...pieConfig} />

7.2 柱状图

const columnConfig = {
    data: [{ name: '类别', value: 100 }],
    xField: 'name',
    yField: 'value',
    style: {
        radiusTopLeft: 6,
        radiusTopRight: 6,
        fill: statColors.primary,
        fillOpacity: 0.85,
    },
    label: {
        text: (d) => `${d.value}`,
        textBaseline: 'bottom' as const,
        style: { dy: -4, fontSize: 12 },
    },
    axis: {
        y: { title: false },
        x: { title: false },
    },
};
<Column {...columnConfig} />

7.3 图表容器

<Card title="图表标题" styles={{ body: { padding: '16px 24px' } }}>
    <div style={{ height: 320 }}>
        <ChartComponent {...config} />
    </div>
</Card>
  • 图表容器高度统一 320px
  • 空数据时显示居中灰色提示文字
  • 图表颜色优先使用 statColors 中的色值

8. Tag 状态映射

8.1 通用状态

// 启用/禁用
<Tag color={record.is_active ? 'green' : 'red'}>
    {record.is_active ? '正常' : '禁用'}
</Tag>

// 设备状态
const statusMap = {
    in_stock: { color: 'default', text: '库存中' },
    bound:    { color: 'green',   text: '已绑定' },
    offline:  { color: 'red',     text: '离线' },
};

// 布尔属性
<Tag color={value ? 'purple' : 'default'}>{value ? '是' : '否'}</Tag>

// 缺失数据
record.field || <Tag color="orange">未导入</Tag>

8.2 角色

const roleMap = {
    super_admin: { text: '超级管理员', color: 'red' },
    admin:       { text: '管理员',     color: 'blue' },
    operator:    { text: '操作员',     color: 'default' },
};

9. 响应式断点

基于 Ant Design Grid 的断点系统:

// 统计卡片 (6列网格)
<Col xs={24} sm={12} lg={8} xl={4}>

// 图表区域 (2列)
<Col xs={24} lg={12}>

// 详情页统计 (4列)
<Col xs={12} sm={6}>

// Descriptions 响应式列数
<Descriptions column={{ xs: 1, sm: 2, md: 3 }}>

10. 样式规则

10.1 使用优先级

  1. Ant Design Theme Token → 优先使用 theme.useToken() 获取的值
  2. statColors → 统计、图表颜色用 statColors
  3. CSS 变量 → 阴影、过渡动画用 var(--xxx)
  4. 内联 style → 布局、间距等用内联样式
  5. 全局 CSS → 仅用于 Ant Design 组件覆盖(已有的 CSS 文件)

10.2 禁止事项

  • 不要 硬编码 #1890ff(旧主题色),使用 themeToken.colorPrimarystatColors.primary
  • 不要 创建 .module.css 文件,保持现有内联样式模式
  • 不要 在页面组件中直接引用 CSS 文件,全局 CSS 已在 App.tsx 统一导入
  • 不要 使用 style={{ color: '#999' }},用 #6b7280#9ca3af 代替
  • 不要 遗漏 ProTable 的 cardBordered 属性

10.3 获取主题 Token

import { theme } from 'antd';

const MyComponent: React.FC = () => {
    const { token: themeToken } = theme.useToken();

    return (
        <div style={{
            background: themeToken.colorBgContainer,
            borderRadius: themeToken.borderRadiusLG,
            color: themeToken.colorPrimary,
        }}>
            ...
        </div>
    );
};

11. 文件结构

src/
├── theme/
│   ├── tokens.ts          # 主题配置 (themeConfig + statColors)
│   └── sidebar.css        # 侧边栏样式
├── styles/
│   └── protable-theme.css # ProTable 全局样式
├── components/
│   └── Layout/
│       └── index.tsx      # 主布局 (侧边栏 + 头部 + Content)
├── pages/
│   ├── Login/index.tsx    # 登录页 (独立布局)
│   ├── Dashboard/index.tsx
│   ├── [Module]/
│   │   ├── index.tsx      # 列表页
│   │   └── Detail.tsx     # 详情页 (如有)
├── api/                   # API 层
├── store/                 # Zustand 状态
├── routes/index.tsx       # 路由配置
├── App.tsx                # 根组件 (ConfigProvider + 主题)
└── index.css              # CSS 变量 + 全局基础样式

12. 新增页面检查清单

新开发一个页面时,逐项检查:

  • ProTable 添加了 cardBordered
  • 使用 statColors 而非硬编码颜色
  • Tag 状态映射与已有页面一致
  • Modal Form 使用 layout="vertical" + style={{ marginTop: 24 }}
  • 删除操作使用 Popconfirm
  • 时间列使用 valueType: 'dateTime'
  • 页面标题 <Title level={4}> + marginBottom: 24
  • 响应式布局使用 Row/Col + 正确断点
  • 详情页有返回按钮 (type="text")
  • 空状态文字颜色使用 #9ca3af
  • 图表容器高度 320px
  • 分页配置 defaultPageSize: 10, showSizeChanger: true

13. 新增路由步骤

  1. src/pages/ 下创建页面组件
  2. src/routes/index.tsxchildren 数组中添加路由
  3. src/components/Layout/index.tsxmenuItems 中添加菜单项
  4. src/api/ 下创建对应的 API 文件
// routes/index.tsx
{
    path: 'new-page',
    element: <NewPage />,
}

// Layout/index.tsx menuItems
{
    key: '/new-page',
    icon: <SomeOutlined />,
    label: '新页面',
}