All checks were successful
Build and Deploy Web / build-and-deploy (push) Successful in 1m38s
18 KiB
18 KiB
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;
关键规则:
- ProTable 必须添加
cardBordered属性 - Modal 内 Form 统一
layout="vertical"+style={{ marginTop: 24 }} - 删除操作必须用
Popconfirm确认 - ID 列:
width: 80, search: false - 时间列:
valueType: 'dateTime', width: 180, search: false - 操作列:
search: false,按钮用type="link" size="small" - 分页统一:
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 使用优先级
- Ant Design Theme Token → 优先使用
theme.useToken()获取的值 - statColors → 统计、图表颜色用
statColors - CSS 变量 → 阴影、过渡动画用
var(--xxx) - 内联 style → 布局、间距等用内联样式
- 全局 CSS → 仅用于 Ant Design 组件覆盖(已有的 CSS 文件)
10.2 禁止事项
- 不要 硬编码
#1890ff(旧主题色),使用themeToken.colorPrimary或statColors.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. 新增路由步骤
- 在
src/pages/下创建页面组件 - 在
src/routes/index.tsx的children数组中添加路由 - 在
src/components/Layout/index.tsx的menuItems中添加菜单项 - 在
src/api/下创建对应的 API 文件
// routes/index.tsx
{
path: 'new-page',
element: <NewPage />,
}
// Layout/index.tsx menuItems
{
key: '/new-page',
icon: <SomeOutlined />,
label: '新页面',
}