All checks were successful
Build and Deploy Web / build-and-deploy (push) Successful in 1m38s
648 lines
18 KiB
Markdown
648 lines
18 KiB
Markdown
# 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: '新页面',
|
||
}
|
||
```
|