fix ui
All checks were successful
Build and Deploy Web / build-and-deploy (push) Successful in 1m38s
All checks were successful
Build and Deploy Web / build-and-deploy (push) Successful in 1m38s
This commit is contained in:
parent
463b731da3
commit
b4108961fa
@ -1,2 +1,3 @@
|
||||
# 环境变量配置
|
||||
VITE_API_BASE_URL=http://localhost:8001
|
||||
VITE_LOG_CENTER_URL=http://localhost:8002
|
||||
|
||||
@ -1,2 +1,3 @@
|
||||
# 生产环境配置
|
||||
VITE_API_BASE_URL=https://qiyuan-rtc-api.airlabs.art
|
||||
VITE_LOG_CENTER_URL=https://qiyuan-log-center-api.airlabs.art
|
||||
|
||||
647
DESIGN_SPEC.md
Normal file
647
DESIGN_SPEC.md
Normal file
@ -0,0 +1,647 @@
|
||||
# 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: '新页面',
|
||||
}
|
||||
```
|
||||
2006
package-lock.json
generated
2006
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -10,6 +10,7 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/charts": "^2.6.7",
|
||||
"@ant-design/icons": "^6.1.0",
|
||||
"@ant-design/pro-components": "^2.8.10",
|
||||
"antd": "^5.29.3",
|
||||
|
||||
21
src/App.tsx
21
src/App.tsx
@ -2,22 +2,17 @@ import { RouterProvider } from 'react-router-dom';
|
||||
import { ConfigProvider } from 'antd';
|
||||
import zhCN from 'antd/locale/zh_CN';
|
||||
import router from './routes';
|
||||
import { themeConfig } from './theme/tokens';
|
||||
import 'dayjs/locale/zh-cn';
|
||||
import './theme/sidebar.css';
|
||||
import './styles/protable-theme.css';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<ConfigProvider
|
||||
locale={zhCN}
|
||||
theme={{
|
||||
token: {
|
||||
colorPrimary: '#1890ff',
|
||||
borderRadius: 6,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<RouterProvider router={router} />
|
||||
</ConfigProvider>
|
||||
);
|
||||
return (
|
||||
<ConfigProvider locale={zhCN} theme={themeConfig}>
|
||||
<RouterProvider router={router} />
|
||||
</ConfigProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
||||
@ -43,6 +43,7 @@ export const getBatches = (params?: {
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
device_type?: number;
|
||||
batch_no?: string;
|
||||
}) => {
|
||||
return request.get<unknown, ApiResponse<PaginatedResponse<DeviceBatch>>>('/api/admin/device-batches/', { params });
|
||||
};
|
||||
|
||||
@ -13,7 +13,13 @@ export interface DeviceType {
|
||||
}
|
||||
|
||||
// 获取设备类型列表
|
||||
export const getDeviceTypes = (params?: { page?: number; page_size?: number }) => {
|
||||
export const getDeviceTypes = (params?: {
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
brand?: string;
|
||||
product_code?: string;
|
||||
name?: string;
|
||||
}) => {
|
||||
return request.get<unknown, ApiResponse<PaginatedResponse<DeviceType>>>('/api/admin/device-types/', { params });
|
||||
};
|
||||
|
||||
|
||||
@ -41,16 +41,18 @@ function reportToLogCenter(error: Error, context?: Record<string, unknown>) {
|
||||
};
|
||||
|
||||
// 使用 sendBeacon 确保页面关闭时也能发送
|
||||
const body = JSON.stringify(payload);
|
||||
if (navigator.sendBeacon) {
|
||||
const blob = new Blob([body], { type: 'application/json' });
|
||||
navigator.sendBeacon(
|
||||
`${LOG_CENTER_URL}/api/v1/logs/report`,
|
||||
JSON.stringify(payload)
|
||||
blob
|
||||
);
|
||||
} else {
|
||||
fetch(`${LOG_CENTER_URL}/api/v1/logs/report`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
body,
|
||||
keepalive: true,
|
||||
}).catch(() => { }); // 静默失败
|
||||
}
|
||||
@ -85,6 +87,10 @@ request.interceptors.request.use(
|
||||
// 响应拦截器 - 统一处理响应
|
||||
request.interceptors.response.use(
|
||||
(response) => {
|
||||
// 文件下载(blob)直接返回完整 response
|
||||
if (response.config.responseType === 'blob') {
|
||||
return response;
|
||||
}
|
||||
const data = response.data;
|
||||
// 业务错误处理
|
||||
if (data.code !== 0) {
|
||||
|
||||
@ -48,7 +48,7 @@ export const toggleAppUserStatus = (id: number) => {
|
||||
};
|
||||
|
||||
// 获取管理员列表
|
||||
export const getAdminUsers = (params?: { page?: number; page_size?: number }) => {
|
||||
export const getAdminUsers = (params?: { page?: number; page_size?: number; username?: string }) => {
|
||||
return request.get<unknown, ApiResponse<PaginatedResponse<AdminUser>>>('/api/admin/admins/', { params });
|
||||
};
|
||||
|
||||
|
||||
@ -8,6 +8,7 @@ import {
|
||||
theme,
|
||||
Button,
|
||||
Space,
|
||||
Tag,
|
||||
} from 'antd';
|
||||
import {
|
||||
DashboardOutlined,
|
||||
@ -92,21 +93,23 @@ const MainLayout: React.FC = () => {
|
||||
];
|
||||
|
||||
const getRoleLabel = (role?: string) => {
|
||||
const roleMap: Record<string, string> = {
|
||||
super_admin: '超级管理员',
|
||||
admin: '管理员',
|
||||
operator: '操作员',
|
||||
const roleMap: Record<string, { label: string; color: string }> = {
|
||||
super_admin: { label: '超级管理员', color: 'purple' },
|
||||
admin: { label: '管理员', color: 'blue' },
|
||||
operator: { label: '操作员', color: 'default' },
|
||||
};
|
||||
return roleMap[role || ''] || role;
|
||||
return roleMap[role || ''] || { label: role, color: 'default' };
|
||||
};
|
||||
|
||||
const roleInfo = getRoleLabel(adminInfo?.role);
|
||||
|
||||
return (
|
||||
<Layout style={{ minHeight: '100vh' }}>
|
||||
<Sider
|
||||
trigger={null}
|
||||
collapsible
|
||||
collapsed={collapsed}
|
||||
theme="dark"
|
||||
className="rtc-sidebar"
|
||||
style={{
|
||||
overflow: 'auto',
|
||||
height: '100vh',
|
||||
@ -116,19 +119,9 @@ const MainLayout: React.FC = () => {
|
||||
bottom: 0,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
height: 64,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: '#fff',
|
||||
fontSize: collapsed ? 16 : 20,
|
||||
fontWeight: 'bold',
|
||||
borderBottom: '1px solid rgba(255,255,255,0.1)',
|
||||
}}
|
||||
>
|
||||
{collapsed ? 'RTC' : 'RTC 管理后台'}
|
||||
<div className="rtc-sidebar-logo">
|
||||
<div className="rtc-sidebar-logo-icon">R</div>
|
||||
{!collapsed && <span>RTC 管理后台</span>}
|
||||
</div>
|
||||
<Menu
|
||||
theme="dark"
|
||||
@ -136,9 +129,15 @@ const MainLayout: React.FC = () => {
|
||||
selectedKeys={[location.pathname]}
|
||||
items={menuItems}
|
||||
onClick={handleMenuClick}
|
||||
style={{ borderRight: 0 }}
|
||||
/>
|
||||
</Sider>
|
||||
<Layout style={{ marginLeft: collapsed ? 80 : 200, transition: 'margin-left 0.2s' }}>
|
||||
<Layout
|
||||
style={{
|
||||
marginLeft: collapsed ? 80 : 200,
|
||||
transition: 'margin-left 0.2s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
}}
|
||||
>
|
||||
<Header
|
||||
style={{
|
||||
padding: '0 24px',
|
||||
@ -146,26 +145,40 @@ const MainLayout: React.FC = () => {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
boxShadow: '0 1px 4px rgba(0,0,0,0.08)',
|
||||
boxShadow: '0 1px 3px rgba(0,0,0,0.06)',
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
zIndex: 10,
|
||||
borderBottom: '1px solid rgba(0,0,0,0.04)',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
|
||||
onClick={() => setCollapsed(!collapsed)}
|
||||
style={{ fontSize: 16 }}
|
||||
style={{ fontSize: 16, color: '#6b7280' }}
|
||||
/>
|
||||
<Space>
|
||||
<Space size={12}>
|
||||
<Dropdown menu={{ items: userMenuItems }} placement="bottomRight">
|
||||
<Space style={{ cursor: 'pointer' }}>
|
||||
<Avatar icon={<UserOutlined />} style={{ backgroundColor: themeToken.colorPrimary }} />
|
||||
<span>{adminInfo?.name || adminInfo?.username}</span>
|
||||
<span style={{ color: themeToken.colorTextSecondary, fontSize: 12 }}>
|
||||
({getRoleLabel(adminInfo?.role)})
|
||||
</span>
|
||||
<Space style={{ cursor: 'pointer', padding: '4px 8px', borderRadius: 8 }}>
|
||||
<Avatar
|
||||
size={34}
|
||||
icon={<UserOutlined />}
|
||||
style={{
|
||||
background: `linear-gradient(135deg, ${themeToken.colorPrimary}, #8b5cf6)`,
|
||||
}}
|
||||
/>
|
||||
<div style={{ lineHeight: 1.3 }}>
|
||||
<div style={{ fontSize: 14, fontWeight: 500, color: '#1f2937' }}>
|
||||
{adminInfo?.name || adminInfo?.username}
|
||||
</div>
|
||||
<Tag
|
||||
color={roleInfo.color}
|
||||
style={{ fontSize: 11, lineHeight: '18px', margin: 0, padding: '0 6px' }}
|
||||
>
|
||||
{roleInfo.label}
|
||||
</Tag>
|
||||
</div>
|
||||
</Space>
|
||||
</Dropdown>
|
||||
</Space>
|
||||
@ -177,6 +190,7 @@ const MainLayout: React.FC = () => {
|
||||
background: themeToken.colorBgContainer,
|
||||
borderRadius: themeToken.borderRadiusLG,
|
||||
minHeight: 280,
|
||||
boxShadow: '0 1px 2px rgba(0,0,0,0.03)',
|
||||
}}
|
||||
>
|
||||
<Outlet />
|
||||
|
||||
@ -1,41 +1,62 @@
|
||||
:root {
|
||||
--color-primary: #6366f1;
|
||||
--color-primary-light: #818cf8;
|
||||
--color-primary-dark: #4f46e5;
|
||||
--color-bg-base: #f5f5f7;
|
||||
--color-bg-elevated: #ffffff;
|
||||
--color-border: #e5e7eb;
|
||||
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.04);
|
||||
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.07), 0 2px 4px -2px rgba(0, 0, 0, 0.05);
|
||||
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.08), 0 4px 6px -4px rgba(0, 0, 0, 0.04);
|
||||
--transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--transition-base: 200ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--transition-slow: 300ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body, #root {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Inter', 'Helvetica Neue', sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
background: var(--color-bg-base);
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
/* Scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #c1c1c1;
|
||||
border-radius: 4px;
|
||||
background: #d1d5db;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #a1a1a1;
|
||||
background: #9ca3af;
|
||||
}
|
||||
|
||||
/* ProTable 调整 */
|
||||
.ant-pro-table-list-toolbar-title {
|
||||
font-weight: 600;
|
||||
/* Smooth transitions */
|
||||
a, button, .ant-btn, .ant-card, .ant-tag {
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
/* Card hover */
|
||||
.ant-card.ant-card-hoverable:hover {
|
||||
box-shadow: var(--shadow-md);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
16
src/main.tsx
16
src/main.tsx
@ -2,6 +2,22 @@ import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import App from './App.tsx'
|
||||
import './index.css'
|
||||
import { reportToLogCenter } from './api/request.ts'
|
||||
|
||||
// 全局未捕获错误上报
|
||||
window.onerror = (_message, source, lineno, _colno, error) => {
|
||||
if (error) {
|
||||
reportToLogCenter(error, { source, lineno })
|
||||
}
|
||||
}
|
||||
|
||||
// 全局未处理 Promise 拒绝上报
|
||||
window.onunhandledrejection = (event) => {
|
||||
const error = event.reason instanceof Error
|
||||
? event.reason
|
||||
: new Error(String(event.reason))
|
||||
reportToLogCenter(error, { type: 'unhandledrejection' })
|
||||
}
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
|
||||
@ -172,7 +172,7 @@ const AdminPage: React.FC = () => {
|
||||
|
||||
if (!isSuperAdmin) {
|
||||
return (
|
||||
<div style={{ textAlign: 'center', padding: 100, color: '#999' }}>
|
||||
<div style={{ textAlign: 'center', padding: 100, color: '#9ca3af' }}>
|
||||
仅超级管理员可访问此页面
|
||||
</div>
|
||||
);
|
||||
@ -185,11 +185,13 @@ const AdminPage: React.FC = () => {
|
||||
rowKey="id"
|
||||
actionRef={actionRef}
|
||||
columns={columns}
|
||||
cardBordered
|
||||
request={async (params) => {
|
||||
try {
|
||||
const res = await getAdminUsers({
|
||||
page: params.current,
|
||||
page_size: params.pageSize,
|
||||
username: params.username,
|
||||
});
|
||||
return {
|
||||
data: res.data.items,
|
||||
|
||||
@ -21,6 +21,7 @@ import { ProTable } from '@ant-design/pro-components';
|
||||
import type { ProColumns } from '@ant-design/pro-components';
|
||||
import { getBatch, getBatchDevices, exportBatchExcel } from '../../api/batch';
|
||||
import type { Device, DeviceBatch } from '../../api/batch';
|
||||
import { statColors } from '../../theme/tokens';
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
@ -107,22 +108,34 @@ const BatchDetail: React.FC = () => {
|
||||
}
|
||||
|
||||
if (!batch) {
|
||||
return <div>批次不存在</div>;
|
||||
return <div style={{ textAlign: 'center', padding: 100, color: '#9ca3af' }}>批次不存在</div>;
|
||||
}
|
||||
|
||||
const statItems = batch.statistics ? [
|
||||
{ title: '总数量', value: batch.statistics.total, color: statColors.primary },
|
||||
{ title: '已导入MAC', value: batch.statistics.with_mac, color: statColors.success },
|
||||
{ title: '库存中', value: batch.statistics.in_stock, color: statColors.warning },
|
||||
{ title: '已绑定', value: batch.statistics.bound, color: statColors.info },
|
||||
] : [];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Space style={{ marginBottom: 24 }}>
|
||||
<Button icon={<ArrowLeftOutlined />} onClick={() => navigate('/batches')}>
|
||||
<Space style={{ marginBottom: 24 }} align="center">
|
||||
<Button
|
||||
icon={<ArrowLeftOutlined />}
|
||||
onClick={() => navigate('/batches')}
|
||||
type="text"
|
||||
style={{ color: '#6b7280' }}
|
||||
>
|
||||
返回
|
||||
</Button>
|
||||
<Title level={4} style={{ margin: 0 }}>
|
||||
<Title level={4} style={{ margin: 0, color: '#1f2937' }}>
|
||||
批次详情 - {batch.batch_no}
|
||||
</Title>
|
||||
</Space>
|
||||
|
||||
<Card style={{ marginBottom: 24 }}>
|
||||
<Descriptions column={3}>
|
||||
<Card style={{ marginBottom: 16 }}>
|
||||
<Descriptions column={{ xs: 1, sm: 2, md: 3 }}>
|
||||
<Descriptions.Item label="设备类型">
|
||||
{batch.device_type_info?.name}
|
||||
</Descriptions.Item>
|
||||
@ -138,39 +151,19 @@ const BatchDetail: React.FC = () => {
|
||||
</Descriptions>
|
||||
</Card>
|
||||
|
||||
{batch.statistics && (
|
||||
<Row gutter={16} style={{ marginBottom: 24 }}>
|
||||
<Col span={6}>
|
||||
<Card>
|
||||
<Statistic title="总数量" value={batch.statistics.total} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="已导入MAC"
|
||||
value={batch.statistics.with_mac}
|
||||
valueStyle={{ color: '#52c41a' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="库存中"
|
||||
value={batch.statistics.in_stock}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="已绑定"
|
||||
value={batch.statistics.bound}
|
||||
valueStyle={{ color: '#1890ff' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
{statItems.length > 0 && (
|
||||
<Row gutter={[16, 16]} style={{ marginBottom: 16 }}>
|
||||
{statItems.map((item, index) => (
|
||||
<Col xs={12} sm={6} key={index}>
|
||||
<Card styles={{ body: { padding: '16px 20px' } }}>
|
||||
<Statistic
|
||||
title={item.title}
|
||||
value={item.value}
|
||||
valueStyle={{ color: item.color, fontWeight: 600 }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
)}
|
||||
|
||||
@ -179,6 +172,7 @@ const BatchDetail: React.FC = () => {
|
||||
rowKey="id"
|
||||
columns={deviceColumns}
|
||||
search={false}
|
||||
cardBordered
|
||||
request={async (params) => {
|
||||
try {
|
||||
const res = await getBatchDevices(parseInt(id!), {
|
||||
|
||||
@ -85,13 +85,19 @@ const BatchPage: React.FC = () => {
|
||||
const handleExport = async (batchId: number) => {
|
||||
try {
|
||||
const res = await exportBatchExcel(batchId);
|
||||
const blob = new Blob([res as unknown as BlobPart], {
|
||||
// res 是完整的 AxiosResponse,data 已经是 Blob
|
||||
const blob = new Blob([res.data], {
|
||||
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
});
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `batch_${batchId}_sn_codes.xlsx`;
|
||||
// 尝试从 Content-Disposition 获取文件名
|
||||
const disposition = res.headers?.['content-disposition'];
|
||||
const filename = disposition
|
||||
? decodeURIComponent(disposition.split('filename=')[1]?.replace(/"/g, '') || `batch_${batchId}.xlsx`)
|
||||
: `batch_${batchId}_sn_codes.xlsx`;
|
||||
link.download = filename;
|
||||
link.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
message.success('导出成功');
|
||||
@ -262,11 +268,13 @@ const BatchPage: React.FC = () => {
|
||||
rowKey="id"
|
||||
actionRef={actionRef}
|
||||
columns={columns}
|
||||
cardBordered
|
||||
request={async (params) => {
|
||||
try {
|
||||
const res = await getBatches({
|
||||
page: params.current,
|
||||
page_size: params.pageSize,
|
||||
batch_no: params.batch_no,
|
||||
});
|
||||
return {
|
||||
data: res.data.items,
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Row, Col, Card, Statistic, Typography, Spin } from 'antd';
|
||||
import { Row, Col, Card, Typography, Spin } from 'antd';
|
||||
import {
|
||||
UserOutlined,
|
||||
MobileOutlined,
|
||||
@ -8,7 +8,9 @@ import {
|
||||
CheckCircleOutlined,
|
||||
DatabaseOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Pie, Column } from '@ant-design/charts';
|
||||
import request from '../../api/request';
|
||||
import { statColors } from '../../theme/tokens';
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
@ -21,7 +23,6 @@ interface DashboardStats {
|
||||
in_stock_device_count: number;
|
||||
}
|
||||
|
||||
// 调用真实后端API
|
||||
const getDashboardStats = async (): Promise<DashboardStats> => {
|
||||
const res = await request.get<unknown, { data: DashboardStats }>('/api/admin/dashboard/stats/');
|
||||
return res.data;
|
||||
@ -29,7 +30,7 @@ const getDashboardStats = async (): Promise<DashboardStats> => {
|
||||
|
||||
const Dashboard: React.FC = () => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [stats, setStats] = useState({
|
||||
const [stats, setStats] = useState<DashboardStats>({
|
||||
user_count: 0,
|
||||
device_count: 0,
|
||||
device_type_count: 0,
|
||||
@ -58,99 +59,190 @@ const Dashboard: React.FC = () => {
|
||||
title: '用户总数',
|
||||
value: stats.user_count,
|
||||
icon: <UserOutlined />,
|
||||
color: '#1890ff',
|
||||
color: statColors.primary,
|
||||
bgColor: 'rgba(99, 102, 241, 0.08)',
|
||||
},
|
||||
{
|
||||
title: '设备总数',
|
||||
value: stats.device_count,
|
||||
icon: <MobileOutlined />,
|
||||
color: '#52c41a',
|
||||
color: statColors.success,
|
||||
bgColor: 'rgba(16, 185, 129, 0.08)',
|
||||
},
|
||||
{
|
||||
title: '设备类型',
|
||||
value: stats.device_type_count,
|
||||
icon: <AppstoreOutlined />,
|
||||
color: '#722ed1',
|
||||
color: statColors.purple,
|
||||
bgColor: 'rgba(139, 92, 246, 0.08)',
|
||||
},
|
||||
{
|
||||
title: '入库批次',
|
||||
value: stats.batch_count,
|
||||
icon: <InboxOutlined />,
|
||||
color: '#fa8c16',
|
||||
color: statColors.warning,
|
||||
bgColor: 'rgba(245, 158, 11, 0.08)',
|
||||
},
|
||||
{
|
||||
title: '已绑定设备',
|
||||
value: stats.bound_device_count,
|
||||
icon: <CheckCircleOutlined />,
|
||||
color: '#13c2c2',
|
||||
color: statColors.cyan,
|
||||
bgColor: 'rgba(6, 182, 212, 0.08)',
|
||||
},
|
||||
{
|
||||
title: '库存设备',
|
||||
value: stats.in_stock_device_count,
|
||||
icon: <DatabaseOutlined />,
|
||||
color: '#eb2f96',
|
||||
color: statColors.pink,
|
||||
bgColor: 'rgba(236, 72, 153, 0.08)',
|
||||
},
|
||||
];
|
||||
|
||||
// 设备状态分布数据
|
||||
const otherCount = Math.max(0, stats.device_count - stats.in_stock_device_count - stats.bound_device_count);
|
||||
const pieData = [
|
||||
{ type: '库存中', value: stats.in_stock_device_count },
|
||||
{ type: '已绑定', value: stats.bound_device_count },
|
||||
...(otherCount > 0 ? [{ type: '其他', value: otherCount }] : []),
|
||||
];
|
||||
|
||||
const pieConfig = {
|
||||
data: pieData,
|
||||
angleField: 'value',
|
||||
colorField: 'type',
|
||||
radius: 0.85,
|
||||
innerRadius: 0.6,
|
||||
color: [statColors.primary, statColors.success, statColors.warning],
|
||||
label: {
|
||||
text: (d: { type: string; value: number }) => `${d.type}\n${d.value}`,
|
||||
style: { fontSize: 12, fontWeight: 500 },
|
||||
},
|
||||
legend: {
|
||||
color: {
|
||||
position: 'bottom' as const,
|
||||
layout: { justifyContent: 'center' as const },
|
||||
},
|
||||
},
|
||||
interaction: {
|
||||
elementHighlight: true,
|
||||
},
|
||||
};
|
||||
|
||||
// 数据概览柱状图
|
||||
const columnData = [
|
||||
{ name: '用户', value: stats.user_count, type: '数量' },
|
||||
{ name: '设备', value: stats.device_count, type: '数量' },
|
||||
{ name: '已绑定', value: stats.bound_device_count, type: '数量' },
|
||||
{ name: '库存', value: stats.in_stock_device_count, type: '数量' },
|
||||
{ name: '批次', value: stats.batch_count, type: '数量' },
|
||||
{ name: '类型', value: stats.device_type_count, type: '数量' },
|
||||
];
|
||||
|
||||
const columnConfig = {
|
||||
data: columnData,
|
||||
xField: 'name',
|
||||
yField: 'value',
|
||||
color: statColors.primary,
|
||||
style: {
|
||||
radiusTopLeft: 6,
|
||||
radiusTopRight: 6,
|
||||
fill: statColors.primary,
|
||||
fillOpacity: 0.85,
|
||||
},
|
||||
label: {
|
||||
text: (d: { value: number }) => `${d.value}`,
|
||||
textBaseline: 'bottom' as const,
|
||||
style: { dy: -4, fontSize: 12 },
|
||||
},
|
||||
axis: {
|
||||
y: { title: false },
|
||||
x: { title: false },
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Title level={4} style={{ marginBottom: 24 }}>
|
||||
<Title level={4} style={{ marginBottom: 24, color: '#1f2937' }}>
|
||||
仪表盘
|
||||
</Title>
|
||||
|
||||
<Spin spinning={loading}>
|
||||
<Row gutter={[24, 24]}>
|
||||
{/* Stat Cards */}
|
||||
<Row gutter={[16, 16]}>
|
||||
{statCards.map((stat, index) => (
|
||||
<Col xs={24} sm={12} lg={8} xl={4} key={index}>
|
||||
<Card hoverable>
|
||||
<Statistic
|
||||
title={stat.title}
|
||||
value={stat.value}
|
||||
prefix={
|
||||
<span style={{ color: stat.color, fontSize: 24, marginRight: 8 }}>
|
||||
{stat.icon}
|
||||
</span>
|
||||
}
|
||||
valueStyle={{ color: stat.color }}
|
||||
/>
|
||||
<Card
|
||||
hoverable
|
||||
styles={{ body: { padding: '20px' } }}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
|
||||
<div
|
||||
style={{
|
||||
width: 44,
|
||||
height: 44,
|
||||
borderRadius: 12,
|
||||
background: stat.bgColor,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: 20,
|
||||
color: stat.color,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
{stat.icon}
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: 12, color: '#6b7280', marginBottom: 2 }}>
|
||||
{stat.title}
|
||||
</div>
|
||||
<div style={{ fontSize: 24, fontWeight: 700, color: '#1f2937', lineHeight: 1.2 }}>
|
||||
{stat.value.toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
</Spin>
|
||||
|
||||
<Row gutter={[24, 24]} style={{ marginTop: 24 }}>
|
||||
<Col xs={24} lg={12}>
|
||||
<Card title="设备状态分布" style={{ height: 300 }}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
height: 200,
|
||||
color: '#999',
|
||||
flexDirection: 'column',
|
||||
}}>
|
||||
<div style={{ fontSize: 48, marginBottom: 16 }}>📊</div>
|
||||
<div>图表功能开发中...</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} lg={12}>
|
||||
<Card title="快捷操作" style={{ height: 300 }}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
height: 200,
|
||||
color: '#999',
|
||||
flexDirection: 'column',
|
||||
}}>
|
||||
<div style={{ fontSize: 48, marginBottom: 16 }}>🚀</div>
|
||||
<div>使用左侧菜单进行管理操作</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
{/* Charts */}
|
||||
<Row gutter={[16, 16]} style={{ marginTop: 16 }}>
|
||||
<Col xs={24} lg={12}>
|
||||
<Card
|
||||
title="设备状态分布"
|
||||
styles={{ body: { padding: '16px 24px' } }}
|
||||
>
|
||||
<div style={{ height: 320 }}>
|
||||
{stats.device_count > 0 ? (
|
||||
<Pie {...pieConfig} />
|
||||
) : (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: '100%',
|
||||
color: '#9ca3af',
|
||||
}}>
|
||||
暂无设备数据
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} lg={12}>
|
||||
<Card
|
||||
title="数据概览"
|
||||
styles={{ body: { padding: '16px 24px' } }}
|
||||
>
|
||||
<div style={{ height: 320 }}>
|
||||
<Column {...columnConfig} />
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</Spin>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -51,6 +51,7 @@ const DevicePage: React.FC = () => {
|
||||
headerTitle="设备列表"
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
cardBordered
|
||||
request={async (params) => {
|
||||
try {
|
||||
// 先获取所有批次,再获取设备
|
||||
|
||||
@ -90,7 +90,7 @@ const DeviceTypePage: React.FC = () => {
|
||||
width: 100,
|
||||
search: false,
|
||||
render: (_, record) => (
|
||||
<Tag color={record.is_network_required ? 'blue' : 'default'}>
|
||||
<Tag color={record.is_network_required ? 'purple' : 'default'}>
|
||||
{record.is_network_required ? '联网' : '离线'}
|
||||
</Tag>
|
||||
),
|
||||
@ -135,11 +135,15 @@ const DeviceTypePage: React.FC = () => {
|
||||
headerTitle="设备类型管理"
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
cardBordered
|
||||
request={async (params) => {
|
||||
try {
|
||||
const res = await getDeviceTypes({
|
||||
page: params.current,
|
||||
page_size: params.pageSize,
|
||||
brand: params.brand,
|
||||
product_code: params.product_code,
|
||||
name: params.name,
|
||||
});
|
||||
return {
|
||||
data: res.data.items,
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Form, Input, Button, Card, message, Typography } from 'antd';
|
||||
import { Form, Input, Button, message, Typography } from 'antd';
|
||||
import { UserOutlined, LockOutlined } from '@ant-design/icons';
|
||||
import { login } from '../../api/auth';
|
||||
import { useAuthStore } from '../../store/useAuthStore';
|
||||
|
||||
const { Title } = Typography;
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
const LoginPage: React.FC = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
@ -33,22 +33,69 @@ const LoginPage: React.FC = () => {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
background: 'linear-gradient(135deg, #312e81 0%, #6366f1 50%, #8b5cf6 100%)',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<Card
|
||||
{/* Decorative elements */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
width: 400,
|
||||
boxShadow: '0 20px 60px rgba(0,0,0,0.3)',
|
||||
borderRadius: 16,
|
||||
height: 400,
|
||||
borderRadius: '50%',
|
||||
background: 'rgba(255,255,255,0.05)',
|
||||
top: -100,
|
||||
right: -100,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
width: 300,
|
||||
height: 300,
|
||||
borderRadius: '50%',
|
||||
background: 'rgba(255,255,255,0.03)',
|
||||
bottom: -80,
|
||||
left: -80,
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
style={{
|
||||
width: 420,
|
||||
padding: 40,
|
||||
background: 'rgba(255, 255, 255, 0.95)',
|
||||
backdropFilter: 'blur(20px)',
|
||||
borderRadius: 20,
|
||||
boxShadow: '0 25px 50px rgba(99, 102, 241, 0.25), 0 0 0 1px rgba(255,255,255,0.2)',
|
||||
position: 'relative',
|
||||
zIndex: 1,
|
||||
}}
|
||||
bordered={false}
|
||||
>
|
||||
<div style={{ textAlign: 'center', marginBottom: 32 }}>
|
||||
<Title level={2} style={{ marginBottom: 8, color: '#1890ff' }}>
|
||||
<div style={{ textAlign: 'center', marginBottom: 36 }}>
|
||||
<div
|
||||
style={{
|
||||
width: 56,
|
||||
height: 56,
|
||||
borderRadius: 16,
|
||||
background: 'linear-gradient(135deg, #6366f1, #8b5cf6)',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginBottom: 16,
|
||||
boxShadow: '0 8px 20px rgba(99, 102, 241, 0.3)',
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 24, fontWeight: 800, color: '#fff' }}>R</span>
|
||||
</div>
|
||||
<Title level={3} style={{ marginBottom: 4, color: '#1f2937' }}>
|
||||
RTC 管理后台
|
||||
</Title>
|
||||
<Typography.Text type="secondary">设备管理 · 库存管理 · 用户管理</Typography.Text>
|
||||
<Text type="secondary" style={{ fontSize: 14 }}>
|
||||
设备管理 · 库存管理 · 用户管理
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<Form
|
||||
@ -62,8 +109,9 @@ const LoginPage: React.FC = () => {
|
||||
rules={[{ required: true, message: '请输入用户名' }]}
|
||||
>
|
||||
<Input
|
||||
prefix={<UserOutlined style={{ color: '#bfbfbf' }} />}
|
||||
prefix={<UserOutlined style={{ color: '#9ca3af' }} />}
|
||||
placeholder="用户名"
|
||||
style={{ height: 48, borderRadius: 10 }}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
@ -72,24 +120,31 @@ const LoginPage: React.FC = () => {
|
||||
rules={[{ required: true, message: '请输入密码' }]}
|
||||
>
|
||||
<Input.Password
|
||||
prefix={<LockOutlined style={{ color: '#bfbfbf' }} />}
|
||||
prefix={<LockOutlined style={{ color: '#9ca3af' }} />}
|
||||
placeholder="密码"
|
||||
style={{ height: 48, borderRadius: 10 }}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item style={{ marginBottom: 0 }}>
|
||||
<Form.Item style={{ marginBottom: 0, marginTop: 8 }}>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
loading={loading}
|
||||
block
|
||||
style={{ height: 48, fontSize: 16 }}
|
||||
style={{
|
||||
height: 48,
|
||||
fontSize: 16,
|
||||
fontWeight: 600,
|
||||
borderRadius: 10,
|
||||
boxShadow: '0 4px 12px rgba(99, 102, 241, 0.4)',
|
||||
}}
|
||||
>
|
||||
登 录
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,15 +1,18 @@
|
||||
import React from 'react';
|
||||
import React, { useRef } from 'react';
|
||||
import { Button, message, Tag, Space, Popconfirm } from 'antd';
|
||||
import { ProTable } from '@ant-design/pro-components';
|
||||
import type { ProColumns } from '@ant-design/pro-components';
|
||||
import type { ProColumns, ActionType } from '@ant-design/pro-components';
|
||||
import { getAppUsers, toggleAppUserStatus } from '../../api/user';
|
||||
import type { AppUser } from '../../api/user';
|
||||
|
||||
const UserPage: React.FC = () => {
|
||||
const actionRef = useRef<ActionType>(null);
|
||||
|
||||
const handleToggleStatus = async (id: number, currentStatus: boolean) => {
|
||||
try {
|
||||
await toggleAppUserStatus(id);
|
||||
message.success(currentStatus ? '已禁用' : '已启用');
|
||||
actionRef.current?.reload();
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '操作失败');
|
||||
}
|
||||
@ -90,7 +93,9 @@ const UserPage: React.FC = () => {
|
||||
<ProTable<AppUser>
|
||||
headerTitle="用户管理"
|
||||
rowKey="id"
|
||||
actionRef={actionRef}
|
||||
columns={columns}
|
||||
cardBordered
|
||||
request={async (params) => {
|
||||
try {
|
||||
const res = await getAppUsers({
|
||||
|
||||
45
src/styles/protable-theme.css
Normal file
45
src/styles/protable-theme.css
Normal file
@ -0,0 +1,45 @@
|
||||
/* ProTable global enhancements */
|
||||
|
||||
.ant-pro-table-list-toolbar-title {
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
/* Table header */
|
||||
.ant-pro-table .ant-table-thead > tr > th,
|
||||
.ant-pro-table .ant-table-thead > tr > td {
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* Row hover */
|
||||
.ant-pro-table .ant-table-tbody > tr:hover > td {
|
||||
background: #f9fafb !important;
|
||||
}
|
||||
|
||||
/* Link buttons in tables */
|
||||
.ant-pro-table .ant-btn-link {
|
||||
padding: 2px 6px;
|
||||
height: auto;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* Search form */
|
||||
.ant-pro-table-search {
|
||||
border-radius: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
/* Pagination */
|
||||
.ant-pro-table .ant-pagination {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
/* Tag styles in tables */
|
||||
.ant-pro-table .ant-tag {
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
padding: 1px 8px;
|
||||
}
|
||||
72
src/theme/sidebar.css
Normal file
72
src/theme/sidebar.css
Normal file
@ -0,0 +1,72 @@
|
||||
/* Modern dark sidebar with gradient */
|
||||
.rtc-sidebar {
|
||||
background: linear-gradient(180deg, #1e1b4b 0%, #312e81 100%) !important;
|
||||
box-shadow: 2px 0 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.rtc-sidebar::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: radial-gradient(
|
||||
ellipse at top left,
|
||||
rgba(99, 102, 241, 0.12) 0%,
|
||||
transparent 60%
|
||||
);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Logo area */
|
||||
.rtc-sidebar-logo {
|
||||
height: 64px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
color: #e0e7ff;
|
||||
font-weight: 700;
|
||||
font-size: 17px;
|
||||
letter-spacing: 0.3px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.rtc-sidebar-logo-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: linear-gradient(135deg, #6366f1, #8b5cf6);
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
color: #fff;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Menu selected item with left accent */
|
||||
.rtc-sidebar .ant-menu-item-selected {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.rtc-sidebar .ant-menu-item-selected::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 25%;
|
||||
bottom: 25%;
|
||||
width: 3px;
|
||||
background: #818cf8;
|
||||
border-radius: 0 2px 2px 0;
|
||||
}
|
||||
|
||||
/* Menu items hover */
|
||||
.rtc-sidebar .ant-menu-item {
|
||||
margin-block: 2px;
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
66
src/theme/tokens.ts
Normal file
66
src/theme/tokens.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import type { ThemeConfig } from 'antd';
|
||||
|
||||
export const themeConfig: ThemeConfig = {
|
||||
token: {
|
||||
colorPrimary: '#6366f1',
|
||||
colorSuccess: '#10b981',
|
||||
colorWarning: '#f59e0b',
|
||||
colorError: '#ef4444',
|
||||
colorInfo: '#3b82f6',
|
||||
colorBgLayout: '#f5f5f7',
|
||||
borderRadius: 8,
|
||||
borderRadiusLG: 12,
|
||||
borderRadiusSM: 6,
|
||||
fontFamily:
|
||||
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Inter", "Helvetica Neue", sans-serif',
|
||||
},
|
||||
components: {
|
||||
Layout: {
|
||||
headerBg: '#ffffff',
|
||||
headerHeight: 64,
|
||||
headerPadding: '0 24px',
|
||||
siderBg: '#1e1b4b',
|
||||
},
|
||||
Card: {
|
||||
borderRadiusLG: 12,
|
||||
},
|
||||
Table: {
|
||||
headerBg: '#fafafb',
|
||||
borderRadius: 8,
|
||||
},
|
||||
Button: {
|
||||
borderRadius: 8,
|
||||
controlHeight: 36,
|
||||
},
|
||||
Input: {
|
||||
borderRadius: 8,
|
||||
},
|
||||
Select: {
|
||||
borderRadius: 8,
|
||||
},
|
||||
Modal: {
|
||||
borderRadiusLG: 12,
|
||||
},
|
||||
Menu: {
|
||||
darkItemBg: 'transparent',
|
||||
darkSubMenuItemBg: 'transparent',
|
||||
darkItemSelectedBg: 'rgba(99, 102, 241, 0.2)',
|
||||
darkItemHoverBg: 'rgba(255, 255, 255, 0.06)',
|
||||
darkItemSelectedColor: '#c7d2fe',
|
||||
itemBorderRadius: 8,
|
||||
itemMarginInline: 8,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// 统计卡片颜色配置
|
||||
export const statColors = {
|
||||
primary: '#6366f1',
|
||||
success: '#10b981',
|
||||
warning: '#f59e0b',
|
||||
error: '#ef4444',
|
||||
info: '#3b82f6',
|
||||
purple: '#8b5cf6',
|
||||
cyan: '#06b6d4',
|
||||
pink: '#ec4899',
|
||||
};
|
||||
@ -8,7 +8,7 @@ export default defineConfig({
|
||||
port: 5174,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8001',
|
||||
target: 'http://192.168.124.24:8000/',
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
},
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user