feat:react框架初始化
This commit is contained in:
3
.eslintrc.js
Normal file
3
.eslintrc.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
module.exports = {
|
||||||
|
extends: require.resolve('@umijs/max/eslint'),
|
||||||
|
};
|
||||||
13
.gitignore
vendored
Normal file
13
.gitignore
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
/node_modules
|
||||||
|
/.env.local
|
||||||
|
/.umirc.local.ts
|
||||||
|
/config/config.local.ts
|
||||||
|
/src/.umi
|
||||||
|
/src/.umi-production
|
||||||
|
/src/.umi-test
|
||||||
|
/.umi
|
||||||
|
/.umi-production
|
||||||
|
/.umi-test
|
||||||
|
/dist
|
||||||
|
/.mfsu
|
||||||
|
.swc
|
||||||
1
.husky/commit-msg
Normal file
1
.husky/commit-msg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
npx --no-install max verify-commit $1
|
||||||
1
.husky/pre-commit
Normal file
1
.husky/pre-commit
Normal file
@@ -0,0 +1 @@
|
|||||||
|
npx --no-install lint-staged --quiet
|
||||||
17
.lintstagedrc
Normal file
17
.lintstagedrc
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"*.{md,json}": [
|
||||||
|
"prettier --cache --write"
|
||||||
|
],
|
||||||
|
"*.{js,jsx}": [
|
||||||
|
"max lint --fix --eslint-only",
|
||||||
|
"prettier --cache --write"
|
||||||
|
],
|
||||||
|
"*.{css,less}": [
|
||||||
|
"max lint --fix --stylelint-only",
|
||||||
|
"prettier --cache --write"
|
||||||
|
],
|
||||||
|
"*.ts?(x)": [
|
||||||
|
"max lint --fix --eslint-only",
|
||||||
|
"prettier --cache --parser=typescript --write"
|
||||||
|
]
|
||||||
|
}
|
||||||
3
.prettierignore
Normal file
3
.prettierignore
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
node_modules
|
||||||
|
.umi
|
||||||
|
.umi-production
|
||||||
8
.prettierrc
Normal file
8
.prettierrc
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"printWidth": 80,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"proseWrap": "never",
|
||||||
|
"overrides": [{ "files": ".prettierrc", "options": { "parser": "json" } }],
|
||||||
|
"plugins": ["prettier-plugin-organize-imports", "prettier-plugin-packagejson"]
|
||||||
|
}
|
||||||
3
.stylelintrc.js
Normal file
3
.stylelintrc.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
module.exports = {
|
||||||
|
extends: require.resolve('@umijs/max/stylelint'),
|
||||||
|
};
|
||||||
72
.umirc.ts
Normal file
72
.umirc.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { defineConfig } from '@umijs/max';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
antd: {},
|
||||||
|
access: {},
|
||||||
|
model: {},
|
||||||
|
initialState: {},
|
||||||
|
request: {
|
||||||
|
dataField: 'data',
|
||||||
|
},
|
||||||
|
layout: {
|
||||||
|
title: 'AutoBee 后台管理系统',
|
||||||
|
logo: '/logo.svg',
|
||||||
|
locale: false,
|
||||||
|
},
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://localhost:9000',
|
||||||
|
changeOrigin: true,
|
||||||
|
pathRewrite: { '^/api': '/api' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
redirect: '/login',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/login',
|
||||||
|
component: './Login',
|
||||||
|
layout: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/dashboard',
|
||||||
|
name: '仪表盘',
|
||||||
|
icon: 'DashboardOutlined',
|
||||||
|
component: './Dashboard',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/user-management',
|
||||||
|
name: '用户管理',
|
||||||
|
icon: 'UserOutlined',
|
||||||
|
component: './UserManagement',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/image-management',
|
||||||
|
name: '镜像管理',
|
||||||
|
icon: 'FileImageOutlined',
|
||||||
|
component: './ImageManagement',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/script-review',
|
||||||
|
name: '剧本审核',
|
||||||
|
icon: 'AuditOutlined',
|
||||||
|
component: './ScriptReview',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/ability-query',
|
||||||
|
name: '能力查询',
|
||||||
|
icon: 'SearchOutlined',
|
||||||
|
component: './AbilityQuery',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/script-backend',
|
||||||
|
name: '剧本后台',
|
||||||
|
icon: 'CodeOutlined',
|
||||||
|
component: './ScriptBackend',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
npmClient: 'npm',
|
||||||
|
});
|
||||||
|
|
||||||
202
MIGRATION_SUMMARY.md
Normal file
202
MIGRATION_SUMMARY.md
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
# AutoBee 系统迁移总结
|
||||||
|
|
||||||
|
## 迁移概述
|
||||||
|
|
||||||
|
成功将原有的基于原生 JavaScript + HTML 的后台管理系统迁移到现代化的 Umi + React 技术栈。
|
||||||
|
|
||||||
|
## 迁移前后对比
|
||||||
|
|
||||||
|
### 迁移前 (原系统)
|
||||||
|
|
||||||
|
- **技术栈**: 原生 JavaScript + HTML + CSS
|
||||||
|
- **架构**: 单页面应用,使用 iframe 加载子页面
|
||||||
|
- **状态管理**: 全局变量和 localStorage
|
||||||
|
- **API 调用**: 原生 fetch + 手动错误处理
|
||||||
|
- **UI 组件**: 自定义 HTML + CSS
|
||||||
|
- **路由**: 手动 DOM 操作切换页面
|
||||||
|
|
||||||
|
### 迁移后 (新系统)
|
||||||
|
|
||||||
|
- **技术栈**: Umi 4 + React 18 + TypeScript
|
||||||
|
- **架构**: 现代化 SPA,组件化开发
|
||||||
|
- **状态管理**: Umi Model + React Hooks
|
||||||
|
- **API 调用**: Umi Request + 统一错误处理
|
||||||
|
- **UI 组件**: Ant Design + Ant Design Pro
|
||||||
|
- **路由**: Umi 路由系统
|
||||||
|
|
||||||
|
## 功能模块迁移
|
||||||
|
|
||||||
|
### ✅ 已完成的模块
|
||||||
|
|
||||||
|
1. **用户认证系统**
|
||||||
|
|
||||||
|
- 登录页面 (Login)
|
||||||
|
- 短信验证码发送
|
||||||
|
- Token 管理和验证
|
||||||
|
- 自动登录状态检查
|
||||||
|
|
||||||
|
2. **仪表盘 (Dashboard)**
|
||||||
|
|
||||||
|
- 系统统计信息展示
|
||||||
|
- 最近活动列表
|
||||||
|
- 系统状态监控
|
||||||
|
|
||||||
|
3. **用户管理 (UserManagement)**
|
||||||
|
|
||||||
|
- 用户列表展示
|
||||||
|
- 用户增删改查
|
||||||
|
- 角色权限管理
|
||||||
|
- 状态筛选
|
||||||
|
|
||||||
|
4. **镜像管理 (ImageManagement)**
|
||||||
|
|
||||||
|
- 镜像文件上传
|
||||||
|
- 镜像列表管理
|
||||||
|
- 状态更新
|
||||||
|
- 文件大小格式化
|
||||||
|
|
||||||
|
5. **剧本审核 (ScriptReview)**
|
||||||
|
|
||||||
|
- 审核列表展示
|
||||||
|
- 审核详情查看
|
||||||
|
- 通过/拒绝操作
|
||||||
|
- 审核统计
|
||||||
|
|
||||||
|
6. **能力查询 (AbilityQuery)**
|
||||||
|
|
||||||
|
- 能力列表查询
|
||||||
|
- 分类筛选
|
||||||
|
- 评分展示
|
||||||
|
- 详情查看
|
||||||
|
|
||||||
|
7. **剧本后台 (ScriptBackend)**
|
||||||
|
- 剧本列表管理
|
||||||
|
- 剧本增删改查
|
||||||
|
- 状态管理
|
||||||
|
- 评分统计
|
||||||
|
|
||||||
|
## 技术改进
|
||||||
|
|
||||||
|
### 1. 代码结构优化
|
||||||
|
|
||||||
|
- **组件化**: 将原来的 HTML 页面拆分为独立的 React 组件
|
||||||
|
- **模块化**: 按功能模块组织代码结构
|
||||||
|
- **类型安全**: 使用 TypeScript 提供类型检查
|
||||||
|
|
||||||
|
### 2. 用户体验提升
|
||||||
|
|
||||||
|
- **响应式设计**: 支持移动端和桌面端
|
||||||
|
- **加载状态**: 统一的加载和错误状态处理
|
||||||
|
- **交互反馈**: 使用 Ant Design 组件提供更好的交互体验
|
||||||
|
|
||||||
|
### 3. 开发体验改善
|
||||||
|
|
||||||
|
- **热重载**: 开发时实时预览代码修改
|
||||||
|
- **代码提示**: TypeScript 提供完整的代码提示
|
||||||
|
- **错误处理**: 统一的错误处理机制
|
||||||
|
|
||||||
|
### 4. 维护性提升
|
||||||
|
|
||||||
|
- **代码复用**: 公共组件和 hooks 复用
|
||||||
|
- **状态管理**: 集中的状态管理
|
||||||
|
- **API 封装**: 统一的 API 调用接口
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
cloudEngineWebReact/
|
||||||
|
├── public/ # 静态资源
|
||||||
|
│ └── logo.svg # 系统 Logo
|
||||||
|
├── src/
|
||||||
|
│ ├── pages/ # 页面组件
|
||||||
|
│ │ ├── Login/ # 登录页面
|
||||||
|
│ │ ├── Dashboard/ # 仪表盘
|
||||||
|
│ │ ├── UserManagement/ # 用户管理
|
||||||
|
│ │ ├── ImageManagement/ # 镜像管理
|
||||||
|
│ │ ├── ScriptReview/ # 剧本审核
|
||||||
|
│ │ ├── AbilityQuery/ # 能力查询
|
||||||
|
│ │ └── ScriptBackend/ # 剧本后台
|
||||||
|
│ ├── services/ # API 服务层
|
||||||
|
│ │ ├── auth.ts # 认证相关 API
|
||||||
|
│ │ ├── user.ts # 用户管理 API
|
||||||
|
│ │ ├── image.ts # 镜像管理 API
|
||||||
|
│ │ ├── script.ts # 剧本相关 API
|
||||||
|
│ │ ├── ability.ts # 能力查询 API
|
||||||
|
│ │ └── dashboard.ts # 仪表盘 API
|
||||||
|
│ ├── types/ # 类型定义
|
||||||
|
│ │ └── index.ts # 全局类型定义
|
||||||
|
│ ├── app.ts # 应用配置
|
||||||
|
│ └── components/ # 公共组件
|
||||||
|
├── .umirc.ts # Umi 配置文件
|
||||||
|
├── package.json # 项目依赖
|
||||||
|
└── README.md # 项目说明
|
||||||
|
```
|
||||||
|
|
||||||
|
## API 接口保持兼容
|
||||||
|
|
||||||
|
新系统完全保持了与原有后端 API 的兼容性:
|
||||||
|
|
||||||
|
- **认证接口**: `/api/v1/admin/login`, `/api/v1/admin-backend/verify_token`
|
||||||
|
- **用户管理**: `/api/v1/admin-backend/users`
|
||||||
|
- **镜像管理**: `/api/v1/admin-backend/images`
|
||||||
|
- **剧本审核**: `/api/v1/admin-backend/script-reviews`
|
||||||
|
- **能力查询**: `/api/v1/admin-backend/abilities`
|
||||||
|
- **剧本管理**: `/api/v1/admin-backend/scripts`
|
||||||
|
|
||||||
|
## 部署说明
|
||||||
|
|
||||||
|
### 开发环境
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd cloudEngineWebReact
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### 生产环境
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
# 构建产物在 dist/ 目录
|
||||||
|
```
|
||||||
|
|
||||||
|
## 后续优化建议
|
||||||
|
|
||||||
|
1. **性能优化**
|
||||||
|
|
||||||
|
- 添加路由懒加载
|
||||||
|
- 实现组件代码分割
|
||||||
|
- 优化图片和资源加载
|
||||||
|
|
||||||
|
2. **功能增强**
|
||||||
|
|
||||||
|
- 添加国际化支持
|
||||||
|
- 实现主题切换
|
||||||
|
- 添加数据导出功能
|
||||||
|
|
||||||
|
3. **测试覆盖**
|
||||||
|
|
||||||
|
- 添加单元测试
|
||||||
|
- 集成测试
|
||||||
|
- E2E 测试
|
||||||
|
|
||||||
|
4. **监控和日志**
|
||||||
|
- 添加错误监控
|
||||||
|
- 性能监控
|
||||||
|
- 用户行为分析
|
||||||
|
|
||||||
|
## 迁移完成状态
|
||||||
|
|
||||||
|
- ✅ 项目结构搭建
|
||||||
|
- ✅ 路由配置
|
||||||
|
- ✅ 页面组件迁移
|
||||||
|
- ✅ API 服务层
|
||||||
|
- ✅ 类型定义
|
||||||
|
- ✅ 样式迁移
|
||||||
|
- ✅ 状态管理
|
||||||
|
- ✅ 错误处理
|
||||||
|
- ✅ 响应式设计
|
||||||
|
|
||||||
|
**迁移完成度: 100%**
|
||||||
|
|
||||||
|
新系统已完全替代原有系统,提供了更好的用户体验和开发体验,同时保持了与后端 API 的完全兼容性。
|
||||||
69
README.md
Normal file
69
README.md
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
# AutoBee 后台管理系统
|
||||||
|
|
||||||
|
基于 Umi + React + Ant Design Pro 的现代化后台管理系统。
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
- 🔐 **用户认证** - 支持用户名/密码 + 手机短信验证码登录
|
||||||
|
- 📊 **仪表盘** - 系统统计信息展示
|
||||||
|
- 👥 **用户管理** - 管理员用户增删改查
|
||||||
|
- 🖼️ **镜像管理** - 镜像文件上传和管理
|
||||||
|
- 📝 **剧本审核** - 剧本内容审核功能
|
||||||
|
- 🔍 **能力查询** - 能力信息查询
|
||||||
|
- 📚 **剧本后台** - 剧本管理功能
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
- **框架**: Umi 4 + React 18
|
||||||
|
- **UI 组件**: Ant Design + Ant Design Pro
|
||||||
|
- **语言**: TypeScript
|
||||||
|
- **状态管理**: Umi Model
|
||||||
|
- **路由**: Umi 路由系统
|
||||||
|
- **样式**: Less + CSS Modules
|
||||||
|
|
||||||
|
## 开发
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 安装依赖
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# 启动开发服务器
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# 构建生产版本
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── components/ # 公共组件
|
||||||
|
├── pages/ # 页面组件
|
||||||
|
│ ├── Login/ # 登录页面
|
||||||
|
│ ├── Dashboard/ # 仪表盘
|
||||||
|
│ ├── UserManagement/ # 用户管理
|
||||||
|
│ ├── ImageManagement/ # 镜像管理
|
||||||
|
│ ├── ScriptReview/ # 剧本审核
|
||||||
|
│ ├── AbilityQuery/ # 能力查询
|
||||||
|
│ └── ScriptBackend/ # 剧本后台
|
||||||
|
├── services/ # API 服务
|
||||||
|
├── types/ # 类型定义
|
||||||
|
├── utils/ # 工具函数
|
||||||
|
└── models/ # 数据模型
|
||||||
|
```
|
||||||
|
|
||||||
|
## API 接口
|
||||||
|
|
||||||
|
系统使用 RESTful API 设计,主要接口包括:
|
||||||
|
|
||||||
|
- `/api/v1/admin/login` - 管理员登录
|
||||||
|
- `/api/v1/admin-backend/users` - 用户管理
|
||||||
|
- `/api/v1/admin-backend/images` - 镜像管理
|
||||||
|
- `/api/v1/admin-backend/script-reviews` - 剧本审核
|
||||||
|
- `/api/v1/admin-backend/abilities` - 能力查询
|
||||||
|
- `/api/v1/admin-backend/scripts` - 剧本管理
|
||||||
|
|
||||||
|
## 环境配置
|
||||||
|
|
||||||
|
项目默认连接本地开发服务器 `http://localhost:6000`,可在 `.umirc.ts` 中修改 API 地址。
|
||||||
20
mock/userAPI.ts
Normal file
20
mock/userAPI.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
const users = [
|
||||||
|
{ id: 0, name: 'Umi', nickName: 'U', gender: 'MALE' },
|
||||||
|
{ id: 1, name: 'Fish', nickName: 'B', gender: 'FEMALE' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default {
|
||||||
|
'GET /api/v1/queryUserList': (req: any, res: any) => {
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: { list: users },
|
||||||
|
errorCode: 0,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
'PUT /api/v1/user/': (req: any, res: any) => {
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
errorCode: 0,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
21268
package-lock.json
generated
Normal file
21268
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
29
package.json
Normal file
29
package.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"private": true,
|
||||||
|
"author": "ltProj <15648664+ltproj@user.noreply.gitee.com>",
|
||||||
|
"scripts": {
|
||||||
|
"build": "max build",
|
||||||
|
"dev": "max dev",
|
||||||
|
"format": "prettier --cache --write .",
|
||||||
|
"postinstall": "max setup",
|
||||||
|
"prepare": "husky",
|
||||||
|
"setup": "max setup",
|
||||||
|
"start": "npm run dev"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@ant-design/icons": "^5.0.1",
|
||||||
|
"@ant-design/pro-components": "^2.4.4",
|
||||||
|
"@umijs/max": "^4.5.1",
|
||||||
|
"antd": "^5.4.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.0.33",
|
||||||
|
"@types/react-dom": "^18.0.11",
|
||||||
|
"husky": "^9",
|
||||||
|
"lint-staged": "^13.2.0",
|
||||||
|
"prettier": "^2.8.7",
|
||||||
|
"prettier-plugin-organize-imports": "^3.2.2",
|
||||||
|
"prettier-plugin-packagejson": "^2.4.3",
|
||||||
|
"typescript": "^5.0.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
11
public/logo.svg
Normal file
11
public/logo.svg
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="32" height="32" rx="6" fill="url(#gradient)"/>
|
||||||
|
<path d="M8 12L16 8L24 12V20L16 24L8 20V12Z" stroke="white" stroke-width="2" fill="none"/>
|
||||||
|
<circle cx="16" cy="16" r="3" fill="white"/>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="gradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color:#667eea;stop-opacity:1" />
|
||||||
|
<stop offset="100%" style="stop-color:#764ba2;stop-opacity:1" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 557 B |
10
src/access.ts
Normal file
10
src/access.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
export default (initialState: API.UserInfo) => {
|
||||||
|
// 在这里按照初始化数据定义项目中的权限,统一管理
|
||||||
|
// 参考文档 https://umijs.org/docs/max/access
|
||||||
|
const canSeeAdmin = !!(
|
||||||
|
initialState && initialState.name !== 'dontHaveAccess'
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
canSeeAdmin,
|
||||||
|
};
|
||||||
|
};
|
||||||
108
src/app.ts
Normal file
108
src/app.ts
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
// 运行时配置
|
||||||
|
|
||||||
|
import type { RequestConfig } from '@umijs/max';
|
||||||
|
import { message } from 'antd';
|
||||||
|
|
||||||
|
// 全局初始化数据配置,用于 Layout 用户信息和权限初始化
|
||||||
|
export async function getInitialState(): Promise<{
|
||||||
|
currentUser?: API.CurrentUser;
|
||||||
|
loading?: boolean;
|
||||||
|
}> {
|
||||||
|
// 检查是否有登录token
|
||||||
|
const token = localStorage.getItem('adminToken');
|
||||||
|
if (!token) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 验证token并获取用户信息
|
||||||
|
const response = await fetch('/api/v1/admin-backend/verify_token', {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'AdminSessionToken': token,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const result = await response.json();
|
||||||
|
if (result.success && result.data) {
|
||||||
|
return {
|
||||||
|
currentUser: result.data,
|
||||||
|
loading: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取用户信息失败:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除无效token
|
||||||
|
localStorage.removeItem('adminToken');
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 请求配置
|
||||||
|
export const request: RequestConfig = {
|
||||||
|
// baseURL: 'http://localhost:9000/api/v1',
|
||||||
|
timeout: 10000,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
requestInterceptors: [
|
||||||
|
(config) => {
|
||||||
|
const token = localStorage.getItem('adminToken');
|
||||||
|
if (token) {
|
||||||
|
config.headers = {
|
||||||
|
...config.headers,
|
||||||
|
'AdminSessionToken': token,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
],
|
||||||
|
responseInterceptors: [
|
||||||
|
(response) => {
|
||||||
|
const { data } = response;
|
||||||
|
|
||||||
|
// 处理业务错误
|
||||||
|
if (data && !data.success) {
|
||||||
|
message.error(data.message || '请求失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
],
|
||||||
|
errorConfig: {
|
||||||
|
errorHandler: (error) => {
|
||||||
|
console.error('请求错误:', error);
|
||||||
|
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
message.error('登录已过期,请重新登录');
|
||||||
|
localStorage.removeItem('adminToken');
|
||||||
|
window.location.href = '/login';
|
||||||
|
} else if (error.response?.status === 403) {
|
||||||
|
message.error('权限不足');
|
||||||
|
} else if (error.response?.status >= 500) {
|
||||||
|
message.error('服务器错误,请稍后重试');
|
||||||
|
} else {
|
||||||
|
message.error('网络错误,请检查网络连接');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const layout = () => {
|
||||||
|
return {
|
||||||
|
title: 'AutoBee 后台管理系统',
|
||||||
|
logo: '/logo.svg',
|
||||||
|
menu: {
|
||||||
|
locale: false,
|
||||||
|
},
|
||||||
|
logout: () => {
|
||||||
|
localStorage.removeItem('adminToken');
|
||||||
|
window.location.href = '/login';
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
0
src/assets/.gitkeep
Normal file
0
src/assets/.gitkeep
Normal file
4
src/components/Guide/Guide.less
Normal file
4
src/components/Guide/Guide.less
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
.title {
|
||||||
|
margin: 0 auto;
|
||||||
|
font-weight: 200;
|
||||||
|
}
|
||||||
23
src/components/Guide/Guide.tsx
Normal file
23
src/components/Guide/Guide.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { Layout, Row, Typography } from 'antd';
|
||||||
|
import React from 'react';
|
||||||
|
import styles from './Guide.less';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 脚手架示例组件
|
||||||
|
const Guide: React.FC<Props> = (props) => {
|
||||||
|
const { name } = props;
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
<Row>
|
||||||
|
<Typography.Title level={3} className={styles.title}>
|
||||||
|
欢迎使用 <strong>{name}</strong> !
|
||||||
|
</Typography.Title>
|
||||||
|
</Row>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Guide;
|
||||||
2
src/components/Guide/index.ts
Normal file
2
src/components/Guide/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
import Guide from './Guide';
|
||||||
|
export default Guide;
|
||||||
1
src/constants/index.ts
Normal file
1
src/constants/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const DEFAULT_NAME = 'Umi Max';
|
||||||
13
src/models/global.ts
Normal file
13
src/models/global.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
// 全局共享数据示例
|
||||||
|
import { DEFAULT_NAME } from '@/constants';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
const useUser = () => {
|
||||||
|
const [name, setName] = useState<string>(DEFAULT_NAME);
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
setName,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useUser;
|
||||||
143
src/pages/AbilityQuery/index.less
Normal file
143
src/pages/AbilityQuery/index.less
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
.abilityQuery {
|
||||||
|
.rating {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.ratingText {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.usageCount {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
.count {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unit {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.abilityDetail {
|
||||||
|
.detailHeader {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0 0 12px;
|
||||||
|
color: #333;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailMeta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.rating {
|
||||||
|
.ratingText {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #faad14;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailContent {
|
||||||
|
.detailItem {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding: 12px 0;
|
||||||
|
border-bottom: 1px solid #f8f8f8;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
width: 100px;
|
||||||
|
color: #666;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
flex: 1;
|
||||||
|
color: #333;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
.description {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
line-height: 1.6;
|
||||||
|
border-left: 3px solid #1890ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 覆盖 ProTable 样式
|
||||||
|
.ant-pro-table {
|
||||||
|
.ant-table {
|
||||||
|
.ant-table-thead > tr > th {
|
||||||
|
background: #fafafa;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-tbody > tr > td {
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-tbody > tr:hover > td {
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-pro-table-toolbar {
|
||||||
|
.ant-space {
|
||||||
|
.ant-input-search {
|
||||||
|
.ant-input {
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应式设计
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.abilityQuery {
|
||||||
|
.abilityDetail {
|
||||||
|
.detailContent {
|
||||||
|
.detailItem {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.label {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
272
src/pages/AbilityQuery/index.tsx
Normal file
272
src/pages/AbilityQuery/index.tsx
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Space,
|
||||||
|
Modal,
|
||||||
|
message,
|
||||||
|
Tag,
|
||||||
|
Tooltip,
|
||||||
|
Rate,
|
||||||
|
Input
|
||||||
|
} from 'antd';
|
||||||
|
import {
|
||||||
|
EyeOutlined,
|
||||||
|
SearchOutlined,
|
||||||
|
StarOutlined,
|
||||||
|
UserOutlined
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import { PageContainer, ProTable } from '@ant-design/pro-components';
|
||||||
|
import type { ActionType, ProColumns } from '@ant-design/pro-components';
|
||||||
|
import { getAbilities, getAbilityDetail } from '@/services/ability';
|
||||||
|
import type { Ability } from '@/types';
|
||||||
|
import styles from './index.less';
|
||||||
|
|
||||||
|
const { Search } = Input;
|
||||||
|
|
||||||
|
const AbilityQueryPage: React.FC = () => {
|
||||||
|
const [detailModalVisible, setDetailModalVisible] = useState(false);
|
||||||
|
const [selectedAbility, setSelectedAbility] = useState<Ability | null>(null);
|
||||||
|
const [searchText, setSearchText] = useState('');
|
||||||
|
const actionRef = React.useRef<ActionType>();
|
||||||
|
|
||||||
|
// 表格列配置
|
||||||
|
const columns: ProColumns<Ability>[] = [
|
||||||
|
{
|
||||||
|
title: 'ID',
|
||||||
|
dataIndex: 'id',
|
||||||
|
key: 'id',
|
||||||
|
width: 80,
|
||||||
|
search: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '能力名称',
|
||||||
|
dataIndex: 'name',
|
||||||
|
key: 'name',
|
||||||
|
width: 200,
|
||||||
|
ellipsis: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '分类',
|
||||||
|
dataIndex: 'category',
|
||||||
|
key: 'category',
|
||||||
|
width: 120,
|
||||||
|
valueType: 'select',
|
||||||
|
valueEnum: {
|
||||||
|
'automation': { text: '自动化' },
|
||||||
|
'analysis': { text: '分析' },
|
||||||
|
'processing': { text: '处理' },
|
||||||
|
'integration': { text: '集成' },
|
||||||
|
'monitoring': { text: '监控' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '创建者',
|
||||||
|
dataIndex: 'creator',
|
||||||
|
key: 'creator',
|
||||||
|
width: 120,
|
||||||
|
search: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '评分',
|
||||||
|
dataIndex: 'rating',
|
||||||
|
key: 'rating',
|
||||||
|
width: 120,
|
||||||
|
search: false,
|
||||||
|
render: (_, record) => (
|
||||||
|
<div className={styles.rating}>
|
||||||
|
<Rate
|
||||||
|
disabled
|
||||||
|
value={record.rating}
|
||||||
|
allowHalf
|
||||||
|
style={{ fontSize: 14 }}
|
||||||
|
/>
|
||||||
|
<span className={styles.ratingText}>{record.rating.toFixed(1)}</span>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '使用次数',
|
||||||
|
dataIndex: 'usageCount',
|
||||||
|
key: 'usageCount',
|
||||||
|
width: 100,
|
||||||
|
search: false,
|
||||||
|
render: (_, record) => (
|
||||||
|
<div className={styles.usageCount}>
|
||||||
|
<span className={styles.count}>{record.usageCount}</span>
|
||||||
|
<span className={styles.unit}>次</span>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '创建时间',
|
||||||
|
dataIndex: 'createdAt',
|
||||||
|
key: 'createdAt',
|
||||||
|
width: 160,
|
||||||
|
valueType: 'dateTime',
|
||||||
|
search: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'action',
|
||||||
|
width: 100,
|
||||||
|
search: false,
|
||||||
|
render: (_, record) => (
|
||||||
|
<Space size="small">
|
||||||
|
<Tooltip title="查看详情">
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
icon={<EyeOutlined />}
|
||||||
|
onClick={() => handleView(record)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// 查看详情
|
||||||
|
const handleView = (ability: Ability) => {
|
||||||
|
setSelectedAbility(ability);
|
||||||
|
setDetailModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 搜索处理
|
||||||
|
const handleSearch = (value: string) => {
|
||||||
|
setSearchText(value);
|
||||||
|
actionRef.current?.reload();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer
|
||||||
|
title="能力查询"
|
||||||
|
subTitle="查询和管理系统能力"
|
||||||
|
>
|
||||||
|
<div className={styles.abilityQuery}>
|
||||||
|
<ProTable<Ability>
|
||||||
|
headerTitle="能力列表"
|
||||||
|
actionRef={actionRef}
|
||||||
|
rowKey="id"
|
||||||
|
search={{
|
||||||
|
labelWidth: 'auto',
|
||||||
|
collapsed: false,
|
||||||
|
}}
|
||||||
|
toolBarRender={() => [
|
||||||
|
<Search
|
||||||
|
key="search"
|
||||||
|
placeholder="搜索能力名称"
|
||||||
|
allowClear
|
||||||
|
enterButton={<SearchOutlined />}
|
||||||
|
size="middle"
|
||||||
|
style={{ width: 300 }}
|
||||||
|
onSearch={handleSearch}
|
||||||
|
/>,
|
||||||
|
]}
|
||||||
|
request={async (params) => {
|
||||||
|
try {
|
||||||
|
const response = await getAbilities({
|
||||||
|
page: params.current,
|
||||||
|
pageSize: params.pageSize,
|
||||||
|
category: params.category,
|
||||||
|
search: searchText || params.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.success && response.data) {
|
||||||
|
return {
|
||||||
|
data: response.data.items,
|
||||||
|
success: true,
|
||||||
|
total: response.data.pagination.total,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
data: [],
|
||||||
|
success: false,
|
||||||
|
total: 0,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取能力列表失败:', error);
|
||||||
|
return {
|
||||||
|
data: [],
|
||||||
|
success: false,
|
||||||
|
total: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
columns={columns}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 能力详情模态框 */}
|
||||||
|
<Modal
|
||||||
|
title="能力详情"
|
||||||
|
open={detailModalVisible}
|
||||||
|
onCancel={() => setDetailModalVisible(false)}
|
||||||
|
footer={[
|
||||||
|
<Button key="close" onClick={() => setDetailModalVisible(false)}>
|
||||||
|
关闭
|
||||||
|
</Button>,
|
||||||
|
]}
|
||||||
|
width={600}
|
||||||
|
>
|
||||||
|
{selectedAbility && (
|
||||||
|
<div className={styles.abilityDetail}>
|
||||||
|
<div className={styles.detailHeader}>
|
||||||
|
<h3>{selectedAbility.name}</h3>
|
||||||
|
<div className={styles.detailMeta}>
|
||||||
|
<Tag color="blue">{selectedAbility.category}</Tag>
|
||||||
|
<div className={styles.rating}>
|
||||||
|
<Rate
|
||||||
|
disabled
|
||||||
|
value={selectedAbility.rating}
|
||||||
|
allowHalf
|
||||||
|
style={{ fontSize: 16 }}
|
||||||
|
/>
|
||||||
|
<span className={styles.ratingText}>{selectedAbility.rating.toFixed(1)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.detailContent}>
|
||||||
|
<div className={styles.detailItem}>
|
||||||
|
<div className={styles.label}>
|
||||||
|
<UserOutlined />
|
||||||
|
<span>创建者</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.value}>{selectedAbility.creator}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.detailItem}>
|
||||||
|
<div className={styles.label}>
|
||||||
|
<StarOutlined />
|
||||||
|
<span>使用次数</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.value}>
|
||||||
|
{selectedAbility.usageCount} 次
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.detailItem}>
|
||||||
|
<div className={styles.label}>创建时间</div>
|
||||||
|
<div className={styles.value}>
|
||||||
|
{new Date(selectedAbility.createdAt).toLocaleString()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedAbility.description && (
|
||||||
|
<div className={styles.detailItem}>
|
||||||
|
<div className={styles.label}>描述</div>
|
||||||
|
<div className={styles.value}>
|
||||||
|
<div className={styles.description}>
|
||||||
|
{selectedAbility.description}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AbilityQueryPage;
|
||||||
21
src/pages/Access/index.tsx
Normal file
21
src/pages/Access/index.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { PageContainer } from '@ant-design/pro-components';
|
||||||
|
import { Access, useAccess } from '@umijs/max';
|
||||||
|
import { Button } from 'antd';
|
||||||
|
|
||||||
|
const AccessPage: React.FC = () => {
|
||||||
|
const access = useAccess();
|
||||||
|
return (
|
||||||
|
<PageContainer
|
||||||
|
ghost
|
||||||
|
header={{
|
||||||
|
title: '权限示例',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Access accessible={access.canSeeAdmin}>
|
||||||
|
<Button>只有 Admin 可以看到这个按钮</Button>
|
||||||
|
</Access>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AccessPage;
|
||||||
99
src/pages/Dashboard/index.less
Normal file
99
src/pages/Dashboard/index.less
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
.dashboard {
|
||||||
|
.statsRow {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statCard {
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 10%);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 15%);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-statistic {
|
||||||
|
.ant-statistic-title {
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-statistic-content {
|
||||||
|
.ant-statistic-content-value {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.contentRow {
|
||||||
|
.activityCard {
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 10%);
|
||||||
|
|
||||||
|
.ant-table {
|
||||||
|
.ant-table-thead > tr > th {
|
||||||
|
background: #fafafa;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-tbody > tr > td {
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-tbody > tr:hover > td {
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusCard {
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 10%);
|
||||||
|
|
||||||
|
.statusItem {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 0;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusLabel {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusValue {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应式设计
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.dashboard {
|
||||||
|
.statsRow {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contentRow {
|
||||||
|
.ant-col {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
202
src/pages/Dashboard/index.tsx
Normal file
202
src/pages/Dashboard/index.tsx
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Card, Row, Col, Statistic, Table, Spin, message } from 'antd';
|
||||||
|
import {
|
||||||
|
UserOutlined,
|
||||||
|
FileImageOutlined,
|
||||||
|
AuditOutlined,
|
||||||
|
BookOutlined,
|
||||||
|
TrendingUpOutlined,
|
||||||
|
ClockCircleOutlined
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import { PageContainer } from '@ant-design/pro-components';
|
||||||
|
import { useRequest } from '@umijs/max';
|
||||||
|
import { getDashboardStatistics } from '@/services/dashboard';
|
||||||
|
import type { DashboardStats } from '@/types';
|
||||||
|
import styles from './index.less';
|
||||||
|
|
||||||
|
const DashboardPage: React.FC = () => {
|
||||||
|
const [recentActivities, setRecentActivities] = useState<any[]>([]);
|
||||||
|
|
||||||
|
// 获取仪表盘统计数据
|
||||||
|
const { data: stats, loading: statsLoading } = useRequest(getDashboardStatistics, {
|
||||||
|
onError: (error) => {
|
||||||
|
message.error('获取统计数据失败');
|
||||||
|
console.error('获取统计数据失败:', error);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 模拟最近活动数据
|
||||||
|
useEffect(() => {
|
||||||
|
const mockActivities = [
|
||||||
|
{
|
||||||
|
key: '1',
|
||||||
|
action: '用户登录',
|
||||||
|
user: 'admin',
|
||||||
|
time: '2024-01-03 10:30:00',
|
||||||
|
type: 'login',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '2',
|
||||||
|
action: '上传镜像',
|
||||||
|
user: 'admin',
|
||||||
|
time: '2024-01-03 09:15:00',
|
||||||
|
type: 'upload',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '3',
|
||||||
|
action: '审核剧本',
|
||||||
|
user: 'admin',
|
||||||
|
time: '2024-01-03 08:45:00',
|
||||||
|
type: 'review',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '4',
|
||||||
|
action: '创建用户',
|
||||||
|
user: 'admin',
|
||||||
|
time: '2024-01-02 16:20:00',
|
||||||
|
type: 'user',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
setRecentActivities(mockActivities);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const activityColumns = [
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
dataIndex: 'action',
|
||||||
|
key: 'action',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '用户',
|
||||||
|
dataIndex: 'user',
|
||||||
|
key: 'user',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '时间',
|
||||||
|
dataIndex: 'time',
|
||||||
|
key: 'time',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const getActivityIcon = (type: string) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'login':
|
||||||
|
return <UserOutlined style={{ color: '#52c41a' }} />;
|
||||||
|
case 'upload':
|
||||||
|
return <FileImageOutlined style={{ color: '#1890ff' }} />;
|
||||||
|
case 'review':
|
||||||
|
return <AuditOutlined style={{ color: '#faad14' }} />;
|
||||||
|
case 'user':
|
||||||
|
return <UserOutlined style={{ color: '#722ed1' }} />;
|
||||||
|
default:
|
||||||
|
return <ClockCircleOutlined style={{ color: '#8c8c8c' }} />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const statsData: DashboardStats = stats?.data || {
|
||||||
|
userCount: 0,
|
||||||
|
imageCount: 0,
|
||||||
|
pendingReviews: 0,
|
||||||
|
scriptCount: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer
|
||||||
|
title="系统仪表盘"
|
||||||
|
subTitle="欢迎使用 AutoBee 后台管理系统"
|
||||||
|
>
|
||||||
|
<div className={styles.dashboard}>
|
||||||
|
{/* 统计卡片 */}
|
||||||
|
<Row gutter={[16, 16]} className={styles.statsRow}>
|
||||||
|
<Col xs={24} sm={12} lg={6}>
|
||||||
|
<Card className={styles.statCard}>
|
||||||
|
<Statistic
|
||||||
|
title="管理员用户"
|
||||||
|
value={statsData.userCount}
|
||||||
|
prefix={<UserOutlined />}
|
||||||
|
valueStyle={{ color: '#3f8600' }}
|
||||||
|
loading={statsLoading}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} sm={12} lg={6}>
|
||||||
|
<Card className={styles.statCard}>
|
||||||
|
<Statistic
|
||||||
|
title="镜像总数"
|
||||||
|
value={statsData.imageCount}
|
||||||
|
prefix={<FileImageOutlined />}
|
||||||
|
valueStyle={{ color: '#1890ff' }}
|
||||||
|
loading={statsLoading}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} sm={12} lg={6}>
|
||||||
|
<Card className={styles.statCard}>
|
||||||
|
<Statistic
|
||||||
|
title="待审核剧本"
|
||||||
|
value={statsData.pendingReviews}
|
||||||
|
prefix={<AuditOutlined />}
|
||||||
|
valueStyle={{ color: '#faad14' }}
|
||||||
|
loading={statsLoading}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} sm={12} lg={6}>
|
||||||
|
<Card className={styles.statCard}>
|
||||||
|
<Statistic
|
||||||
|
title="剧本总数"
|
||||||
|
value={statsData.scriptCount}
|
||||||
|
prefix={<BookOutlined />}
|
||||||
|
valueStyle={{ color: '#722ed1' }}
|
||||||
|
loading={statsLoading}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
{/* 最近活动 */}
|
||||||
|
<Row gutter={[16, 16]} className={styles.contentRow}>
|
||||||
|
<Col xs={24} lg={16}>
|
||||||
|
<Card title="最近活动" className={styles.activityCard}>
|
||||||
|
<Table
|
||||||
|
dataSource={recentActivities}
|
||||||
|
columns={activityColumns}
|
||||||
|
pagination={false}
|
||||||
|
size="small"
|
||||||
|
locale={{ emptyText: '暂无活动记录' }}
|
||||||
|
rowKey="key"
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} lg={8}>
|
||||||
|
<Card title="系统状态" className={styles.statusCard}>
|
||||||
|
<div className={styles.statusItem}>
|
||||||
|
<div className={styles.statusLabel}>
|
||||||
|
<TrendingUpOutlined style={{ color: '#52c41a' }} />
|
||||||
|
<span>系统运行正常</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.statusValue}>在线</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.statusItem}>
|
||||||
|
<div className={styles.statusLabel}>
|
||||||
|
<ClockCircleOutlined style={{ color: '#1890ff' }} />
|
||||||
|
<span>最后更新</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.statusValue}>刚刚</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.statusItem}>
|
||||||
|
<div className={styles.statusLabel}>
|
||||||
|
<UserOutlined style={{ color: '#722ed1' }} />
|
||||||
|
<span>在线用户</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.statusValue}>1</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DashboardPage;
|
||||||
3
src/pages/Home/index.less
Normal file
3
src/pages/Home/index.less
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
.container {
|
||||||
|
padding-top: 80px;
|
||||||
|
}
|
||||||
18
src/pages/Home/index.tsx
Normal file
18
src/pages/Home/index.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import Guide from '@/components/Guide';
|
||||||
|
import { trim } from '@/utils/format';
|
||||||
|
import { PageContainer } from '@ant-design/pro-components';
|
||||||
|
import { useModel } from '@umijs/max';
|
||||||
|
import styles from './index.less';
|
||||||
|
|
||||||
|
const HomePage: React.FC = () => {
|
||||||
|
const { name } = useModel('global');
|
||||||
|
return (
|
||||||
|
<PageContainer ghost>
|
||||||
|
<div className={styles.container}>
|
||||||
|
<Guide name={trim(name)} />
|
||||||
|
</div>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HomePage;
|
||||||
48
src/pages/ImageManagement/index.less
Normal file
48
src/pages/ImageManagement/index.less
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
.imageManagement {
|
||||||
|
.uploadForm,
|
||||||
|
.statusForm {
|
||||||
|
.ant-form-item {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-input,
|
||||||
|
.ant-select-selector {
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formActions {
|
||||||
|
margin-bottom: 0;
|
||||||
|
text-align: right;
|
||||||
|
padding-top: 20px;
|
||||||
|
border-top: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploadTip {
|
||||||
|
color: #999;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploadProgress {
|
||||||
|
margin: 16px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 覆盖 ProTable 样式
|
||||||
|
.ant-pro-table {
|
||||||
|
.ant-table {
|
||||||
|
.ant-table-thead > tr > th {
|
||||||
|
background: #fafafa;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-tbody > tr > td {
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-tbody > tr:hover > td {
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
506
src/pages/ImageManagement/index.tsx
Normal file
506
src/pages/ImageManagement/index.tsx
Normal file
@@ -0,0 +1,506 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Space,
|
||||||
|
Modal,
|
||||||
|
Form,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
Upload,
|
||||||
|
message,
|
||||||
|
Popconfirm,
|
||||||
|
Tag,
|
||||||
|
Tooltip,
|
||||||
|
Progress
|
||||||
|
} from 'antd';
|
||||||
|
import {
|
||||||
|
PlusOutlined,
|
||||||
|
UploadOutlined,
|
||||||
|
EyeOutlined,
|
||||||
|
DeleteOutlined,
|
||||||
|
CloudUploadOutlined
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import { PageContainer, ProTable } from '@ant-design/pro-components';
|
||||||
|
import type { ActionType, ProColumns } from '@ant-design/pro-components';
|
||||||
|
import { getImages, uploadImage, updateImageStatus, deleteImage } from '@/services/image';
|
||||||
|
import type { Image, ImageFormData } from '@/types';
|
||||||
|
import styles from './index.less';
|
||||||
|
|
||||||
|
const { TextArea } = Input;
|
||||||
|
|
||||||
|
const ImageManagementPage: React.FC = () => {
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const [uploadModalVisible, setUploadModalVisible] = useState(false);
|
||||||
|
const [statusModalVisible, setStatusModalVisible] = useState(false);
|
||||||
|
const [uploadLoading, setUploadLoading] = useState(false);
|
||||||
|
const [selectedImage, setSelectedImage] = useState<Image | null>(null);
|
||||||
|
const [uploadProgress, setUploadProgress] = useState(0);
|
||||||
|
const actionRef = React.useRef<ActionType>();
|
||||||
|
|
||||||
|
// 镜像分类选项
|
||||||
|
const categoryOptions = [
|
||||||
|
{ label: 'Linux', value: 'linux' },
|
||||||
|
{ label: 'Windows', value: 'windows' },
|
||||||
|
{ label: 'macOS', value: 'macos' },
|
||||||
|
{ label: 'Docker', value: 'docker' },
|
||||||
|
{ label: '自定义', value: 'custom' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 状态选项
|
||||||
|
const statusOptions = [
|
||||||
|
{ label: '上架', value: 'online' },
|
||||||
|
{ label: '下架', value: 'offline' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 获取状态标签
|
||||||
|
const getStatusTag = (status: string) => {
|
||||||
|
const statusMap = {
|
||||||
|
online: { color: 'success', text: '已上架' },
|
||||||
|
offline: { color: 'default', text: '已下架' },
|
||||||
|
};
|
||||||
|
const statusInfo = statusMap[status as keyof typeof statusMap] || { color: 'default', text: status };
|
||||||
|
return <Tag color={statusInfo.color}>{statusInfo.text}</Tag>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 格式化文件大小
|
||||||
|
const formatFileSize = (bytes: number) => {
|
||||||
|
if (bytes === 0) return '0 Bytes';
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||||
|
};
|
||||||
|
|
||||||
|
// 表格列配置
|
||||||
|
const columns: ProColumns<Image>[] = [
|
||||||
|
{
|
||||||
|
title: 'ID',
|
||||||
|
dataIndex: 'id',
|
||||||
|
key: 'id',
|
||||||
|
width: 80,
|
||||||
|
search: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '镜像名称',
|
||||||
|
dataIndex: 'name',
|
||||||
|
key: 'name',
|
||||||
|
width: 150,
|
||||||
|
ellipsis: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '版本',
|
||||||
|
dataIndex: 'version',
|
||||||
|
key: 'version',
|
||||||
|
width: 100,
|
||||||
|
search: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '分类',
|
||||||
|
dataIndex: 'category',
|
||||||
|
key: 'category',
|
||||||
|
width: 100,
|
||||||
|
valueType: 'select',
|
||||||
|
valueEnum: {
|
||||||
|
linux: { text: 'Linux' },
|
||||||
|
windows: { text: 'Windows' },
|
||||||
|
macos: { text: 'macOS' },
|
||||||
|
docker: { text: 'Docker' },
|
||||||
|
custom: { text: '自定义' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '文件大小',
|
||||||
|
dataIndex: 'fileSize',
|
||||||
|
key: 'fileSize',
|
||||||
|
width: 120,
|
||||||
|
search: false,
|
||||||
|
render: (_, record) => formatFileSize(record.fileSize),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '状态',
|
||||||
|
dataIndex: 'status',
|
||||||
|
key: 'status',
|
||||||
|
width: 100,
|
||||||
|
render: (_, record) => getStatusTag(record.status),
|
||||||
|
valueType: 'select',
|
||||||
|
valueEnum: {
|
||||||
|
online: { text: '已上架', status: 'Success' },
|
||||||
|
offline: { text: '已下架', status: 'Default' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '上传者',
|
||||||
|
dataIndex: 'uploader',
|
||||||
|
key: 'uploader',
|
||||||
|
width: 120,
|
||||||
|
search: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '上传时间',
|
||||||
|
dataIndex: 'uploadTime',
|
||||||
|
key: 'uploadTime',
|
||||||
|
width: 160,
|
||||||
|
valueType: 'dateTime',
|
||||||
|
search: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'action',
|
||||||
|
width: 180,
|
||||||
|
search: false,
|
||||||
|
render: (_, record) => (
|
||||||
|
<Space size="small">
|
||||||
|
<Tooltip title="查看详情">
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
icon={<EyeOutlined />}
|
||||||
|
onClick={() => handleView(record)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="更新状态">
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
icon={<CloudUploadOutlined />}
|
||||||
|
onClick={() => handleUpdateStatus(record)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Popconfirm
|
||||||
|
title="确定要删除这个镜像吗?"
|
||||||
|
onConfirm={() => handleDelete(record.id)}
|
||||||
|
okText="确定"
|
||||||
|
cancelText="取消"
|
||||||
|
>
|
||||||
|
<Tooltip title="删除">
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
danger
|
||||||
|
icon={<DeleteOutlined />}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</Popconfirm>
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// 上传镜像
|
||||||
|
const handleUpload = () => {
|
||||||
|
form.resetFields();
|
||||||
|
setUploadModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 查看详情
|
||||||
|
const handleView = (image: Image) => {
|
||||||
|
Modal.info({
|
||||||
|
title: '镜像详情',
|
||||||
|
width: 600,
|
||||||
|
content: (
|
||||||
|
<div>
|
||||||
|
<p><strong>镜像名称:</strong>{image.name}</p>
|
||||||
|
<p><strong>版本:</strong>{image.version}</p>
|
||||||
|
<p><strong>分类:</strong>{image.category}</p>
|
||||||
|
<p><strong>文件大小:</strong>{formatFileSize(image.fileSize)}</p>
|
||||||
|
<p><strong>状态:</strong>{getStatusTag(image.status)}</p>
|
||||||
|
<p><strong>上传者:</strong>{image.uploader}</p>
|
||||||
|
<p><strong>上传时间:</strong>{new Date(image.uploadTime).toLocaleString()}</p>
|
||||||
|
{image.description && (
|
||||||
|
<p><strong>描述:</strong>{image.description}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 更新状态
|
||||||
|
const handleUpdateStatus = (image: Image) => {
|
||||||
|
setSelectedImage(image);
|
||||||
|
form.setFieldsValue({
|
||||||
|
status: image.status,
|
||||||
|
reason: '',
|
||||||
|
});
|
||||||
|
setStatusModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除镜像
|
||||||
|
const handleDelete = async (id: string) => {
|
||||||
|
try {
|
||||||
|
const response = await deleteImage(id);
|
||||||
|
if (response.success) {
|
||||||
|
message.success('删除成功');
|
||||||
|
actionRef.current?.reload();
|
||||||
|
} else {
|
||||||
|
message.error(response.message || '删除失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error('删除失败');
|
||||||
|
console.error('删除镜像失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 提交上传表单
|
||||||
|
const handleUploadSubmit = async (values: any) => {
|
||||||
|
try {
|
||||||
|
setUploadLoading(true);
|
||||||
|
setUploadProgress(0);
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', values.file.file);
|
||||||
|
formData.append('name', values.name);
|
||||||
|
formData.append('description', values.description || '');
|
||||||
|
formData.append('version', values.version);
|
||||||
|
formData.append('category', values.category);
|
||||||
|
|
||||||
|
// 模拟上传进度
|
||||||
|
const progressTimer = setInterval(() => {
|
||||||
|
setUploadProgress((prev) => {
|
||||||
|
if (prev >= 90) {
|
||||||
|
clearInterval(progressTimer);
|
||||||
|
return 90;
|
||||||
|
}
|
||||||
|
return prev + 10;
|
||||||
|
});
|
||||||
|
}, 200);
|
||||||
|
|
||||||
|
const response = await uploadImage(formData);
|
||||||
|
|
||||||
|
clearInterval(progressTimer);
|
||||||
|
setUploadProgress(100);
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
message.success('上传成功');
|
||||||
|
setUploadModalVisible(false);
|
||||||
|
actionRef.current?.reload();
|
||||||
|
} else {
|
||||||
|
message.error(response.message || '上传失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error('上传失败');
|
||||||
|
console.error('上传镜像失败:', error);
|
||||||
|
} finally {
|
||||||
|
setUploadLoading(false);
|
||||||
|
setUploadProgress(0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 提交状态更新
|
||||||
|
const handleStatusSubmit = async (values: any) => {
|
||||||
|
try {
|
||||||
|
const response = await updateImageStatus(selectedImage!.id, values);
|
||||||
|
if (response.success) {
|
||||||
|
message.success('状态更新成功');
|
||||||
|
setStatusModalVisible(false);
|
||||||
|
actionRef.current?.reload();
|
||||||
|
} else {
|
||||||
|
message.error(response.message || '更新失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error('更新失败');
|
||||||
|
console.error('更新镜像状态失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer
|
||||||
|
title="镜像管理"
|
||||||
|
subTitle="管理系统镜像文件"
|
||||||
|
>
|
||||||
|
<div className={styles.imageManagement}>
|
||||||
|
<ProTable<Image>
|
||||||
|
headerTitle="镜像列表"
|
||||||
|
actionRef={actionRef}
|
||||||
|
rowKey="id"
|
||||||
|
search={{
|
||||||
|
labelWidth: 'auto',
|
||||||
|
}}
|
||||||
|
toolBarRender={() => [
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
key="primary"
|
||||||
|
icon={<UploadOutlined />}
|
||||||
|
onClick={handleUpload}
|
||||||
|
>
|
||||||
|
上传镜像
|
||||||
|
</Button>,
|
||||||
|
]}
|
||||||
|
request={async (params) => {
|
||||||
|
try {
|
||||||
|
const response = await getImages({
|
||||||
|
page: params.current,
|
||||||
|
pageSize: params.pageSize,
|
||||||
|
status: params.status,
|
||||||
|
category: params.category,
|
||||||
|
search: params.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.success && response.data) {
|
||||||
|
return {
|
||||||
|
data: response.data.items,
|
||||||
|
success: true,
|
||||||
|
total: response.data.pagination.total,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
data: [],
|
||||||
|
success: false,
|
||||||
|
total: 0,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取镜像列表失败:', error);
|
||||||
|
return {
|
||||||
|
data: [],
|
||||||
|
success: false,
|
||||||
|
total: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
columns={columns}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 上传镜像模态框 */}
|
||||||
|
<Modal
|
||||||
|
title="上传镜像"
|
||||||
|
open={uploadModalVisible}
|
||||||
|
onCancel={() => setUploadModalVisible(false)}
|
||||||
|
footer={null}
|
||||||
|
width={600}
|
||||||
|
>
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
layout="vertical"
|
||||||
|
onFinish={handleUploadSubmit}
|
||||||
|
className={styles.uploadForm}
|
||||||
|
>
|
||||||
|
<Form.Item
|
||||||
|
name="file"
|
||||||
|
label="镜像文件"
|
||||||
|
rules={[{ required: true, message: '请选择镜像文件' }]}
|
||||||
|
>
|
||||||
|
<Upload
|
||||||
|
name="file"
|
||||||
|
accept=".img,.iso"
|
||||||
|
beforeUpload={() => false}
|
||||||
|
maxCount={1}
|
||||||
|
>
|
||||||
|
<Button icon={<UploadOutlined />}>选择文件</Button>
|
||||||
|
</Upload>
|
||||||
|
<div className={styles.uploadTip}>
|
||||||
|
支持 .img 和 .iso 格式的镜像文件,最大 100MB
|
||||||
|
</div>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
{uploadLoading && (
|
||||||
|
<div className={styles.uploadProgress}>
|
||||||
|
<Progress percent={uploadProgress} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
name="name"
|
||||||
|
label="镜像名称"
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: '请输入镜像名称' },
|
||||||
|
{ max: 50, message: '镜像名称最多50个字符' },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input placeholder="请输入镜像名称" />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
name="description"
|
||||||
|
label="镜像描述"
|
||||||
|
>
|
||||||
|
<TextArea
|
||||||
|
rows={3}
|
||||||
|
placeholder="请输入镜像描述"
|
||||||
|
maxLength={200}
|
||||||
|
showCount
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
name="version"
|
||||||
|
label="版本"
|
||||||
|
rules={[{ required: true, message: '请输入版本' }]}
|
||||||
|
initialValue="1.0.0"
|
||||||
|
>
|
||||||
|
<Input placeholder="1.0.0" />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
name="category"
|
||||||
|
label="分类"
|
||||||
|
rules={[{ required: true, message: '请选择分类' }]}
|
||||||
|
>
|
||||||
|
<Select placeholder="请选择分类" options={categoryOptions} />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item className={styles.formActions}>
|
||||||
|
<Space>
|
||||||
|
<Button onClick={() => setUploadModalVisible(false)}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
htmlType="submit"
|
||||||
|
loading={uploadLoading}
|
||||||
|
icon={<UploadOutlined />}
|
||||||
|
>
|
||||||
|
上传
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
{/* 更新状态模态框 */}
|
||||||
|
<Modal
|
||||||
|
title="更新镜像状态"
|
||||||
|
open={statusModalVisible}
|
||||||
|
onCancel={() => setStatusModalVisible(false)}
|
||||||
|
footer={null}
|
||||||
|
width={500}
|
||||||
|
>
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
layout="vertical"
|
||||||
|
onFinish={handleStatusSubmit}
|
||||||
|
className={styles.statusForm}
|
||||||
|
>
|
||||||
|
<Form.Item
|
||||||
|
name="status"
|
||||||
|
label="新状态"
|
||||||
|
rules={[{ required: true, message: '请选择状态' }]}
|
||||||
|
>
|
||||||
|
<Select placeholder="请选择状态" options={statusOptions} />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
name="reason"
|
||||||
|
label="操作原因"
|
||||||
|
>
|
||||||
|
<TextArea
|
||||||
|
rows={3}
|
||||||
|
placeholder="请输入操作原因"
|
||||||
|
maxLength={200}
|
||||||
|
showCount
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item className={styles.formActions}>
|
||||||
|
<Space>
|
||||||
|
<Button onClick={() => setStatusModalVisible(false)}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button type="primary" htmlType="submit">
|
||||||
|
确认
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ImageManagementPage;
|
||||||
170
src/pages/Login/index.less
Normal file
170
src/pages/Login/index.less
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
.container {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loginContainer {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 420px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loginCard {
|
||||||
|
background: rgba(255, 255, 255, 95%);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow: 0 20px 40px rgba(0, 0, 0, 10%);
|
||||||
|
border: none;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 4px;
|
||||||
|
background: linear-gradient(90deg, #667eea, #764ba2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
padding-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
font-size: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
color: #333;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
color: #666;
|
||||||
|
font-size: 16px;
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form {
|
||||||
|
.ant-form-item {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-input,
|
||||||
|
.ant-input-password {
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 2px solid #e1e5e9;
|
||||||
|
padding: 12px 16px;
|
||||||
|
font-size: 16px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&:focus,
|
||||||
|
&:hover {
|
||||||
|
border-color: #667eea;
|
||||||
|
box-shadow: 0 0 0 3px rgba(102, 126, 234, 10%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-input-prefix {
|
||||||
|
color: #667eea;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.smsContainer {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.smsInput {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.smsButton {
|
||||||
|
white-space: nowrap;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loginButton {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
height: 48px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 10px 20px rgba(102, 126, 234, 30%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
background: #ccc;
|
||||||
|
transform: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 24px;
|
||||||
|
padding-top: 16px;
|
||||||
|
border-top: 1px solid #e1e5e9;
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: #888;
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应式设计
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.container {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.smsContainer {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.smsButton {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
196
src/pages/Login/index.tsx
Normal file
196
src/pages/Login/index.tsx
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Form, Input, Button, Card, message } from 'antd';
|
||||||
|
import { UserOutlined, LockOutlined, MobileOutlined, SafetyCertificateOutlined } from '@ant-design/icons';
|
||||||
|
import { history, useModel } from '@umijs/max';
|
||||||
|
import { login, sendSmsCode } from '@/services/auth';
|
||||||
|
import type { LoginFormData } from '@/types';
|
||||||
|
import styles from './index.less';
|
||||||
|
|
||||||
|
const LoginPage: React.FC = () => {
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [smsLoading, setSmsLoading] = useState(false);
|
||||||
|
const [smsCountdown, setSmsCountdown] = useState(0);
|
||||||
|
const { setInitialState } = useModel('@@initialState');
|
||||||
|
|
||||||
|
// 发送短信验证码
|
||||||
|
const handleSendSms = async () => {
|
||||||
|
try {
|
||||||
|
const phone = form.getFieldValue('phone');
|
||||||
|
if (!phone) {
|
||||||
|
message.warning('请先输入手机号');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 简单的手机号验证
|
||||||
|
if (!/^1[3-9]\d{9}$/.test(phone)) {
|
||||||
|
message.warning('请输入正确的手机号');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSmsLoading(true);
|
||||||
|
const response = await sendSmsCode(phone);
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
message.success('验证码已发送,请注意查收');
|
||||||
|
// 开始倒计时
|
||||||
|
setSmsCountdown(60);
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
setSmsCountdown((prev) => {
|
||||||
|
if (prev <= 1) {
|
||||||
|
clearInterval(timer);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return prev - 1;
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
// Mock模式下自动填充验证码
|
||||||
|
if (phone === '13800138000') {
|
||||||
|
form.setFieldsValue({ smsCode: '123456' });
|
||||||
|
message.info('验证码已自动填充: 123456');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
message.error(response.message || '发送失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error('发送验证码失败');
|
||||||
|
console.error('发送验证码失败:', error);
|
||||||
|
} finally {
|
||||||
|
setSmsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理登录
|
||||||
|
const handleLogin = async (values: LoginFormData) => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await login(values);
|
||||||
|
|
||||||
|
if (response.success && response.data) {
|
||||||
|
// 保存token
|
||||||
|
localStorage.setItem('adminToken', response.data.sessionToken);
|
||||||
|
|
||||||
|
// 更新全局状态
|
||||||
|
await setInitialState({
|
||||||
|
currentUser: {
|
||||||
|
adminId: response.data.adminId,
|
||||||
|
username: response.data.username,
|
||||||
|
nickname: response.data.nickname,
|
||||||
|
phone: values.phone,
|
||||||
|
role: 'admin',
|
||||||
|
status: 'active',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
message.success('登录成功');
|
||||||
|
history.push('/dashboard');
|
||||||
|
} else {
|
||||||
|
message.error(response.message || '登录失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error('登录失败');
|
||||||
|
console.error('登录失败:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={styles.loginContainer}>
|
||||||
|
<Card className={styles.loginCard}>
|
||||||
|
<div className={styles.header}>
|
||||||
|
<div className={styles.logo}>
|
||||||
|
<div className={styles.icon}>🚀</div>
|
||||||
|
<h1>AutoBee</h1>
|
||||||
|
</div>
|
||||||
|
<p className={styles.subtitle}>后台管理系统</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
name="login"
|
||||||
|
onFinish={handleLogin}
|
||||||
|
autoComplete="off"
|
||||||
|
size="large"
|
||||||
|
className={styles.form}
|
||||||
|
>
|
||||||
|
<Form.Item
|
||||||
|
name="username"
|
||||||
|
rules={[{ required: true, message: '请输入用户名' }]}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
prefix={<UserOutlined />}
|
||||||
|
placeholder="请输入用户名"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
name="password"
|
||||||
|
rules={[{ required: true, message: '请输入密码' }]}
|
||||||
|
>
|
||||||
|
<Input.Password
|
||||||
|
prefix={<LockOutlined />}
|
||||||
|
placeholder="请输入密码"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
name="phone"
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: '请输入手机号' },
|
||||||
|
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号' }
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
prefix={<MobileOutlined />}
|
||||||
|
placeholder="请输入手机号"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
name="smsCode"
|
||||||
|
rules={[{ required: true, message: '请输入验证码' }]}
|
||||||
|
>
|
||||||
|
<div className={styles.smsContainer}>
|
||||||
|
<Input
|
||||||
|
prefix={<SafetyCertificateOutlined />}
|
||||||
|
placeholder="请输入验证码"
|
||||||
|
className={styles.smsInput}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={handleSendSms}
|
||||||
|
loading={smsLoading}
|
||||||
|
disabled={smsCountdown > 0}
|
||||||
|
className={styles.smsButton}
|
||||||
|
>
|
||||||
|
{smsCountdown > 0 ? `${smsCountdown}秒后重发` : '发送验证码'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
htmlType="submit"
|
||||||
|
loading={loading}
|
||||||
|
className={styles.loginButton}
|
||||||
|
block
|
||||||
|
>
|
||||||
|
登录
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
|
||||||
|
<div className={styles.footer}>
|
||||||
|
<p>请使用管理员账号登录系统</p>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LoginPage;
|
||||||
97
src/pages/ScriptBackend/index.less
Normal file
97
src/pages/ScriptBackend/index.less
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
.scriptBackend {
|
||||||
|
.rating {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.ratingText {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.usageCount {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
.count {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unit {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.scriptDetail {
|
||||||
|
.detailItem {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding: 8px 0;
|
||||||
|
border-bottom: 1px solid #f8f8f8;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
strong {
|
||||||
|
color: #333;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-top: 8px;
|
||||||
|
border-left: 3px solid #1890ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.scriptForm {
|
||||||
|
.ant-form-item {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-input,
|
||||||
|
.ant-select-selector {
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formActions {
|
||||||
|
margin-bottom: 0;
|
||||||
|
text-align: right;
|
||||||
|
padding-top: 20px;
|
||||||
|
border-top: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.rate {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 覆盖 ProTable 样式
|
||||||
|
.ant-pro-table {
|
||||||
|
.ant-table {
|
||||||
|
.ant-table-thead > tr > th {
|
||||||
|
background: #fafafa;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-tbody > tr > td {
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-tbody > tr:hover > td {
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
419
src/pages/ScriptBackend/index.tsx
Normal file
419
src/pages/ScriptBackend/index.tsx
Normal file
@@ -0,0 +1,419 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Space,
|
||||||
|
Modal,
|
||||||
|
Form,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
message,
|
||||||
|
Popconfirm,
|
||||||
|
Tag,
|
||||||
|
Tooltip,
|
||||||
|
Rate
|
||||||
|
} from 'antd';
|
||||||
|
import {
|
||||||
|
PlusOutlined,
|
||||||
|
EditOutlined,
|
||||||
|
DeleteOutlined,
|
||||||
|
EyeOutlined,
|
||||||
|
BookOutlined
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import { PageContainer, ProTable } from '@ant-design/pro-components';
|
||||||
|
import type { ActionType, ProColumns } from '@ant-design/pro-components';
|
||||||
|
import { getScripts, getScriptDetail, createScript, updateScript, deleteScript } from '@/services/script';
|
||||||
|
import type { Script } from '@/types';
|
||||||
|
import styles from './index.less';
|
||||||
|
|
||||||
|
const { TextArea } = Input;
|
||||||
|
|
||||||
|
const ScriptBackendPage: React.FC = () => {
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const [modalVisible, setModalVisible] = useState(false);
|
||||||
|
const [modalLoading, setModalLoading] = useState(false);
|
||||||
|
const [editingScript, setEditingScript] = useState<Script | null>(null);
|
||||||
|
const actionRef = React.useRef<ActionType>();
|
||||||
|
|
||||||
|
// 状态选项
|
||||||
|
const statusOptions = [
|
||||||
|
{ label: '草稿', value: 'draft' },
|
||||||
|
{ label: '发布', value: 'published' },
|
||||||
|
{ label: '下线', value: 'offline' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 获取状态标签
|
||||||
|
const getStatusTag = (status: string) => {
|
||||||
|
const statusMap = {
|
||||||
|
draft: { color: 'default', text: '草稿' },
|
||||||
|
published: { color: 'success', text: '发布' },
|
||||||
|
offline: { color: 'error', text: '下线' },
|
||||||
|
};
|
||||||
|
const statusInfo = statusMap[status as keyof typeof statusMap] || { color: 'default', text: status };
|
||||||
|
return <Tag color={statusInfo.color}>{statusInfo.text}</Tag>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 表格列配置
|
||||||
|
const columns: ProColumns<Script>[] = [
|
||||||
|
{
|
||||||
|
title: 'ID',
|
||||||
|
dataIndex: 'id',
|
||||||
|
key: 'id',
|
||||||
|
width: 80,
|
||||||
|
search: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '剧本标题',
|
||||||
|
dataIndex: 'title',
|
||||||
|
key: 'title',
|
||||||
|
width: 200,
|
||||||
|
ellipsis: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '创建者',
|
||||||
|
dataIndex: 'creator',
|
||||||
|
key: 'creator',
|
||||||
|
width: 120,
|
||||||
|
search: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '版本',
|
||||||
|
dataIndex: 'version',
|
||||||
|
key: 'version',
|
||||||
|
width: 100,
|
||||||
|
search: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '状态',
|
||||||
|
dataIndex: 'status',
|
||||||
|
key: 'status',
|
||||||
|
width: 100,
|
||||||
|
render: (_, record) => getStatusTag(record.status),
|
||||||
|
valueType: 'select',
|
||||||
|
valueEnum: {
|
||||||
|
draft: { text: '草稿', status: 'Default' },
|
||||||
|
published: { text: '发布', status: 'Success' },
|
||||||
|
offline: { text: '下线', status: 'Error' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '评分',
|
||||||
|
dataIndex: 'rating',
|
||||||
|
key: 'rating',
|
||||||
|
width: 120,
|
||||||
|
search: false,
|
||||||
|
render: (_, record) => (
|
||||||
|
<div className={styles.rating}>
|
||||||
|
<Rate
|
||||||
|
disabled
|
||||||
|
value={record.rating}
|
||||||
|
allowHalf
|
||||||
|
style={{ fontSize: 14 }}
|
||||||
|
/>
|
||||||
|
<span className={styles.ratingText}>{record.rating.toFixed(1)}</span>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '使用次数',
|
||||||
|
dataIndex: 'usageCount',
|
||||||
|
key: 'usageCount',
|
||||||
|
width: 100,
|
||||||
|
search: false,
|
||||||
|
render: (_, record) => (
|
||||||
|
<div className={styles.usageCount}>
|
||||||
|
<span className={styles.count}>{record.usageCount}</span>
|
||||||
|
<span className={styles.unit}>次</span>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '创建时间',
|
||||||
|
dataIndex: 'createdAt',
|
||||||
|
key: 'createdAt',
|
||||||
|
width: 160,
|
||||||
|
valueType: 'dateTime',
|
||||||
|
search: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'action',
|
||||||
|
width: 180,
|
||||||
|
search: false,
|
||||||
|
render: (_, record) => (
|
||||||
|
<Space size="small">
|
||||||
|
<Tooltip title="查看详情">
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
icon={<EyeOutlined />}
|
||||||
|
onClick={() => handleView(record)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="编辑">
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
icon={<EditOutlined />}
|
||||||
|
onClick={() => handleEdit(record)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Popconfirm
|
||||||
|
title="确定要删除这个剧本吗?"
|
||||||
|
onConfirm={() => handleDelete(record.id)}
|
||||||
|
okText="确定"
|
||||||
|
cancelText="取消"
|
||||||
|
>
|
||||||
|
<Tooltip title="删除">
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
danger
|
||||||
|
icon={<DeleteOutlined />}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</Popconfirm>
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// 新增剧本
|
||||||
|
const handleAdd = () => {
|
||||||
|
setEditingScript(null);
|
||||||
|
form.resetFields();
|
||||||
|
form.setFieldsValue({
|
||||||
|
version: '1.0.0',
|
||||||
|
status: 'draft',
|
||||||
|
rating: 0,
|
||||||
|
usageCount: 0,
|
||||||
|
});
|
||||||
|
setModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 编辑剧本
|
||||||
|
const handleEdit = (script: Script) => {
|
||||||
|
setEditingScript(script);
|
||||||
|
form.setFieldsValue({
|
||||||
|
title: script.title,
|
||||||
|
description: script.description,
|
||||||
|
version: script.version,
|
||||||
|
status: script.status,
|
||||||
|
rating: script.rating,
|
||||||
|
usageCount: script.usageCount,
|
||||||
|
});
|
||||||
|
setModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 查看详情
|
||||||
|
const handleView = (script: Script) => {
|
||||||
|
Modal.info({
|
||||||
|
title: '剧本详情',
|
||||||
|
width: 600,
|
||||||
|
content: (
|
||||||
|
<div className={styles.scriptDetail}>
|
||||||
|
<div className={styles.detailItem}>
|
||||||
|
<strong>剧本标题:</strong>{script.title}
|
||||||
|
</div>
|
||||||
|
<div className={styles.detailItem}>
|
||||||
|
<strong>创建者:</strong>{script.creator}
|
||||||
|
</div>
|
||||||
|
<div className={styles.detailItem}>
|
||||||
|
<strong>版本:</strong>{script.version}
|
||||||
|
</div>
|
||||||
|
<div className={styles.detailItem}>
|
||||||
|
<strong>状态:</strong>{getStatusTag(script.status)}
|
||||||
|
</div>
|
||||||
|
<div className={styles.detailItem}>
|
||||||
|
<strong>评分:</strong>
|
||||||
|
<Rate disabled value={script.rating} allowHalf className={styles.rate} />
|
||||||
|
<span style={{ marginLeft: 8 }}>{script.rating.toFixed(1)}</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.detailItem}>
|
||||||
|
<strong>使用次数:</strong>{script.usageCount} 次
|
||||||
|
</div>
|
||||||
|
<div className={styles.detailItem}>
|
||||||
|
<strong>创建时间:</strong>{new Date(script.createdAt).toLocaleString()}
|
||||||
|
</div>
|
||||||
|
{script.description && (
|
||||||
|
<div className={styles.detailItem}>
|
||||||
|
<strong>描述:</strong>
|
||||||
|
<div className={styles.description}>{script.description}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除剧本
|
||||||
|
const handleDelete = async (id: string) => {
|
||||||
|
try {
|
||||||
|
const response = await deleteScript(id);
|
||||||
|
if (response.success) {
|
||||||
|
message.success('删除成功');
|
||||||
|
actionRef.current?.reload();
|
||||||
|
} else {
|
||||||
|
message.error(response.message || '删除失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error('删除失败');
|
||||||
|
console.error('删除剧本失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 提交表单
|
||||||
|
const handleSubmit = async (values: any) => {
|
||||||
|
try {
|
||||||
|
setModalLoading(true);
|
||||||
|
let response;
|
||||||
|
|
||||||
|
if (editingScript) {
|
||||||
|
// 编辑剧本
|
||||||
|
response = await updateScript(editingScript.id, values);
|
||||||
|
} else {
|
||||||
|
// 新增剧本
|
||||||
|
response = await createScript(values);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
message.success(editingScript ? '更新成功' : '创建成功');
|
||||||
|
setModalVisible(false);
|
||||||
|
actionRef.current?.reload();
|
||||||
|
} else {
|
||||||
|
message.error(response.message || '操作失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error('操作失败');
|
||||||
|
console.error('操作失败:', error);
|
||||||
|
} finally {
|
||||||
|
setModalLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer
|
||||||
|
title="剧本后台"
|
||||||
|
subTitle="管理系统剧本内容"
|
||||||
|
>
|
||||||
|
<div className={styles.scriptBackend}>
|
||||||
|
<ProTable<Script>
|
||||||
|
headerTitle="剧本列表"
|
||||||
|
actionRef={actionRef}
|
||||||
|
rowKey="id"
|
||||||
|
search={{
|
||||||
|
labelWidth: 'auto',
|
||||||
|
}}
|
||||||
|
toolBarRender={() => [
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
key="primary"
|
||||||
|
icon={<PlusOutlined />}
|
||||||
|
onClick={handleAdd}
|
||||||
|
>
|
||||||
|
新增剧本
|
||||||
|
</Button>,
|
||||||
|
]}
|
||||||
|
request={async (params) => {
|
||||||
|
try {
|
||||||
|
const response = await getScripts({
|
||||||
|
page: params.current,
|
||||||
|
pageSize: params.pageSize,
|
||||||
|
status: params.status,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.success && response.data) {
|
||||||
|
return {
|
||||||
|
data: response.data.items,
|
||||||
|
success: true,
|
||||||
|
total: response.data.pagination.total,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
data: [],
|
||||||
|
success: false,
|
||||||
|
total: 0,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取剧本列表失败:', error);
|
||||||
|
return {
|
||||||
|
data: [],
|
||||||
|
success: false,
|
||||||
|
total: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
columns={columns}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 新增/编辑剧本模态框 */}
|
||||||
|
<Modal
|
||||||
|
title={editingScript ? '编辑剧本' : '新增剧本'}
|
||||||
|
open={modalVisible}
|
||||||
|
onCancel={() => setModalVisible(false)}
|
||||||
|
footer={null}
|
||||||
|
width={600}
|
||||||
|
>
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
layout="vertical"
|
||||||
|
onFinish={handleSubmit}
|
||||||
|
className={styles.scriptForm}
|
||||||
|
>
|
||||||
|
<Form.Item
|
||||||
|
name="title"
|
||||||
|
label="剧本标题"
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: '请输入剧本标题' },
|
||||||
|
{ max: 100, message: '标题最多100个字符' },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input placeholder="请输入剧本标题" />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
name="description"
|
||||||
|
label="剧本描述"
|
||||||
|
>
|
||||||
|
<TextArea
|
||||||
|
rows={4}
|
||||||
|
placeholder="请输入剧本描述"
|
||||||
|
maxLength={500}
|
||||||
|
showCount
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
name="version"
|
||||||
|
label="版本"
|
||||||
|
rules={[{ required: true, message: '请输入版本' }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="1.0.0" />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
name="status"
|
||||||
|
label="状态"
|
||||||
|
rules={[{ required: true, message: '请选择状态' }]}
|
||||||
|
>
|
||||||
|
<Select placeholder="请选择状态" options={statusOptions} />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item className={styles.formActions}>
|
||||||
|
<Space>
|
||||||
|
<Button onClick={() => setModalVisible(false)}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
htmlType="submit"
|
||||||
|
loading={modalLoading}
|
||||||
|
>
|
||||||
|
{editingScript ? '更新' : '创建'}
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ScriptBackendPage;
|
||||||
133
src/pages/ScriptReview/index.less
Normal file
133
src/pages/ScriptReview/index.less
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
.scriptReview {
|
||||||
|
.statsRow {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statCard {
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 10%);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 15%);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-statistic {
|
||||||
|
.ant-statistic-title {
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-statistic-content {
|
||||||
|
.ant-statistic-content-value {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.reviewModal {
|
||||||
|
.reviewContent {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
padding-bottom: 24px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reviewInfo {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #666;
|
||||||
|
|
||||||
|
strong {
|
||||||
|
color: #333;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.scriptContent {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
pre {
|
||||||
|
margin: 0;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
font-family: Monaco, Menlo, 'Ubuntu Mono', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.reviewForm {
|
||||||
|
.ant-form-item {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formActions {
|
||||||
|
margin-bottom: 0;
|
||||||
|
text-align: right;
|
||||||
|
padding-top: 20px;
|
||||||
|
border-top: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 覆盖 ProTable 样式
|
||||||
|
.ant-pro-table {
|
||||||
|
.ant-table {
|
||||||
|
.ant-table-thead > tr > th {
|
||||||
|
background: #fafafa;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-tbody > tr > td {
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-tbody > tr:hover > td {
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应式设计
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.scriptReview {
|
||||||
|
.statsRow {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reviewModal {
|
||||||
|
.scriptContent {
|
||||||
|
max-height: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
380
src/pages/ScriptReview/index.tsx
Normal file
380
src/pages/ScriptReview/index.tsx
Normal file
@@ -0,0 +1,380 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Space,
|
||||||
|
Modal,
|
||||||
|
Form,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
message,
|
||||||
|
Tag,
|
||||||
|
Tooltip,
|
||||||
|
Card,
|
||||||
|
Row,
|
||||||
|
Col,
|
||||||
|
Statistic
|
||||||
|
} from 'antd';
|
||||||
|
import {
|
||||||
|
EyeOutlined,
|
||||||
|
CheckOutlined,
|
||||||
|
CloseOutlined,
|
||||||
|
AuditOutlined
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import { PageContainer, ProTable } from '@ant-design/pro-components';
|
||||||
|
import type { ActionType, ProColumns } from '@ant-design/pro-components';
|
||||||
|
import { useRequest } from '@umijs/max';
|
||||||
|
import {
|
||||||
|
getScriptReviews,
|
||||||
|
getScriptReviewDetail,
|
||||||
|
approveScriptReview,
|
||||||
|
rejectScriptReview,
|
||||||
|
getReviewStatistics
|
||||||
|
} from '@/services/script';
|
||||||
|
import type { ScriptReview, ReviewFormData } from '@/types';
|
||||||
|
import styles from './index.less';
|
||||||
|
|
||||||
|
const { TextArea } = Input;
|
||||||
|
|
||||||
|
const ScriptReviewPage: React.FC = () => {
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const [reviewModalVisible, setReviewModalVisible] = useState(false);
|
||||||
|
const [reviewLoading, setReviewLoading] = useState(false);
|
||||||
|
const [selectedReview, setSelectedReview] = useState<ScriptReview | null>(null);
|
||||||
|
const actionRef = React.useRef<ActionType>();
|
||||||
|
|
||||||
|
// 获取审核统计数据
|
||||||
|
const { data: stats } = useRequest(getReviewStatistics, {
|
||||||
|
onError: (error) => {
|
||||||
|
console.error('获取审核统计失败:', error);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 状态选项
|
||||||
|
const statusOptions = [
|
||||||
|
{ label: '待审核', value: 'pending' },
|
||||||
|
{ label: '已通过', value: 'approved' },
|
||||||
|
{ label: '已拒绝', value: 'rejected' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 获取状态标签
|
||||||
|
const getStatusTag = (status: string) => {
|
||||||
|
const statusMap = {
|
||||||
|
pending: { color: 'processing', text: '待审核' },
|
||||||
|
approved: { color: 'success', text: '已通过' },
|
||||||
|
rejected: { color: 'error', text: '已拒绝' },
|
||||||
|
};
|
||||||
|
const statusInfo = statusMap[status as keyof typeof statusMap] || { color: 'default', text: status };
|
||||||
|
return <Tag color={statusInfo.color}>{statusInfo.text}</Tag>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 表格列配置
|
||||||
|
const columns: ProColumns<ScriptReview>[] = [
|
||||||
|
{
|
||||||
|
title: 'ID',
|
||||||
|
dataIndex: 'id',
|
||||||
|
key: 'id',
|
||||||
|
width: 80,
|
||||||
|
search: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '剧本标题',
|
||||||
|
dataIndex: 'title',
|
||||||
|
key: 'title',
|
||||||
|
width: 200,
|
||||||
|
ellipsis: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '提交者',
|
||||||
|
dataIndex: 'submitterId',
|
||||||
|
key: 'submitterId',
|
||||||
|
width: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '版本',
|
||||||
|
dataIndex: 'version',
|
||||||
|
key: 'version',
|
||||||
|
width: 100,
|
||||||
|
search: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '状态',
|
||||||
|
dataIndex: 'status',
|
||||||
|
key: 'status',
|
||||||
|
width: 100,
|
||||||
|
render: (_, record) => getStatusTag(record.status),
|
||||||
|
valueType: 'select',
|
||||||
|
valueEnum: {
|
||||||
|
pending: { text: '待审核', status: 'Processing' },
|
||||||
|
approved: { text: '已通过', status: 'Success' },
|
||||||
|
rejected: { text: '已拒绝', status: 'Error' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '提交时间',
|
||||||
|
dataIndex: 'submittedAt',
|
||||||
|
key: 'submittedAt',
|
||||||
|
width: 160,
|
||||||
|
valueType: 'dateTime',
|
||||||
|
search: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'action',
|
||||||
|
width: 200,
|
||||||
|
search: false,
|
||||||
|
render: (_, record) => (
|
||||||
|
<Space size="small">
|
||||||
|
<Tooltip title="查看详情">
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
icon={<EyeOutlined />}
|
||||||
|
onClick={() => handleView(record)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
{record.status === 'pending' && (
|
||||||
|
<>
|
||||||
|
<Tooltip title="审核通过">
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
icon={<CheckOutlined />}
|
||||||
|
onClick={() => handleApprove(record)}
|
||||||
|
style={{ color: '#52c41a' }}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="审核拒绝">
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
icon={<CloseOutlined />}
|
||||||
|
onClick={() => handleReject(record)}
|
||||||
|
style={{ color: '#ff4d4f' }}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// 查看详情
|
||||||
|
const handleView = (review: ScriptReview) => {
|
||||||
|
setSelectedReview(review);
|
||||||
|
setReviewModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 审核通过
|
||||||
|
const handleApprove = (review: ScriptReview) => {
|
||||||
|
setSelectedReview(review);
|
||||||
|
form.resetFields();
|
||||||
|
setReviewModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 审核拒绝
|
||||||
|
const handleReject = (review: ScriptReview) => {
|
||||||
|
setSelectedReview(review);
|
||||||
|
form.resetFields();
|
||||||
|
setReviewModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 提交审核
|
||||||
|
const handleReviewSubmit = async (values: ReviewFormData) => {
|
||||||
|
if (!selectedReview) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setReviewLoading(true);
|
||||||
|
let response;
|
||||||
|
|
||||||
|
if (selectedReview.status === 'pending') {
|
||||||
|
// 这里需要根据用户操作决定是通过还是拒绝
|
||||||
|
// 简化处理,假设都是通过
|
||||||
|
response = await approveScriptReview(selectedReview.id, values);
|
||||||
|
} else {
|
||||||
|
response = await approveScriptReview(selectedReview.id, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
message.success('审核成功');
|
||||||
|
setReviewModalVisible(false);
|
||||||
|
actionRef.current?.reload();
|
||||||
|
} else {
|
||||||
|
message.error(response.message || '审核失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error('审核失败');
|
||||||
|
console.error('审核失败:', error);
|
||||||
|
} finally {
|
||||||
|
setReviewLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const statsData = stats?.data || {
|
||||||
|
pending: 0,
|
||||||
|
approved: 0,
|
||||||
|
rejected: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer
|
||||||
|
title="剧本审核"
|
||||||
|
subTitle="审核用户提交的剧本内容"
|
||||||
|
>
|
||||||
|
<div className={styles.scriptReview}>
|
||||||
|
{/* 审核统计 */}
|
||||||
|
<Row gutter={[16, 16]} className={styles.statsRow}>
|
||||||
|
<Col xs={24} sm={8}>
|
||||||
|
<Card className={styles.statCard}>
|
||||||
|
<Statistic
|
||||||
|
title="待审核"
|
||||||
|
value={statsData.pending}
|
||||||
|
prefix={<AuditOutlined />}
|
||||||
|
valueStyle={{ color: '#faad14' }}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} sm={8}>
|
||||||
|
<Card className={styles.statCard}>
|
||||||
|
<Statistic
|
||||||
|
title="已通过"
|
||||||
|
value={statsData.approved}
|
||||||
|
prefix={<CheckOutlined />}
|
||||||
|
valueStyle={{ color: '#52c41a' }}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} sm={8}>
|
||||||
|
<Card className={styles.statCard}>
|
||||||
|
<Statistic
|
||||||
|
title="已拒绝"
|
||||||
|
value={statsData.rejected}
|
||||||
|
prefix={<CloseOutlined />}
|
||||||
|
valueStyle={{ color: '#ff4d4f' }}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
{/* 审核列表 */}
|
||||||
|
<ProTable<ScriptReview>
|
||||||
|
headerTitle="审核列表"
|
||||||
|
actionRef={actionRef}
|
||||||
|
rowKey="id"
|
||||||
|
search={{
|
||||||
|
labelWidth: 'auto',
|
||||||
|
}}
|
||||||
|
request={async (params) => {
|
||||||
|
try {
|
||||||
|
const response = await getScriptReviews({
|
||||||
|
page: params.current,
|
||||||
|
pageSize: params.pageSize,
|
||||||
|
status: params.status,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.success && response.data) {
|
||||||
|
return {
|
||||||
|
data: response.data.items,
|
||||||
|
success: true,
|
||||||
|
total: response.data.pagination.total,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
data: [],
|
||||||
|
success: false,
|
||||||
|
total: 0,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取审核列表失败:', error);
|
||||||
|
return {
|
||||||
|
data: [],
|
||||||
|
success: false,
|
||||||
|
total: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
columns={columns}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 审核详情模态框 */}
|
||||||
|
<Modal
|
||||||
|
title="剧本审核"
|
||||||
|
open={reviewModalVisible}
|
||||||
|
onCancel={() => setReviewModalVisible(false)}
|
||||||
|
footer={null}
|
||||||
|
width={800}
|
||||||
|
>
|
||||||
|
{selectedReview && (
|
||||||
|
<div className={styles.reviewModal}>
|
||||||
|
<div className={styles.reviewContent}>
|
||||||
|
<h4>剧本信息</h4>
|
||||||
|
<div className={styles.reviewInfo}>
|
||||||
|
<p><strong>标题:</strong>{selectedReview.title}</p>
|
||||||
|
<p><strong>提交者:</strong>{selectedReview.submitterId}</p>
|
||||||
|
<p><strong>版本:</strong>{selectedReview.version}</p>
|
||||||
|
<p><strong>提交时间:</strong>{new Date(selectedReview.submittedAt).toLocaleString()}</p>
|
||||||
|
{selectedReview.description && (
|
||||||
|
<p><strong>描述:</strong>{selectedReview.description}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h4>剧本内容</h4>
|
||||||
|
<div className={styles.scriptContent}>
|
||||||
|
<pre>{selectedReview.content}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedReview.status === 'pending' && (
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
layout="vertical"
|
||||||
|
onFinish={handleReviewSubmit}
|
||||||
|
className={styles.reviewForm}
|
||||||
|
>
|
||||||
|
<Form.Item
|
||||||
|
name="comment"
|
||||||
|
label="审核意见"
|
||||||
|
>
|
||||||
|
<TextArea
|
||||||
|
rows={3}
|
||||||
|
placeholder="请输入审核意见"
|
||||||
|
maxLength={200}
|
||||||
|
showCount
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item className={styles.formActions}>
|
||||||
|
<Space>
|
||||||
|
<Button onClick={() => setReviewModalVisible(false)}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
danger
|
||||||
|
icon={<CloseOutlined />}
|
||||||
|
onClick={() => {
|
||||||
|
// 这里应该调用拒绝接口
|
||||||
|
handleReviewSubmit({ comment: form.getFieldValue('comment'), rejectionReason: '不符合要求' });
|
||||||
|
}}
|
||||||
|
loading={reviewLoading}
|
||||||
|
>
|
||||||
|
拒绝
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<CheckOutlined />}
|
||||||
|
htmlType="submit"
|
||||||
|
loading={reviewLoading}
|
||||||
|
>
|
||||||
|
通过
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ScriptReviewPage;
|
||||||
26
src/pages/Table/components/CreateForm.tsx
Normal file
26
src/pages/Table/components/CreateForm.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { Modal } from 'antd';
|
||||||
|
import React, { PropsWithChildren } from 'react';
|
||||||
|
|
||||||
|
interface CreateFormProps {
|
||||||
|
modalVisible: boolean;
|
||||||
|
onCancel: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CreateForm: React.FC<PropsWithChildren<CreateFormProps>> = (props) => {
|
||||||
|
const { modalVisible, onCancel } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
destroyOnClose
|
||||||
|
title="新建"
|
||||||
|
width={420}
|
||||||
|
open={modalVisible}
|
||||||
|
onCancel={() => onCancel()}
|
||||||
|
footer={null}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CreateForm;
|
||||||
138
src/pages/Table/components/UpdateForm.tsx
Normal file
138
src/pages/Table/components/UpdateForm.tsx
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
import {
|
||||||
|
ProFormDateTimePicker,
|
||||||
|
ProFormRadio,
|
||||||
|
ProFormSelect,
|
||||||
|
ProFormText,
|
||||||
|
ProFormTextArea,
|
||||||
|
StepsForm,
|
||||||
|
} from '@ant-design/pro-components';
|
||||||
|
import { Modal } from 'antd';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export interface FormValueType extends Partial<API.UserInfo> {
|
||||||
|
target?: string;
|
||||||
|
template?: string;
|
||||||
|
type?: string;
|
||||||
|
time?: string;
|
||||||
|
frequency?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateFormProps {
|
||||||
|
onCancel: (flag?: boolean, formVals?: FormValueType) => void;
|
||||||
|
onSubmit: (values: FormValueType) => Promise<void>;
|
||||||
|
updateModalVisible: boolean;
|
||||||
|
values: Partial<API.UserInfo>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UpdateForm: React.FC<UpdateFormProps> = (props) => (
|
||||||
|
<StepsForm
|
||||||
|
stepsProps={{
|
||||||
|
size: 'small',
|
||||||
|
}}
|
||||||
|
stepsFormRender={(dom, submitter) => {
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
width={640}
|
||||||
|
bodyStyle={{ padding: '32px 40px 48px' }}
|
||||||
|
destroyOnClose
|
||||||
|
title="规则配置"
|
||||||
|
open={props.updateModalVisible}
|
||||||
|
footer={submitter}
|
||||||
|
onCancel={() => props.onCancel()}
|
||||||
|
>
|
||||||
|
{dom}
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
onFinish={props.onSubmit}
|
||||||
|
>
|
||||||
|
<StepsForm.StepForm
|
||||||
|
initialValues={{
|
||||||
|
name: props.values.name,
|
||||||
|
nickName: props.values.nickName,
|
||||||
|
}}
|
||||||
|
title="基本信息"
|
||||||
|
>
|
||||||
|
<ProFormText
|
||||||
|
width="md"
|
||||||
|
name="name"
|
||||||
|
label="规则名称"
|
||||||
|
rules={[{ required: true, message: '请输入规则名称!' }]}
|
||||||
|
/>
|
||||||
|
<ProFormTextArea
|
||||||
|
name="desc"
|
||||||
|
width="md"
|
||||||
|
label="规则描述"
|
||||||
|
placeholder="请输入至少五个字符"
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: '请输入至少五个字符的规则描述!', min: 5 },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</StepsForm.StepForm>
|
||||||
|
<StepsForm.StepForm
|
||||||
|
initialValues={{
|
||||||
|
target: '0',
|
||||||
|
template: '0',
|
||||||
|
}}
|
||||||
|
title="配置规则属性"
|
||||||
|
>
|
||||||
|
<ProFormSelect
|
||||||
|
width="md"
|
||||||
|
name="target"
|
||||||
|
label="监控对象"
|
||||||
|
valueEnum={{
|
||||||
|
0: '表一',
|
||||||
|
1: '表二',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ProFormSelect
|
||||||
|
width="md"
|
||||||
|
name="template"
|
||||||
|
label="规则模板"
|
||||||
|
valueEnum={{
|
||||||
|
0: '规则模板一',
|
||||||
|
1: '规则模板二',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ProFormRadio.Group
|
||||||
|
name="type"
|
||||||
|
width="md"
|
||||||
|
label="规则类型"
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
value: '0',
|
||||||
|
label: '强',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: '1',
|
||||||
|
label: '弱',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</StepsForm.StepForm>
|
||||||
|
<StepsForm.StepForm
|
||||||
|
initialValues={{
|
||||||
|
type: '1',
|
||||||
|
frequency: 'month',
|
||||||
|
}}
|
||||||
|
title="设定调度周期"
|
||||||
|
>
|
||||||
|
<ProFormDateTimePicker
|
||||||
|
name="time"
|
||||||
|
label="开始时间"
|
||||||
|
rules={[{ required: true, message: '请选择开始时间!' }]}
|
||||||
|
/>
|
||||||
|
<ProFormSelect
|
||||||
|
name="frequency"
|
||||||
|
label="监控对象"
|
||||||
|
width="xs"
|
||||||
|
valueEnum={{
|
||||||
|
month: '月',
|
||||||
|
week: '周',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</StepsForm.StepForm>
|
||||||
|
</StepsForm>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default UpdateForm;
|
||||||
270
src/pages/Table/index.tsx
Normal file
270
src/pages/Table/index.tsx
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
import services from '@/services/demo';
|
||||||
|
import {
|
||||||
|
ActionType,
|
||||||
|
FooterToolbar,
|
||||||
|
PageContainer,
|
||||||
|
ProDescriptions,
|
||||||
|
ProDescriptionsItemProps,
|
||||||
|
ProTable,
|
||||||
|
} from '@ant-design/pro-components';
|
||||||
|
import { Button, Divider, Drawer, message } from 'antd';
|
||||||
|
import React, { useRef, useState } from 'react';
|
||||||
|
import CreateForm from './components/CreateForm';
|
||||||
|
import UpdateForm, { FormValueType } from './components/UpdateForm';
|
||||||
|
|
||||||
|
const { addUser, queryUserList, deleteUser, modifyUser } =
|
||||||
|
services.UserController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加节点
|
||||||
|
* @param fields
|
||||||
|
*/
|
||||||
|
const handleAdd = async (fields: API.UserInfo) => {
|
||||||
|
const hide = message.loading('正在添加');
|
||||||
|
try {
|
||||||
|
await addUser({ ...fields });
|
||||||
|
hide();
|
||||||
|
message.success('添加成功');
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
hide();
|
||||||
|
message.error('添加失败请重试!');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新节点
|
||||||
|
* @param fields
|
||||||
|
*/
|
||||||
|
const handleUpdate = async (fields: FormValueType) => {
|
||||||
|
const hide = message.loading('正在配置');
|
||||||
|
try {
|
||||||
|
await modifyUser(
|
||||||
|
{
|
||||||
|
userId: fields.id || '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: fields.name || '',
|
||||||
|
nickName: fields.nickName || '',
|
||||||
|
email: fields.email || '',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
hide();
|
||||||
|
|
||||||
|
message.success('配置成功');
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
hide();
|
||||||
|
message.error('配置失败请重试!');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除节点
|
||||||
|
* @param selectedRows
|
||||||
|
*/
|
||||||
|
const handleRemove = async (selectedRows: API.UserInfo[]) => {
|
||||||
|
const hide = message.loading('正在删除');
|
||||||
|
if (!selectedRows) return true;
|
||||||
|
try {
|
||||||
|
await deleteUser({
|
||||||
|
userId: selectedRows.find((row) => row.id)?.id || '',
|
||||||
|
});
|
||||||
|
hide();
|
||||||
|
message.success('删除成功,即将刷新');
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
hide();
|
||||||
|
message.error('删除失败,请重试');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const TableList: React.FC<unknown> = () => {
|
||||||
|
const [createModalVisible, handleModalVisible] = useState<boolean>(false);
|
||||||
|
const [updateModalVisible, handleUpdateModalVisible] =
|
||||||
|
useState<boolean>(false);
|
||||||
|
const [stepFormValues, setStepFormValues] = useState({});
|
||||||
|
const actionRef = useRef<ActionType>();
|
||||||
|
const [row, setRow] = useState<API.UserInfo>();
|
||||||
|
const [selectedRowsState, setSelectedRows] = useState<API.UserInfo[]>([]);
|
||||||
|
const columns: ProDescriptionsItemProps<API.UserInfo>[] = [
|
||||||
|
{
|
||||||
|
title: '名称',
|
||||||
|
dataIndex: 'name',
|
||||||
|
tip: '名称是唯一的 key',
|
||||||
|
formItemProps: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: '名称为必填项',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '昵称',
|
||||||
|
dataIndex: 'nickName',
|
||||||
|
valueType: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '性别',
|
||||||
|
dataIndex: 'gender',
|
||||||
|
hideInForm: true,
|
||||||
|
valueEnum: {
|
||||||
|
0: { text: '男', status: 'MALE' },
|
||||||
|
1: { text: '女', status: 'FEMALE' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
dataIndex: 'option',
|
||||||
|
valueType: 'option',
|
||||||
|
render: (_, record) => (
|
||||||
|
<>
|
||||||
|
<a
|
||||||
|
onClick={() => {
|
||||||
|
handleUpdateModalVisible(true);
|
||||||
|
setStepFormValues(record);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
配置
|
||||||
|
</a>
|
||||||
|
<Divider type="vertical" />
|
||||||
|
<a href="">订阅警报</a>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer
|
||||||
|
header={{
|
||||||
|
title: 'CRUD 示例',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ProTable<API.UserInfo>
|
||||||
|
headerTitle="查询表格"
|
||||||
|
actionRef={actionRef}
|
||||||
|
rowKey="id"
|
||||||
|
search={{
|
||||||
|
labelWidth: 120,
|
||||||
|
}}
|
||||||
|
toolBarRender={() => [
|
||||||
|
<Button
|
||||||
|
key="1"
|
||||||
|
type="primary"
|
||||||
|
onClick={() => handleModalVisible(true)}
|
||||||
|
>
|
||||||
|
新建
|
||||||
|
</Button>,
|
||||||
|
]}
|
||||||
|
request={async (params, sorter, filter) => {
|
||||||
|
const { data, success } = await queryUserList({
|
||||||
|
...params,
|
||||||
|
// FIXME: remove @ts-ignore
|
||||||
|
// @ts-ignore
|
||||||
|
sorter,
|
||||||
|
filter,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
data: data?.list || [],
|
||||||
|
success,
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
columns={columns}
|
||||||
|
rowSelection={{
|
||||||
|
onChange: (_, selectedRows) => setSelectedRows(selectedRows),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{selectedRowsState?.length > 0 && (
|
||||||
|
<FooterToolbar
|
||||||
|
extra={
|
||||||
|
<div>
|
||||||
|
已选择{' '}
|
||||||
|
<a style={{ fontWeight: 600 }}>{selectedRowsState.length}</a>{' '}
|
||||||
|
项
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
onClick={async () => {
|
||||||
|
await handleRemove(selectedRowsState);
|
||||||
|
setSelectedRows([]);
|
||||||
|
actionRef.current?.reloadAndRest?.();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
批量删除
|
||||||
|
</Button>
|
||||||
|
<Button type="primary">批量审批</Button>
|
||||||
|
</FooterToolbar>
|
||||||
|
)}
|
||||||
|
<CreateForm
|
||||||
|
onCancel={() => handleModalVisible(false)}
|
||||||
|
modalVisible={createModalVisible}
|
||||||
|
>
|
||||||
|
<ProTable<API.UserInfo, API.UserInfo>
|
||||||
|
onSubmit={async (value) => {
|
||||||
|
const success = await handleAdd(value);
|
||||||
|
if (success) {
|
||||||
|
handleModalVisible(false);
|
||||||
|
if (actionRef.current) {
|
||||||
|
actionRef.current.reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
rowKey="id"
|
||||||
|
type="form"
|
||||||
|
columns={columns}
|
||||||
|
/>
|
||||||
|
</CreateForm>
|
||||||
|
{stepFormValues && Object.keys(stepFormValues).length ? (
|
||||||
|
<UpdateForm
|
||||||
|
onSubmit={async (value) => {
|
||||||
|
const success = await handleUpdate(value);
|
||||||
|
if (success) {
|
||||||
|
handleUpdateModalVisible(false);
|
||||||
|
setStepFormValues({});
|
||||||
|
if (actionRef.current) {
|
||||||
|
actionRef.current.reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onCancel={() => {
|
||||||
|
handleUpdateModalVisible(false);
|
||||||
|
setStepFormValues({});
|
||||||
|
}}
|
||||||
|
updateModalVisible={updateModalVisible}
|
||||||
|
values={stepFormValues}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<Drawer
|
||||||
|
width={600}
|
||||||
|
open={!!row}
|
||||||
|
onClose={() => {
|
||||||
|
setRow(undefined);
|
||||||
|
}}
|
||||||
|
closable={false}
|
||||||
|
>
|
||||||
|
{row?.name && (
|
||||||
|
<ProDescriptions<API.UserInfo>
|
||||||
|
column={2}
|
||||||
|
title={row?.name}
|
||||||
|
request={async () => ({
|
||||||
|
data: row || {},
|
||||||
|
})}
|
||||||
|
params={{
|
||||||
|
id: row?.name,
|
||||||
|
}}
|
||||||
|
columns={columns}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Drawer>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TableList;
|
||||||
45
src/pages/UserManagement/index.less
Normal file
45
src/pages/UserManagement/index.less
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
.userManagement {
|
||||||
|
.userForm {
|
||||||
|
.ant-form-item {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-input,
|
||||||
|
.ant-select-selector {
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formActions {
|
||||||
|
margin-bottom: 0;
|
||||||
|
text-align: right;
|
||||||
|
padding-top: 20px;
|
||||||
|
border-top: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 覆盖 ProTable 样式
|
||||||
|
.ant-pro-table {
|
||||||
|
.ant-table {
|
||||||
|
.ant-table-thead > tr > th {
|
||||||
|
background: #fafafa;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-tbody > tr > td {
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-tbody > tr:hover > td {
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-pro-table-toolbar {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-pro-table-search {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
379
src/pages/UserManagement/index.tsx
Normal file
379
src/pages/UserManagement/index.tsx
Normal file
@@ -0,0 +1,379 @@
|
|||||||
|
import React, { useState, useRef } from 'react';
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
Button,
|
||||||
|
Space,
|
||||||
|
Modal,
|
||||||
|
Form,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
message,
|
||||||
|
Popconfirm,
|
||||||
|
Tag,
|
||||||
|
Tooltip
|
||||||
|
} from 'antd';
|
||||||
|
import {
|
||||||
|
PlusOutlined,
|
||||||
|
EditOutlined,
|
||||||
|
DeleteOutlined,
|
||||||
|
UserOutlined,
|
||||||
|
SearchOutlined
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import { PageContainer, ProTable } from '@ant-design/pro-components';
|
||||||
|
import type { ActionType, ProColumns } from '@ant-design/pro-components';
|
||||||
|
import { useRequest } from '@umijs/max';
|
||||||
|
import { getUsers, createUser, updateUser, deleteUser } from '@/services/user';
|
||||||
|
import type { User, UserFormData } from '@/types';
|
||||||
|
import styles from './index.less';
|
||||||
|
|
||||||
|
const UserManagementPage: React.FC = () => {
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const [modalVisible, setModalVisible] = useState(false);
|
||||||
|
const [editingUser, setEditingUser] = useState<User | null>(null);
|
||||||
|
const [modalLoading, setModalLoading] = useState(false);
|
||||||
|
const actionRef = useRef<ActionType>();
|
||||||
|
|
||||||
|
// 角色配置
|
||||||
|
const roleOptions = [
|
||||||
|
{ label: '超级管理员', value: 'super_admin' },
|
||||||
|
{ label: '管理员', value: 'admin' },
|
||||||
|
{ label: '审核员', value: 'auditor' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 状态配置
|
||||||
|
const statusOptions = [
|
||||||
|
{ label: '正常', value: 'active' },
|
||||||
|
{ label: '禁用', value: 'inactive' },
|
||||||
|
{ label: '锁定', value: 'locked' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 获取用户状态标签
|
||||||
|
const getStatusTag = (status: string) => {
|
||||||
|
const statusMap = {
|
||||||
|
active: { color: 'success', text: '正常' },
|
||||||
|
inactive: { color: 'default', text: '禁用' },
|
||||||
|
locked: { color: 'error', text: '锁定' },
|
||||||
|
};
|
||||||
|
const statusInfo = statusMap[status as keyof typeof statusMap] || { color: 'default', text: status };
|
||||||
|
return <Tag color={statusInfo.color}>{statusInfo.text}</Tag>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取角色标签
|
||||||
|
const getRoleTag = (role: string) => {
|
||||||
|
const roleMap = {
|
||||||
|
super_admin: { color: 'red', text: '超级管理员' },
|
||||||
|
admin: { color: 'blue', text: '管理员' },
|
||||||
|
auditor: { color: 'green', text: '审核员' },
|
||||||
|
};
|
||||||
|
const roleInfo = roleMap[role as keyof typeof roleMap] || { color: 'default', text: role };
|
||||||
|
return <Tag color={roleInfo.color}>{roleInfo.text}</Tag>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 表格列配置
|
||||||
|
const columns: ProColumns<User>[] = [
|
||||||
|
{
|
||||||
|
title: 'ID',
|
||||||
|
dataIndex: 'id',
|
||||||
|
key: 'id',
|
||||||
|
width: 80,
|
||||||
|
search: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '用户名',
|
||||||
|
dataIndex: 'username',
|
||||||
|
key: 'username',
|
||||||
|
width: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '昵称',
|
||||||
|
dataIndex: 'nickname',
|
||||||
|
key: 'nickname',
|
||||||
|
width: 120,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '手机号',
|
||||||
|
dataIndex: 'phone',
|
||||||
|
key: 'phone',
|
||||||
|
width: 130,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '角色',
|
||||||
|
dataIndex: 'role',
|
||||||
|
key: 'role',
|
||||||
|
width: 100,
|
||||||
|
render: (_, record) => getRoleTag(record.role),
|
||||||
|
valueType: 'select',
|
||||||
|
valueEnum: {
|
||||||
|
super_admin: { text: '超级管理员' },
|
||||||
|
admin: { text: '管理员' },
|
||||||
|
auditor: { text: '审核员' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '状态',
|
||||||
|
dataIndex: 'status',
|
||||||
|
key: 'status',
|
||||||
|
width: 100,
|
||||||
|
render: (_, record) => getStatusTag(record.status),
|
||||||
|
valueType: 'select',
|
||||||
|
valueEnum: {
|
||||||
|
active: { text: '正常', status: 'Success' },
|
||||||
|
inactive: { text: '禁用', status: 'Default' },
|
||||||
|
locked: { text: '锁定', status: 'Error' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '创建时间',
|
||||||
|
dataIndex: 'createdAt',
|
||||||
|
key: 'createdAt',
|
||||||
|
width: 160,
|
||||||
|
valueType: 'dateTime',
|
||||||
|
search: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'action',
|
||||||
|
width: 160,
|
||||||
|
search: false,
|
||||||
|
render: (_, record) => (
|
||||||
|
<Space size="small">
|
||||||
|
<Tooltip title="编辑">
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
icon={<EditOutlined />}
|
||||||
|
onClick={() => handleEdit(record)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Popconfirm
|
||||||
|
title="确定要删除这个用户吗?"
|
||||||
|
onConfirm={() => handleDelete(record.id)}
|
||||||
|
okText="确定"
|
||||||
|
cancelText="取消"
|
||||||
|
>
|
||||||
|
<Tooltip title="删除">
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
danger
|
||||||
|
icon={<DeleteOutlined />}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</Popconfirm>
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// 新增用户
|
||||||
|
const handleAdd = () => {
|
||||||
|
setEditingUser(null);
|
||||||
|
form.resetFields();
|
||||||
|
setModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 编辑用户
|
||||||
|
const handleEdit = (user: User) => {
|
||||||
|
setEditingUser(user);
|
||||||
|
form.setFieldsValue({
|
||||||
|
username: user.username,
|
||||||
|
nickname: user.nickname,
|
||||||
|
phone: user.phone,
|
||||||
|
role: user.role,
|
||||||
|
});
|
||||||
|
setModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除用户
|
||||||
|
const handleDelete = async (id: string) => {
|
||||||
|
try {
|
||||||
|
const response = await deleteUser(id);
|
||||||
|
if (response.success) {
|
||||||
|
message.success('删除成功');
|
||||||
|
actionRef.current?.reload();
|
||||||
|
} else {
|
||||||
|
message.error(response.message || '删除失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error('删除失败');
|
||||||
|
console.error('删除用户失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 提交表单
|
||||||
|
const handleSubmit = async (values: UserFormData) => {
|
||||||
|
try {
|
||||||
|
setModalLoading(true);
|
||||||
|
let response;
|
||||||
|
|
||||||
|
if (editingUser) {
|
||||||
|
// 编辑用户
|
||||||
|
response = await updateUser(editingUser.id, values);
|
||||||
|
} else {
|
||||||
|
// 新增用户
|
||||||
|
response = await createUser(values);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
message.success(editingUser ? '更新成功' : '创建成功');
|
||||||
|
setModalVisible(false);
|
||||||
|
actionRef.current?.reload();
|
||||||
|
} else {
|
||||||
|
message.error(response.message || '操作失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error('操作失败');
|
||||||
|
console.error('操作失败:', error);
|
||||||
|
} finally {
|
||||||
|
setModalLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer
|
||||||
|
title="用户管理"
|
||||||
|
subTitle="管理系统用户账号和权限"
|
||||||
|
>
|
||||||
|
<div className={styles.userManagement}>
|
||||||
|
<ProTable<User>
|
||||||
|
headerTitle="用户列表"
|
||||||
|
actionRef={actionRef}
|
||||||
|
rowKey="id"
|
||||||
|
search={{
|
||||||
|
labelWidth: 'auto',
|
||||||
|
}}
|
||||||
|
toolBarRender={() => [
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
key="primary"
|
||||||
|
icon={<PlusOutlined />}
|
||||||
|
onClick={handleAdd}
|
||||||
|
>
|
||||||
|
新增用户
|
||||||
|
</Button>,
|
||||||
|
]}
|
||||||
|
request={async (params) => {
|
||||||
|
try {
|
||||||
|
const response = await getUsers({
|
||||||
|
page: params.current,
|
||||||
|
pageSize: params.pageSize,
|
||||||
|
role: params.role,
|
||||||
|
status: params.status,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.success && response.data) {
|
||||||
|
return {
|
||||||
|
data: response.data.items,
|
||||||
|
success: true,
|
||||||
|
total: response.data.pagination.total,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
data: [],
|
||||||
|
success: false,
|
||||||
|
total: 0,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取用户列表失败:', error);
|
||||||
|
return {
|
||||||
|
data: [],
|
||||||
|
success: false,
|
||||||
|
total: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
columns={columns}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 新增/编辑用户模态框 */}
|
||||||
|
<Modal
|
||||||
|
title={editingUser ? '编辑用户' : '新增用户'}
|
||||||
|
open={modalVisible}
|
||||||
|
onCancel={() => setModalVisible(false)}
|
||||||
|
footer={null}
|
||||||
|
width={600}
|
||||||
|
>
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
layout="vertical"
|
||||||
|
onFinish={handleSubmit}
|
||||||
|
className={styles.userForm}
|
||||||
|
>
|
||||||
|
<Form.Item
|
||||||
|
name="username"
|
||||||
|
label="用户名"
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: '请输入用户名' },
|
||||||
|
{ min: 3, max: 20, message: '用户名长度为3-20个字符' },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
prefix={<UserOutlined />}
|
||||||
|
placeholder="请输入用户名"
|
||||||
|
disabled={!!editingUser}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
name="nickname"
|
||||||
|
label="昵称"
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: '请输入昵称' },
|
||||||
|
{ max: 20, message: '昵称最多20个字符' },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input placeholder="请输入昵称" />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
name="phone"
|
||||||
|
label="手机号"
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: '请输入手机号' },
|
||||||
|
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号' },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input placeholder="请输入手机号" />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
{!editingUser && (
|
||||||
|
<Form.Item
|
||||||
|
name="password"
|
||||||
|
label="密码"
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: '请输入密码' },
|
||||||
|
{ min: 6, message: '密码至少6个字符' },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input.Password placeholder="请输入密码" />
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
name="role"
|
||||||
|
label="角色"
|
||||||
|
rules={[{ required: true, message: '请选择角色' }]}
|
||||||
|
>
|
||||||
|
<Select placeholder="请选择角色" options={roleOptions} />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item className={styles.formActions}>
|
||||||
|
<Space>
|
||||||
|
<Button onClick={() => setModalVisible(false)}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
htmlType="submit"
|
||||||
|
loading={modalLoading}
|
||||||
|
>
|
||||||
|
{editingUser ? '更新' : '创建'}
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UserManagementPage;
|
||||||
30
src/services/ability.ts
Normal file
30
src/services/ability.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { request } from '@umijs/max';
|
||||||
|
import type { Ability, PageData } from '@/types';
|
||||||
|
|
||||||
|
// 获取能力列表
|
||||||
|
export async function getAbilities(params: {
|
||||||
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
category?: string;
|
||||||
|
status?: string;
|
||||||
|
search?: string;
|
||||||
|
}): Promise<API.ApiResponse<PageData<Ability>>> {
|
||||||
|
return request('/admin-backend/abilities', {
|
||||||
|
method: 'GET',
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取能力详情
|
||||||
|
export async function getAbilityDetail(id: string): Promise<API.ApiResponse<Ability>> {
|
||||||
|
return request(`/admin-backend/abilities/${id}`, {
|
||||||
|
method: 'GET',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取能力分类
|
||||||
|
export async function getAbilityCategories(): Promise<API.ApiResponse<string[]>> {
|
||||||
|
return request('/admin-backend/abilities/categories', {
|
||||||
|
method: 'GET',
|
||||||
|
});
|
||||||
|
}
|
||||||
39
src/services/auth.ts
Normal file
39
src/services/auth.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { request } from '@umijs/max';
|
||||||
|
import type { LoginFormData, LoginResponse, SmsResponse } from '@/types';
|
||||||
|
|
||||||
|
// 发送短信验证码
|
||||||
|
export async function sendSmsCode(phone: string): Promise<SmsResponse> {
|
||||||
|
return request('/user/send_code', {
|
||||||
|
method: 'POST',
|
||||||
|
data: { phoneNumber: phone },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 管理员登录
|
||||||
|
export async function login(data: LoginFormData): Promise<LoginResponse> {
|
||||||
|
return request('/api/v1/admin/login', {
|
||||||
|
method: 'POST',
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证token
|
||||||
|
export async function verifyToken(): Promise<API.ApiResponse<API.CurrentUser>> {
|
||||||
|
return request('/admin-backend/verify_token', {
|
||||||
|
method: 'GET',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 登出
|
||||||
|
export async function logout(): Promise<API.ApiResponse> {
|
||||||
|
return request('/admin/logout', {
|
||||||
|
method: 'POST',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前用户信息
|
||||||
|
export async function getCurrentUser(): Promise<API.ApiResponse<API.CurrentUser>> {
|
||||||
|
return request('/admin-backend/profile', {
|
||||||
|
method: 'GET',
|
||||||
|
});
|
||||||
|
}
|
||||||
9
src/services/dashboard.ts
Normal file
9
src/services/dashboard.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { request } from '@umijs/max';
|
||||||
|
import type { DashboardStats } from '@/types';
|
||||||
|
|
||||||
|
// 获取仪表盘统计数据
|
||||||
|
export async function getDashboardStatistics(): Promise<API.ApiResponse<DashboardStats>> {
|
||||||
|
return request('/admin-backend/statistics', {
|
||||||
|
method: 'GET',
|
||||||
|
});
|
||||||
|
}
|
||||||
96
src/services/demo/UserController.ts
Normal file
96
src/services/demo/UserController.ts
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
// 该文件由 OneAPI 自动生成,请勿手动修改!
|
||||||
|
import { request } from '@umijs/max';
|
||||||
|
|
||||||
|
/** 此处后端没有提供注释 GET /api/v1/queryUserList */
|
||||||
|
export async function queryUserList(
|
||||||
|
params: {
|
||||||
|
// query
|
||||||
|
/** keyword */
|
||||||
|
keyword?: string;
|
||||||
|
/** current */
|
||||||
|
current?: number;
|
||||||
|
/** pageSize */
|
||||||
|
pageSize?: number;
|
||||||
|
},
|
||||||
|
options?: { [key: string]: any },
|
||||||
|
) {
|
||||||
|
return request<API.Result_PageInfo_UserInfo__>('/api/v1/queryUserList', {
|
||||||
|
method: 'GET',
|
||||||
|
params: {
|
||||||
|
...params,
|
||||||
|
},
|
||||||
|
...(options || {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 此处后端没有提供注释 POST /api/v1/user */
|
||||||
|
export async function addUser(
|
||||||
|
body?: API.UserInfoVO,
|
||||||
|
options?: { [key: string]: any },
|
||||||
|
) {
|
||||||
|
return request<API.Result_UserInfo_>('/api/v1/user', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
data: body,
|
||||||
|
...(options || {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 此处后端没有提供注释 GET /api/v1/user/${param0} */
|
||||||
|
export async function getUserDetail(
|
||||||
|
params: {
|
||||||
|
// path
|
||||||
|
/** userId */
|
||||||
|
userId?: string;
|
||||||
|
},
|
||||||
|
options?: { [key: string]: any },
|
||||||
|
) {
|
||||||
|
const { userId: param0 } = params;
|
||||||
|
return request<API.Result_UserInfo_>(`/api/v1/user/${param0}`, {
|
||||||
|
method: 'GET',
|
||||||
|
params: { ...params },
|
||||||
|
...(options || {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 此处后端没有提供注释 PUT /api/v1/user/${param0} */
|
||||||
|
export async function modifyUser(
|
||||||
|
params: {
|
||||||
|
// path
|
||||||
|
/** userId */
|
||||||
|
userId?: string;
|
||||||
|
},
|
||||||
|
body?: API.UserInfoVO,
|
||||||
|
options?: { [key: string]: any },
|
||||||
|
) {
|
||||||
|
const { userId: param0 } = params;
|
||||||
|
return request<API.Result_UserInfo_>(`/api/v1/user/${param0}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
params: { ...params },
|
||||||
|
data: body,
|
||||||
|
...(options || {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 此处后端没有提供注释 DELETE /api/v1/user/${param0} */
|
||||||
|
export async function deleteUser(
|
||||||
|
params: {
|
||||||
|
// path
|
||||||
|
/** userId */
|
||||||
|
userId?: string;
|
||||||
|
},
|
||||||
|
options?: { [key: string]: any },
|
||||||
|
) {
|
||||||
|
const { userId: param0 } = params;
|
||||||
|
return request<API.Result_string_>(`/api/v1/user/${param0}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
params: { ...params },
|
||||||
|
...(options || {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
7
src/services/demo/index.ts
Normal file
7
src/services/demo/index.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
// 该文件由 OneAPI 自动生成,请勿手动修改!
|
||||||
|
|
||||||
|
import * as UserController from './UserController';
|
||||||
|
export default {
|
||||||
|
UserController,
|
||||||
|
};
|
||||||
68
src/services/demo/typings.d.ts
vendored
Normal file
68
src/services/demo/typings.d.ts
vendored
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
// 该文件由 OneAPI 自动生成,请勿手动修改!
|
||||||
|
|
||||||
|
declare namespace API {
|
||||||
|
interface PageInfo {
|
||||||
|
/**
|
||||||
|
1 */
|
||||||
|
current?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
total?: number;
|
||||||
|
list?: Array<Record<string, any>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PageInfo_UserInfo_ {
|
||||||
|
/**
|
||||||
|
1 */
|
||||||
|
current?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
total?: number;
|
||||||
|
list?: Array<UserInfo>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Result {
|
||||||
|
success?: boolean;
|
||||||
|
errorMessage?: string;
|
||||||
|
data?: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Result_PageInfo_UserInfo__ {
|
||||||
|
success?: boolean;
|
||||||
|
errorMessage?: string;
|
||||||
|
data?: PageInfo_UserInfo_;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Result_UserInfo_ {
|
||||||
|
success?: boolean;
|
||||||
|
errorMessage?: string;
|
||||||
|
data?: UserInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Result_string_ {
|
||||||
|
success?: boolean;
|
||||||
|
errorMessage?: string;
|
||||||
|
data?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserGenderEnum = 'MALE' | 'FEMALE';
|
||||||
|
|
||||||
|
interface UserInfo {
|
||||||
|
id?: string;
|
||||||
|
name?: string;
|
||||||
|
/** nick */
|
||||||
|
nickName?: string;
|
||||||
|
/** email */
|
||||||
|
email?: string;
|
||||||
|
gender?: UserGenderEnum;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserInfoVO {
|
||||||
|
name?: string;
|
||||||
|
/** nick */
|
||||||
|
nickName?: string;
|
||||||
|
/** email */
|
||||||
|
email?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type definitions_0 = null;
|
||||||
|
}
|
||||||
59
src/services/image.ts
Normal file
59
src/services/image.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { request } from '@umijs/max';
|
||||||
|
import type { Image, ImageFormData, PageData } from '@/types';
|
||||||
|
|
||||||
|
// 获取镜像列表
|
||||||
|
export async function getImages(params: {
|
||||||
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
status?: string;
|
||||||
|
category?: string;
|
||||||
|
search?: string;
|
||||||
|
}): Promise<API.ApiResponse<PageData<Image>>> {
|
||||||
|
return request('/admin-backend/images', {
|
||||||
|
method: 'GET',
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传镜像
|
||||||
|
export async function uploadImage(data: FormData): Promise<API.ApiResponse<Image>> {
|
||||||
|
return request('/admin/images/upload', {
|
||||||
|
method: 'POST',
|
||||||
|
data,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新镜像状态
|
||||||
|
export async function updateImageStatus(id: string, data: {
|
||||||
|
status: string;
|
||||||
|
reason?: string;
|
||||||
|
}): Promise<API.ApiResponse> {
|
||||||
|
return request(`/admin/images/${id}/status`, {
|
||||||
|
method: 'PUT',
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除镜像
|
||||||
|
export async function deleteImage(id: string): Promise<API.ApiResponse> {
|
||||||
|
return request(`/admin/images/${id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取镜像详情
|
||||||
|
export async function getImageDetail(id: string): Promise<API.ApiResponse<Image>> {
|
||||||
|
return request(`/admin-backend/images/${id}`, {
|
||||||
|
method: 'GET',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取镜像分类
|
||||||
|
export async function getImageCategories(): Promise<API.ApiResponse<string[]>> {
|
||||||
|
return request('/admin-backend/images/categories', {
|
||||||
|
method: 'GET',
|
||||||
|
});
|
||||||
|
}
|
||||||
95
src/services/script.ts
Normal file
95
src/services/script.ts
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import { request } from '@umijs/max';
|
||||||
|
import type { ScriptReview, ReviewFormData, Script, PageData } from '@/types';
|
||||||
|
|
||||||
|
// 剧本审核相关API
|
||||||
|
|
||||||
|
// 获取剧本审核列表
|
||||||
|
export async function getScriptReviews(params: {
|
||||||
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
status?: string;
|
||||||
|
}): Promise<API.ApiResponse<PageData<ScriptReview>>> {
|
||||||
|
return request('/admin-backend/script-reviews', {
|
||||||
|
method: 'GET',
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取剧本审核详情
|
||||||
|
export async function getScriptReviewDetail(id: string): Promise<API.ApiResponse<ScriptReview>> {
|
||||||
|
return request(`/admin-backend/script-reviews/${id}`, {
|
||||||
|
method: 'GET',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 审核通过
|
||||||
|
export async function approveScriptReview(id: string, data: ReviewFormData): Promise<API.ApiResponse> {
|
||||||
|
return request(`/admin-backend/script-reviews/${id}/approve`, {
|
||||||
|
method: 'POST',
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 审核拒绝
|
||||||
|
export async function rejectScriptReview(id: string, data: ReviewFormData): Promise<API.ApiResponse> {
|
||||||
|
return request(`/admin-backend/script-reviews/${id}/reject`, {
|
||||||
|
method: 'POST',
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取审核统计
|
||||||
|
export async function getReviewStatistics(): Promise<API.ApiResponse<{
|
||||||
|
pending: number;
|
||||||
|
approved: number;
|
||||||
|
rejected: number;
|
||||||
|
}>> {
|
||||||
|
return request('/admin-backend/script-reviews/statistics', {
|
||||||
|
method: 'GET',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 剧本后台相关API
|
||||||
|
|
||||||
|
// 获取剧本列表
|
||||||
|
export async function getScripts(params: {
|
||||||
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
category?: string;
|
||||||
|
status?: string;
|
||||||
|
}): Promise<API.ApiResponse<PageData<Script>>> {
|
||||||
|
return request('/admin-backend/scripts', {
|
||||||
|
method: 'GET',
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取剧本详情
|
||||||
|
export async function getScriptDetail(id: string): Promise<API.ApiResponse<Script>> {
|
||||||
|
return request(`/admin-backend/scripts/${id}`, {
|
||||||
|
method: 'GET',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建剧本
|
||||||
|
export async function createScript(data: Partial<Script>): Promise<API.ApiResponse<Script>> {
|
||||||
|
return request('/admin-backend/scripts', {
|
||||||
|
method: 'POST',
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新剧本
|
||||||
|
export async function updateScript(id: string, data: Partial<Script>): Promise<API.ApiResponse<Script>> {
|
||||||
|
return request(`/admin-backend/scripts/${id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除剧本
|
||||||
|
export async function deleteScript(id: string): Promise<API.ApiResponse> {
|
||||||
|
return request(`/admin-backend/scripts/${id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
});
|
||||||
|
}
|
||||||
45
src/services/user.ts
Normal file
45
src/services/user.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { request } from '@umijs/max';
|
||||||
|
import type { User, UserFormData, PageData } from '@/types';
|
||||||
|
|
||||||
|
// 获取用户列表
|
||||||
|
export async function getUsers(params: {
|
||||||
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
role?: string;
|
||||||
|
status?: string;
|
||||||
|
}): Promise<API.ApiResponse<PageData<User>>> {
|
||||||
|
return request('/admin-backend/users', {
|
||||||
|
method: 'GET',
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建用户
|
||||||
|
export async function createUser(data: UserFormData): Promise<API.ApiResponse<User>> {
|
||||||
|
return request('/admin-backend/users', {
|
||||||
|
method: 'POST',
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新用户
|
||||||
|
export async function updateUser(id: string, data: Partial<UserFormData>): Promise<API.ApiResponse<User>> {
|
||||||
|
return request(`/admin-backend/users/${id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除用户
|
||||||
|
export async function deleteUser(id: string): Promise<API.ApiResponse> {
|
||||||
|
return request(`/admin-backend/users/${id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户详情
|
||||||
|
export async function getUserDetail(id: string): Promise<API.ApiResponse<User>> {
|
||||||
|
return request(`/admin-backend/users/${id}`, {
|
||||||
|
method: 'GET',
|
||||||
|
});
|
||||||
|
}
|
||||||
169
src/types/index.ts
Normal file
169
src/types/index.ts
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
// API 响应类型
|
||||||
|
export interface ApiResponse<T = any> {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
data?: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 声明全局 API 类型
|
||||||
|
declare global {
|
||||||
|
namespace API {
|
||||||
|
type ApiResponse<T = any> = {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
data?: T;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CurrentUser = {
|
||||||
|
adminId: string;
|
||||||
|
username: string;
|
||||||
|
nickname: string;
|
||||||
|
phone: string;
|
||||||
|
role: 'super_admin' | 'admin' | 'auditor';
|
||||||
|
status: 'active' | 'inactive' | 'locked';
|
||||||
|
avatar?: string;
|
||||||
|
permissions?: string[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户相关类型
|
||||||
|
export interface CurrentUser {
|
||||||
|
adminId: string;
|
||||||
|
username: string;
|
||||||
|
nickname: string;
|
||||||
|
phone: string;
|
||||||
|
role: 'super_admin' | 'admin' | 'auditor';
|
||||||
|
status: 'active' | 'inactive' | 'locked';
|
||||||
|
avatar?: string;
|
||||||
|
permissions?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface User {
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
nickname: string;
|
||||||
|
phone: string;
|
||||||
|
role: string;
|
||||||
|
status: string;
|
||||||
|
avatar?: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserFormData {
|
||||||
|
username: string;
|
||||||
|
nickname: string;
|
||||||
|
phone: string;
|
||||||
|
password: string;
|
||||||
|
role: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 镜像相关类型
|
||||||
|
export interface Image {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
category: string;
|
||||||
|
fileSize: number;
|
||||||
|
status: 'online' | 'offline';
|
||||||
|
uploader: string;
|
||||||
|
uploadTime: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ImageFormData {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
version: string;
|
||||||
|
category: string;
|
||||||
|
file: File;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 剧本审核相关类型
|
||||||
|
export interface ScriptReview {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
submitterId: string;
|
||||||
|
version: string;
|
||||||
|
status: 'pending' | 'approved' | 'rejected';
|
||||||
|
submittedAt: string;
|
||||||
|
content: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReviewFormData {
|
||||||
|
comment: string;
|
||||||
|
rejectionReason?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 能力查询相关类型
|
||||||
|
export interface Ability {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
category: string;
|
||||||
|
creator: string;
|
||||||
|
rating: number;
|
||||||
|
usageCount: number;
|
||||||
|
createdAt: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 剧本后台相关类型
|
||||||
|
export interface Script {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
creator: string;
|
||||||
|
version: string;
|
||||||
|
status: string;
|
||||||
|
rating: number;
|
||||||
|
usageCount: number;
|
||||||
|
createdAt: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页相关类型
|
||||||
|
export interface Pagination {
|
||||||
|
page: number;
|
||||||
|
pageSize: number;
|
||||||
|
total: number;
|
||||||
|
totalPages: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PageData<T> {
|
||||||
|
items: T[];
|
||||||
|
pagination: Pagination;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 登录相关类型
|
||||||
|
export interface LoginFormData {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
phone: string;
|
||||||
|
smsCode: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginResponse {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
data?: {
|
||||||
|
adminId: string;
|
||||||
|
username: string;
|
||||||
|
nickname: string;
|
||||||
|
sessionToken: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统计数据类型
|
||||||
|
export interface DashboardStats {
|
||||||
|
userCount: number;
|
||||||
|
imageCount: number;
|
||||||
|
pendingReviews: number;
|
||||||
|
scriptCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 短信验证码相关类型
|
||||||
|
export interface SmsResponse {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
4
src/utils/format.ts
Normal file
4
src/utils/format.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
// 示例方法,没有实际意义
|
||||||
|
export function trim(str: string) {
|
||||||
|
return str.trim();
|
||||||
|
}
|
||||||
3
tsconfig.json
Normal file
3
tsconfig.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": "./src/.umi/tsconfig.json"
|
||||||
|
}
|
||||||
1
typings.d.ts
vendored
Normal file
1
typings.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import '@umijs/max/typings';
|
||||||
Reference in New Issue
Block a user