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

648 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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`
```typescript
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 文件中使用:
```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` |
**常见用法:**
```tsx
// 页面标题与内容
<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 页面)
这是最常见的页面类型,用于设备类型、批次、用户、管理员等管理页面。
```tsx
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 详情页
```tsx
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 风格的统计卡片模板:
```tsx
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`,导入方式:
```typescript
import { Pie, Column, Line, Area } from '@ant-design/charts';
```
### 7.1 饼图
```tsx
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 柱状图
```tsx
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 图表容器
```tsx
<Card title="图表标题" styles={{ body: { padding: '16px 24px' } }}>
<div style={{ height: 320 }}>
<ChartComponent {...config} />
</div>
</Card>
```
- 图表容器高度统一 `320px`
- 空数据时显示居中灰色提示文字
- 图表颜色优先使用 `statColors` 中的色值
---
## 8. Tag 状态映射
### 8.1 通用状态
```tsx
// 启用/禁用
<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 角色
```tsx
const roleMap = {
super_admin: { text: '超级管理员', color: 'red' },
admin: { text: '管理员', color: 'blue' },
operator: { text: '操作员', color: 'default' },
};
```
---
## 9. 响应式断点
基于 Ant Design Grid 的断点系统:
```tsx
// 统计卡片 (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.colorPrimary``statColors.primary`
- **不要** 创建 `.module.css` 文件,保持现有内联样式模式
- **不要** 在页面组件中直接引用 CSS 文件,全局 CSS 已在 `App.tsx` 统一导入
- **不要** 使用 `style={{ color: '#999' }}`,用 `#6b7280``#9ca3af` 代替
- **不要** 遗漏 ProTable 的 `cardBordered` 属性
### 10.3 获取主题 Token
```tsx
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.tsx``children` 数组中添加路由
3.`src/components/Layout/index.tsx``menuItems` 中添加菜单项
4.`src/api/` 下创建对应的 API 文件
```tsx
// routes/index.tsx
{
path: 'new-page',
element: <NewPage />,
}
// Layout/index.tsx menuItems
{
key: '/new-page',
icon: <SomeOutlined />,
label: '新页面',
}
```