0.1.1
parent
1932461762
commit
c163adf178
|
|
@ -0,0 +1,21 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
env: { browser: true, es2020: true },
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:react/recommended',
|
||||
'plugin:react/jsx-runtime',
|
||||
'plugin:react-hooks/recommended',
|
||||
],
|
||||
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
|
||||
settings: { react: { version: '18.2' } },
|
||||
plugins: ['react-refresh'],
|
||||
rules: {
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
'react/prop-types': 'off',
|
||||
},
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
# 依赖文件
|
||||
node_modules/
|
||||
.pnp
|
||||
.pnp.js
|
||||
|
||||
# 构建产物
|
||||
dist/
|
||||
build/
|
||||
*.local
|
||||
|
||||
# 日志
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# 编辑器
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# 环境变量
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# 临时文件
|
||||
.cache
|
||||
.temp
|
||||
*.tmp
|
||||
|
|
@ -0,0 +1,607 @@
|
|||
# Nex Design 设计规范指南
|
||||
|
||||
> 面向 React + Ant Design + Tailwind CSS 的前端设计语言规范
|
||||
|
||||
## 目录
|
||||
|
||||
- [概述](#概述)
|
||||
- [设计原则](#设计原则)
|
||||
- [颜色系统](#颜色系统)
|
||||
- [排版规范](#排版规范)
|
||||
- [间距系统](#间距系统)
|
||||
- [组件规范](#组件规范)
|
||||
- [布局规范](#布局规范)
|
||||
- [交互规范](#交互规范)
|
||||
- [响应式设计](#响应式设计)
|
||||
- [页面模板](#页面模板)
|
||||
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
Nex Design 是为开发团队打造的一套标准化设计语言系统,旨在提供统一、高效、易维护的前端设计规范。
|
||||
|
||||
### 技术栈
|
||||
|
||||
- **框架**: React 18+
|
||||
- **组件库**: Ant Design 5.x
|
||||
- **样式方案**: Tailwind CSS 3.x
|
||||
- **包管理**: Yarn
|
||||
- **运行时**: Node.js 16+
|
||||
|
||||
### 设计目标
|
||||
|
||||
1. **一致性**: 确保所有页面和组件保持视觉与交互的一致性
|
||||
2. **高效性**: 提供可复用的设计模式,加快开发速度
|
||||
3. **可维护性**: 建立清晰的规范体系,降低维护成本
|
||||
4. **用户体验**: 优先考虑用户体验,打造直观易用的界面
|
||||
|
||||
---
|
||||
|
||||
## 设计原则
|
||||
|
||||
### 1. 清晰明确
|
||||
界面设计应当清晰直观,避免歧义,让用户能够快速理解和操作。
|
||||
|
||||
### 2. 一致性优先
|
||||
保持视觉、交互、用词的一致性,减少用户学习成本。
|
||||
|
||||
### 3. 效率至上
|
||||
优化操作流程,减少用户的操作步骤,提高工作效率。
|
||||
|
||||
### 4. 反馈及时
|
||||
对用户的每一个操作都应给予明确的反馈,包括成功、失败、加载等状态。
|
||||
|
||||
### 5. 容错友好
|
||||
预防用户错误,在错误发生时提供清晰的提示和解决方案。
|
||||
|
||||
---
|
||||
|
||||
## 颜色系统
|
||||
|
||||
### 主色调
|
||||
|
||||
```css
|
||||
/* 品牌主色 - Primary (基于 NEX Logo 紫红色) */
|
||||
--primary-50: #fce7f6;
|
||||
--primary-100: #f5bae6;
|
||||
--primary-200: #ee8dd6;
|
||||
--primary-300: #e760c6;
|
||||
--primary-400: #e033b6;
|
||||
--primary-500: #b8178d; /* 主色 - 匹配 NEX Logo */
|
||||
--primary-600: #9c1477;
|
||||
--primary-700: #801161;
|
||||
--primary-800: #640d4b;
|
||||
--primary-900: #480a35;
|
||||
```
|
||||
|
||||
### 辅助色系
|
||||
|
||||
```css
|
||||
/* 蓝色 - Blue (用于信息提示) */
|
||||
--blue-50: #e6f4ff;
|
||||
--blue-100: #bae0ff;
|
||||
--blue-200: #91caff;
|
||||
--blue-300: #69b1ff;
|
||||
--blue-400: #4096ff;
|
||||
--blue-500: #1677ff; /* 信息色 */
|
||||
--blue-600: #0958d9;
|
||||
--blue-700: #003eb3;
|
||||
--blue-800: #002c8c;
|
||||
--blue-900: #001d66;
|
||||
```
|
||||
|
||||
### 功能色
|
||||
|
||||
```css
|
||||
/* 成功 - Success */
|
||||
--success: #52c41a;
|
||||
--success-bg: #f6ffed;
|
||||
--success-border: #b7eb8f;
|
||||
|
||||
/* 警告 - Warning */
|
||||
--warning: #faad14;
|
||||
--warning-bg: #fffbe6;
|
||||
--warning-border: #ffe58f;
|
||||
|
||||
/* 错误 - Error */
|
||||
--error: #ff4d4f;
|
||||
--error-bg: #fff2f0;
|
||||
--error-border: #ffccc7;
|
||||
|
||||
/* 信息 - Info */
|
||||
--info: #1677ff;
|
||||
--info-bg: #e6f4ff;
|
||||
--info-border: #91caff;
|
||||
```
|
||||
|
||||
### 中性色
|
||||
|
||||
```css
|
||||
/* 文本颜色 */
|
||||
--text-primary: rgba(0, 0, 0, 0.88); /* 主要文本 */
|
||||
--text-secondary: rgba(0, 0, 0, 0.65); /* 次要文本 */
|
||||
--text-tertiary: rgba(0, 0, 0, 0.45); /* 辅助文本 */
|
||||
--text-disabled: rgba(0, 0, 0, 0.25); /* 禁用文本 */
|
||||
|
||||
/* 背景颜色 */
|
||||
--bg-primary: #ffffff;
|
||||
--bg-secondary: #fafafa;
|
||||
--bg-tertiary: #f5f5f5;
|
||||
--bg-disabled: #f0f0f0;
|
||||
|
||||
/* 边框颜色 */
|
||||
--border-primary: #d9d9d9;
|
||||
--border-secondary: #f0f0f0;
|
||||
```
|
||||
|
||||
### 颜色使用规范
|
||||
|
||||
1. **主色使用**: 主要用于关键操作按钮、重要信息高亮、链接等
|
||||
2. **功能色使用**: 严格按照语义使用,不可混淆
|
||||
3. **中性色使用**: 用于文本、背景、边框等基础元素
|
||||
4. **对比度**: 确保文本与背景的对比度符合 WCAG 2.0 标准(至少 4.5:1)
|
||||
|
||||
---
|
||||
|
||||
## 排版规范
|
||||
|
||||
### 字体家族
|
||||
|
||||
```css
|
||||
/* 默认字体栈 */
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
||||
'Helvetica Neue', Arial, 'Noto Sans', sans-serif,
|
||||
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
|
||||
'Noto Color Emoji';
|
||||
|
||||
/* 等宽字体(代码、数字) */
|
||||
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code',
|
||||
'Fira Mono', 'Droid Sans Mono', 'Source Code Pro', monospace;
|
||||
```
|
||||
|
||||
### 字号规范
|
||||
|
||||
| 用途 | 字号 | 行高 | Tailwind Class | 使用场景 |
|
||||
|------|------|------|----------------|----------|
|
||||
| 特大标题 | 32px | 1.35 | `text-4xl` | 页面主标题 |
|
||||
| 大标题 | 24px | 1.35 | `text-2xl` | 区块标题 |
|
||||
| 中标题 | 20px | 1.4 | `text-xl` | 卡片标题 |
|
||||
| 小标题 | 16px | 1.5 | `text-base` | 表单标签 |
|
||||
| 正文 | 14px | 1.5714 | `text-sm` | 正文内容 |
|
||||
| 辅助文字 | 12px | 1.6667 | `text-xs` | 说明文字 |
|
||||
|
||||
### 字重规范
|
||||
|
||||
- **Regular (400)**: 正文内容
|
||||
- **Medium (500)**: 表单标签、列表项
|
||||
- **Semibold (600)**: 小标题、强调文本
|
||||
- **Bold (700)**: 标题、重要信息
|
||||
|
||||
### 文本颜色使用
|
||||
|
||||
```jsx
|
||||
// 主要文本
|
||||
<p className="text-gray-900">主要内容</p>
|
||||
|
||||
// 次要文本
|
||||
<p className="text-gray-600">次要内容</p>
|
||||
|
||||
// 辅助文本
|
||||
<p className="text-gray-400">辅助说明</p>
|
||||
|
||||
// 禁用文本
|
||||
<p className="text-gray-300">禁用状态</p>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 间距系统
|
||||
|
||||
### 基础间距单位
|
||||
|
||||
基于 **8px** 网格系统,所有间距应为 8 的倍数。
|
||||
|
||||
| 名称 | 数值 | Tailwind Class | 用途 |
|
||||
|------|------|----------------|------|
|
||||
| xs | 4px | `space-1` / `p-1` / `m-1` | 紧凑间距 |
|
||||
| sm | 8px | `space-2` / `p-2` / `m-2` | 小间距 |
|
||||
| md | 16px | `space-4` / `p-4` / `m-4` | 标准间距 |
|
||||
| lg | 24px | `space-6` / `p-6` / `m-6` | 大间距 |
|
||||
| xl | 32px | `space-8` / `p-8` / `m-8` | 超大间距 |
|
||||
| 2xl | 48px | `space-12` / `p-12` / `m-12` | 区块间距 |
|
||||
|
||||
### 间距使用场景
|
||||
|
||||
1. **组件内边距**: 通常使用 12px (p-3) 或 16px (p-4)
|
||||
2. **组件外边距**: 标准使用 16px (m-4) 或 24px (m-6)
|
||||
3. **区块间距**: 使用 32px (mb-8) 或 48px (mb-12)
|
||||
4. **栅格间距**: 标准使用 16px 或 24px
|
||||
|
||||
---
|
||||
|
||||
## 组件规范
|
||||
|
||||
### 按钮 (Button)
|
||||
|
||||
#### 类型与场景
|
||||
|
||||
```jsx
|
||||
import { Button } from 'antd';
|
||||
|
||||
// 主要按钮 - 用于主要操作
|
||||
<Button type="primary">确定</Button>
|
||||
|
||||
// 次要按钮 - 用于次要操作
|
||||
<Button>取消</Button>
|
||||
|
||||
// 文本按钮 - 用于辅助操作
|
||||
<Button type="text">详情</Button>
|
||||
|
||||
// 链接按钮 - 用于跳转
|
||||
<Button type="link">查看更多</Button>
|
||||
|
||||
// 危险按钮 - 用于删除等危险操作
|
||||
<Button danger>删除</Button>
|
||||
```
|
||||
|
||||
#### 尺寸规范
|
||||
|
||||
- **Large**: 高度 40px,用于页面主要操作
|
||||
- **Middle**: 高度 32px,默认尺寸
|
||||
- **Small**: 高度 24px,用于紧凑场景
|
||||
|
||||
#### 使用规范
|
||||
|
||||
1. 一个操作区域最多只有一个主要按钮
|
||||
2. 按钮文字应简洁明确,建议不超过 4 个字
|
||||
3. 危险操作必须使用二次确认
|
||||
4. 按钮组内按钮间距为 8px
|
||||
|
||||
### 表单 (Form)
|
||||
|
||||
#### 布局规范
|
||||
|
||||
```jsx
|
||||
import { Form, Input } from 'antd';
|
||||
|
||||
<Form layout="vertical" className="max-w-2xl">
|
||||
<Form.Item
|
||||
label="用户名"
|
||||
name="username"
|
||||
rules={[{ required: true, message: '请输入用户名' }]}
|
||||
>
|
||||
<Input placeholder="请输入用户名" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
```
|
||||
|
||||
#### 表单规范
|
||||
|
||||
1. **标签对齐**: 默认使用垂直布局 (vertical)
|
||||
2. **必填标识**: 使用红色星号 (*) 标识
|
||||
3. **字段宽度**: 根据内容长度设置合理宽度
|
||||
4. **错误提示**: 实时验证,错误信息显示在字段下方
|
||||
5. **表单间距**: 表单项之间间距 24px
|
||||
|
||||
### 表格 (Table)
|
||||
|
||||
#### 基础配置
|
||||
|
||||
```jsx
|
||||
import { Table } from 'antd';
|
||||
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={data}
|
||||
pagination={{
|
||||
pageSize: 10,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
}}
|
||||
scroll={{ x: 1200 }}
|
||||
/>
|
||||
```
|
||||
|
||||
#### 表格规范
|
||||
|
||||
1. **分页**: 默认每页 10 条,提供分页器
|
||||
2. **行高**: 标准行高 54px (middle 模式)
|
||||
3. **操作列**: 固定在右侧,宽度根据操作数量调整
|
||||
4. **空状态**: 使用统一的空状态提示
|
||||
5. **加载状态**: 使用 loading 属性显示加载状态
|
||||
|
||||
### 卡片 (Card)
|
||||
|
||||
```jsx
|
||||
import { Card } from 'antd';
|
||||
|
||||
<Card
|
||||
title="卡片标题"
|
||||
extra={<Button type="link">更多</Button>}
|
||||
className="mb-4"
|
||||
>
|
||||
<p>卡片内容</p>
|
||||
</Card>
|
||||
```
|
||||
|
||||
#### 卡片规范
|
||||
|
||||
1. **内边距**: 标准 24px
|
||||
2. **圆角**: 8px (rounded-lg)
|
||||
3. **阴影**: 默认使用 `shadow-sm`
|
||||
4. **间距**: 卡片之间间距 16px
|
||||
|
||||
---
|
||||
|
||||
## 布局规范
|
||||
|
||||
### 页面布局结构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ Header (64px) │
|
||||
├─────────────────────────────────────┤
|
||||
│ Sider │ Content Area │
|
||||
│ (200px)│ │
|
||||
│ │ │
|
||||
│ │ │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 栅格系统
|
||||
|
||||
采用 24 栏栅格系统,基于 Ant Design Grid 组件。
|
||||
|
||||
```jsx
|
||||
import { Row, Col } from 'antd';
|
||||
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col xs={24} sm={12} md={8} lg={6} xl={4}>
|
||||
内容
|
||||
</Col>
|
||||
</Row>
|
||||
```
|
||||
|
||||
### 布局规范
|
||||
|
||||
1. **页面内边距**: 24px
|
||||
2. **内容最大宽度**: 1200px (根据实际需求调整)
|
||||
3. **侧边栏宽度**: 200px (收起后 64px)
|
||||
4. **顶部导航高度**: 64px
|
||||
5. **栅格间距**: 水平 16px,垂直 16px
|
||||
|
||||
---
|
||||
|
||||
## 交互规范
|
||||
|
||||
### 反馈
|
||||
|
||||
#### 全局提示 (Message)
|
||||
|
||||
```jsx
|
||||
import { message } from 'antd';
|
||||
|
||||
// 成功提示
|
||||
message.success('操作成功');
|
||||
|
||||
// 错误提示
|
||||
message.error('操作失败,请重试');
|
||||
|
||||
// 警告提示
|
||||
message.warning('请注意数据安全');
|
||||
|
||||
// 加载提示
|
||||
const hide = message.loading('加载中...');
|
||||
```
|
||||
|
||||
#### 通知提醒 (Notification)
|
||||
|
||||
```jsx
|
||||
import { notification } from 'antd';
|
||||
|
||||
notification.open({
|
||||
message: '系统通知',
|
||||
description: '您有新的消息,请及时查看',
|
||||
duration: 4.5,
|
||||
});
|
||||
```
|
||||
|
||||
#### 模态对话框 (Modal)
|
||||
|
||||
```jsx
|
||||
import { Modal } from 'antd';
|
||||
|
||||
// 确认对话框
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: '删除后数据无法恢复,确定要删除吗?',
|
||||
okText: '确定',
|
||||
cancelText: '取消',
|
||||
onOk() {
|
||||
// 处理确认
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### 加载状态
|
||||
|
||||
1. **局部加载**: 使用 Spin 组件
|
||||
2. **按钮加载**: 设置 loading 属性
|
||||
3. **页面加载**: 骨架屏 (Skeleton)
|
||||
4. **表格加载**: Table 的 loading 属性
|
||||
|
||||
### 动画效果
|
||||
|
||||
1. **过渡时间**: 标准 300ms
|
||||
2. **缓动函数**: ease-in-out
|
||||
3. **使用场景**: 展开/收起、显示/隐藏、页面切换
|
||||
|
||||
---
|
||||
|
||||
## 响应式设计
|
||||
|
||||
### 断点定义
|
||||
|
||||
| 断点 | 屏幕宽度 | 设备类型 | Tailwind 前缀 |
|
||||
|------|----------|----------|---------------|
|
||||
| xs | < 576px | 手机竖屏 | - |
|
||||
| sm | ≥ 576px | 手机横屏 | `sm:` |
|
||||
| md | ≥ 768px | 平板 | `md:` |
|
||||
| lg | ≥ 992px | 桌面显示器 | `lg:` |
|
||||
| xl | ≥ 1200px | 大桌面显示器 | `xl:` |
|
||||
| 2xl | ≥ 1600px | 超大显示器 | `2xl:` |
|
||||
|
||||
### 响应式策略
|
||||
|
||||
1. **移动优先**: 先设计移动端,再扩展到桌面端
|
||||
2. **弹性布局**: 使用 Flexbox 和 Grid
|
||||
3. **自适应图片**: 使用相对单位和 max-width
|
||||
4. **触摸友好**: 移动端可点击区域不小于 44x44px
|
||||
|
||||
---
|
||||
|
||||
## 页面模板
|
||||
|
||||
### 通用页面结构
|
||||
|
||||
```jsx
|
||||
import { Layout, Breadcrumb } from 'antd';
|
||||
const { Content } = Layout;
|
||||
|
||||
function PageTemplate() {
|
||||
return (
|
||||
<Content className="p-6">
|
||||
{/* 面包屑导航 */}
|
||||
<Breadcrumb className="mb-4">
|
||||
<Breadcrumb.Item>首页</Breadcrumb.Item>
|
||||
<Breadcrumb.Item>当前页面</Breadcrumb.Item>
|
||||
</Breadcrumb>
|
||||
|
||||
{/* 页面标题区 */}
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-semibold text-gray-900">页面标题</h1>
|
||||
<p className="text-sm text-gray-500 mt-1">页面描述信息</p>
|
||||
</div>
|
||||
|
||||
{/* 主要内容区 */}
|
||||
<div className="bg-white rounded-lg shadow-sm p-6">
|
||||
{/* 页面内容 */}
|
||||
</div>
|
||||
</Content>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 已实现的页面模板
|
||||
|
||||
#### 1. 主框架页面 ✓
|
||||
|
||||
完整的应用框架布局,包含:
|
||||
- 侧边菜单栏(支持收起/展开,两级菜单)
|
||||
- 顶部导航栏(搜索、消息、用户信息)
|
||||
- 内容区域(可滚动)
|
||||
|
||||
**详细文档**: [主框架页面设计规范](../docs/pages/main-layout.md)
|
||||
|
||||
**主要特性**:
|
||||
- 基于 NEX Logo 的品牌配色 (#b8178d)
|
||||
- 菜单数据 JSON 配置
|
||||
- 响应式布局
|
||||
- 徽章系统(HOT/NEW)
|
||||
- 用户下拉菜单
|
||||
|
||||
**使用示例**:
|
||||
```jsx
|
||||
import MainLayout from './components/MainLayout'
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<MainLayout>
|
||||
{/* 页面内容 */}
|
||||
</MainLayout>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. 概览页 (Dashboard) ✓
|
||||
|
||||
数据概览页面,展示:
|
||||
- 统计卡片(证书总量、资产数量等)
|
||||
- 环形进度图表
|
||||
- 数据走势图
|
||||
|
||||
**主要组件**:
|
||||
- 响应式栅格布局 (Row/Col)
|
||||
- 统计卡片 (Statistic)
|
||||
- 进度条 (Progress)
|
||||
- 图表区域
|
||||
|
||||
### 待完善的页面模板
|
||||
|
||||
后续将添加以下页面模板:
|
||||
|
||||
- [ ] **列表页**: 数据表格、筛选、操作
|
||||
- [ ] **详情页**: 信息展示、关联数据
|
||||
- [ ] **表单页**: 数据录入、验证、提交
|
||||
- [ ] **设置页**: 配置管理、偏好设置
|
||||
|
||||
---
|
||||
|
||||
## 开发规范
|
||||
|
||||
### 代码组织
|
||||
|
||||
```
|
||||
src/
|
||||
├── components/ # 公共组件
|
||||
│ ├── Button/
|
||||
│ ├── Card/
|
||||
│ └── ...
|
||||
├── pages/ # 页面组件
|
||||
│ ├── Dashboard/
|
||||
│ ├── List/
|
||||
│ └── ...
|
||||
├── styles/ # 样式文件
|
||||
│ ├── globals.css
|
||||
│ └── variables.css
|
||||
├── utils/ # 工具函数
|
||||
└── constants/ # 常量定义
|
||||
```
|
||||
|
||||
### 命名规范
|
||||
|
||||
1. **组件命名**: 大驼峰 (PascalCase),如 `UserList`
|
||||
2. **文件命名**: 与组件同名,如 `UserList.jsx`
|
||||
3. **样式类命名**: 小写短横线 (kebab-case)
|
||||
4. **常量命名**: 全大写下划线 (UPPER_SNAKE_CASE)
|
||||
|
||||
### CSS 使用规范
|
||||
|
||||
1. **优先使用 Tailwind**: 布局、间距、颜色等
|
||||
2. **Ant Design 定制**: 通过主题配置实现
|
||||
3. **自定义样式**: 仅在必要时使用,放在组件目录下
|
||||
|
||||
---
|
||||
|
||||
## 版本记录
|
||||
|
||||
| 版本 | 日期 | 说明 |
|
||||
|------|------|------|
|
||||
| 1.0.0 | 2024-11-04 | 初始版本,建立基础设计规范 |
|
||||
| 1.1.0 | 2024-11-04 | 更新品牌配色,完成主框架页面和概览页 |
|
||||
|
||||
---
|
||||
|
||||
## 参考资源
|
||||
|
||||
- [Ant Design 官方文档](https://ant.design/)
|
||||
- [Tailwind CSS 官方文档](https://tailwindcss.com/)
|
||||
- [React 官方文档](https://react.dev/)
|
||||
- [Material Design 设计指南](https://material.io/design)
|
||||
|
||||
---
|
||||
|
||||
**维护者**: Nex Design Team
|
||||
**最后更新**: 2024-11-04
|
||||
|
|
@ -0,0 +1,146 @@
|
|||
# Nex Design
|
||||
|
||||
> 面向 React + Ant Design + Tailwind CSS 的前端设计语言规范
|
||||
|
||||
## 项目简介
|
||||
|
||||
Nex Design 是为开发团队打造的一套标准化设计语言系统,提供统一、高效、易维护的前端设计规范和页面模板。
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **框架**: React 18+
|
||||
- **构建工具**: Vite 5.x
|
||||
- **组件库**: Ant Design 5.x
|
||||
- **样式方案**: Tailwind CSS 3.x
|
||||
- **路由**: React Router v6
|
||||
- **包管理**: Yarn
|
||||
- **运行时**: Node.js 16+
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 环境要求
|
||||
|
||||
- Node.js >= 16.0.0
|
||||
- Yarn >= 1.22.0
|
||||
|
||||
### 安装依赖
|
||||
|
||||
```bash
|
||||
yarn install
|
||||
```
|
||||
|
||||
### 开发模式
|
||||
|
||||
```bash
|
||||
yarn dev
|
||||
```
|
||||
|
||||
访问 http://localhost:5173 查看项目
|
||||
|
||||
### 构建生产版本
|
||||
|
||||
```bash
|
||||
yarn build
|
||||
```
|
||||
|
||||
### 预览生产版本
|
||||
|
||||
```bash
|
||||
yarn preview
|
||||
```
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
Nex Design/
|
||||
├── docs/ # 文档目录
|
||||
│ └── pages/ # 页面设计文档
|
||||
├── public/ # 静态资源
|
||||
├── src/ # 源代码
|
||||
│ ├── assets/ # 资源文件(图片、字体等)
|
||||
│ ├── components/ # 公共组件
|
||||
│ ├── pages/ # 页面组件
|
||||
│ ├── styles/ # 全局样式
|
||||
│ ├── utils/ # 工具函数
|
||||
│ └── constants/ # 常量定义
|
||||
├── DESIGN_GUIDE.md # 设计规范指南
|
||||
├── package.json # 项目配置
|
||||
└── README.md # 项目说明
|
||||
```
|
||||
|
||||
## 设计规范
|
||||
|
||||
详细的设计规范请查看 [DESIGN_GUIDE.md](./DESIGN_GUIDE.md),包含:
|
||||
|
||||
- 设计原则
|
||||
- 颜色系统
|
||||
- 排版规范
|
||||
- 间距系统
|
||||
- 组件规范
|
||||
- 布局规范
|
||||
- 交互规范
|
||||
- 响应式设计
|
||||
- 页面模板
|
||||
|
||||
## 页面模板
|
||||
|
||||
项目将提供以下标准页面模板:
|
||||
|
||||
- [ ] 首页 (Dashboard) - 数据概览、快捷入口
|
||||
- [ ] 主框架页面 - 导航布局、侧边栏、顶部栏
|
||||
- [ ] 列表页 - 数据表格、筛选、操作
|
||||
- [ ] 详情页 - 信息展示、关联数据
|
||||
- [ ] 表单页 - 数据录入、验证、提交
|
||||
- [ ] 设置页 - 配置管理、偏好设置
|
||||
|
||||
每个模板都会提供完整的设计规范说明和代码实现。
|
||||
|
||||
## 开发规范
|
||||
|
||||
### 代码风格
|
||||
|
||||
- 使用 ESLint 进行代码检查
|
||||
- 组件命名使用大驼峰 (PascalCase)
|
||||
- 文件命名与组件同名
|
||||
- 优先使用函数式组件和 Hooks
|
||||
|
||||
### CSS 使用规范
|
||||
|
||||
1. 优先使用 Tailwind CSS 工具类
|
||||
2. 通过 Ant Design 主题配置进行定制
|
||||
3. 仅在必要时编写自定义样式
|
||||
|
||||
### 组件开发规范
|
||||
|
||||
- 保持组件的单一职责
|
||||
- 合理拆分组件,提高复用性
|
||||
- 使用 PropTypes 或 TypeScript 进行类型检查
|
||||
- 编写必要的注释和文档
|
||||
|
||||
## 参与贡献
|
||||
|
||||
1. Fork 本仓库
|
||||
2. 创建特性分支 (`git checkout -b feature/AmazingFeature`)
|
||||
3. 提交更改 (`git commit -m 'Add some AmazingFeature'`)
|
||||
4. 推送到分支 (`git push origin feature/AmazingFeature`)
|
||||
5. 开启 Pull Request
|
||||
|
||||
## 版本历史
|
||||
|
||||
- **v1.0.0** (2024-11-04)
|
||||
- 初始化项目
|
||||
- 创建基础设计规范文档
|
||||
- 建立项目结构
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT License
|
||||
|
||||
## 联系方式
|
||||
|
||||
如有问题或建议,请联系开发团队。
|
||||
|
||||
---
|
||||
|
||||
**维护团队**: Nex Design Team
|
||||
**最后更新**: 2024-11-04
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
# Nex Design - 抽屉(Drawer)宽度标准
|
||||
|
||||
## 📐 抽屉宽度定义
|
||||
|
||||
为了保持设计一致性,我们定义了三种标准抽屉宽度模式:
|
||||
|
||||
### 1. 小型抽屉 (Small) - 480px
|
||||
**适用场景:**
|
||||
- 简单的信息展示
|
||||
- 少量字段的表单(1-3个字段)
|
||||
- 快速操作面板
|
||||
- 通知详情
|
||||
|
||||
**示例:**
|
||||
```jsx
|
||||
<Drawer width={480} ... />
|
||||
```
|
||||
|
||||
### 2. 中型抽屉 (Medium) - 720px ⭐️ 推荐
|
||||
**适用场景:**
|
||||
- 详细信息展示(如主机详情)
|
||||
- 中等复杂度的表单(4-10个字段)
|
||||
- 数据编辑面板
|
||||
- 配置设置
|
||||
|
||||
**示例:**
|
||||
```jsx
|
||||
<Drawer width={720} ... />
|
||||
```
|
||||
**当前主机列表页面使用此宽度模式**
|
||||
|
||||
### 3. 大型抽屉 (Large) - 1080px
|
||||
**适用场景:**
|
||||
- 复杂的多步骤表单
|
||||
- 需要并排展示多列信息
|
||||
- 包含图表或复杂可视化内容
|
||||
- 嵌套子表格或列表
|
||||
|
||||
**示例:**
|
||||
```jsx
|
||||
<Drawer width={1080} ... />
|
||||
```
|
||||
|
||||
## 📱 响应式建议
|
||||
|
||||
```css
|
||||
/* 移动端自适应 */
|
||||
@media (max-width: 768px) {
|
||||
.ant-drawer {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 平板端 */
|
||||
@media (max-width: 1024px) {
|
||||
.ant-drawer-large {
|
||||
width: 90% !important;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 💡 使用建议
|
||||
|
||||
1. **优先选择中型抽屉(720px)**:适合大多数场景,既不会太窄导致内容拥挤,也不会太宽浪费空间。
|
||||
|
||||
2. **避免自定义宽度**:保持使用标准宽度,确保整个系统的一致性。
|
||||
|
||||
3. **移动端优先**:在响应式设计中,小屏幕设备应使用全宽抽屉。
|
||||
|
||||
4. **内容决定宽度**:根据内容复杂度选择合适的宽度,而不是随意设置。
|
||||
|
||||
## 🎨 设计原则
|
||||
|
||||
- **一致性**:相同类型的页面使用相同宽度的抽屉
|
||||
- **可读性**:确保内容在抽屉中有足够的呼吸空间
|
||||
- **响应式**:考虑不同设备的显示效果
|
||||
- **用户体验**:抽屉不应遮挡主要内容过多
|
||||
|
|
@ -0,0 +1,495 @@
|
|||
# 主框架页面设计规范
|
||||
|
||||
> Nex Design 主框架布局设计文档
|
||||
|
||||
## 概述
|
||||
|
||||
主框架页面是应用的基础布局结构,包含侧边菜单栏、顶部导航栏和内容区域。本文档详细说明了主框架的设计规范、交互逻辑和实现要点。
|
||||
|
||||
---
|
||||
|
||||
## 页面结构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ 顶部导航栏 (64px) │
|
||||
├──────────┬──────────────────────────────────────────┤
|
||||
│ │ │
|
||||
│ 侧边栏 │ 内容区域 │
|
||||
│ (200px) │ (可向下滚动) │
|
||||
│ │ │
|
||||
│ │ │
|
||||
└──────────┴──────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 1. 侧边栏 (Sider)
|
||||
|
||||
### 1.1 基础规范
|
||||
|
||||
| 属性 | 展开状态 | 收起状态 |
|
||||
|------|---------|---------|
|
||||
| 宽度 | 200px | 64px |
|
||||
| 背景色 | #001529 (深色) | #001529 |
|
||||
| 位置 | 固定左侧 | 固定左侧 |
|
||||
| 层级 | z-index: 10 | z-index: 10 |
|
||||
|
||||
### 1.2 Logo 区域
|
||||
|
||||
**设计规范**:
|
||||
- 高度:64px
|
||||
- 背景:rgba(255, 255, 255, 0.05)
|
||||
- 底部边框:1px solid rgba(255, 255, 255, 0.05)
|
||||
- 内边距:12px 16px
|
||||
- 对齐:居中
|
||||
|
||||
**展开状态**:
|
||||
- 显示完整 Logo(logo-full.png)
|
||||
- Logo 高度:40px,宽度自适应
|
||||
|
||||
**收起状态**:
|
||||
- 显示方形 Logo(logo-small.png)
|
||||
- Logo 尺寸:40px × 40px
|
||||
- 圆角:8px
|
||||
|
||||
### 1.3 菜单系统
|
||||
|
||||
#### 菜单层级
|
||||
|
||||
支持**两级菜单**结构:
|
||||
- **一级菜单**:带图标,可展开/收起
|
||||
- **二级菜单**:文字列表,可带徽章标识
|
||||
|
||||
#### 菜单数据格式
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"key": "overview",
|
||||
"label": "概览",
|
||||
"icon": "DashboardOutlined",
|
||||
"path": "/overview"
|
||||
},
|
||||
{
|
||||
"key": "certificate",
|
||||
"label": "证书管理",
|
||||
"icon": "SafetyCertificateOutlined",
|
||||
"children": [
|
||||
{
|
||||
"key": "ssl-cert",
|
||||
"label": "SSL证书管理",
|
||||
"path": "/certificate/ssl"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
#### 菜单状态
|
||||
|
||||
**正常状态**:
|
||||
- 背景色:透明
|
||||
- 文字颜色:rgba(255, 255, 255, 0.65)
|
||||
|
||||
**悬停状态**:
|
||||
- 背景色:rgba(184, 23, 141, 0.2)
|
||||
- 文字颜色:#fff
|
||||
|
||||
**选中状态**:
|
||||
- 背景色:#b8178d (品牌主色)
|
||||
- 文字颜色:#fff
|
||||
- 左侧边框:3px solid #b8178d
|
||||
|
||||
**展开状态**:
|
||||
- 二级菜单背景:rgba(0, 0, 0, 0.15)
|
||||
- 二级菜单内边距:左侧 48px
|
||||
|
||||
#### 徽章系统
|
||||
|
||||
支持在菜单项上显示徽章标识:
|
||||
|
||||
- **HOT 徽章**:
|
||||
- 背景色:#ff4d4f (红色)
|
||||
- 文字:白色
|
||||
- 尺寸:18px 高度,圆角 9px
|
||||
|
||||
- **NEW 徽章**:
|
||||
- 背景色:#52c41a (绿色)
|
||||
- 文字:白色
|
||||
- 尺寸:18px 高度,圆角 9px
|
||||
|
||||
**注意**:收起状态下徽章自动隐藏。
|
||||
|
||||
#### 收起状态行为
|
||||
|
||||
- 仅显示一级菜单图标
|
||||
- 鼠标悬停时不展开子菜单
|
||||
- 点击跳转到该分类的默认页面
|
||||
|
||||
### 1.4 滚动条样式
|
||||
|
||||
```css
|
||||
宽度:6px
|
||||
轨道:透明
|
||||
滑块:rgba(255, 255, 255, 0.2)
|
||||
滑块悬停:rgba(255, 255, 255, 0.3)
|
||||
圆角:3px
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 顶部导航栏 (Header)
|
||||
|
||||
### 2.1 基础规范
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|---|
|
||||
| 高度 | 64px |
|
||||
| 背景色 | #ffffff |
|
||||
| 阴影 | 0 1px 4px rgba(0, 21, 41, 0.08) |
|
||||
| 位置 | sticky,top: 0 |
|
||||
| 层级 | z-index: 9 |
|
||||
| 内边距 | 0 24px |
|
||||
|
||||
### 2.2 左侧区域
|
||||
|
||||
**折叠按钮**:
|
||||
- 图标尺寸:18px
|
||||
- 颜色:rgba(0, 0, 0, 0.65)
|
||||
- 悬停色:#b8178d (品牌主色)
|
||||
- 悬停背景:rgba(0, 0, 0, 0.03)
|
||||
- 内边距:8px
|
||||
- 圆角:4px
|
||||
- 功能:切换侧边栏展开/收起状态
|
||||
|
||||
**工作台标识**:
|
||||
- 字号:14px
|
||||
- 字重:500 (Medium)
|
||||
- 颜色:rgba(0, 0, 0, 0.88)
|
||||
- 左侧间距:16px
|
||||
|
||||
### 2.3 右侧区域
|
||||
|
||||
从左到右依次包含:
|
||||
|
||||
1. **搜索框**
|
||||
- 宽度:200px
|
||||
- 高度:32px
|
||||
- 圆角:16px (胶囊形)
|
||||
- 占位文字:"搜索..."
|
||||
- 前缀图标:SearchOutlined
|
||||
|
||||
2. **帮助图标**
|
||||
- 图标:QuestionCircleOutlined
|
||||
- 尺寸:16px
|
||||
- 颜色:rgba(0, 0, 0, 0.65)
|
||||
- 悬停色:#b8178d
|
||||
|
||||
3. **功能链接**(ICP 备案、企业、支持)
|
||||
- 字号:14px
|
||||
- 颜色:rgba(0, 0, 0, 0.65)
|
||||
- 悬停色:#b8178d
|
||||
- 悬停背景:rgba(0, 0, 0, 0.03)
|
||||
- 内边距:4px 8px
|
||||
- 圆角:4px
|
||||
|
||||
4. **工单图标**
|
||||
- 图标:SettingOutlined
|
||||
- 样式同帮助图标
|
||||
|
||||
5. **消息中心**
|
||||
- 图标:BellOutlined
|
||||
- 带徽章:Badge count={5}
|
||||
- 徽章位置:右上角,offset: [-3, 3]
|
||||
- 徽章尺寸:small
|
||||
|
||||
6. **用户信息**
|
||||
- 头像:32px × 32px 圆形
|
||||
- 用户名:14px,Medium 字重
|
||||
- 颜色:rgba(0, 0, 0, 0.88)
|
||||
- 整体内边距:4px 8px
|
||||
- 悬停背景:rgba(0, 0, 0, 0.03)
|
||||
- 点击显示下拉菜单
|
||||
|
||||
**间距**:各元素之间间距 16-20px
|
||||
|
||||
---
|
||||
|
||||
## 3. 内容区域 (Content)
|
||||
|
||||
### 3.1 基础规范
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|---|
|
||||
| 背景色 | #f5f5f5 |
|
||||
| 内边距 | 24px |
|
||||
| 高度 | calc(100vh - 64px) |
|
||||
| 滚动 | overflow-y: auto |
|
||||
|
||||
### 3.2 内容容器
|
||||
|
||||
- 最大宽度:根据业务需求,建议 1200-1600px
|
||||
- 内边距:24px
|
||||
- 背景色:根据内容类型,卡片为 #fff
|
||||
|
||||
### 3.3 滚动行为
|
||||
|
||||
- 仅内容区域可滚动
|
||||
- 顶部导航栏和侧边栏保持固定
|
||||
- 滚动条样式与全局一致
|
||||
|
||||
---
|
||||
|
||||
## 4. 响应式适配
|
||||
|
||||
### 4.1 断点规则
|
||||
|
||||
| 断点 | 行为 |
|
||||
|------|------|
|
||||
| < 768px | 侧边栏默认收起,通过遮罩层展开 |
|
||||
| ≥ 768px | 侧边栏可正常展开/收起 |
|
||||
| ≥ 1200px | 建议默认展开侧边栏 |
|
||||
|
||||
### 4.2 移动端优化
|
||||
|
||||
- 侧边栏改为抽屉模式 (Drawer)
|
||||
- 顶部搜索框宽度减小或移至下拉菜单
|
||||
- 功能链接收起至"更多"菜单
|
||||
- 用户信息简化显示
|
||||
|
||||
---
|
||||
|
||||
## 5. 主题配色
|
||||
|
||||
### 5.1 品牌主色
|
||||
|
||||
基于 NEX Logo 的紫红色:
|
||||
|
||||
```css
|
||||
--primary-color: #b8178d;
|
||||
--primary-hover: #9c1477;
|
||||
--primary-active: #801161;
|
||||
```
|
||||
|
||||
### 5.2 辅助色
|
||||
|
||||
- **蓝色**(信息色):#1677ff
|
||||
- **绿色**(成功):#52c41a
|
||||
- **红色**(错误/警告):#ff4d4f
|
||||
- **橙色**(警告):#faad14
|
||||
|
||||
### 5.3 中性色
|
||||
|
||||
```css
|
||||
--text-primary: rgba(0, 0, 0, 0.88);
|
||||
--text-secondary: rgba(0, 0, 0, 0.65);
|
||||
--text-tertiary: rgba(0, 0, 0, 0.45);
|
||||
--bg-primary: #ffffff;
|
||||
--bg-secondary: #fafafa;
|
||||
--bg-tertiary: #f5f5f5;
|
||||
--border-color: #d9d9d9;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 交互规范
|
||||
|
||||
### 6.1 侧边栏折叠
|
||||
|
||||
**触发方式**:
|
||||
- 点击顶部折叠按钮
|
||||
- 可选:在设置中保存用户偏好
|
||||
|
||||
**动画**:
|
||||
- 过渡时间:200ms
|
||||
- 缓动函数:ease-in-out
|
||||
- 影响元素:侧边栏宽度、Logo、菜单文字
|
||||
|
||||
**状态保持**:
|
||||
- 使用 localStorage 保存用户折叠状态
|
||||
- 页面刷新后保持用户选择
|
||||
|
||||
### 6.2 菜单导航
|
||||
|
||||
**展开逻辑**:
|
||||
- 点击一级菜单展开/收起二级菜单
|
||||
- 默认展开当前路由所在的菜单组
|
||||
- 支持手风琴模式(可选)
|
||||
|
||||
**高亮逻辑**:
|
||||
- 根据当前路由自动高亮对应菜单项
|
||||
- 二级菜单选中时,一级菜单也显示激活状态
|
||||
|
||||
**跳转方式**:
|
||||
- 使用 React Router 进行路由跳转
|
||||
- 支持浏览器前进/后退
|
||||
|
||||
### 6.3 用户下拉菜单
|
||||
|
||||
**菜单项**:
|
||||
- 个人中心
|
||||
- 账户设置
|
||||
- 分割线
|
||||
- 退出登录
|
||||
|
||||
**交互**:
|
||||
- 点击用户信息区域展开
|
||||
- 点击菜单项执行对应操作
|
||||
- 点击外部区域关闭
|
||||
|
||||
---
|
||||
|
||||
## 7. 代码实现
|
||||
|
||||
### 7.1 组件结构
|
||||
|
||||
```
|
||||
MainLayout/
|
||||
├── MainLayout.jsx # 主布局组件
|
||||
├── MainLayout.css # 布局样式
|
||||
├── AppSider.jsx # 侧边栏组件
|
||||
├── AppSider.css # 侧边栏样式
|
||||
├── AppHeader.jsx # 顶部栏组件
|
||||
├── AppHeader.css # 顶部栏样式
|
||||
└── index.js # 导出文件
|
||||
```
|
||||
|
||||
### 7.2 菜单数据配置
|
||||
|
||||
菜单数据独立维护在 `src/constants/menuData.json`,便于更新和管理。
|
||||
|
||||
### 7.3 关键技术点
|
||||
|
||||
1. **状态管理**:
|
||||
- collapsed 状态通过 props 传递
|
||||
- 菜单展开状态 (openKeys) 在 AppSider 内部管理
|
||||
|
||||
2. **路由集成**:
|
||||
- 使用 useNavigate 进行路由跳转
|
||||
- 使用 useLocation 获取当前路由
|
||||
|
||||
3. **图标映射**:
|
||||
- 通过 iconMap 对象将字符串转换为图标组件
|
||||
|
||||
4. **主题定制**:
|
||||
- 在 src/main.jsx 中配置 Ant Design 主题
|
||||
- 使用 ConfigProvider 包裹应用
|
||||
|
||||
---
|
||||
|
||||
## 8. 示例页面
|
||||
|
||||
### 8.1 概览页 (Overview)
|
||||
|
||||
作为主框架的示例页面,展示了:
|
||||
- 统计卡片布局
|
||||
- 图表展示
|
||||
- 数据可视化
|
||||
- 响应式栅格系统
|
||||
|
||||
详细设计见:[概览页设计文档](./overview.md)
|
||||
|
||||
---
|
||||
|
||||
## 9. 可访问性
|
||||
|
||||
### 9.1 键盘导航
|
||||
|
||||
- 支持 Tab 键在可交互元素间切换
|
||||
- 支持 Enter 键激活菜单项
|
||||
- 支持方向键在菜单间导航
|
||||
|
||||
### 9.2 语义化标签
|
||||
|
||||
- 使用 nav 标签包裹导航菜单
|
||||
- 使用 header 标签包裹顶部栏
|
||||
- 使用 main 标签包裹主内容区
|
||||
|
||||
### 9.3 对比度
|
||||
|
||||
- 所有文本与背景对比度 ≥ 4.5:1
|
||||
- 图标与背景对比度 ≥ 3:1
|
||||
|
||||
---
|
||||
|
||||
## 10. 性能优化
|
||||
|
||||
### 10.1 懒加载
|
||||
|
||||
- 页面组件使用 React.lazy 懒加载
|
||||
- 减少首屏加载时间
|
||||
|
||||
### 10.2 防抖优化
|
||||
|
||||
- 搜索框输入使用防抖处理
|
||||
- 窗口大小变化使用节流处理
|
||||
|
||||
### 10.3 虚拟滚动
|
||||
|
||||
- 菜单项较多时考虑虚拟滚动
|
||||
- 长列表使用虚拟化技术
|
||||
|
||||
---
|
||||
|
||||
## 11. 开发指南
|
||||
|
||||
### 11.1 添加新菜单
|
||||
|
||||
1. 编辑 `src/constants/menuData.json`
|
||||
2. 添加菜单项配置
|
||||
3. 如需新图标,在 `AppSider.jsx` 的 iconMap 中添加映射
|
||||
4. 创建对应的页面组件
|
||||
5. 在 `App.jsx` 中添加路由
|
||||
|
||||
### 11.2 自定义主题
|
||||
|
||||
1. 编辑 `src/main.jsx` 中的 theme 配置
|
||||
2. 修改 `tailwind.config.js` 中的颜色系统
|
||||
3. 更新 `src/styles/globals.css` 中的 CSS 变量
|
||||
|
||||
### 11.3 扩展功能
|
||||
|
||||
- 添加面包屑导航
|
||||
- 添加页签 (Tabs) 功能
|
||||
- 添加全局设置抽屉
|
||||
- 添加主题切换(亮色/暗色)
|
||||
|
||||
---
|
||||
|
||||
## 12. 常见问题
|
||||
|
||||
### Q1: 如何修改侧边栏默认展开状态?
|
||||
|
||||
在 `MainLayout.jsx` 中修改 `collapsed` 的初始值:
|
||||
|
||||
```jsx
|
||||
const [collapsed, setCollapsed] = useState(false) // false 为展开
|
||||
```
|
||||
|
||||
### Q2: 如何添加三级菜单?
|
||||
|
||||
当前设计仅支持两级菜单。如需三级菜单,需要:
|
||||
1. 修改 menuData.json 数据结构
|
||||
2. 修改 AppSider.jsx 中的 getMenuItems 函数
|
||||
3. 考虑 UI 空间和用户体验
|
||||
|
||||
### Q3: 如何实现菜单权限控制?
|
||||
|
||||
建议:
|
||||
1. 在菜单数据中添加 `roles` 或 `permissions` 字段
|
||||
2. 在渲染菜单前根据用户权限过滤
|
||||
3. 在路由层面也要做权限校验
|
||||
|
||||
---
|
||||
|
||||
## 版本记录
|
||||
|
||||
| 版本 | 日期 | 说明 |
|
||||
|------|------|------|
|
||||
| 1.0.0 | 2024-11-04 | 初始版本,完成主框架设计 |
|
||||
|
||||
---
|
||||
|
||||
**维护者**: Nex Design Team
|
||||
**最后更新**: 2024-11-04
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Nex Design - 设计规范系统</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
{
|
||||
"name": "nex-design",
|
||||
"version": "1.0.0",
|
||||
"description": "面向 React + Ant Design + Tailwind CSS 的前端设计语言规范",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint src --ext js,jsx --report-unused-disable-directives --max-warnings 0"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"antd": "^5.12.0",
|
||||
"react-router-dom": "^6.20.0",
|
||||
"@ant-design/icons": "^5.2.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.43",
|
||||
"@types/react-dom": "^18.2.17",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"vite": "^5.0.8",
|
||||
"eslint": "^8.55.0",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.5",
|
||||
"tailwindcss": "^3.3.6",
|
||||
"postcss": "^8.4.32",
|
||||
"autoprefixer": "^10.4.16"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0",
|
||||
"yarn": ">=1.22.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import { Routes, Route } from 'react-router-dom'
|
||||
import MainLayout from './components/MainLayout'
|
||||
import OverviewPage from './pages/OverviewPage'
|
||||
import HostListPage from './pages/HostListPage'
|
||||
import UserListPage from './pages/UserListPage'
|
||||
import ImageListPage from './pages/ImageListPage'
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<MainLayout>
|
||||
<Routes>
|
||||
<Route path="/" element={<OverviewPage />} />
|
||||
<Route path="/overview" element={<OverviewPage />} />
|
||||
<Route path="/host/list" element={<HostListPage />} />
|
||||
<Route path="/user/list" element={<UserListPage />} />
|
||||
<Route path="/image/list" element={<ImageListPage />} />
|
||||
{/* 其他路由将在后续添加 */}
|
||||
</Routes>
|
||||
</MainLayout>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.2 KiB |
|
|
@ -0,0 +1,138 @@
|
|||
import { Modal } from 'antd'
|
||||
import { ExclamationCircleOutlined, DeleteOutlined } from '@ant-design/icons'
|
||||
|
||||
/**
|
||||
* 标准确认对话框组件
|
||||
* @param {Object} options - 对话框配置
|
||||
* @param {string} options.title - 标题
|
||||
* @param {string|ReactNode} options.content - 内容
|
||||
* @param {string} options.okText - 确认按钮文字
|
||||
* @param {string} options.cancelText - 取消按钮文字
|
||||
* @param {string} options.type - 类型: 'warning', 'danger', 'info'
|
||||
* @param {Function} options.onOk - 确认回调
|
||||
* @param {Function} options.onCancel - 取消回调
|
||||
*/
|
||||
const ConfirmDialog = {
|
||||
/**
|
||||
* 显示删除确认对话框(单个项目)
|
||||
*/
|
||||
delete: ({ title = '确认删除', itemName, itemInfo, onOk, onCancel }) => {
|
||||
Modal.confirm({
|
||||
title,
|
||||
content: (
|
||||
<div>
|
||||
<p>您确定要删除以下项目吗?</p>
|
||||
<div style={{ marginTop: 12, padding: 12, background: '#f5f5f5', borderRadius: 6 }}>
|
||||
<p style={{ margin: 0, fontWeight: 500 }}>{itemName}</p>
|
||||
{itemInfo && (
|
||||
<p style={{ margin: '4px 0 0 0', fontSize: 13, color: '#666' }}>{itemInfo}</p>
|
||||
)}
|
||||
</div>
|
||||
<p style={{ marginTop: 12, color: '#ff4d4f', fontSize: 13 }}>
|
||||
此操作不可恢复,请谨慎操作!
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
okText: '确认删除',
|
||||
cancelText: '取消',
|
||||
okType: 'danger',
|
||||
centered: true,
|
||||
icon: <DeleteOutlined style={{ color: '#ff4d4f' }} />,
|
||||
onOk,
|
||||
onCancel,
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 显示批量删除确认对话框
|
||||
*/
|
||||
batchDelete: ({ count, items, onOk, onCancel }) => {
|
||||
Modal.confirm({
|
||||
title: '批量删除确认',
|
||||
content: (
|
||||
<div>
|
||||
<p>您确定要删除选中的 {count} 个项目吗?</p>
|
||||
<div
|
||||
style={{
|
||||
marginTop: 12,
|
||||
padding: 12,
|
||||
background: '#f5f5f5',
|
||||
borderRadius: 6,
|
||||
maxHeight: 200,
|
||||
overflowY: 'auto',
|
||||
}}
|
||||
>
|
||||
{items.map((item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
style={{
|
||||
padding: '6px 0',
|
||||
borderBottom: index < items.length - 1 ? '1px solid #e8e8e8' : 'none',
|
||||
}}
|
||||
>
|
||||
<span style={{ fontWeight: 500 }}>{item.name}</span>
|
||||
{item.info && (
|
||||
<span style={{ marginLeft: 12, fontSize: 13, color: '#666' }}>
|
||||
({item.info})
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p style={{ marginTop: 12, color: '#ff4d4f', fontSize: 13 }}>
|
||||
此操作不可恢复,请谨慎操作!
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
okText: '确认删除',
|
||||
cancelText: '取消',
|
||||
okType: 'danger',
|
||||
centered: true,
|
||||
icon: <DeleteOutlined style={{ color: '#ff4d4f' }} />,
|
||||
onOk,
|
||||
onCancel,
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 显示警告确认对话框
|
||||
*/
|
||||
warning: ({ title, content, okText = '确定', cancelText = '取消', onOk, onCancel }) => {
|
||||
Modal.confirm({
|
||||
title,
|
||||
content,
|
||||
okText,
|
||||
cancelText,
|
||||
centered: true,
|
||||
icon: <ExclamationCircleOutlined style={{ color: '#faad14' }} />,
|
||||
onOk,
|
||||
onCancel,
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 显示通用确认对话框
|
||||
*/
|
||||
confirm: ({
|
||||
title,
|
||||
content,
|
||||
okText = '确定',
|
||||
cancelText = '取消',
|
||||
okType = 'primary',
|
||||
onOk,
|
||||
onCancel,
|
||||
}) => {
|
||||
Modal.confirm({
|
||||
title,
|
||||
content,
|
||||
okText,
|
||||
cancelText,
|
||||
okType,
|
||||
centered: true,
|
||||
onOk,
|
||||
onCancel,
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
export default ConfirmDialog
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
.app-header {
|
||||
background: #fff;
|
||||
padding: 0 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
||||
height: 64px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
/* 左侧区域 */
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
/* Logo 区域 */
|
||||
.header-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 168px;
|
||||
transition: width 0.2s;
|
||||
}
|
||||
|
||||
.logo-small {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.logo-full {
|
||||
height: 32px;
|
||||
width: auto;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.trigger {
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
transition: color 0.3s;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.trigger:hover {
|
||||
color: #b8178d;
|
||||
background: rgba(184, 23, 141, 0.06);
|
||||
}
|
||||
|
||||
/* 右侧区域 */
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.header-search {
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header-icon {
|
||||
font-size: 16px;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.header-icon:hover {
|
||||
color: #b8178d;
|
||||
background: rgba(184, 23, 141, 0.06);
|
||||
}
|
||||
|
||||
.header-link {
|
||||
font-size: 14px;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.header-link:hover {
|
||||
color: #b8178d;
|
||||
background: rgba(184, 23, 141, 0.06);
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.user-info:hover {
|
||||
background: rgba(184, 23, 141, 0.06);
|
||||
}
|
||||
|
||||
.username {
|
||||
font-size: 14px;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.ml-1 {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
|
@ -0,0 +1,121 @@
|
|||
import { Layout, Input, Badge, Avatar, Dropdown, Space } from 'antd'
|
||||
import {
|
||||
MenuFoldOutlined,
|
||||
MenuUnfoldOutlined,
|
||||
SearchOutlined,
|
||||
BellOutlined,
|
||||
QuestionCircleOutlined,
|
||||
FileTextOutlined,
|
||||
CustomerServiceOutlined,
|
||||
UserOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import headerMenuData from '../../constants/headerMenuData.json'
|
||||
import logoFull from '../../assets/logo-full.png'
|
||||
import './AppHeader.css'
|
||||
|
||||
const { Header } = Layout
|
||||
|
||||
// 图标映射
|
||||
const iconMap = {
|
||||
QuestionCircleOutlined: <QuestionCircleOutlined />,
|
||||
FileTextOutlined: <FileTextOutlined />,
|
||||
CustomerServiceOutlined: <CustomerServiceOutlined />,
|
||||
}
|
||||
|
||||
function AppHeader({ collapsed, onToggle }) {
|
||||
// 用户下拉菜单
|
||||
const userMenuItems = [
|
||||
{
|
||||
key: 'profile',
|
||||
label: '个人中心',
|
||||
},
|
||||
{
|
||||
key: 'settings',
|
||||
label: '账户设置',
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
},
|
||||
{
|
||||
key: 'logout',
|
||||
label: '退出登录',
|
||||
},
|
||||
]
|
||||
|
||||
const handleUserMenuClick = ({ key }) => {
|
||||
if (key === 'logout') {
|
||||
console.log('退出登录')
|
||||
}
|
||||
}
|
||||
|
||||
const handleHeaderMenuClick = (key) => {
|
||||
console.log('Header menu clicked:', key)
|
||||
}
|
||||
|
||||
return (
|
||||
<Header className="app-header">
|
||||
{/* 左侧:Logo + 折叠按钮 */}
|
||||
<div className="header-left">
|
||||
{/* Logo 区域 */}
|
||||
<div className="header-logo">
|
||||
<img src={logoFull} alt="NEX Console" className="logo-full" />
|
||||
</div>
|
||||
|
||||
{/* 折叠按钮 */}
|
||||
<div className="trigger" onClick={onToggle}>
|
||||
{collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 右侧:搜索 + 功能按钮 + 用户信息 */}
|
||||
<div className="header-right">
|
||||
{/* 搜索框 */}
|
||||
<Input
|
||||
className="header-search"
|
||||
placeholder="搜索..."
|
||||
prefix={<SearchOutlined />}
|
||||
style={{ width: 200 }}
|
||||
/>
|
||||
|
||||
{/* 功能图标 */}
|
||||
<Space size={16} className="header-actions">
|
||||
{/* 动态渲染 header 菜单 */}
|
||||
{headerMenuData.map((item) => (
|
||||
<div
|
||||
key={item.key}
|
||||
className="header-link"
|
||||
title={item.label}
|
||||
onClick={() => handleHeaderMenuClick(item.key)}
|
||||
>
|
||||
{iconMap[item.icon]}
|
||||
<span className="ml-1">{item.label}</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* 消息中心 */}
|
||||
<Badge count={5} size="small" offset={[-3, 3]}>
|
||||
<div className="header-icon" title="消息中心">
|
||||
<BellOutlined />
|
||||
</div>
|
||||
</Badge>
|
||||
|
||||
{/* 用户下拉菜单 */}
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: userMenuItems,
|
||||
onClick: handleUserMenuClick,
|
||||
}}
|
||||
placement="bottomRight"
|
||||
>
|
||||
<div className="user-info">
|
||||
<Avatar size={32} icon={<UserOutlined />} />
|
||||
<span className="username">Admin</span>
|
||||
</div>
|
||||
</Dropdown>
|
||||
</Space>
|
||||
</div>
|
||||
</Header>
|
||||
)
|
||||
}
|
||||
|
||||
export default AppHeader
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
.app-sider {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
background: #fafafa;
|
||||
border-right: 1px solid #f0f0f0;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.app-sider::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.app-sider::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.app-sider::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* 菜单样式 */
|
||||
.sider-menu {
|
||||
border-right: none;
|
||||
padding-top: 8px;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
/* 收起状态下的图标放大 */
|
||||
:global(.ant-layout-sider-collapsed) .sider-menu :global(.ant-menu-item) {
|
||||
padding: 0 !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 56px;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
/* 收起状态下的 SubMenu 样式 */
|
||||
:global(.ant-layout-sider-collapsed) .sider-menu :global(.ant-menu-submenu) {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
:global(.ant-layout-sider-collapsed) .sider-menu :global(.ant-menu-submenu-title) {
|
||||
padding: 0 !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 56px;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
:global(.ant-layout-sider-collapsed) .sider-menu :global(.anticon) {
|
||||
font-size: 24px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 收起状态下的 Tooltip */
|
||||
:global(.ant-layout-sider-collapsed) .sider-menu :global(.ant-menu-item-icon) {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
:global(.ant-layout-sider-collapsed) .sider-menu :global(.ant-menu-submenu-title) :global(.anticon) {
|
||||
font-size: 24px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 菜单项徽章 */
|
||||
.menu-item-with-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.menu-badge {
|
||||
font-size: 10px;
|
||||
height: 18px;
|
||||
line-height: 18px;
|
||||
border-radius: 9px;
|
||||
padding: 0 6px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.badge-hot :global(.ant-badge-count) {
|
||||
background: #ff4d4f;
|
||||
}
|
||||
|
||||
.badge-new :global(.ant-badge-count) {
|
||||
background: #52c41a;
|
||||
}
|
||||
|
||||
/* 收起状态下隐藏徽章 */
|
||||
:global(.ant-layout-sider-collapsed) .menu-badge {
|
||||
display: none;
|
||||
}
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
import { Layout, Menu, Badge, Tooltip } from 'antd'
|
||||
import { useNavigate, useLocation } from 'react-router-dom'
|
||||
import {
|
||||
DashboardOutlined,
|
||||
GlobalOutlined,
|
||||
CloudServerOutlined,
|
||||
UserOutlined,
|
||||
AppstoreOutlined,
|
||||
SettingOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import menuData from '../../constants/menuData.json'
|
||||
import './AppSider.css'
|
||||
|
||||
const { Sider } = Layout
|
||||
const { SubMenu } = Menu
|
||||
|
||||
// 图标映射
|
||||
const iconMap = {
|
||||
DashboardOutlined: DashboardOutlined,
|
||||
GlobalOutlined: GlobalOutlined,
|
||||
CloudServerOutlined: CloudServerOutlined,
|
||||
UserOutlined: UserOutlined,
|
||||
AppstoreOutlined: AppstoreOutlined,
|
||||
SettingOutlined: SettingOutlined,
|
||||
}
|
||||
|
||||
function AppSider({ collapsed, onToggle }) {
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
const [openKeys, setOpenKeys] = useState([])
|
||||
|
||||
// 根据当前路径获取应该打开的父菜单
|
||||
const getDefaultOpenKeys = () => {
|
||||
const path = location.pathname
|
||||
for (const item of menuData) {
|
||||
if (item.children) {
|
||||
const hasChild = item.children.some((c) => c.path === path)
|
||||
if (hasChild) {
|
||||
return [item.key]
|
||||
}
|
||||
}
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
// 监听路径变化和收拢状态,自动打开父菜单
|
||||
useEffect(() => {
|
||||
if (!collapsed) {
|
||||
const defaultKeys = getDefaultOpenKeys()
|
||||
setOpenKeys(defaultKeys)
|
||||
}
|
||||
}, [location.pathname, collapsed])
|
||||
|
||||
const handleMenuClick = ({ key }) => {
|
||||
// 查找对应的路径
|
||||
for (const item of menuData) {
|
||||
if (item.key === key && item.path) {
|
||||
navigate(item.path)
|
||||
return
|
||||
}
|
||||
if (item.children) {
|
||||
const child = item.children.find((c) => c.key === key)
|
||||
if (child) {
|
||||
navigate(child.path)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleOpenChange = (keys) => {
|
||||
setOpenKeys(keys)
|
||||
}
|
||||
|
||||
// 获取当前选中的菜单项
|
||||
const getSelectedKey = () => {
|
||||
const path = location.pathname
|
||||
for (const item of menuData) {
|
||||
if (item.path === path) return item.key
|
||||
if (item.children) {
|
||||
const child = item.children.find((c) => c.path === path)
|
||||
if (child) return child.key
|
||||
}
|
||||
}
|
||||
return 'overview'
|
||||
}
|
||||
|
||||
// 渲染菜单项
|
||||
const renderMenuItems = () => {
|
||||
return menuData.map((item) => {
|
||||
const IconComponent = iconMap[item.icon]
|
||||
const icon = IconComponent ? <IconComponent /> : null
|
||||
|
||||
// 如果有子菜单
|
||||
if (item.children) {
|
||||
// 在收拢状态下,用 Tooltip 包装 SubMenu 的图标
|
||||
const subMenuIcon = collapsed ? (
|
||||
<Tooltip title={item.label} placement="right">
|
||||
{icon}
|
||||
</Tooltip>
|
||||
) : (
|
||||
icon
|
||||
)
|
||||
|
||||
return (
|
||||
<SubMenu
|
||||
key={item.key}
|
||||
icon={subMenuIcon}
|
||||
title={item.label}
|
||||
popupClassName="sider-submenu-popup"
|
||||
>
|
||||
{item.children.map((child) => (
|
||||
<Menu.Item key={child.key}>
|
||||
{child.badge ? (
|
||||
<span className="menu-item-with-badge">
|
||||
{child.label}
|
||||
<Badge
|
||||
count={child.badge}
|
||||
className={`menu-badge ${child.badge === 'HOT' ? 'badge-hot' : 'badge-new'}`}
|
||||
/>
|
||||
</span>
|
||||
) : (
|
||||
child.label
|
||||
)}
|
||||
</Menu.Item>
|
||||
))}
|
||||
</SubMenu>
|
||||
)
|
||||
}
|
||||
|
||||
// 普通菜单项 - 也用 Tooltip 包装
|
||||
const menuIcon = collapsed ? (
|
||||
<Tooltip title={item.label} placement="right">
|
||||
{icon}
|
||||
</Tooltip>
|
||||
) : (
|
||||
icon
|
||||
)
|
||||
|
||||
return (
|
||||
<Menu.Item key={item.key} icon={menuIcon} title="">
|
||||
{item.label}
|
||||
</Menu.Item>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Sider
|
||||
className="app-sider"
|
||||
collapsed={collapsed}
|
||||
width={200}
|
||||
collapsedWidth={64}
|
||||
trigger={null}
|
||||
>
|
||||
{/* 菜单 */}
|
||||
<Menu
|
||||
mode="inline"
|
||||
selectedKeys={[getSelectedKey()]}
|
||||
openKeys={collapsed ? [] : openKeys}
|
||||
onOpenChange={handleOpenChange}
|
||||
onClick={handleMenuClick}
|
||||
className="sider-menu"
|
||||
>
|
||||
{renderMenuItems()}
|
||||
</Menu>
|
||||
</Sider>
|
||||
)
|
||||
}
|
||||
|
||||
export default AppSider
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
.main-layout {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.main-content-wrapper {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
height: calc(100vh - 64px);
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
background: #f5f5f5;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
padding: 24px;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import { useState } from 'react'
|
||||
import { Layout } from 'antd'
|
||||
import AppSider from './AppSider'
|
||||
import AppHeader from './AppHeader'
|
||||
import './MainLayout.css'
|
||||
|
||||
const { Content } = Layout
|
||||
|
||||
function MainLayout({ children }) {
|
||||
const [collapsed, setCollapsed] = useState(false)
|
||||
|
||||
const toggleCollapsed = () => {
|
||||
setCollapsed(!collapsed)
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout className="main-layout">
|
||||
<AppHeader collapsed={collapsed} onToggle={toggleCollapsed} />
|
||||
<Layout className="main-content-wrapper">
|
||||
<AppSider collapsed={collapsed} onToggle={toggleCollapsed} />
|
||||
<Content className="main-content">
|
||||
<div className="content-wrapper">
|
||||
{children}
|
||||
</div>
|
||||
</Content>
|
||||
</Layout>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
export default MainLayout
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
export { default } from './MainLayout'
|
||||
export { default as MainLayout } from './MainLayout'
|
||||
export { default as AppSider } from './AppSider'
|
||||
export { default as AppHeader } from './AppHeader'
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
.page-header-standard {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 12px;
|
||||
padding: 24px 28px;
|
||||
margin-bottom: 24px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.15);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.page-header-standard::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
right: -10%;
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.page-header-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
color: #ffffff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.back-button:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
transform: translateX(-2px);
|
||||
}
|
||||
|
||||
.page-header-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.page-header-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 12px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
backdrop-filter: blur(10px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
color: #ffffff;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.page-header-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.page-header-title {
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
margin: 0;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.page-header-description {
|
||||
font-size: 14px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
margin: 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.page-header-extra {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.page-header-standard {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.page-header-extra {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
import { ArrowLeftOutlined } from '@ant-design/icons'
|
||||
import './PageHeader.css'
|
||||
|
||||
function PageHeader({
|
||||
title,
|
||||
description,
|
||||
icon,
|
||||
showBack = false,
|
||||
onBack,
|
||||
extra
|
||||
}) {
|
||||
return (
|
||||
<div className="page-header-standard">
|
||||
<div className="page-header-main">
|
||||
{showBack && (
|
||||
<button className="back-button" onClick={onBack}>
|
||||
<ArrowLeftOutlined />
|
||||
</button>
|
||||
)}
|
||||
<div className="page-header-content">
|
||||
{icon && <div className="page-header-icon">{icon}</div>}
|
||||
<div className="page-header-text">
|
||||
<h1 className="page-header-title">{title}</h1>
|
||||
{description && (
|
||||
<p className="page-header-description">{description}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{extra && <div className="page-header-extra">{extra}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PageHeader
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
.page-title-bar {
|
||||
background: linear-gradient(135deg, #e0e7ff 0%, #f3e8ff 100%);
|
||||
border-radius: 12px;
|
||||
padding: 16px 24px;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(139, 92, 246, 0.1);
|
||||
}
|
||||
|
||||
.page-title-bar::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
right: -5%;
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
background: rgba(139, 92, 246, 0.05);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.title-bar-content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.title-bar-left {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.title-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
margin: 0;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.title-badge {
|
||||
background: rgba(139, 92, 246, 0.15);
|
||||
color: #7c3aed;
|
||||
padding: 2px 10px;
|
||||
border-radius: 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.page-description {
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.title-bar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.title-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.title-actions button {
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.title-actions button.primary {
|
||||
background: #7c3aed;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.title-actions button.primary:hover {
|
||||
background: #6d28d9;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(124, 58, 237, 0.25);
|
||||
}
|
||||
|
||||
.title-actions button.secondary {
|
||||
background: rgba(139, 92, 246, 0.1);
|
||||
color: #7c3aed;
|
||||
border: 1px solid rgba(139, 92, 246, 0.2);
|
||||
}
|
||||
|
||||
.title-actions button.secondary:hover {
|
||||
background: rgba(139, 92, 246, 0.15);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.toggle-button {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 6px;
|
||||
background: rgba(139, 92, 246, 0.1);
|
||||
border: 1px solid rgba(139, 92, 246, 0.2);
|
||||
color: #7c3aed;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.toggle-button:hover {
|
||||
background: rgba(139, 92, 246, 0.2);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* 响应式适配 */
|
||||
@media (max-width: 768px) {
|
||||
.title-bar-content {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.title-row {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.page-description {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.title-bar-right {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.title-actions {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.title-actions button {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
import { useState } from 'react'
|
||||
import { UpOutlined, DownOutlined } from '@ant-design/icons'
|
||||
import './PageTitleBar.css'
|
||||
|
||||
function PageTitleBar({
|
||||
title,
|
||||
badge,
|
||||
description,
|
||||
actions,
|
||||
showToggle = false,
|
||||
onToggle,
|
||||
defaultExpanded = true,
|
||||
}) {
|
||||
const [expanded, setExpanded] = useState(defaultExpanded)
|
||||
|
||||
const handleToggle = () => {
|
||||
const newExpanded = !expanded
|
||||
setExpanded(newExpanded)
|
||||
if (onToggle) {
|
||||
onToggle(newExpanded)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="page-title-bar">
|
||||
<div className="title-bar-content">
|
||||
<div className="title-bar-left">
|
||||
<div className="title-row">
|
||||
<div className="title-group">
|
||||
<h1 className="page-title">{title}</h1>
|
||||
{badge && <span className="title-badge">{badge}</span>}
|
||||
</div>
|
||||
{description && <p className="page-description">{description}</p>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="title-bar-right">
|
||||
{actions && <div className="title-actions">{actions}</div>}
|
||||
{showToggle && (
|
||||
<button
|
||||
className="toggle-button"
|
||||
onClick={handleToggle}
|
||||
title={expanded ? '收起信息面板' : '展开信息面板'}
|
||||
>
|
||||
{expanded ? <UpOutlined /> : <DownOutlined />}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PageTitleBar
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
import { notification } from 'antd'
|
||||
import {
|
||||
CheckCircleOutlined,
|
||||
CloseCircleOutlined,
|
||||
ExclamationCircleOutlined,
|
||||
InfoCircleOutlined,
|
||||
} from '@ant-design/icons'
|
||||
|
||||
// 配置全局通知位置和样式
|
||||
notification.config({
|
||||
placement: 'topRight',
|
||||
top: 24,
|
||||
duration: 3,
|
||||
maxCount: 3,
|
||||
})
|
||||
|
||||
/**
|
||||
* 标准通知反馈组件
|
||||
* 从右上角滑出,默认3秒后消失
|
||||
*/
|
||||
const Toast = {
|
||||
/**
|
||||
* 成功通知
|
||||
* @param {string} message - 消息内容
|
||||
* @param {string} description - 详细描述(可选)
|
||||
* @param {number} duration - 显示时长(秒),默认3秒
|
||||
*/
|
||||
success: (message, description = '', duration = 3) => {
|
||||
notification.success({
|
||||
message,
|
||||
description,
|
||||
duration,
|
||||
icon: <CheckCircleOutlined style={{ color: '#52c41a' }} />,
|
||||
style: {
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 错误通知
|
||||
* @param {string} message - 消息内容
|
||||
* @param {string} description - 详细描述(可选)
|
||||
* @param {number} duration - 显示时长(秒),默认3秒
|
||||
*/
|
||||
error: (message, description = '', duration = 3) => {
|
||||
notification.error({
|
||||
message,
|
||||
description,
|
||||
duration,
|
||||
icon: <CloseCircleOutlined style={{ color: '#ff4d4f' }} />,
|
||||
style: {
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 警告通知
|
||||
* @param {string} message - 消息内容
|
||||
* @param {string} description - 详细描述(可选)
|
||||
* @param {number} duration - 显示时长(秒),默认3秒
|
||||
*/
|
||||
warning: (message, description = '', duration = 3) => {
|
||||
notification.warning({
|
||||
message,
|
||||
description,
|
||||
duration,
|
||||
icon: <ExclamationCircleOutlined style={{ color: '#faad14' }} />,
|
||||
style: {
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 信息通知
|
||||
* @param {string} message - 消息内容
|
||||
* @param {string} description - 详细描述(可选)
|
||||
* @param {number} duration - 显示时长(秒),默认3秒
|
||||
*/
|
||||
info: (message, description = '', duration = 3) => {
|
||||
notification.info({
|
||||
message,
|
||||
description,
|
||||
duration,
|
||||
icon: <InfoCircleOutlined style={{ color: '#1677ff' }} />,
|
||||
style: {
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 自定义通知
|
||||
* @param {Object} config - 完整的通知配置对象
|
||||
*/
|
||||
custom: (config) => {
|
||||
notification.open({
|
||||
...config,
|
||||
style: {
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
|
||||
...config.style,
|
||||
},
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
export default Toast
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
[
|
||||
{
|
||||
"type": "link",
|
||||
"label": "帮助",
|
||||
"key": "help",
|
||||
"icon": "QuestionCircleOutlined"
|
||||
},
|
||||
{
|
||||
"type": "link",
|
||||
"label": "文档",
|
||||
"key": "docs",
|
||||
"icon": "FileTextOutlined"
|
||||
},
|
||||
{
|
||||
"type": "link",
|
||||
"label": "支持",
|
||||
"key": "support",
|
||||
"icon": "CustomerServiceOutlined"
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
[
|
||||
{
|
||||
"key": "overview",
|
||||
"label": "平台概览",
|
||||
"icon": "DashboardOutlined",
|
||||
"path": "/overview"
|
||||
},
|
||||
{
|
||||
"key": "network",
|
||||
"label": "网络管理",
|
||||
"icon": "GlobalOutlined",
|
||||
"children": [
|
||||
{
|
||||
"key": "network-config",
|
||||
"label": "网络配置",
|
||||
"path": "/network/config"
|
||||
},
|
||||
{
|
||||
"key": "network-monitor",
|
||||
"label": "流量监控",
|
||||
"path": "/network/monitor"
|
||||
},
|
||||
{
|
||||
"key": "network-security",
|
||||
"label": "安全组",
|
||||
"path": "/network/security"
|
||||
},
|
||||
{
|
||||
"key": "network-vpc",
|
||||
"label": "VPC 管理",
|
||||
"path": "/network/vpc"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "host",
|
||||
"label": "终端管理",
|
||||
"icon": "CloudServerOutlined",
|
||||
"children": [
|
||||
{
|
||||
"key": "host-list",
|
||||
"label": "终端列表",
|
||||
"path": "/host/list"
|
||||
},
|
||||
{
|
||||
"key": "host-monitor",
|
||||
"label": "终端组管理",
|
||||
"path": "/host/monitor"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "user",
|
||||
"label": "用户管理",
|
||||
"icon": "UserOutlined",
|
||||
"children": [
|
||||
{
|
||||
"key": "user-list",
|
||||
"label": "用户列表",
|
||||
"path": "/user/list"
|
||||
},
|
||||
{
|
||||
"key": "user-role",
|
||||
"label": "用户组管理",
|
||||
"path": "/user/role"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "image",
|
||||
"label": "镜像管理",
|
||||
"icon": "AppstoreOutlined",
|
||||
"children": [
|
||||
{
|
||||
"key": "image-system",
|
||||
"label": "系统镜像",
|
||||
"path": "/image/list"
|
||||
},
|
||||
{
|
||||
"key": "image-vm",
|
||||
"label": "虚拟机镜像",
|
||||
"path": "/image/vm"
|
||||
},
|
||||
{
|
||||
"key": "image-desktop",
|
||||
"label": "桌面镜像",
|
||||
"path": "/image/desktop"
|
||||
},
|
||||
{
|
||||
"key": "image-tools",
|
||||
"label": "镜像工具",
|
||||
"path": "/image/tools"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "system",
|
||||
"label": "系统管理",
|
||||
"icon": "SettingOutlined",
|
||||
"children": [
|
||||
{
|
||||
"key": "system-menu",
|
||||
"label": "菜单管理",
|
||||
"path": "/system/menu"
|
||||
},
|
||||
{
|
||||
"key": "system-permission",
|
||||
"label": "权限管理",
|
||||
"path": "/system/permission"
|
||||
},
|
||||
{
|
||||
"key": "system-dict",
|
||||
"label": "字典管理",
|
||||
"path": "/system/dict"
|
||||
},
|
||||
{
|
||||
"key": "system-config",
|
||||
"label": "参数配置",
|
||||
"path": "/system/config"
|
||||
},
|
||||
{
|
||||
"key": "system-log",
|
||||
"label": "操作日志",
|
||||
"path": "/system/log"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,198 @@
|
|||
{
|
||||
"hostGroups": [
|
||||
{
|
||||
"id": "1",
|
||||
"name": "全部主机",
|
||||
"children": [
|
||||
{
|
||||
"id": "1-1",
|
||||
"name": "生产环境",
|
||||
"children": [
|
||||
{ "id": "1-1-1", "name": "Web服务器" },
|
||||
{ "id": "1-1-2", "name": "数据库服务器" },
|
||||
{ "id": "1-1-3", "name": "缓存服务器" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "1-2",
|
||||
"name": "测试环境",
|
||||
"children": [
|
||||
{ "id": "1-2-1", "name": "测试服务器" },
|
||||
{ "id": "1-2-2", "name": "开发服务器" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "1-3",
|
||||
"name": "办公环境",
|
||||
"children": [
|
||||
{ "id": "1-3-1", "name": "行政部门" },
|
||||
{ "id": "1-3-2", "name": "研发部门" },
|
||||
{ "id": "1-3-3", "name": "市场部门" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"hosts": [
|
||||
{
|
||||
"id": "1",
|
||||
"name": "Web-Server-01",
|
||||
"mac": "00:1B:44:11:3A:B7",
|
||||
"group": "Web服务器",
|
||||
"groupId": "1-1-1",
|
||||
"status": "online",
|
||||
"ip": "192.168.1.101",
|
||||
"dataDisk": "500GB",
|
||||
"systemDisk": "100GB",
|
||||
"cpu": "Intel Xeon E5-2680 v4",
|
||||
"memory": "32GB",
|
||||
"os": "Ubuntu 20.04 LTS",
|
||||
"uptime": "15天3小时",
|
||||
"lastOnline": "2025-01-15 14:23:05"
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"name": "Web-Server-02",
|
||||
"mac": "00:1B:44:11:3A:B8",
|
||||
"group": "Web服务器",
|
||||
"groupId": "1-1-1",
|
||||
"status": "online",
|
||||
"ip": "192.168.1.102",
|
||||
"dataDisk": "500GB",
|
||||
"systemDisk": "100GB",
|
||||
"cpu": "Intel Xeon E5-2680 v4",
|
||||
"memory": "32GB",
|
||||
"os": "Ubuntu 20.04 LTS",
|
||||
"uptime": "15天3小时",
|
||||
"lastOnline": "2025-01-15 14:23:05"
|
||||
},
|
||||
{
|
||||
"id": "3",
|
||||
"name": "DB-Master-01",
|
||||
"mac": "00:1B:44:11:3A:C1",
|
||||
"group": "数据库服务器",
|
||||
"groupId": "1-1-2",
|
||||
"status": "online",
|
||||
"ip": "192.168.1.201",
|
||||
"dataDisk": "2TB",
|
||||
"systemDisk": "200GB",
|
||||
"cpu": "Intel Xeon Gold 6248R",
|
||||
"memory": "128GB",
|
||||
"os": "CentOS 7.9",
|
||||
"uptime": "45天12小时",
|
||||
"lastOnline": "2025-01-15 14:23:05"
|
||||
},
|
||||
{
|
||||
"id": "4",
|
||||
"name": "DB-Slave-01",
|
||||
"mac": "00:1B:44:11:3A:C2",
|
||||
"group": "数据库服务器",
|
||||
"groupId": "1-1-2",
|
||||
"status": "offline",
|
||||
"ip": "192.168.1.202",
|
||||
"dataDisk": "2TB",
|
||||
"systemDisk": "200GB",
|
||||
"cpu": "Intel Xeon Gold 6248R",
|
||||
"memory": "128GB",
|
||||
"os": "CentOS 7.9",
|
||||
"uptime": "0天0小时",
|
||||
"lastOnline": "2025-01-14 18:45:32"
|
||||
},
|
||||
{
|
||||
"id": "5",
|
||||
"name": "Cache-Server-01",
|
||||
"mac": "00:1B:44:11:3A:D1",
|
||||
"group": "缓存服务器",
|
||||
"groupId": "1-1-3",
|
||||
"status": "online",
|
||||
"ip": "192.168.1.301",
|
||||
"dataDisk": "1TB",
|
||||
"systemDisk": "100GB",
|
||||
"cpu": "AMD EPYC 7542",
|
||||
"memory": "64GB",
|
||||
"os": "Ubuntu 22.04 LTS",
|
||||
"uptime": "30天8小时",
|
||||
"lastOnline": "2025-01-15 14:23:05"
|
||||
},
|
||||
{
|
||||
"id": "6",
|
||||
"name": "Test-Server-01",
|
||||
"mac": "00:1B:44:11:3A:E1",
|
||||
"group": "测试服务器",
|
||||
"groupId": "1-2-1",
|
||||
"status": "online",
|
||||
"ip": "192.168.2.101",
|
||||
"dataDisk": "200GB",
|
||||
"systemDisk": "50GB",
|
||||
"cpu": "Intel Core i7-9700K",
|
||||
"memory": "16GB",
|
||||
"os": "Windows Server 2019",
|
||||
"uptime": "5天18小时",
|
||||
"lastOnline": "2025-01-15 14:23:05"
|
||||
},
|
||||
{
|
||||
"id": "7",
|
||||
"name": "Dev-Server-01",
|
||||
"mac": "00:1B:44:11:3A:F1",
|
||||
"group": "开发服务器",
|
||||
"groupId": "1-2-2",
|
||||
"status": "offline",
|
||||
"ip": "192.168.2.201",
|
||||
"dataDisk": "500GB",
|
||||
"systemDisk": "100GB",
|
||||
"cpu": "AMD Ryzen 9 5900X",
|
||||
"memory": "32GB",
|
||||
"os": "Ubuntu 22.04 LTS",
|
||||
"uptime": "0天0小时",
|
||||
"lastOnline": "2025-01-15 09:12:18"
|
||||
},
|
||||
{
|
||||
"id": "8",
|
||||
"name": "Office-PC-01",
|
||||
"mac": "00:1B:44:11:3A:G1",
|
||||
"group": "行政部门",
|
||||
"groupId": "1-3-1",
|
||||
"status": "online",
|
||||
"ip": "192.168.3.101",
|
||||
"dataDisk": "1TB",
|
||||
"systemDisk": "256GB",
|
||||
"cpu": "Intel Core i5-11400",
|
||||
"memory": "16GB",
|
||||
"os": "Windows 10 Pro",
|
||||
"uptime": "2天6小时",
|
||||
"lastOnline": "2025-01-15 14:23:05"
|
||||
},
|
||||
{
|
||||
"id": "9",
|
||||
"name": "Dev-PC-01",
|
||||
"mac": "00:1B:44:11:3A:H1",
|
||||
"group": "研发部门",
|
||||
"groupId": "1-3-2",
|
||||
"status": "online",
|
||||
"ip": "192.168.3.201",
|
||||
"dataDisk": "2TB",
|
||||
"systemDisk": "512GB",
|
||||
"cpu": "Intel Core i9-12900K",
|
||||
"memory": "64GB",
|
||||
"os": "Ubuntu 22.04 LTS",
|
||||
"uptime": "8天14小时",
|
||||
"lastOnline": "2025-01-15 14:23:05"
|
||||
},
|
||||
{
|
||||
"id": "10",
|
||||
"name": "Marketing-PC-01",
|
||||
"mac": "00:1B:44:11:3A:I1",
|
||||
"group": "市场部门",
|
||||
"groupId": "1-3-3",
|
||||
"status": "offline",
|
||||
"ip": "192.168.3.301",
|
||||
"dataDisk": "500GB",
|
||||
"systemDisk": "256GB",
|
||||
"cpu": "Intel Core i7-11700",
|
||||
"memory": "32GB",
|
||||
"os": "Windows 11 Pro",
|
||||
"uptime": "0天0小时",
|
||||
"lastOnline": "2025-01-15 12:30:22"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
{
|
||||
"imageGroups": [
|
||||
{
|
||||
"id": "1",
|
||||
"name": "默认分组"
|
||||
}
|
||||
],
|
||||
"images": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "uos-desktop-20-professional-hwe-1070-amd64",
|
||||
"os": "linux",
|
||||
"version": "202412",
|
||||
"status": "成功",
|
||||
"uploadTime": "1759199107299",
|
||||
"fileName": "uos-desktop-20-professional-hwe-1070-amd64-202412.iso",
|
||||
"filePath": "/vms/iso/uos-desktop-20-professional-hwe-1070-amd64-202412.iso",
|
||||
"btPath": "--",
|
||||
"description": "--"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Kylin-Desktop-V10-SP1-HWE-Release-2303-X86_64",
|
||||
"os": "linux",
|
||||
"version": "2303",
|
||||
"status": "成功",
|
||||
"uploadTime": "2025-10-09 09:55:41",
|
||||
"fileName": "Kylin-Desktop-V10-SP1-HWE-Release-2303-X86_64.iso",
|
||||
"filePath": "/vms/iso/Kylin-Desktop-V10-SP1-HWE-Release-2303-X86_64.iso",
|
||||
"btPath": "--",
|
||||
"description": "--"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "ky2203",
|
||||
"os": "linux",
|
||||
"version": "2203",
|
||||
"status": "成功",
|
||||
"uploadTime": "2025-10-13 09:41:50",
|
||||
"fileName": "ky2203.iso",
|
||||
"filePath": "/vms/iso/Kylin-Desktop-V10-SP1-General-Release-2203-X86_64.iso",
|
||||
"btPath": "--",
|
||||
"description": "--"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "windows-10",
|
||||
"os": "windows",
|
||||
"version": "1",
|
||||
"status": "成功",
|
||||
"uploadTime": "2025-10-14 16:54:38",
|
||||
"fileName": "windows-10.iso",
|
||||
"filePath": "/vms/iso/cn_windows_10_business.iso",
|
||||
"btPath": "--",
|
||||
"description": "--"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "windows-11",
|
||||
"os": "windows",
|
||||
"version": "1",
|
||||
"status": "成功",
|
||||
"uploadTime": "2025-10-14 16:54:49",
|
||||
"fileName": "windows-11.iso",
|
||||
"filePath": "/vms/iso/Windows11.iso",
|
||||
"btPath": "--",
|
||||
"description": "--"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"name": "Kylin-Desktop-V10-SP1-2403-HWE-Release-2403-x86_64",
|
||||
"os": "linux",
|
||||
"version": "2403",
|
||||
"status": "成功",
|
||||
"uploadTime": "2025-10-28 10:14:36",
|
||||
"fileName": "Kylin-Desktop-V10-SP1-2403-HWE-Release-2403-x86_64.iso",
|
||||
"filePath": "/vms/iso/Kylin-Desktop-V10-SP1-2403-HWE-Release-2403-x86_64.iso",
|
||||
"btPath": "--",
|
||||
"description": "--"
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"name": "Kylin-Desktop-V10-SP1-2503-X86_64",
|
||||
"os": "linux",
|
||||
"version": "2503",
|
||||
"status": "成功",
|
||||
"uploadTime": "2025-10-28 10:28:32",
|
||||
"fileName": "Kylin-Desktop-V10-SP1-2503-X86_64.iso",
|
||||
"filePath": "/vms/iso/Kylin-Desktop-V10-SP1-2503-HWE-Release-2503-X86_64.iso",
|
||||
"btPath": "--",
|
||||
"description": "--"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,194 @@
|
|||
{
|
||||
"userGroups": [
|
||||
{
|
||||
"id": "1",
|
||||
"name": "默认组织",
|
||||
"children": [
|
||||
{
|
||||
"id": "1-1",
|
||||
"name": "黑名单"
|
||||
},
|
||||
{
|
||||
"id": "1-2",
|
||||
"name": "用户测试分组"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"users": [
|
||||
{
|
||||
"id": 1,
|
||||
"userName": "admin",
|
||||
"userType": "个人用户",
|
||||
"name": "admin",
|
||||
"groupId": "1",
|
||||
"group": "默认组织",
|
||||
"status": "enabled",
|
||||
"grantedTerminals": 8,
|
||||
"grantedImages": 21,
|
||||
"terminals": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "默认终端",
|
||||
"group": "默认终端分组",
|
||||
"mac": "C8:D9:D2:00:B6:4B",
|
||||
"ip": "10.100.51.207",
|
||||
"status": "离线",
|
||||
"grantedImages": 1,
|
||||
"dataDisk": "316GB",
|
||||
"systemDisk": "200GB"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "默认终端",
|
||||
"group": "默认终端分组",
|
||||
"mac": "90:E2:FC:B6:D3:4F",
|
||||
"ip": "10.100.51.162",
|
||||
"status": "离线",
|
||||
"grantedImages": 1,
|
||||
"dataDisk": "120GB",
|
||||
"systemDisk": "200GB"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "默认终端",
|
||||
"group": "默认终端分组",
|
||||
"mac": "68:05:CA:9E:E1:98",
|
||||
"ip": "10.100.51.166",
|
||||
"status": "在线",
|
||||
"grantedImages": 2,
|
||||
"dataDisk": "316GB",
|
||||
"systemDisk": "200GB"
|
||||
}
|
||||
],
|
||||
"images": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "win10开发勾删",
|
||||
"system": "windows-10",
|
||||
"os": "Windows",
|
||||
"createTime": "2025-10-15 01:10:37",
|
||||
"status": "已下发",
|
||||
"method": "自动下载"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"userName": "user",
|
||||
"userType": "个人用户",
|
||||
"name": "user",
|
||||
"groupId": "1",
|
||||
"group": "默认组织",
|
||||
"status": "enabled",
|
||||
"grantedTerminals": 8,
|
||||
"grantedImages": 9,
|
||||
"terminals": [],
|
||||
"images": []
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"userName": "test_user",
|
||||
"userType": "个人用户",
|
||||
"name": "测试用户",
|
||||
"groupId": "1-2",
|
||||
"group": "用户测试分组",
|
||||
"status": "enabled",
|
||||
"grantedTerminals": 8,
|
||||
"grantedImages": 8,
|
||||
"terminals": [],
|
||||
"images": []
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"userName": "nex",
|
||||
"userType": "游客",
|
||||
"name": "匿名用户",
|
||||
"groupId": "1",
|
||||
"group": "默认组织",
|
||||
"status": "enabled",
|
||||
"grantedTerminals": 8,
|
||||
"grantedImages": 10,
|
||||
"terminals": [],
|
||||
"images": []
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"userName": "unis",
|
||||
"userType": "个人用户",
|
||||
"name": "unis",
|
||||
"groupId": "1",
|
||||
"group": "默认组织",
|
||||
"status": "enabled",
|
||||
"grantedTerminals": 8,
|
||||
"grantedImages": 10,
|
||||
"terminals": [],
|
||||
"images": []
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"userName": "ceshi123",
|
||||
"userType": "个人用户",
|
||||
"name": "加密测试",
|
||||
"groupId": "1",
|
||||
"group": "默认组织",
|
||||
"status": "enabled",
|
||||
"grantedTerminals": 6,
|
||||
"grantedImages": 9,
|
||||
"terminals": [],
|
||||
"images": []
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"userName": "ceshi12345",
|
||||
"userType": "个人用户",
|
||||
"name": "测试1",
|
||||
"groupId": "1-1",
|
||||
"group": "黑名单",
|
||||
"status": "enabled",
|
||||
"grantedTerminals": 1,
|
||||
"grantedImages": 4,
|
||||
"terminals": [],
|
||||
"images": []
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"userName": "unis1",
|
||||
"userType": "个人用户",
|
||||
"name": "unis1",
|
||||
"groupId": "1",
|
||||
"group": "默认组织",
|
||||
"status": "enabled",
|
||||
"grantedTerminals": 5,
|
||||
"grantedImages": 6,
|
||||
"terminals": [],
|
||||
"images": []
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"userName": "abcde",
|
||||
"userType": "个人用户",
|
||||
"name": "测试",
|
||||
"groupId": "1",
|
||||
"group": "默认组织",
|
||||
"status": "enabled",
|
||||
"grantedTerminals": 0,
|
||||
"grantedImages": 0,
|
||||
"terminals": [],
|
||||
"images": []
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"userName": "qazwsx",
|
||||
"userType": "个人用户",
|
||||
"name": "qazwsx",
|
||||
"groupId": "1",
|
||||
"group": "默认组织",
|
||||
"status": "enabled",
|
||||
"grantedTerminals": 0,
|
||||
"grantedImages": 0,
|
||||
"terminals": [],
|
||||
"images": []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
import { ConfigProvider } from 'antd'
|
||||
import zhCN from 'antd/locale/zh_CN'
|
||||
import App from './App.jsx'
|
||||
import './styles/globals.css'
|
||||
|
||||
// Ant Design 主题配置
|
||||
const theme = {
|
||||
token: {
|
||||
colorPrimary: '#b8178d', // 品牌主色 - 匹配 NEX LOGO
|
||||
colorInfo: '#1677ff', // 信息色使用蓝色
|
||||
borderRadius: 8,
|
||||
fontSize: 14,
|
||||
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif',
|
||||
},
|
||||
components: {
|
||||
Button: {
|
||||
controlHeight: 32,
|
||||
controlHeightLG: 40,
|
||||
controlHeightSM: 24,
|
||||
},
|
||||
Input: {
|
||||
controlHeight: 32,
|
||||
},
|
||||
Menu: {
|
||||
itemHeight: 48,
|
||||
collapsedIconSize: 24,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<React.StrictMode>
|
||||
<BrowserRouter>
|
||||
<ConfigProvider locale={zhCN} theme={theme}>
|
||||
<App />
|
||||
</ConfigProvider>
|
||||
</BrowserRouter>
|
||||
</React.StrictMode>,
|
||||
)
|
||||
|
|
@ -0,0 +1,582 @@
|
|||
.host-list-page {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 统计面板 */
|
||||
.stats-panel {
|
||||
margin-bottom: 16px;
|
||||
animation: slideDown 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
max-height: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
max-height: 500px;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 统计卡片 */
|
||||
.stat-card-small {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.stat-card-small:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.stat-card-small :global(.ant-statistic-title) {
|
||||
font-size: 13px;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
}
|
||||
|
||||
.stat-card-small :global(.ant-statistic-content) {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 卡片激活状态 */
|
||||
.stat-card-active {
|
||||
border: 2px solid #1677ff;
|
||||
background: linear-gradient(135deg, #e6f4ff 0%, #f0f5ff 100%);
|
||||
box-shadow: 0 4px 16px rgba(22, 119, 255, 0.2);
|
||||
}
|
||||
|
||||
.stat-card-active:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(22, 119, 255, 0.3);
|
||||
}
|
||||
|
||||
/* 卡片变暗状态(非激活状态下其他卡片) */
|
||||
.stat-card-dimmed {
|
||||
opacity: 0.5;
|
||||
filter: grayscale(0.3);
|
||||
}
|
||||
|
||||
.stat-card-dimmed:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* 筛选结果卡片(不可点击) */
|
||||
.stat-card-result {
|
||||
cursor: default !important;
|
||||
}
|
||||
|
||||
.stat-card-result:hover {
|
||||
transform: none !important;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06) !important;
|
||||
}
|
||||
|
||||
/* 操作栏 */
|
||||
.action-bar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.action-bar-left,
|
||||
.action-bar-right {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 搜索和筛选组合 - 使用 Space.Compact */
|
||||
.action-bar-right :global(.ant-space-compact) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.action-bar-right :global(.ant-space-compact .ant-input-search) {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.action-bar-right :global(.ant-space-compact > .ant-btn) {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
/* 筛选弹出框内容 */
|
||||
.filter-popover-content {
|
||||
width: 320px;
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.filter-popover-content .selected-filters {
|
||||
min-height: 40px;
|
||||
padding: 12px;
|
||||
background: #f5f7fa;
|
||||
border-radius: 6px;
|
||||
border: 1px dashed #d9d9d9;
|
||||
}
|
||||
|
||||
.filter-popover-content .filter-tag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.filter-popover-content .filter-label {
|
||||
font-size: 13px;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.filter-popover-content .filter-placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 24px;
|
||||
}
|
||||
|
||||
.filter-popover-content .group-tree-container {
|
||||
max-height: 280px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.filter-popover-content .tree-header {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 12px;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
}
|
||||
|
||||
.filter-popover-content .filter-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* 弹出框样式 */
|
||||
:global(.filter-popover .ant-popover-inner) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
:global(.filter-popover .ant-popover-title) {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: #ffffff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
:global(.filter-popover .ant-popover-inner-content) {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
/* 数据统计 */
|
||||
.data-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.stat-card-small {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.stat-card-small:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.stat-card-small :global(.ant-statistic-title) {
|
||||
font-size: 13px;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
}
|
||||
|
||||
.stat-card-small :global(.ant-statistic-content) {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 表格容器 */
|
||||
.table-container {
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.host-name {
|
||||
font-weight: 500;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
}
|
||||
|
||||
.host-list-page :global(.ant-table-row) {
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.host-list-page :global(.ant-table-row:hover) {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.host-list-page :global(.ant-table-row.row-selected) {
|
||||
background: #e6f4ff;
|
||||
}
|
||||
|
||||
/* 操作列样式优化 - 重新设计 */
|
||||
.host-list-page :global(.ant-table-thead > tr > th:last-child) {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
|
||||
color: #ffffff !important;
|
||||
font-weight: 600;
|
||||
border-left: 2px solid #e8e8e8;
|
||||
}
|
||||
|
||||
.host-list-page :global(.ant-table-tbody > tr > td:last-child) {
|
||||
background: #f8f9ff !important;
|
||||
border-left: 2px solid #e8e8e8;
|
||||
box-shadow: -2px 0 4px rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
.host-list-page :global(.ant-table-tbody > tr:hover > td:last-child) {
|
||||
background: #eef0ff !important;
|
||||
}
|
||||
|
||||
.host-list-page :global(.ant-table-tbody > tr.row-selected > td:last-child) {
|
||||
background: #e1e6ff !important;
|
||||
}
|
||||
|
||||
/* 详情抽屉 */
|
||||
.detail-drawer-content {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 顶部信息区域 - 固定不滚动 */
|
||||
.detail-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 24px;
|
||||
background: #fafafa;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.detail-header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
font-size: 18px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.close-button:hover {
|
||||
color: #1677ff;
|
||||
}
|
||||
|
||||
.header-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.host-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
}
|
||||
|
||||
.detail-header-right {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* 可滚动内容区域 */
|
||||
.detail-scrollable-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* 主机信息面板 */
|
||||
.detail-info-panel {
|
||||
padding: 24px;
|
||||
background: #ffffff;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 操作按钮区 */
|
||||
.detail-actions {
|
||||
margin-top: 20px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
/* 标签页区域 */
|
||||
.detail-tabs {
|
||||
background: #ffffff;
|
||||
padding-top: 16px;
|
||||
padding-left: 12px;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.detail-tabs :global(.ant-tabs) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.detail-tabs :global(.ant-tabs-content-holder) {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.detail-tabs :global(.ant-tabs-nav) {
|
||||
padding: 0;
|
||||
margin: 0 24px;
|
||||
margin-bottom: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.detail-tabs :global(.ant-tabs-nav::before) {
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.detail-tabs :global(.ant-tabs-tab) {
|
||||
padding: 12px 0;
|
||||
margin: 0 32px 0 0;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.detail-tabs :global(.ant-tabs-tab:first-child) {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.detail-tabs :global(.ant-tabs-tab-active .ant-tabs-tab-btn) {
|
||||
color: #d946ef;
|
||||
}
|
||||
|
||||
.detail-tabs :global(.ant-tabs-ink-bar) {
|
||||
background: #d946ef;
|
||||
height: 3px;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
padding: 24px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
/* 卡片列表 */
|
||||
.card-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
/* 镜像卡片 */
|
||||
.image-card {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.image-card:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card-header h4 {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
}
|
||||
|
||||
.card-info-item {
|
||||
display: flex;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.card-label {
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.card-value {
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* 用户卡片 */
|
||||
.user-card {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.user-card:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.user-header-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.user-sequence {
|
||||
font-size: 12px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.user-name-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.user-label {
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
}
|
||||
|
||||
.user-value {
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.user-images {
|
||||
background: #fafafa;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.images-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 旧版详情抽屉样式(保留用于编辑抽屉) */
|
||||
.detail-content {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.status-card {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.status-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.status-header h3 {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.status-header :global(.ant-badge-status-text) {
|
||||
color: #ffffff;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.status-actions {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.status-actions button {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.info-section {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.section-title::before {
|
||||
content: '';
|
||||
width: 3px;
|
||||
height: 16px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 1200px) {
|
||||
.stats-panel :global(.ant-row) {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.action-bar {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.action-bar-left,
|
||||
.action-bar-right {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.stats-panel :global(.ant-row) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,165 @@
|
|||
.image-list-page {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 操作栏 */
|
||||
.action-bar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.action-bar-left,
|
||||
.action-bar-right {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 表格容器 */
|
||||
.table-container {
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.image-list-page :global(.ant-table-row) {
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.image-list-page :global(.ant-table-row:hover) {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.image-list-page :global(.ant-table-row.row-selected) {
|
||||
background: #e6f4ff;
|
||||
}
|
||||
|
||||
/* 操作列样式优化 - 重新设计 */
|
||||
.image-list-page :global(.ant-table-thead > tr > th:last-child) {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
|
||||
color: #ffffff !important;
|
||||
font-weight: 600;
|
||||
border-left: 2px solid #e8e8e8;
|
||||
}
|
||||
|
||||
.image-list-page :global(.ant-table-tbody > tr > td:last-child) {
|
||||
background: #f8f9ff !important;
|
||||
border-left: 2px solid #e8e8e8;
|
||||
box-shadow: -2px 0 4px rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
.image-list-page :global(.ant-table-tbody > tr:hover > td:last-child) {
|
||||
background: #eef0ff !important;
|
||||
}
|
||||
|
||||
.image-list-page :global(.ant-table-tbody > tr.row-selected > td:last-child) {
|
||||
background: #e1e6ff !important;
|
||||
}
|
||||
|
||||
/* 详情抽屉 */
|
||||
.detail-drawer-content {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 顶部信息区域 - 固定不滚动 */
|
||||
.detail-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 24px;
|
||||
background: #fafafa;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.detail-header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
font-size: 18px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.close-button:hover {
|
||||
color: #1677ff;
|
||||
}
|
||||
|
||||
.header-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.image-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
}
|
||||
|
||||
.detail-header-right {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* 可滚动内容区域 */
|
||||
.detail-scrollable-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* 镜像信息面板 */
|
||||
.detail-info-panel {
|
||||
padding: 24px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
white-space: nowrap;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
font-weight: 500;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 768px) {
|
||||
.action-bar {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.action-bar-left,
|
||||
.action-bar-right {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,343 @@
|
|||
import { useState } from 'react'
|
||||
import {
|
||||
Table,
|
||||
Input,
|
||||
Button,
|
||||
Space,
|
||||
Drawer,
|
||||
Row,
|
||||
Col,
|
||||
} from 'antd'
|
||||
import {
|
||||
SearchOutlined,
|
||||
PlusOutlined,
|
||||
DownloadOutlined,
|
||||
EyeOutlined,
|
||||
DeleteOutlined,
|
||||
CloseOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import PageTitleBar from '../components/PageTitleBar/PageTitleBar'
|
||||
import ConfirmDialog from '../components/ConfirmDialog/ConfirmDialog'
|
||||
import Toast from '../components/Toast/Toast'
|
||||
import imageData from '../data/imageData.json'
|
||||
import './ImageListPage.css'
|
||||
|
||||
const { Search } = Input
|
||||
|
||||
function ImageListPage() {
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState([])
|
||||
const [selectedImage, setSelectedImage] = useState(null)
|
||||
const [showDetailDrawer, setShowDetailDrawer] = useState(false)
|
||||
const [searchKeyword, setSearchKeyword] = useState('')
|
||||
const [filteredImages, setFilteredImages] = useState(imageData.images)
|
||||
|
||||
// 表格列定义
|
||||
const columns = [
|
||||
{
|
||||
title: '序号',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
width: 80,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 350,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '操作系统',
|
||||
dataIndex: 'os',
|
||||
key: 'os',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '镜像版本',
|
||||
dataIndex: 'version',
|
||||
key: 'version',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '上传时间',
|
||||
dataIndex: 'uploadTime',
|
||||
key: 'uploadTime',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 220,
|
||||
fixed: 'right',
|
||||
render: (_, record) => (
|
||||
<Space size="small" onClick={(e) => e.stopPropagation()}>
|
||||
<Button type="link" size="small" icon={<DownloadOutlined />}>
|
||||
下载
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={() => handleViewDetail(record)}
|
||||
>
|
||||
详情
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<DeleteOutlined />}
|
||||
danger
|
||||
onClick={() => handleDeleteImage(record)}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
// 行选择配置
|
||||
const rowSelection = {
|
||||
selectedRowKeys,
|
||||
onChange: (newSelectedRowKeys) => {
|
||||
setSelectedRowKeys(newSelectedRowKeys)
|
||||
},
|
||||
}
|
||||
|
||||
// 处理搜索
|
||||
const handleSearch = (value) => {
|
||||
setSearchKeyword(value)
|
||||
if (value) {
|
||||
const filtered = imageData.images.filter(
|
||||
(image) =>
|
||||
image.name.toLowerCase().includes(value.toLowerCase()) ||
|
||||
image.os.toLowerCase().includes(value.toLowerCase())
|
||||
)
|
||||
setFilteredImages(filtered)
|
||||
} else {
|
||||
setFilteredImages(imageData.images)
|
||||
}
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
const handleViewDetail = (record) => {
|
||||
setSelectedImage(record)
|
||||
setShowDetailDrawer(true)
|
||||
}
|
||||
|
||||
// 处理行点击
|
||||
const handleRowClick = (record) => {
|
||||
setSelectedImage(record)
|
||||
setShowDetailDrawer(true)
|
||||
}
|
||||
|
||||
// 删除镜像
|
||||
const handleDeleteImage = (record) => {
|
||||
ConfirmDialog.delete({
|
||||
itemName: `镜像名称:${record.name}`,
|
||||
itemInfo: `文件名:${record.fileName}`,
|
||||
onOk() {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
const newImages = filteredImages.filter((img) => img.id !== record.id)
|
||||
setFilteredImages(newImages)
|
||||
resolve()
|
||||
Toast.success('删除成功', `镜像 "${record.name}" 已成功删除`)
|
||||
}, 1000)
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 批量删除
|
||||
const handleBatchDelete = () => {
|
||||
const selectedImages = filteredImages.filter((img) => selectedRowKeys.includes(img.id))
|
||||
const items = selectedImages.map((image) => ({
|
||||
name: image.name,
|
||||
info: image.fileName,
|
||||
}))
|
||||
|
||||
ConfirmDialog.batchDelete({
|
||||
count: selectedRowKeys.length,
|
||||
items,
|
||||
onOk() {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
const count = selectedRowKeys.length
|
||||
const newImages = filteredImages.filter((img) => !selectedRowKeys.includes(img.id))
|
||||
setFilteredImages(newImages)
|
||||
setSelectedRowKeys([])
|
||||
resolve()
|
||||
Toast.success('批量删除成功', `已成功删除 ${count} 个镜像`)
|
||||
}, 1000)
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="image-list-page">
|
||||
{/* 页面标题栏 */}
|
||||
<PageTitleBar
|
||||
title="系统镜像"
|
||||
description="管理系统镜像文件,包括上传、下载和删除操作"
|
||||
showToggle={false}
|
||||
/>
|
||||
|
||||
{/* 操作栏 */}
|
||||
<div className="action-bar">
|
||||
<div className="action-bar-left">
|
||||
<Button type="primary" icon={<PlusOutlined />}>
|
||||
新建
|
||||
</Button>
|
||||
<Button icon={<DeleteOutlined />} disabled={selectedRowKeys.length === 0} danger onClick={handleBatchDelete}>
|
||||
批量删除
|
||||
</Button>
|
||||
</div>
|
||||
<div className="action-bar-right">
|
||||
<Search
|
||||
placeholder="请输入名称"
|
||||
allowClear
|
||||
style={{ width: 280 }}
|
||||
onSearch={handleSearch}
|
||||
onChange={(e) => handleSearch(e.target.value)}
|
||||
value={searchKeyword}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 数据表格 */}
|
||||
<div className="table-container">
|
||||
<Table
|
||||
rowSelection={rowSelection}
|
||||
columns={columns}
|
||||
dataSource={filteredImages}
|
||||
rowKey="id"
|
||||
pagination={{
|
||||
total: filteredImages.length,
|
||||
pageSize: 10,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total) => `共 ${total} 条记录`,
|
||||
}}
|
||||
scroll={{ x: 1200 }}
|
||||
onRow={(record) => ({
|
||||
onClick: () => handleRowClick(record),
|
||||
className: selectedImage?.id === record.id ? 'row-selected' : '',
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 详情抽屉 */}
|
||||
<Drawer
|
||||
title={null}
|
||||
placement="right"
|
||||
width={1080}
|
||||
onClose={() => setShowDetailDrawer(false)}
|
||||
open={showDetailDrawer}
|
||||
closable={false}
|
||||
styles={{ body: { padding: 0 } }}
|
||||
>
|
||||
{selectedImage && (
|
||||
<div className="detail-drawer-content">
|
||||
{/* 顶部标题栏 */}
|
||||
<div className="detail-header">
|
||||
<div className="detail-header-left">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<CloseOutlined />}
|
||||
onClick={() => setShowDetailDrawer(false)}
|
||||
className="close-button"
|
||||
/>
|
||||
<div className="header-info">
|
||||
<h2 className="image-title">{selectedImage.name}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div className="detail-header-right">
|
||||
<Space size="middle">
|
||||
<Button icon={<DownloadOutlined />}>下载</Button>
|
||||
<Button
|
||||
icon={<DeleteOutlined />}
|
||||
danger
|
||||
onClick={() => {
|
||||
setShowDetailDrawer(false)
|
||||
handleDeleteImage(selectedImage)
|
||||
}}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 可滚动内容区域 */}
|
||||
<div className="detail-scrollable-content">
|
||||
{/* 镜像信息面板 */}
|
||||
<div className="detail-info-panel">
|
||||
<Row gutter={[24, 16]}>
|
||||
<Col span={12}>
|
||||
<div className="info-item">
|
||||
<div className="info-label">镜像名称:</div>
|
||||
<div className="info-value">{selectedImage.name}</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<div className="info-item">
|
||||
<div className="info-label">镜像文件:</div>
|
||||
<div className="info-value">{selectedImage.fileName}</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<div className="info-item">
|
||||
<div className="info-label">镜像版本:</div>
|
||||
<div className="info-value">{selectedImage.version}</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<div className="info-item">
|
||||
<div className="info-label">操作系统:</div>
|
||||
<div className="info-value">{selectedImage.os}</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<div className="info-item">
|
||||
<div className="info-label">镜像状态:</div>
|
||||
<div className="info-value">{selectedImage.status}</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<div className="info-item">
|
||||
<div className="info-label">上传时间:</div>
|
||||
<div className="info-value">{selectedImage.uploadTime}</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<div className="info-item">
|
||||
<div className="info-label">镜像存放路径:</div>
|
||||
<div className="info-value">{selectedImage.filePath}</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<div className="info-item">
|
||||
<div className="info-label">BT路径:</div>
|
||||
<div className="info-value">{selectedImage.btPath}</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<div className="info-item">
|
||||
<div className="info-label">描述:</div>
|
||||
<div className="info-value">{selectedImage.description}</div>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Drawer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ImageListPage
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
.overview-page {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 统计卡片样式保留 */
|
||||
.mb-6 {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.stat-card :global(.ant-statistic-title) {
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
font-size: 14px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-card :global(.ant-statistic-content) {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
}
|
||||
|
||||
.stat-trend {
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.stat-detail {
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.trend-up {
|
||||
color: #52c41a;
|
||||
font-size: 13px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
/* 快速链接 */
|
||||
.quick-links {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.quick-link-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.quick-link-item:hover {
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.link-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.link-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.link-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.link-desc {
|
||||
font-size: 12px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
/* 活动列表 */
|
||||
.activity-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.activity-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.activity-item:last-child {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.activity-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
margin-top: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.activity-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.activity-text {
|
||||
font-size: 14px;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.activity-time {
|
||||
font-size: 12px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
/* 图表占位 */
|
||||
.chart-placeholder {
|
||||
min-height: 300px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #fafafa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
|
@ -0,0 +1,247 @@
|
|||
import { Card, Row, Col, Statistic, Table, Tag, Badge } from 'antd'
|
||||
import {
|
||||
DesktopOutlined,
|
||||
MobileOutlined,
|
||||
UserOutlined,
|
||||
TeamOutlined,
|
||||
WarningOutlined,
|
||||
CheckCircleOutlined,
|
||||
LinkOutlined,
|
||||
ClockCircleOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import PageTitleBar from '../components/PageTitleBar/PageTitleBar'
|
||||
import './OverviewPage.css'
|
||||
|
||||
function OverviewPage() {
|
||||
// 模拟报错信息数据
|
||||
const errorColumns = [
|
||||
{
|
||||
title: '时间',
|
||||
dataIndex: 'time',
|
||||
key: 'time',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: '错误类型',
|
||||
dataIndex: 'type',
|
||||
key: 'type',
|
||||
width: 120,
|
||||
render: (type) => (
|
||||
<Tag color={type === '严重' ? 'red' : type === '警告' ? 'orange' : 'blue'}>
|
||||
{type}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '错误信息',
|
||||
dataIndex: 'message',
|
||||
key: 'message',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 100,
|
||||
render: (status) => (
|
||||
<Badge
|
||||
status={status === '已处理' ? 'success' : 'processing'}
|
||||
text={status}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
const errorData = [
|
||||
{
|
||||
key: '1',
|
||||
time: '2024-01-15 14:32:05',
|
||||
type: '严重',
|
||||
message: '网络配置服务连接超时',
|
||||
status: '处理中',
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
time: '2024-01-15 13:20:18',
|
||||
type: '警告',
|
||||
message: '主机 192.168.1.101 CPU 使用率超过 85%',
|
||||
status: '已处理',
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
time: '2024-01-15 12:15:42',
|
||||
type: '信息',
|
||||
message: '用户登录异常:连续失败 3 次',
|
||||
status: '已处理',
|
||||
},
|
||||
{
|
||||
key: '4',
|
||||
time: '2024-01-15 11:08:30',
|
||||
type: '警告',
|
||||
message: '存储空间不足,剩余容量 < 10%',
|
||||
status: '处理中',
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="overview-page">
|
||||
{/* 页面标题区 */}
|
||||
<PageTitleBar
|
||||
title="平台概览"
|
||||
badge="实时"
|
||||
description="查看平台接入的终端、用户情况及系统运行状态"
|
||||
actions={
|
||||
<>
|
||||
<button className="secondary">导出报表</button>
|
||||
<button className="primary">刷新数据</button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* 统计卡片 - 终端和用户情况 */}
|
||||
<Row gutter={[16, 16]} className="mb-6">
|
||||
<Col xs={24} sm={12} lg={6}>
|
||||
<Card className="stat-card">
|
||||
<Statistic
|
||||
title="接入终端数"
|
||||
value={342}
|
||||
prefix={<DesktopOutlined style={{ color: '#1677ff' }} />}
|
||||
suffix="台"
|
||||
/>
|
||||
<div className="stat-trend">
|
||||
<span className="stat-detail">PC: 218台 | 移动: 124台</span>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} sm={12} lg={6}>
|
||||
<Card className="stat-card">
|
||||
<Statistic
|
||||
title="在线终端"
|
||||
value={287}
|
||||
prefix={<CheckCircleOutlined style={{ color: '#52c41a' }} />}
|
||||
suffix="台"
|
||||
/>
|
||||
<div className="stat-trend">
|
||||
<span className="stat-detail">在线率: 83.9%</span>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} sm={12} lg={6}>
|
||||
<Card className="stat-card">
|
||||
<Statistic
|
||||
title="注册用户"
|
||||
value={1856}
|
||||
prefix={<TeamOutlined style={{ color: '#faad14' }} />}
|
||||
suffix="人"
|
||||
/>
|
||||
<div className="stat-trend">
|
||||
<span className="stat-detail">今日新增: 23人</span>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} sm={12} lg={6}>
|
||||
<Card className="stat-card">
|
||||
<Statistic
|
||||
title="活跃用户"
|
||||
value={1247}
|
||||
prefix={<UserOutlined style={{ color: '#b8178d' }} />}
|
||||
suffix="人"
|
||||
/>
|
||||
<div className="stat-trend">
|
||||
<span className="stat-detail">活跃率: 67.2%</span>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{/* 详细数据卡片 */}
|
||||
<Row gutter={[16, 16]}>
|
||||
{/* 近期报错信息 */}
|
||||
<Col xs={24} lg={16}>
|
||||
<Card
|
||||
title={
|
||||
<span>
|
||||
<WarningOutlined style={{ marginRight: 8, color: '#ff4d4f' }} />
|
||||
近期报错信息
|
||||
</span>
|
||||
}
|
||||
extra={<a href="#">查看全部</a>}
|
||||
>
|
||||
<Table
|
||||
columns={errorColumns}
|
||||
dataSource={errorData}
|
||||
pagination={false}
|
||||
size="small"
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
{/* 快速访问入口 */}
|
||||
<Col xs={24} lg={8}>
|
||||
<Card
|
||||
title={
|
||||
<span>
|
||||
<LinkOutlined style={{ marginRight: 8 }} />
|
||||
快速访问
|
||||
</span>
|
||||
}
|
||||
extra={<a href="#">更多</a>}
|
||||
>
|
||||
<div className="quick-links">
|
||||
<div className="quick-link-item" style={{ cursor: 'pointer' }}>
|
||||
<div className="link-icon" style={{ background: '#e6f4ff' }}>
|
||||
<DesktopOutlined style={{ color: '#1677ff' }} />
|
||||
</div>
|
||||
<div className="link-info">
|
||||
<div className="link-title">主机管理</div>
|
||||
<div className="link-desc">查看和管理接入主机</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="quick-link-item" style={{ cursor: 'pointer' }}>
|
||||
<div className="link-icon" style={{ background: '#f6ffed' }}>
|
||||
<TeamOutlined style={{ color: '#52c41a' }} />
|
||||
</div>
|
||||
<div className="link-info">
|
||||
<div className="link-title">用户管理</div>
|
||||
<div className="link-desc">管理系统用户信息</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="quick-link-item" style={{ cursor: 'pointer' }}>
|
||||
<div className="link-icon" style={{ background: '#fff7e6' }}>
|
||||
<ClockCircleOutlined style={{ color: '#faad14' }} />
|
||||
</div>
|
||||
<div className="link-info">
|
||||
<div className="link-title">系统日志</div>
|
||||
<div className="link-desc">查看系统操作日志</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="quick-link-item" style={{ cursor: 'pointer' }}>
|
||||
<div className="link-icon" style={{ background: '#fff0f6' }}>
|
||||
<WarningOutlined style={{ color: '#eb2f96' }} />
|
||||
</div>
|
||||
<div className="link-info">
|
||||
<div className="link-title">告警中心</div>
|
||||
<div className="link-desc">查看系统告警信息</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{/* 图表区域 - 移除或保留作为终端统计 */}
|
||||
<Card title="终端接入趋势" className="mt-4">
|
||||
<div className="chart-placeholder">
|
||||
<p className="text-gray-400 text-center py-12">
|
||||
图表区域(可集成 ECharts、Recharts 等图表库展示终端接入趋势)
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default OverviewPage
|
||||
|
|
@ -0,0 +1,455 @@
|
|||
.user-list-page {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 统计面板 */
|
||||
.stats-panel {
|
||||
margin-bottom: 16px;
|
||||
animation: slideDown 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
max-height: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
max-height: 500px;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 统计卡片 */
|
||||
.stat-card-small {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.stat-card-small:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.stat-card-small :global(.ant-statistic-title) {
|
||||
font-size: 13px;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
}
|
||||
|
||||
.stat-card-small :global(.ant-statistic-content) {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 卡片激活状态 */
|
||||
.stat-card-active {
|
||||
border: 2px solid #1677ff;
|
||||
background: linear-gradient(135deg, #e6f4ff 0%, #f0f5ff 100%);
|
||||
box-shadow: 0 4px 16px rgba(22, 119, 255, 0.2);
|
||||
}
|
||||
|
||||
.stat-card-active:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(22, 119, 255, 0.3);
|
||||
}
|
||||
|
||||
/* 卡片变暗状态 */
|
||||
.stat-card-dimmed {
|
||||
opacity: 0.5;
|
||||
filter: grayscale(0.3);
|
||||
}
|
||||
|
||||
.stat-card-dimmed:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* 筛选结果卡片 */
|
||||
.stat-card-result {
|
||||
cursor: default !important;
|
||||
}
|
||||
|
||||
.stat-card-result:hover {
|
||||
transform: none !important;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06) !important;
|
||||
}
|
||||
|
||||
/* 操作栏 */
|
||||
.action-bar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.action-bar-left,
|
||||
.action-bar-right {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 搜索和筛选组合 */
|
||||
.action-bar-right :global(.ant-space-compact) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.action-bar-right :global(.ant-space-compact .ant-input-search) {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.action-bar-right :global(.ant-space-compact > .ant-btn) {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
/* 筛选弹出框内容 */
|
||||
.filter-popover-content {
|
||||
width: 320px;
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.filter-popover-content .selected-filters {
|
||||
min-height: 40px;
|
||||
padding: 12px;
|
||||
background: #f5f7fa;
|
||||
border-radius: 6px;
|
||||
border: 1px dashed #d9d9d9;
|
||||
}
|
||||
|
||||
.filter-popover-content .filter-tag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.filter-popover-content .filter-label {
|
||||
font-size: 13px;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.filter-popover-content .filter-placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 24px;
|
||||
}
|
||||
|
||||
.filter-popover-content .group-tree-container {
|
||||
max-height: 280px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.filter-popover-content .tree-header {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 12px;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
}
|
||||
|
||||
.filter-popover-content .filter-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* 弹出框样式 */
|
||||
:global(.filter-popover .ant-popover-inner) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
:global(.filter-popover .ant-popover-title) {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: #ffffff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
:global(.filter-popover .ant-popover-inner-content) {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
/* 表格容器 */
|
||||
.table-container {
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.user-name-text {
|
||||
font-weight: 500;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
}
|
||||
|
||||
.user-list-page :global(.ant-table-row) {
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.user-list-page :global(.ant-table-row:hover) {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.user-list-page :global(.ant-table-row.row-selected) {
|
||||
background: #e6f4ff;
|
||||
}
|
||||
|
||||
/* 操作列样式优化 - 重新设计 */
|
||||
.user-list-page :global(.ant-table-thead > tr > th:last-child) {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
|
||||
color: #ffffff !important;
|
||||
font-weight: 600;
|
||||
border-left: 2px solid #e8e8e8;
|
||||
}
|
||||
|
||||
.user-list-page :global(.ant-table-tbody > tr > td:last-child) {
|
||||
background: #f8f9ff !important;
|
||||
border-left: 2px solid #e8e8e8;
|
||||
box-shadow: -2px 0 4px rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
.user-list-page :global(.ant-table-tbody > tr:hover > td:last-child) {
|
||||
background: #eef0ff !important;
|
||||
}
|
||||
|
||||
.user-list-page :global(.ant-table-tbody > tr.row-selected > td:last-child) {
|
||||
background: #e1e6ff !important;
|
||||
}
|
||||
|
||||
/* 详情抽屉 */
|
||||
.detail-drawer-content {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 顶部信息区域 - 固定不滚动 */
|
||||
.detail-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 24px;
|
||||
background: #fafafa;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.detail-header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
font-size: 18px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.close-button:hover {
|
||||
color: #1677ff;
|
||||
}
|
||||
|
||||
.header-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.user-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
}
|
||||
|
||||
.detail-header-right {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* 可滚动内容区域 */
|
||||
.detail-scrollable-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* 用户信息面板 */
|
||||
.detail-info-panel {
|
||||
padding: 24px;
|
||||
background: #ffffff;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 操作按钮区 */
|
||||
.detail-actions {
|
||||
margin-top: 20px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
/* 标签页区域 */
|
||||
.detail-tabs {
|
||||
background: #ffffff;
|
||||
padding-top: 16px;
|
||||
padding-left: 12px;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.detail-tabs :global(.ant-tabs) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.detail-tabs :global(.ant-tabs-content-holder) {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.detail-tabs :global(.ant-tabs-nav) {
|
||||
padding: 0;
|
||||
margin: 0 24px;
|
||||
margin-bottom: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.detail-tabs :global(.ant-tabs-nav::before) {
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.detail-tabs :global(.ant-tabs-tab) {
|
||||
padding: 12px 0;
|
||||
margin: 0 32px 0 0;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.detail-tabs :global(.ant-tabs-tab:first-child) {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.detail-tabs :global(.ant-tabs-tab-active .ant-tabs-tab-btn) {
|
||||
color: #d946ef;
|
||||
}
|
||||
|
||||
.detail-tabs :global(.ant-tabs-ink-bar) {
|
||||
background: #d946ef;
|
||||
height: 3px;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
padding: 24px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
/* 卡片列表 */
|
||||
.card-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
/* 终端卡片 */
|
||||
.terminal-card {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.terminal-card:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
/* 镜像卡片 */
|
||||
.image-card {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.image-card:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card-header h4 {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card-info-item {
|
||||
display: flex;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.card-label {
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.card-value {
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 1200px) {
|
||||
.stats-panel :global(.ant-row) {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.action-bar {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.action-bar-left,
|
||||
.action-bar-right {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.stats-panel :global(.ant-row) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,817 @@
|
|||
import { useState } from 'react'
|
||||
import {
|
||||
Table,
|
||||
Input,
|
||||
Button,
|
||||
Tag,
|
||||
Badge,
|
||||
Drawer,
|
||||
Tree,
|
||||
Dropdown,
|
||||
Space,
|
||||
Form,
|
||||
Select,
|
||||
Divider,
|
||||
Card,
|
||||
Row,
|
||||
Col,
|
||||
Statistic,
|
||||
Popover,
|
||||
Switch,
|
||||
Tabs,
|
||||
} from 'antd'
|
||||
import {
|
||||
UserOutlined,
|
||||
SearchOutlined,
|
||||
FilterOutlined,
|
||||
PlusOutlined,
|
||||
ReloadOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
MoreOutlined,
|
||||
CloseOutlined,
|
||||
CheckCircleOutlined,
|
||||
CloseCircleOutlined,
|
||||
LockOutlined,
|
||||
DesktopOutlined,
|
||||
DatabaseOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import PageTitleBar from '../components/PageTitleBar/PageTitleBar'
|
||||
import ConfirmDialog from '../components/ConfirmDialog/ConfirmDialog'
|
||||
import Toast from '../components/Toast/Toast'
|
||||
import userData from '../data/userData.json'
|
||||
import './UserListPage.css'
|
||||
|
||||
const { Search } = Input
|
||||
|
||||
function UserListPage() {
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState([])
|
||||
const [selectedUser, setSelectedUser] = useState(null)
|
||||
const [showFilterPopover, setShowFilterPopover] = useState(false)
|
||||
const [showDetailDrawer, setShowDetailDrawer] = useState(false)
|
||||
const [showEditDrawer, setShowEditDrawer] = useState(false)
|
||||
const [editMode, setEditMode] = useState('add')
|
||||
const [searchKeyword, setSearchKeyword] = useState('')
|
||||
const [selectedGroup, setSelectedGroup] = useState(null)
|
||||
const [selectedGroupName, setSelectedGroupName] = useState('')
|
||||
const [tempSelectedGroup, setTempSelectedGroup] = useState(null)
|
||||
const [filteredUsers, setFilteredUsers] = useState(userData.users)
|
||||
const [showStatsPanel, setShowStatsPanel] = useState(true)
|
||||
const [statusFilter, setStatusFilter] = useState(null)
|
||||
|
||||
// 表格列定义
|
||||
const columns = [
|
||||
{
|
||||
title: '序号',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
width: 80,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '用户类型',
|
||||
dataIndex: 'userType',
|
||||
key: 'userType',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '用户名',
|
||||
dataIndex: 'userName',
|
||||
key: 'userName',
|
||||
width: 150,
|
||||
render: (text) => (
|
||||
<Space>
|
||||
<UserOutlined style={{ color: '#1677ff' }} />
|
||||
<span className="user-name-text">{text}</span>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '姓名',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '用户分组',
|
||||
dataIndex: 'group',
|
||||
key: 'group',
|
||||
width: 150,
|
||||
render: (text) => <Tag color="blue">{text}</Tag>,
|
||||
},
|
||||
{
|
||||
title: '启停用',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
render: (status) => (
|
||||
<Switch checked={status === 'enabled'} checkedChildren="启用" unCheckedChildren="停用" />
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '授权终端',
|
||||
dataIndex: 'grantedTerminals',
|
||||
key: 'grantedTerminals',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '授权镜像',
|
||||
dataIndex: 'grantedImages',
|
||||
key: 'grantedImages',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 200,
|
||||
fixed: 'right',
|
||||
render: (_, record) => (
|
||||
<Space size="small" onClick={(e) => e.stopPropagation()}>
|
||||
<Button type="link" size="small" icon={<EditOutlined />} onClick={() => handleEditUser(record)}>
|
||||
编辑
|
||||
</Button>
|
||||
<Button type="link" size="small" icon={<LockOutlined />}>
|
||||
重置密码
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<DeleteOutlined />}
|
||||
danger
|
||||
onClick={() => handleDeleteUser(record)}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
// 行选择配置
|
||||
const rowSelection = {
|
||||
selectedRowKeys,
|
||||
onChange: (newSelectedRowKeys) => {
|
||||
setSelectedRowKeys(newSelectedRowKeys)
|
||||
},
|
||||
}
|
||||
|
||||
// 处理搜索
|
||||
const handleSearch = (value) => {
|
||||
setSearchKeyword(value)
|
||||
filterUsers(value, selectedGroup, statusFilter)
|
||||
}
|
||||
|
||||
// 处理分组选择
|
||||
const handleGroupSelect = (selectedKeys, info) => {
|
||||
const groupId = selectedKeys[0]
|
||||
setTempSelectedGroup(groupId)
|
||||
}
|
||||
|
||||
// 确认筛选
|
||||
const handleConfirmFilter = () => {
|
||||
setSelectedGroup(tempSelectedGroup)
|
||||
const findGroupName = (nodes, id) => {
|
||||
for (const node of nodes) {
|
||||
if (node.id === id) return node.name
|
||||
if (node.children) {
|
||||
const found = findGroupName(node.children, id)
|
||||
if (found) return found
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
const groupName = tempSelectedGroup
|
||||
? findGroupName(userData.userGroups, tempSelectedGroup)
|
||||
: ''
|
||||
setSelectedGroupName(groupName)
|
||||
filterUsers(searchKeyword, tempSelectedGroup, statusFilter)
|
||||
setShowFilterPopover(false)
|
||||
}
|
||||
|
||||
// 清除筛选
|
||||
const handleClearFilter = () => {
|
||||
setTempSelectedGroup(null)
|
||||
setSelectedGroup(null)
|
||||
setSelectedGroupName('')
|
||||
filterUsers(searchKeyword, null, statusFilter)
|
||||
setShowFilterPopover(false)
|
||||
}
|
||||
|
||||
// 过滤用户
|
||||
const filterUsers = (keyword, groupId, status = statusFilter) => {
|
||||
let filtered = userData.users
|
||||
|
||||
if (keyword) {
|
||||
filtered = filtered.filter(
|
||||
(user) =>
|
||||
user.userName.toLowerCase().includes(keyword.toLowerCase()) ||
|
||||
user.name.toLowerCase().includes(keyword.toLowerCase())
|
||||
)
|
||||
}
|
||||
|
||||
if (groupId) {
|
||||
filtered = filtered.filter((user) => user.groupId === groupId)
|
||||
}
|
||||
|
||||
if (status) {
|
||||
filtered = filtered.filter((user) => user.status === status)
|
||||
}
|
||||
|
||||
setFilteredUsers(filtered)
|
||||
}
|
||||
|
||||
// 处理状态筛选
|
||||
const handleStatusFilterClick = (status) => {
|
||||
const newStatusFilter = statusFilter === status ? null : status
|
||||
setStatusFilter(newStatusFilter)
|
||||
filterUsers(searchKeyword, selectedGroup, newStatusFilter)
|
||||
}
|
||||
|
||||
// 处理总数点击
|
||||
const handleTotalClick = () => {
|
||||
if (statusFilter !== null) {
|
||||
setStatusFilter(null)
|
||||
filterUsers(searchKeyword, selectedGroup, null)
|
||||
}
|
||||
}
|
||||
|
||||
// 删除用户
|
||||
const handleDeleteUser = (record) => {
|
||||
ConfirmDialog.delete({
|
||||
itemName: `用户名:${record.userName}`,
|
||||
itemInfo: `姓名:${record.name}`,
|
||||
onOk() {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
const newUsers = filteredUsers.filter((u) => u.id !== record.id)
|
||||
setFilteredUsers(newUsers)
|
||||
resolve()
|
||||
Toast.success('删除成功', `用户 "${record.userName}" 已成功删除`)
|
||||
}, 1000)
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 批量删除
|
||||
const handleBatchDelete = () => {
|
||||
const selectedUsers = filteredUsers.filter((u) => selectedRowKeys.includes(u.id))
|
||||
const items = selectedUsers.map((user) => ({
|
||||
name: user.userName,
|
||||
info: user.name,
|
||||
}))
|
||||
|
||||
ConfirmDialog.batchDelete({
|
||||
count: selectedRowKeys.length,
|
||||
items,
|
||||
onOk() {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
const count = selectedRowKeys.length
|
||||
const newUsers = filteredUsers.filter((u) => !selectedRowKeys.includes(u.id))
|
||||
setFilteredUsers(newUsers)
|
||||
setSelectedRowKeys([])
|
||||
resolve()
|
||||
Toast.success('批量删除成功', `已成功删除 ${count} 个用户`)
|
||||
}, 1000)
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 处理行点击
|
||||
const handleRowClick = (record) => {
|
||||
setSelectedUser(record)
|
||||
setShowEditDrawer(false) // 关闭编辑抽屉
|
||||
setShowDetailDrawer(true)
|
||||
}
|
||||
|
||||
// 处理编辑用户
|
||||
const handleEditUser = (record) => {
|
||||
setSelectedUser(record)
|
||||
setEditMode('edit')
|
||||
setShowDetailDrawer(false) // 关闭详情抽屉
|
||||
setShowEditDrawer(true)
|
||||
}
|
||||
|
||||
// 转换树数据
|
||||
const convertTreeData = (nodes) => {
|
||||
return nodes.map((node) => ({
|
||||
title: node.name,
|
||||
key: node.id,
|
||||
children: node.children ? convertTreeData(node.children) : undefined,
|
||||
}))
|
||||
}
|
||||
|
||||
const treeData = convertTreeData(userData.userGroups)
|
||||
|
||||
// 高级筛选面板内容
|
||||
const filterContent = (
|
||||
<div className="filter-popover-content">
|
||||
<div className="selected-filters">
|
||||
{tempSelectedGroup ? (
|
||||
<div className="filter-tag">
|
||||
<span className="filter-label">已选择分组:</span>
|
||||
<Tag color="blue" closable onClose={() => setTempSelectedGroup(null)}>
|
||||
{(() => {
|
||||
const findName = (nodes, id) => {
|
||||
for (const node of nodes) {
|
||||
if (node.id === id) return node.name
|
||||
if (node.children) {
|
||||
const found = findName(node.children, id)
|
||||
if (found) return found
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
return findName(userData.userGroups, tempSelectedGroup)
|
||||
})()}
|
||||
</Tag>
|
||||
</div>
|
||||
) : (
|
||||
<div className="filter-placeholder">
|
||||
<span style={{ color: '#8c8c8c', fontSize: '13px' }}>请选择用户分组进行筛选</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Divider style={{ margin: '12px 0' }} />
|
||||
<div className="group-tree-container">
|
||||
<div className="tree-header">用户分组</div>
|
||||
<Tree
|
||||
treeData={treeData}
|
||||
defaultExpandAll
|
||||
onSelect={handleGroupSelect}
|
||||
selectedKeys={tempSelectedGroup ? [tempSelectedGroup] : []}
|
||||
/>
|
||||
</div>
|
||||
<Divider style={{ margin: '12px 0' }} />
|
||||
<div className="filter-actions">
|
||||
<Space>
|
||||
<Button size="small" onClick={handleClearFilter}>
|
||||
清除筛选
|
||||
</Button>
|
||||
<Button size="small" type="primary" onClick={handleConfirmFilter}>
|
||||
确定
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="user-list-page">
|
||||
{/* 页面标题栏 */}
|
||||
<PageTitleBar
|
||||
title="用户列表"
|
||||
description="管理系统用户,包括用户信息、权限和授权管理"
|
||||
showToggle={true}
|
||||
onToggle={(expanded) => setShowStatsPanel(expanded)}
|
||||
/>
|
||||
|
||||
{/* 数据统计面板 */}
|
||||
{showStatsPanel && (
|
||||
<div className="stats-panel">
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col xs={24} sm={12} lg={6}>
|
||||
<Card
|
||||
className={`stat-card-small ${statusFilter === null ? '' : 'stat-card-dimmed'}`}
|
||||
hoverable
|
||||
onClick={handleTotalClick}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
<Statistic
|
||||
title="总用户数"
|
||||
value={userData.users.length}
|
||||
prefix={<UserOutlined />}
|
||||
valueStyle={{ color: '#1677ff' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} lg={6}>
|
||||
<Card
|
||||
className={`stat-card-small ${statusFilter === 'enabled' ? 'stat-card-active' : statusFilter !== null ? 'stat-card-dimmed' : ''}`}
|
||||
hoverable
|
||||
onClick={() => handleStatusFilterClick('enabled')}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
<Statistic
|
||||
title="启用用户"
|
||||
value={userData.users.filter((u) => u.status === 'enabled').length}
|
||||
prefix={<CheckCircleOutlined />}
|
||||
valueStyle={{ color: '#52c41a' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} lg={6}>
|
||||
<Card
|
||||
className={`stat-card-small ${statusFilter === 'disabled' ? 'stat-card-active' : statusFilter !== null ? 'stat-card-dimmed' : ''}`}
|
||||
hoverable
|
||||
onClick={() => handleStatusFilterClick('disabled')}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
<Statistic
|
||||
title="停用用户"
|
||||
value={userData.users.filter((u) => u.status === 'disabled').length}
|
||||
prefix={<CloseCircleOutlined />}
|
||||
valueStyle={{ color: '#8c8c8c' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} lg={6}>
|
||||
<Card className="stat-card-small stat-card-result">
|
||||
<Statistic
|
||||
title="筛选结果"
|
||||
value={filteredUsers.length}
|
||||
prefix={<SearchOutlined />}
|
||||
valueStyle={{ color: '#faad14' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 操作栏 */}
|
||||
<div className="action-bar">
|
||||
<div className="action-bar-left">
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => {
|
||||
setEditMode('add')
|
||||
setSelectedUser(null)
|
||||
setShowDetailDrawer(false) // 关闭详情抽屉
|
||||
setShowEditDrawer(true)
|
||||
}}
|
||||
>
|
||||
新增用户
|
||||
</Button>
|
||||
<Button icon={<DeleteOutlined />} disabled={selectedRowKeys.length === 0} danger onClick={handleBatchDelete}>
|
||||
批量删除
|
||||
</Button>
|
||||
</div>
|
||||
<div className="action-bar-right">
|
||||
<Space.Compact>
|
||||
<Search
|
||||
placeholder="搜索用户名或姓名"
|
||||
allowClear
|
||||
style={{ width: 280 }}
|
||||
onSearch={handleSearch}
|
||||
onChange={(e) => handleSearch(e.target.value)}
|
||||
value={searchKeyword}
|
||||
/>
|
||||
<Popover
|
||||
content={filterContent}
|
||||
title={
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<FilterOutlined />
|
||||
<span>高级筛选</span>
|
||||
</div>
|
||||
}
|
||||
trigger="click"
|
||||
open={showFilterPopover}
|
||||
onOpenChange={(visible) => {
|
||||
setShowFilterPopover(visible)
|
||||
if (visible) {
|
||||
setTempSelectedGroup(selectedGroup)
|
||||
}
|
||||
}}
|
||||
placement="bottomRight"
|
||||
overlayClassName="filter-popover"
|
||||
>
|
||||
<Button icon={<FilterOutlined />} type={selectedGroup ? 'primary' : 'default'}>
|
||||
{selectedGroupName || '筛选'}
|
||||
</Button>
|
||||
</Popover>
|
||||
</Space.Compact>
|
||||
<Button icon={<ReloadOutlined />}>刷新</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 数据表格 */}
|
||||
<div className="table-container">
|
||||
<Table
|
||||
rowSelection={rowSelection}
|
||||
columns={columns}
|
||||
dataSource={filteredUsers}
|
||||
rowKey="id"
|
||||
pagination={{
|
||||
total: filteredUsers.length,
|
||||
pageSize: 10,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
}}
|
||||
scroll={{ x: 1400 }}
|
||||
onRow={(record) => ({
|
||||
onClick: () => handleRowClick(record),
|
||||
className: selectedUser?.id === record.id ? 'row-selected' : '',
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 详情抽屉 */}
|
||||
<Drawer
|
||||
title={null}
|
||||
placement="right"
|
||||
width={1080}
|
||||
onClose={() => setShowDetailDrawer(false)}
|
||||
open={showDetailDrawer}
|
||||
closable={false}
|
||||
styles={{ body: { padding: 0 } }}
|
||||
>
|
||||
{selectedUser && (
|
||||
<div className="detail-drawer-content">
|
||||
{/* 顶部标题栏 - 固定不滚动 */}
|
||||
<div className="detail-header">
|
||||
<div className="detail-header-left">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<CloseOutlined />}
|
||||
onClick={() => setShowDetailDrawer(false)}
|
||||
className="close-button"
|
||||
/>
|
||||
<div className="header-info">
|
||||
<h2 className="user-title">{selectedUser.userName}</h2>
|
||||
<Tag color={selectedUser.status === 'enabled' ? 'green' : 'default'}>
|
||||
{selectedUser.status === 'enabled' ? '启用' : '停用'}
|
||||
</Tag>
|
||||
</div>
|
||||
</div>
|
||||
<div className="detail-header-right">
|
||||
<Space size="middle">
|
||||
<Button
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => {
|
||||
setEditMode('edit')
|
||||
setShowEditDrawer(true)
|
||||
setShowDetailDrawer(false)
|
||||
}}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Button
|
||||
icon={<DeleteOutlined />}
|
||||
danger
|
||||
onClick={() => {
|
||||
setShowDetailDrawer(false)
|
||||
handleDeleteUser(selectedUser)
|
||||
}}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 可滚动内容区域 */}
|
||||
<div className="detail-scrollable-content">
|
||||
{/* 用户信息面板 */}
|
||||
<div className="detail-info-panel">
|
||||
<Row gutter={[24, 16]}>
|
||||
<Col span={6}>
|
||||
<div className="info-item">
|
||||
<div className="info-label">用户名:</div>
|
||||
<div className="info-value">{selectedUser.userName}</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<div className="info-item">
|
||||
<div className="info-label">用户分组:</div>
|
||||
<div className="info-value">{selectedUser.group}</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<div className="info-item">
|
||||
<div className="info-label">姓名:</div>
|
||||
<div className="info-value">{selectedUser.name}</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<div className="info-item">
|
||||
<div className="info-label">授权镜像:</div>
|
||||
<div className="info-value">{selectedUser.grantedImages}</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<div className="info-item">
|
||||
<div className="info-label">用户类型:</div>
|
||||
<div className="info-value">{selectedUser.userType}</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<div className="info-item">
|
||||
<div className="info-label">授权终端:</div>
|
||||
<div className="info-value">{selectedUser.grantedTerminals}</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<div className="info-item">
|
||||
<div className="info-label">启停用:</div>
|
||||
<div className="info-value">
|
||||
<Tag color={selectedUser.status === 'enabled' ? 'green' : 'default'}>
|
||||
{selectedUser.status === 'enabled' ? '启用' : '停用'}
|
||||
</Tag>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={18}>
|
||||
<div className="info-item">
|
||||
<div className="info-label">描述:</div>
|
||||
<div className="info-value">--</div>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{/* 操作按钮区 */}
|
||||
<div className="detail-actions">
|
||||
<Space size="middle">
|
||||
<Button type="primary">转移分组</Button>
|
||||
<Button>加入黑名单</Button>
|
||||
<Button>重置密码</Button>
|
||||
<Button icon={<LockOutlined />}>停用</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 标签页区域 */}
|
||||
<div className="detail-tabs">
|
||||
<Tabs
|
||||
defaultActiveKey="terminals"
|
||||
type="line"
|
||||
size="large"
|
||||
items={[
|
||||
{
|
||||
key: 'terminals',
|
||||
label: (
|
||||
<span>
|
||||
<DesktopOutlined style={{ marginRight: 8 }} />
|
||||
授权终端
|
||||
</span>
|
||||
),
|
||||
children: (
|
||||
<div className="tab-content">
|
||||
<div className="card-list">
|
||||
{selectedUser.terminals && selectedUser.terminals.length > 0 ? (
|
||||
selectedUser.terminals.map((terminal, index) => (
|
||||
<Card key={terminal.id} className="terminal-card">
|
||||
<div className="card-header">
|
||||
<h4>
|
||||
<DesktopOutlined style={{ marginRight: 8, color: '#1677ff' }} />
|
||||
{terminal.name}
|
||||
</h4>
|
||||
<Badge
|
||||
status={terminal.status === '在线' ? 'success' : 'default'}
|
||||
text={terminal.status}
|
||||
/>
|
||||
</div>
|
||||
<Divider style={{ margin: '12px 0' }} />
|
||||
<Row gutter={[16, 12]}>
|
||||
<Col span={12}>
|
||||
<div className="card-info-item">
|
||||
<span className="card-label">序号:</span>
|
||||
<span className="card-value">{index + 1}</span>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<div className="card-info-item">
|
||||
<span className="card-label">终端分组:</span>
|
||||
<span className="card-value">{terminal.group}</span>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<div className="card-info-item">
|
||||
<span className="card-label">MAC地址:</span>
|
||||
<span className="card-value">{terminal.mac}</span>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<div className="card-info-item">
|
||||
<span className="card-label">IP地址:</span>
|
||||
<span className="card-value">{terminal.ip}</span>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<div className="card-info-item">
|
||||
<span className="card-label">授权镜像:</span>
|
||||
<span className="card-value">{terminal.grantedImages}</span>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<div className="card-info-item">
|
||||
<span className="card-label">数据盘容量:</span>
|
||||
<span className="card-value">{terminal.dataDisk}</span>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<div className="card-info-item">
|
||||
<span className="card-label">系统盘容量:</span>
|
||||
<span className="card-value">{terminal.systemDisk}</span>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
<Divider style={{ margin: '12px 0' }} />
|
||||
<Space size="small">
|
||||
<Button type="link" size="small" icon={<EditOutlined />}>
|
||||
编辑
|
||||
</Button>
|
||||
<Button type="link" size="small" danger icon={<DeleteOutlined />}>
|
||||
移除授权
|
||||
</Button>
|
||||
</Space>
|
||||
</Card>
|
||||
))
|
||||
) : (
|
||||
<p style={{ color: '#8c8c8c', padding: '24px', textAlign: 'center' }}>
|
||||
该用户暂无授权终端
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'images',
|
||||
label: (
|
||||
<span>
|
||||
<DatabaseOutlined style={{ marginRight: 8 }} />
|
||||
授权镜像
|
||||
</span>
|
||||
),
|
||||
children: (
|
||||
<div className="tab-content">
|
||||
<div className="card-list">
|
||||
{selectedUser.images && selectedUser.images.length > 0 ? (
|
||||
selectedUser.images.map((image) => (
|
||||
<Card key={image.id} className="image-card">
|
||||
<div className="card-header">
|
||||
<h4>
|
||||
<DatabaseOutlined style={{ marginRight: 8, color: '#52c41a' }} />
|
||||
{image.name}
|
||||
</h4>
|
||||
<Tag color={image.status === '已下发' ? 'success' : 'default'}>
|
||||
{image.status}
|
||||
</Tag>
|
||||
</div>
|
||||
<Divider style={{ margin: '12px 0' }} />
|
||||
<Row gutter={[16, 12]}>
|
||||
<Col span={12}>
|
||||
<div className="card-info-item">
|
||||
<span className="card-label">系统镜像:</span>
|
||||
<span className="card-value">{image.system}</span>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<div className="card-info-item">
|
||||
<span className="card-label">操作系统:</span>
|
||||
<span className="card-value">{image.os}</span>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<div className="card-info-item">
|
||||
<span className="card-label">创建时间:</span>
|
||||
<span className="card-value">{image.createTime}</span>
|
||||
</div>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<div className="card-info-item">
|
||||
<span className="card-label">下发方式:</span>
|
||||
<span className="card-value">{image.method}</span>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
<Divider style={{ margin: '12px 0' }} />
|
||||
<Space size="small">
|
||||
<Button type="link" size="small" icon={<EditOutlined />}>
|
||||
编辑
|
||||
</Button>
|
||||
<Button type="link" size="small" danger icon={<DeleteOutlined />}>
|
||||
移除授权
|
||||
</Button>
|
||||
</Space>
|
||||
</Card>
|
||||
))
|
||||
) : (
|
||||
<p style={{ color: '#8c8c8c', padding: '24px', textAlign: 'center' }}>
|
||||
该用户暂无授权镜像
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Drawer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default UserListPage
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* 全局样式变量 */
|
||||
:root {
|
||||
/* 主色调 */
|
||||
--primary-color: #1677ff;
|
||||
|
||||
/* 文本颜色 */
|
||||
--text-primary: rgba(0, 0, 0, 0.88);
|
||||
--text-secondary: rgba(0, 0, 0, 0.65);
|
||||
--text-tertiary: rgba(0, 0, 0, 0.45);
|
||||
--text-disabled: rgba(0, 0, 0, 0.25);
|
||||
|
||||
/* 背景颜色 */
|
||||
--bg-primary: #ffffff;
|
||||
--bg-secondary: #fafafa;
|
||||
--bg-tertiary: #f5f5f5;
|
||||
|
||||
/* 边框颜色 */
|
||||
--border-primary: #d9d9d9;
|
||||
--border-secondary: #f0f0f0;
|
||||
|
||||
/* 阴影 */
|
||||
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.03), 0 1px 6px -1px rgba(0, 0, 0, 0.02), 0 2px 4px 0 rgba(0, 0, 0, 0.02);
|
||||
--shadow-md: 0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 9px 28px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
/* 基础样式重置 */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.5714;
|
||||
color: var(--text-primary);
|
||||
background-color: var(--bg-secondary);
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #c1c1c1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #a8a8a8;
|
||||
}
|
||||
|
||||
/* 工具类扩展 */
|
||||
.content-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.card-shadow {
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: {
|
||||
50: '#fce7f6',
|
||||
100: '#f5bae6',
|
||||
200: '#ee8dd6',
|
||||
300: '#e760c6',
|
||||
400: '#e033b6',
|
||||
500: '#b8178d', // 品牌主色 - 匹配 NEX LOGO
|
||||
600: '#9c1477',
|
||||
700: '#801161',
|
||||
800: '#640d4b',
|
||||
900: '#480a35',
|
||||
},
|
||||
// 保留原蓝色作为辅助色
|
||||
blue: {
|
||||
50: '#e6f4ff',
|
||||
100: '#bae0ff',
|
||||
200: '#91caff',
|
||||
300: '#69b1ff',
|
||||
400: '#4096ff',
|
||||
500: '#1677ff',
|
||||
600: '#0958d9',
|
||||
700: '#003eb3',
|
||||
800: '#002c8c',
|
||||
900: '#001d66',
|
||||
},
|
||||
},
|
||||
fontFamily: {
|
||||
sans: [
|
||||
'-apple-system',
|
||||
'BlinkMacSystemFont',
|
||||
'Segoe UI',
|
||||
'Roboto',
|
||||
'Helvetica Neue',
|
||||
'Arial',
|
||||
'Noto Sans',
|
||||
'sans-serif',
|
||||
],
|
||||
mono: [
|
||||
'SF Mono',
|
||||
'Monaco',
|
||||
'Inconsolata',
|
||||
'Fira Code',
|
||||
'Fira Mono',
|
||||
'Droid Sans Mono',
|
||||
'Source Code Pro',
|
||||
'monospace',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
// 与 Ant Design 兼容
|
||||
corePlugins: {
|
||||
preflight: false,
|
||||
},
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import path from 'path'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
port: 5173,
|
||||
open: true,
|
||||
},
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
sourcemap: false,
|
||||
},
|
||||
})
|
||||
Loading…
Reference in New Issue