feat(web端): 项目初始化
parent
2b39922d58
commit
5187fa408f
|
@ -0,0 +1,3 @@
|
|||
module.exports = {
|
||||
extends: require.resolve('@umijs/max/eslint'),
|
||||
};
|
|
@ -0,0 +1,14 @@
|
|||
/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
|
||||
/serve/node_modules
|
|
@ -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"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
registry=https://registry.npmmirror.com/
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
node_modules
|
||||
.umi
|
||||
.umi-production
|
|
@ -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"]
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
module.exports = {
|
||||
extends: require.resolve('@umijs/max/stylelint'),
|
||||
};
|
|
@ -0,0 +1,34 @@
|
|||
import { defineConfig } from '@umijs/max';
|
||||
|
||||
export default defineConfig({
|
||||
antd: {},
|
||||
access: {},
|
||||
model: {},
|
||||
initialState: {},
|
||||
request: {},
|
||||
layout: false,
|
||||
outputPath: 'serve/dist',
|
||||
// 路由配置
|
||||
routes: [
|
||||
{
|
||||
path: '/login',
|
||||
component: '@/pages/login',
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
component: '@/pages/components/Layout/index',
|
||||
routes: [
|
||||
{
|
||||
path: '/images',
|
||||
component: '@/pages/images',
|
||||
},
|
||||
{
|
||||
path: '/profile',
|
||||
component: '@/pages/profile',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
npmClient: 'pnpm',
|
||||
});
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# README
|
||||
|
||||
`@umijs/max` 模板项目,更多功能参考 [Umi Max 简介](https://umijs.org/docs/max/introduce)
|
|
@ -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,
|
||||
});
|
||||
},
|
||||
};
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"private": true,
|
||||
"author": "may505 <1072317596@qq.com>",
|
||||
"scripts": {
|
||||
"dev": "max dev",
|
||||
"build": "max build",
|
||||
"format": "prettier --cache --write .",
|
||||
"postinstall": "max setup",
|
||||
"setup": "max setup",
|
||||
"start": "npm run dev"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^5.0.1",
|
||||
"@ant-design/pro-components": "^2.4.4",
|
||||
"@umijs/max": "^4.4.11",
|
||||
"antd": "^5.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.0.33",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"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"
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,6 @@
|
|||
node_modules
|
||||
npm-debug.log
|
||||
.git
|
||||
.gitignore
|
||||
README.md
|
||||
.DS_Store
|
|
@ -0,0 +1,27 @@
|
|||
# 使用官方 Node.js 运行时作为基础镜像
|
||||
# FROM registry.cn-hangzhou.aliyuncs.com/library/node:18-alpine
|
||||
FROM swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/library/node:lts-slim
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 设置 npm 镜像源
|
||||
RUN npm config set registry https://registry.npmmirror.com
|
||||
|
||||
# 复制所有项目文件
|
||||
COPY . .
|
||||
|
||||
# 安装依赖
|
||||
RUN npm install
|
||||
RUN npm install -g pm2
|
||||
|
||||
# 设置执行权限
|
||||
RUN chmod +x ./start.sh
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 3000
|
||||
|
||||
# 1、使用 start.sh 脚本启动应用
|
||||
# CMD ["./start.sh"]
|
||||
# 2、使用node直接部署
|
||||
CMD ["node", "index.js"]
|
|
@ -0,0 +1,219 @@
|
|||
# 紫光汇智 VDI 管理平台前端服务
|
||||
|
||||
## 项目简介
|
||||
|
||||
本项目是紫光汇智 VDI(虚拟桌面基础设施)管理平台的前端服务,负责为企业级桌面云环境提供高效、可靠的 Web 管理界面。系统支持多种操作系统镜像的管理、用户权限控制、桌面分配与监控等功能,助力企业实现桌面云的集中化、自动化运维。
|
||||
|
||||
## 主要功能
|
||||
- 镜像管理:支持多种操作系统镜像的上传、下载、删除、版本管理
|
||||
- 用户与权限管理:多角色权限分级,安全可靠
|
||||
- 桌面分配与监控:支持桌面分配、状态监控、资源统计
|
||||
- 登录认证:支持多种登录方式,安全便捷
|
||||
- 响应式设计:适配 PC 和移动端
|
||||
|
||||
## 技术栈
|
||||
- 前端框架:React + UmiJS + Ant Design
|
||||
- 服务端:Node.js + Express
|
||||
- 进程管理:PM2
|
||||
- 容器化:Docker 支持
|
||||
|
||||
## 部署方式
|
||||
|
||||
### 1. 使用 Docker 部署
|
||||
- 支持一键构建和运行 Docker 容器,适合云服务器、生产环境
|
||||
- 详见下方 Docker 相关命令
|
||||
|
||||
### 2. 使用 PM2 部署
|
||||
- 适合传统物理机或虚拟机环境,支持进程守护、日志管理
|
||||
|
||||
---
|
||||
|
||||
# VDI 管理平台 - 服务器部署
|
||||
|
||||
## 📋 概述
|
||||
|
||||
这是 VDI 管理平台的前端服务器,使用 Express.js 提供静态文件服务,并通过 PM2 进行进程管理。
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 1. 安装依赖
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
### 2. 启动服务
|
||||
|
||||
#### 方式一:使用启动脚本(推荐)
|
||||
```bash
|
||||
chmod +x start.sh
|
||||
./start.sh
|
||||
```
|
||||
|
||||
#### 方式二:手动启动
|
||||
```bash
|
||||
# 安装 PM2(如果未安装)
|
||||
npm install -g pm2
|
||||
|
||||
# 启动应用
|
||||
pm2 start ecosystem.config.js
|
||||
```
|
||||
|
||||
### 3. 访问应用
|
||||
|
||||
打开浏览器访问:http://localhost:3000
|
||||
|
||||
## 📊 PM2 管理命令
|
||||
|
||||
### 查看应用状态
|
||||
```bash
|
||||
pm2 status
|
||||
```
|
||||
|
||||
### 查看日志
|
||||
```bash
|
||||
# 查看所有日志
|
||||
pm2 logs vdi-web
|
||||
|
||||
# 实时查看日志
|
||||
pm2 logs vdi-web --lines 100 -f
|
||||
```
|
||||
|
||||
### 重启应用
|
||||
```bash
|
||||
pm2 restart vdi-web
|
||||
```
|
||||
|
||||
### 停止应用
|
||||
```bash
|
||||
pm2 stop vdi-web
|
||||
```
|
||||
|
||||
### 删除应用
|
||||
```bash
|
||||
pm2 delete vdi-web
|
||||
```
|
||||
|
||||
### 监控应用
|
||||
```bash
|
||||
pm2 monit
|
||||
```
|
||||
|
||||
## 🔧 开发模式
|
||||
|
||||
### 使用 nodemon 开发
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### 直接启动
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
## 📁 目录结构
|
||||
|
||||
```
|
||||
serve/
|
||||
├── dist/ # 前端构建文件
|
||||
├── logs/ # 日志文件
|
||||
├── index.js # Express 服务器
|
||||
├── package.json # 依赖配置
|
||||
├── ecosystem.config.js # PM2 配置
|
||||
├── start.sh # 启动脚本
|
||||
├── stop.sh # 停止脚本
|
||||
└── README.md # 说明文档
|
||||
```
|
||||
|
||||
## ⚙️ 配置说明
|
||||
|
||||
### 环境变量
|
||||
|
||||
- `PORT`: 服务器端口(默认:3000)
|
||||
- `NODE_ENV`: 运行环境(development/production)
|
||||
|
||||
### PM2 配置
|
||||
|
||||
- **实例数**: 1个实例
|
||||
- **内存限制**: 1GB
|
||||
- **自动重启**: 启用
|
||||
- **日志轮转**: 启用
|
||||
|
||||
## 🔒 安全特性
|
||||
|
||||
- 使用 Helmet 增强安全性
|
||||
- 启用 gzip 压缩
|
||||
- 静态文件缓存
|
||||
- 错误处理中间件
|
||||
|
||||
## 📝 日志
|
||||
|
||||
日志文件位于 `logs/` 目录:
|
||||
- `combined.log`: 合并日志
|
||||
- `out.log`: 标准输出日志
|
||||
- `error.log`: 错误日志
|
||||
|
||||
## 🚨 故障排除
|
||||
|
||||
### 端口被占用
|
||||
```bash
|
||||
# 查看端口占用
|
||||
lsof -i :3000
|
||||
|
||||
# 杀死进程
|
||||
kill -9 <PID>
|
||||
```
|
||||
|
||||
### PM2 进程异常
|
||||
```bash
|
||||
# 重置 PM2
|
||||
pm2 kill
|
||||
pm2 start ecosystem.config.js
|
||||
```
|
||||
|
||||
### 权限问题
|
||||
```bash
|
||||
# 给脚本执行权限
|
||||
chmod +x start.sh stop.sh
|
||||
```
|
||||
|
||||
## 📞 技术支持
|
||||
|
||||
如有问题,请联系紫光汇智技术支持团队。
|
||||
|
||||
## 项目结构
|
||||
|
||||
```text
|
||||
web-fe/
|
||||
├── mock/ # 模拟接口数据
|
||||
├── package.json # 前端依赖配置
|
||||
├── pnpm-lock.yaml # pnpm锁定文件
|
||||
├── README.md # 前端说明文档
|
||||
├── serve/ # 服务端(Express+PM2+Docker)
|
||||
│ ├── dist/ # 前端构建产物(静态文件)
|
||||
│ ├── logs/ # 日志目录
|
||||
│ ├── node_modules/ # 服务端依赖
|
||||
│ ├── Dockerfile # Docker 构建文件
|
||||
│ ├── ecosystem.config.js# PM2 配置
|
||||
│ ├── index.js # Express 启动入口
|
||||
│ ├── package.json # 服务端依赖配置
|
||||
│ ├── start.sh # 启动脚本
|
||||
│ ├── stop.sh # 停止脚本
|
||||
│ ├── README.md # 服务端说明文档
|
||||
│ └── ...
|
||||
├── src/ # 前端源码
|
||||
│ ├── access.ts
|
||||
│ ├── app.ts
|
||||
│ ├── assets/
|
||||
│ ├── components/
|
||||
│ ├── constants/
|
||||
│ ├── models/
|
||||
│ ├── pages/ # 页面目录
|
||||
│ ├── services/ # 接口服务
|
||||
│ ├── utils/ # 工具函数
|
||||
│ └── ...
|
||||
├── tsconfig.json # TypeScript 配置
|
||||
└── typings.d.ts # 全局类型声明
|
||||
```
|
||||
|
||||
---
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,13 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<link rel="stylesheet" href="/umi.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script src="/umi.js"></script>
|
||||
</body>
|
||||
</html>
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1 @@
|
|||
.main-layout{min-height:100vh}.main-sider .logo{height:64px;display:flex;align-items:center;justify-content:center;color:#fff;font-size:18px;font-weight:700;border-bottom:1px solid #303030}.main-header{background:#fff;padding:0 24px;display:flex;align-items:center;justify-content:space-between;box-shadow:0 2px 8px #0000001a}.main-header .trigger{font-size:18px;color:#666}.main-header .trigger:hover{color:#1890ff}.main-header .header-right{display:flex;align-items:center;gap:16px}.main-header .header-right .welcome-text{color:#666;font-size:14px}.main-header .header-right .user-avatar{cursor:pointer;background:#1890ff}.main-header .header-right .user-avatar:hover{opacity:.8}.main-content{background:#fff;border-radius:8px;box-shadow:0 2px 8px #0000001a;min-height:calc(100vh - 112px)}
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1 @@
|
|||
.main-layout{min-height:100vh}.main-sider .logo{height:64px;display:flex;align-items:center;justify-content:center;color:#fff;font-size:18px;font-weight:700;border-bottom:1px solid #303030}.main-header{background:#fff;padding:0 24px;display:flex;align-items:center;justify-content:space-between;box-shadow:0 2px 8px #0000001a}.main-header .trigger{font-size:18px;color:#666}.main-header .trigger:hover{color:#1890ff}.main-header .header-right{display:flex;align-items:center;gap:16px}.main-header .header-right .welcome-text{color:#666;font-size:14px}.main-header .header-right .user-avatar{cursor:pointer;background:#1890ff}.main-header .header-right .user-avatar:hover{opacity:.8}.main-content{background:#fff;border-radius:8px;box-shadow:0 2px 8px #0000001a;min-height:calc(100vh - 112px)}
|
|
@ -0,0 +1 @@
|
|||
"use strict";(self.webpackChunk=self.webpackChunk||[]).push([[661],{9665:function(b,m,t){t.r(m),t.d(m,{default:function(){return z}});var T=t(48305),d=t.n(T),u=t(75271),E=t(97102),v=t(39032),h=t(36646),B=t(44126),r=t(74970),D=t(50),y=t(78032),A=t(12944),Z=t(21317),I=t(10770),e=t(52676),p=function(){var N=(0,u.useState)([]),x=d()(N,2),c=x[0],F=x[1],O=(0,u.useState)(!1),f=d()(O,2),L=f[0],C=f[1],P=(0,u.useState)(null),j=d()(P,2),i=j[0],k=j[1],G=(0,u.useState)(!1),g=d()(G,2),V=g[0],o=g[1];(0,u.useEffect)(function(){w()},[]);var w=function(){C(!0),setTimeout(function(){var n=[{id:"1",name:"Windows 10 \u4E13\u4E1A\u7248",version:"v1.0.0",size:"15.2 GB",status:"active",createTime:"2024-01-15 10:30:00",description:"Windows 10 \u4E13\u4E1A\u7248\u955C\u50CF\uFF0C\u5305\u542B\u5E38\u7528\u529E\u516C\u8F6F\u4EF6"},{id:"2",name:"Ubuntu 22.04 LTS",version:"v2.1.0",size:"8.5 GB",status:"active",createTime:"2024-01-10 14:20:00",description:"Ubuntu 22.04 LTS \u670D\u52A1\u5668\u7248\u672C\uFF0C\u9002\u7528\u4E8E\u5F00\u53D1\u73AF\u5883"},{id:"3",name:"CentOS 8",version:"v1.5.0",size:"12.1 GB",status:"building",createTime:"2024-01-20 09:15:00",description:"CentOS 8 \u4F01\u4E1A\u7EA7\u670D\u52A1\u5668\u64CD\u4F5C\u7CFB\u7EDF"},{id:"4",name:"macOS Monterey",version:"v1.2.0",size:"18.7 GB",status:"inactive",createTime:"2024-01-05 16:45:00",description:"macOS Monterey \u5F00\u53D1\u73AF\u5883\u955C\u50CF"}];F(n),C(!1)},1e3)},S=function(n){var l={active:{color:"green",text:"\u53EF\u7528"},inactive:{color:"red",text:"\u4E0D\u53EF\u7528"},building:{color:"orange",text:"\u6784\u5EFA\u4E2D"}},s=l[n];return(0,e.jsx)(E.Z,{color:s.color,children:s.text})},M=function(n){k(n),o(!0)},K=function(n){v.ZP.info("\u7F16\u8F91\u955C\u50CF\uFF1A".concat(n.name))},U=function(n){h.Z.confirm({title:"\u786E\u8BA4\u5220\u9664",content:'\u786E\u5B9A\u8981\u5220\u9664\u955C\u50CF "'.concat(n.name,'" \u5417\uFF1F'),onOk:function(){F(c.filter(function(s){return s.id!==n.id})),v.ZP.success("\u5220\u9664\u6210\u529F")}})},W=[{title:"\u955C\u50CF\u540D\u79F0",dataIndex:"name",key:"name",width:200},{title:"\u7248\u672C",dataIndex:"version",key:"version",width:100},{title:"\u5927\u5C0F",dataIndex:"size",key:"size",width:100},{title:"\u72B6\u6001",dataIndex:"status",key:"status",width:100,render:function(n){return S(n)}},{title:"\u521B\u5EFA\u65F6\u95F4",dataIndex:"createTime",key:"createTime",width:180},{title:"\u64CD\u4F5C",key:"action",width:200,render:function(n,l){return(0,e.jsxs)(B.Z,{size:"small",children:[(0,e.jsx)(r.ZP,{type:"text",icon:(0,e.jsx)(Z.Z,{}),onClick:function(){return M(l)},title:"\u67E5\u770B\u8BE6\u60C5"}),(0,e.jsx)(D.Z,{title:"\u786E\u5B9A\u8981\u5220\u9664\u8FD9\u4E2A\u955C\u50CF\u5417\uFF1F",description:"\u5220\u9664\u540E\u65E0\u6CD5\u6062\u590D\uFF0C\u8BF7\u8C28\u614E\u64CD\u4F5C\u3002",onConfirm:function(){return U(l)},okText:"\u786E\u5B9A",cancelText:"\u53D6\u6D88",children:(0,e.jsx)(r.ZP,{type:"text",icon:(0,e.jsx)(I.Z,{}),title:"\u5220\u9664",danger:!0})})]})}}];return(0,e.jsxs)("div",{className:"image-list",children:[(0,e.jsx)(y.Z,{children:(0,e.jsx)(A.Z,{columns:W,dataSource:c,rowKey:"id",loading:L,pagination:{total:c.length,pageSize:10,showSizeChanger:!0,showQuickJumper:!0,showTotal:function(n){return"\u5171 ".concat(n," \u6761\u8BB0\u5F55")}}})}),(0,e.jsx)(h.Z,{title:"\u955C\u50CF\u8BE6\u60C5",open:V,onCancel:function(){return o(!1)},footer:[(0,e.jsx)(r.ZP,{onClick:function(){return o(!1)},children:"\u5173\u95ED"},"close")],width:600,children:i&&(0,e.jsxs)("div",{className:"image-detail",children:[(0,e.jsxs)("div",{className:"detail-item",children:[(0,e.jsx)("label",{children:"\u955C\u50CF\u540D\u79F0\uFF1A"}),(0,e.jsx)("span",{children:i.name})]}),(0,e.jsxs)("div",{className:"detail-item",children:[(0,e.jsx)("label",{children:"\u7248\u672C\uFF1A"}),(0,e.jsx)("span",{children:i.version})]}),(0,e.jsxs)("div",{className:"detail-item",children:[(0,e.jsx)("label",{children:"\u5927\u5C0F\uFF1A"}),(0,e.jsx)("span",{children:i.size})]}),(0,e.jsxs)("div",{className:"detail-item",children:[(0,e.jsx)("label",{children:"\u72B6\u6001\uFF1A"}),(0,e.jsx)("span",{children:S(i.status)})]}),(0,e.jsxs)("div",{className:"detail-item",children:[(0,e.jsx)("label",{children:"\u521B\u5EFA\u65F6\u95F4\uFF1A"}),(0,e.jsx)("span",{children:i.createTime})]}),(0,e.jsxs)("div",{className:"detail-item",children:[(0,e.jsx)("label",{children:"\u63CF\u8FF0\uFF1A"}),(0,e.jsx)("p",{children:i.description})]})]})})]})},z=p}}]);
|
|
@ -0,0 +1 @@
|
|||
.page-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:24px}.page-header h2{margin:0;font-size:24px;font-weight:600;color:#333}.image-list .image-detail .detail-item{margin-bottom:16px}.image-list .image-detail .detail-item label{font-weight:600;color:#333;display:inline-block;width:100px}.image-list .image-detail .detail-item span{color:#666}.image-list .image-detail .detail-item p{margin:8px 0 0 100px;color:#666;line-height:1.6}.profile-page .profile-content .profile-header{display:flex;align-items:center;gap:16px;margin-bottom:16px}.profile-page .profile-content .profile-header .profile-info h3{margin:0 0 4px;font-size:20px;font-weight:600;color:#333}.profile-page .profile-content .profile-header .profile-info p{margin:0;color:#666;font-size:14px}.profile-page .profile-content .quick-actions{display:flex;gap:12px;flex-wrap:wrap}@media (max-width: 768px){.page-header{flex-direction:column;align-items:flex-start;gap:12px}.profile-content .profile-header{flex-direction:column;text-align:center}.profile-content .quick-actions{justify-content:center}}
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1 @@
|
|||
.login-container{display:flex;min-height:100vh;background:linear-gradient(135deg,#1890ff,#722ed1)}.login-container:before{content:"";position:absolute;top:0;left:0;right:0;bottom:0;background:url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="grid" width="10" height="10" patternUnits="userSpaceOnUse"><path d="M 10 0 L 0 0 0 10" fill="none" stroke="rgba%28255,255,255,0.1%29" stroke-width="0.5"/></pattern></defs><rect width="100" height="100" fill="url%28%23grid%29"/></svg>');opacity:.3}.login-left{flex:1 1;display:flex;align-items:center;justify-content:center;position:relative;overflow:hidden}.brand-content{text-align:center;color:#fff;z-index:1;position:relative;max-width:400px;padding:40px}.brand-logo{margin-bottom:30px}.brand-logo .logo-icon{font-size:60px;margin-bottom:20px;display:block;color:#ffffffe6}.brand-logo .brand-title{font-size:36px;font-weight:700;margin:0;color:#fff;text-shadow:0 2px 4px rgba(0,0,0,.3)}.brand-subtitle{font-size:20px;font-weight:500;margin-bottom:30px;color:#ffffffe6}.brand-description{margin-bottom:40px}.brand-description p{font-size:16px;line-height:1.6;margin:8px 0;color:#fffc}.brand-features{display:flex;justify-content:space-around;flex-wrap:wrap;gap:20px}.brand-features .feature-item{display:flex;flex-direction:column;align-items:center;gap:8px}.brand-features .feature-item .feature-icon{font-size:24px}.brand-features .feature-item span:last-child{font-size:14px;color:#fffc}.login-right{flex:1 1;display:flex;align-items:center;justify-content:center;padding:40px}.login-form-container{width:100%;max-width:400px}.login-header{text-align:center;margin-bottom:40px}.login-header .login-title{font-size:28px;font-weight:600;color:#fff;margin:0 0 8px}.login-header .login-subtitle{font-size:16px;color:#fff;margin:0}.login-form .ant-form-item{margin-bottom:24px}.login-input{height:48px;border-radius:8px;border:1px solid #d9d9d9;font-size:16px}.login-input:hover,.login-input:focus,.login-input.ant-input-focused{border-color:#1890ff;box-shadow:0 0 0 2px #1890ff33}.login-input .input-icon{color:#bfbfbf;font-size:16px}.login-button{height:48px;border-radius:8px;font-size:16px;font-weight:500;background:linear-gradient(135deg,#1890ff,#722ed1);border:none;margin-top:8px}.login-button:hover{background:linear-gradient(135deg,#40a9ff,#9254de);transform:translateY(-1px);box-shadow:0 4px 12px #1890ff4d}.login-button:active{transform:translateY(0)}.login-tips{text-align:center;margin-top:24px;padding:16px;background-color:#f6f8fa;border-radius:8px;border:1px solid #e8e8e8}.login-tips p{margin:0;color:#595959;font-size:14px}.login-footer{text-align:center;margin-top:40px}.login-footer p{color:#8c8c8c;font-size:12px;margin:0}@media (max-width: 768px){.login-container{flex-direction:column}.login-left{flex:none;height:200px;padding:20px}.brand-content{padding:20px}.brand-logo .logo-icon{font-size:40px}.brand-title{font-size:24px}.brand-subtitle{font-size:16px}.brand-description p{font-size:14px}.login-right{flex:1 1;padding:20px}}
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1 @@
|
|||
.page-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:24px}.page-header h2{margin:0;font-size:24px;font-weight:600;color:#333}.image-list .image-detail .detail-item{margin-bottom:16px}.image-list .image-detail .detail-item label{font-weight:600;color:#333;display:inline-block;width:100px}.image-list .image-detail .detail-item span{color:#666}.image-list .image-detail .detail-item p{margin:8px 0 0 100px;color:#666;line-height:1.6}.profile-page .profile-content .profile-header{display:flex;align-items:center;gap:16px;margin-bottom:16px}.profile-page .profile-content .profile-header .profile-info h3{margin:0 0 4px;font-size:20px;font-weight:600;color:#333}.profile-page .profile-content .profile-header .profile-info p{margin:0;color:#666;font-size:14px}.profile-page .profile-content .quick-actions{display:flex;gap:12px;flex-wrap:wrap}@media (max-width: 768px){.page-header{flex-direction:column;align-items:flex-start;gap:12px}.profile-content .profile-header{flex-direction:column;text-align:center}.profile-content .quick-actions{justify-content:center}}
|
|
@ -0,0 +1 @@
|
|||
html,body{width:100%;height:100%}input::-ms-clear,input::-ms-reveal{display:none}*,*:before,*:after{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-ms-overflow-style:scrollbar;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{margin:0}[tabindex="-1"]:focus{outline:none}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5em;font-weight:500}p{margin-top:0;margin-bottom:1em}abbr[title],abbr[data-original-title]{text-decoration:underline dotted;border-bottom:0;cursor:help}address{margin-bottom:1em;font-style:normal;line-height:inherit}input[type=text],input[type=password],input[type=number],textarea{-webkit-appearance:none}ol,ul,dl{margin-top:0;margin-bottom:1em}ol ol,ul ul,ol ul,ul ol{margin-bottom:0}dt{font-weight:500}dd{margin-bottom:.5em;margin-left:0}blockquote{margin:0 0 1em}dfn{font-style:italic}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}pre,code,kbd,samp{font-size:1em;font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace}pre{margin-top:0;margin-bottom:1em;overflow:auto}figure{margin:0 0 1em}img{vertical-align:middle;border-style:none}a,area,button,[role=button],input:not([type=range]),label,select,summary,textarea{touch-action:manipulation}table{border-collapse:collapse}caption{padding-top:.75em;padding-bottom:.3em;text-align:left;caption-side:bottom}input,button,select,optgroup,textarea{margin:0;color:inherit;font-size:inherit;font-family:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}button,html [type=button],[type=reset],[type=submit]{-webkit-appearance:button}button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{padding:0;border-style:none}input[type=radio],input[type=checkbox]{box-sizing:border-box;padding:0}input[type=date],input[type=time],input[type=datetime-local],input[type=month]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;margin:0;padding:0;border:0}legend{display:block;width:100%;max-width:100%;margin-bottom:.5em;padding:0;color:inherit;font-size:1.5em;line-height:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item}template{display:none}[hidden]{display:none!important}mark{padding:.2em;background-color:#feffe6}
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,21 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Docker 专用启动脚本
|
||||
|
||||
echo "🐳 Docker 容器启动中..."
|
||||
|
||||
# 创建日志目录
|
||||
mkdir -p logs
|
||||
|
||||
# 设置环境变量
|
||||
export NODE_ENV=production
|
||||
export HOST=0.0.0.0
|
||||
export PORT=3000
|
||||
|
||||
# 直接启动 Node.js 应用
|
||||
echo "🚀 启动 Node.js 应用..."
|
||||
echo "📍 绑定地址: 0.0.0.0:3000"
|
||||
echo "🌐 访问地址: http://localhost:3000"
|
||||
|
||||
# 启动应用并保持前台运行
|
||||
exec node index.js
|
|
@ -0,0 +1,60 @@
|
|||
module.exports = {
|
||||
apps: [
|
||||
{
|
||||
name: 'vdi-web',
|
||||
script: 'index.js',
|
||||
cwd: __dirname,
|
||||
instances: 1,
|
||||
exec_mode: 'fork',
|
||||
env: {
|
||||
NODE_ENV: 'production',
|
||||
PORT: 3000
|
||||
},
|
||||
env_development: {
|
||||
NODE_ENV: 'development',
|
||||
PORT: 3000
|
||||
},
|
||||
// 日志配置
|
||||
log_file: './logs/combined.log',
|
||||
out_file: './logs/out.log',
|
||||
error_file: './logs/error.log',
|
||||
log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
|
||||
|
||||
// 自动重启配置
|
||||
autorestart: true,
|
||||
watch: false,
|
||||
max_memory_restart: '1G',
|
||||
|
||||
// 启动配置
|
||||
min_uptime: '10s',
|
||||
max_restarts: 10,
|
||||
|
||||
// 监控配置
|
||||
merge_logs: true,
|
||||
|
||||
// 优雅关闭
|
||||
kill_timeout: 5000,
|
||||
listen_timeout: 3000,
|
||||
|
||||
// 环境变量
|
||||
env_production: {
|
||||
NODE_ENV: 'production',
|
||||
PORT: 3000
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
// 部署配置(可选)
|
||||
deploy: {
|
||||
production: {
|
||||
user: 'node',
|
||||
host: 'localhost',
|
||||
ref: 'origin/main',
|
||||
repo: 'git@github.com:your-repo/vdi-web.git',
|
||||
path: '/var/www/vdi-web',
|
||||
'pre-deploy-local': '',
|
||||
'post-deploy': 'npm install && pm2 reload ecosystem.config.js --env production',
|
||||
'pre-setup': ''
|
||||
}
|
||||
}
|
||||
};
|
|
@ -0,0 +1,46 @@
|
|||
const express = require('express');
|
||||
const path = require('path');
|
||||
const compression = require('compression');
|
||||
const helmet = require('helmet');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
// 安全中间件
|
||||
app.use(helmet({
|
||||
contentSecurityPolicy: false, // 允许内联脚本和样式
|
||||
}));
|
||||
|
||||
// 压缩中间件
|
||||
app.use(compression());
|
||||
|
||||
// 静态文件服务
|
||||
app.use(express.static(path.join(__dirname, 'dist')));
|
||||
|
||||
// 处理 SPA 路由,所有未匹配的路由都返回 index.html
|
||||
app.get('*', (req, res) => {
|
||||
res.sendFile(path.join(__dirname, 'dist', 'index.html'));
|
||||
});
|
||||
|
||||
// 错误处理中间件
|
||||
app.use((err, req, res, next) => {
|
||||
console.error('服务器错误:', err.stack);
|
||||
res.status(500).send('服务器内部错误');
|
||||
});
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`🚀 VDI 管理平台服务器已启动`);
|
||||
console.log(`📍 访问地址: http://localhost:${PORT}`);
|
||||
console.log(`⏰ 启动时间: ${new Date().toLocaleString()}`);
|
||||
});
|
||||
|
||||
// 优雅关闭
|
||||
process.on('SIGTERM', () => {
|
||||
console.log('收到 SIGTERM 信号,正在关闭服务器...');
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
console.log('收到 SIGINT 信号,正在关闭服务器...');
|
||||
process.exit(0);
|
||||
});
|
|
@ -0,0 +1,2 @@
|
|||
# 日志目录
|
||||
# 此文件用于保持 logs 目录在 git 中
|
|
@ -0,0 +1,3 @@
|
|||
2025-07-30 11:05:04 +08:00: 🚀 VDI 管理平台服务器已启动
|
||||
2025-07-30 11:05:04 +08:00: 📍 访问地址: http://localhost:3000
|
||||
2025-07-30 11:05:04 +08:00: ⏰ 启动时间: 2025/7/30 11:05:04
|
|
@ -0,0 +1,3 @@
|
|||
2025-07-30 11:05:04 +08:00: 🚀 VDI 管理平台服务器已启动
|
||||
2025-07-30 11:05:04 +08:00: 📍 访问地址: http://localhost:3000
|
||||
2025-07-30 11:05:04 +08:00: ⏰ 启动时间: 2025/7/30 11:05:04
|
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"name": "vdi-web-server",
|
||||
"version": "1.0.0",
|
||||
"description": "VDI 管理平台前端服务器",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "node index.js",
|
||||
"dev": "nodemon index.js",
|
||||
"pm2:start": "pm2 start ecosystem.config.js",
|
||||
"pm2:stop": "pm2 stop vdi-web",
|
||||
"pm2:restart": "pm2 restart vdi-web",
|
||||
"pm2:delete": "pm2 delete vdi-web",
|
||||
"pm2:logs": "pm2 logs vdi-web",
|
||||
"pm2:monit": "pm2 monit"
|
||||
},
|
||||
"keywords": [
|
||||
"vdi",
|
||||
"express",
|
||||
"pm2",
|
||||
"frontend"
|
||||
],
|
||||
"author": "紫光汇智",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"compression": "^1.7.4",
|
||||
"express": "^4.18.2",
|
||||
"helmet": "^7.1.0",
|
||||
"pm2": "^6.0.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.2"
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,31 @@
|
|||
#!/bin/bash
|
||||
|
||||
# VDI 管理平台启动脚本
|
||||
|
||||
echo "🚀 启动 VDI 管理平台..."
|
||||
|
||||
# # 检查 Node.js 是否安装
|
||||
# if ! command -v node &> /dev/null; then
|
||||
# echo "❌ 错误: Node.js 未安装,请先安装 Node.js"
|
||||
# exit 1
|
||||
# fi
|
||||
|
||||
# 检查 PM2 是否安装
|
||||
if ! command -v pm2 &> /dev/null; then
|
||||
echo "📦 正在安装 PM2..."
|
||||
npm install -g pm2
|
||||
fi
|
||||
|
||||
# # 安装依赖
|
||||
# echo "📦 安装依赖..."
|
||||
# npm install
|
||||
|
||||
# 创建日志目录
|
||||
mkdir -p logs
|
||||
|
||||
# 启动应用 - 使用前台模式
|
||||
echo "🚀 使用 PM2 启动应用..."
|
||||
pm2 start ecosystem.config.js --no-daemon
|
||||
|
||||
# 注意:--no-daemon 参数让 PM2 在前台运行,不会退出
|
||||
# 这样 Docker 容器就能保持运行状态
|
|
@ -0,0 +1,12 @@
|
|||
#!/bin/bash
|
||||
|
||||
# VDI 管理平台停止脚本
|
||||
|
||||
echo "🛑 停止 VDI 管理平台..."
|
||||
|
||||
# 停止应用
|
||||
pm2 stop vdi-web
|
||||
|
||||
echo "✅ 应用已停止!"
|
||||
echo "📊 查看状态: pm2 status"
|
||||
echo "🚀 重新启动: pm2 start vdi-web"
|
|
@ -0,0 +1,10 @@
|
|||
export default (initialState: API.UserInfo) => {
|
||||
// 在这里按照初始化数据定义项目中的权限,统一管理
|
||||
// 参考文档 https://umijs.org/docs/max/access
|
||||
const canSeeAdmin = !!(
|
||||
initialState && initialState.name !== 'dontHaveAccess'
|
||||
);
|
||||
return {
|
||||
canSeeAdmin,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,16 @@
|
|||
// 运行时配置
|
||||
|
||||
// 全局初始化数据配置,用于 Layout 用户信息和权限初始化
|
||||
// 更多信息见文档:https://umijs.org/docs/api/runtime-config#getinitialstate
|
||||
export async function getInitialState(): Promise<{ name: string }> {
|
||||
return { name: '@umijs/max' };
|
||||
}
|
||||
|
||||
// export const layout = () => {
|
||||
// return {
|
||||
// logo: 'https://img.alicdn.com/tfs/TB1YHEpwUT1gK0jSZFhXXaAtVXa-28-27.svg',
|
||||
// menu: {
|
||||
// locale: false,
|
||||
// },
|
||||
// };
|
||||
// };
|
|
@ -0,0 +1,4 @@
|
|||
.title {
|
||||
margin: 0 auto;
|
||||
font-weight: 200;
|
||||
}
|
|
@ -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;
|
|
@ -0,0 +1,64 @@
|
|||
.main-layout {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.main-sider {
|
||||
.logo {
|
||||
height: 64px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
border-bottom: 1px solid #303030;
|
||||
}
|
||||
}
|
||||
|
||||
.main-header {
|
||||
background: #fff;
|
||||
padding: 0 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
|
||||
.trigger {
|
||||
font-size: 18px;
|
||||
color: #666;
|
||||
|
||||
&:hover {
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
.welcome-text {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
cursor: pointer;
|
||||
background: #1890ff;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.main-content {
|
||||
// margin: 24px;
|
||||
// padding: 24px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
min-height: calc(100vh - 112px);
|
||||
}
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { Layout, Menu, Button, Avatar, Dropdown, message } from 'antd';
|
||||
import {
|
||||
AppstoreOutlined,
|
||||
UserOutlined,
|
||||
LogoutOutlined,
|
||||
MenuFoldOutlined,
|
||||
MenuUnfoldOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { history, useLocation, Outlet } from 'umi';
|
||||
import './index.less';
|
||||
|
||||
const { Header, Sider, Content } = Layout;
|
||||
|
||||
const MainLayout: React.FC = () => {
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const [username, setUsername] = useState('');
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
// 检查登录状态
|
||||
const isLoggedIn = localStorage.getItem('isLoggedIn');
|
||||
const currentUsername = localStorage.getItem('username');
|
||||
|
||||
if (!isLoggedIn) {
|
||||
message.error('请先登录!');
|
||||
history.push('/login');
|
||||
return;
|
||||
}
|
||||
|
||||
setUsername(currentUsername || '');
|
||||
}, []);
|
||||
|
||||
const handleMenuClick = (key: string) => {
|
||||
// 使用路由导航
|
||||
history.push(`/${key}`);
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
localStorage.removeItem('isLoggedIn');
|
||||
localStorage.removeItem('username');
|
||||
message.success('已退出登录');
|
||||
history.push('/login');
|
||||
};
|
||||
|
||||
const userMenu = (
|
||||
<Menu>
|
||||
<Menu.Item key="profile" icon={<UserOutlined />} onClick={() => history.push('/profile')}>
|
||||
个人资料
|
||||
</Menu.Item>
|
||||
<Menu.Divider />
|
||||
<Menu.Item key="logout" icon={<LogoutOutlined />} onClick={handleLogout}>
|
||||
退出登录
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
// 根据当前路径确定选中的菜单项
|
||||
const getSelectedKey = () => {
|
||||
const path = location.pathname;
|
||||
if (path === '/images') return 'images';
|
||||
if (path === '/profile') return 'profile';
|
||||
return 'images'; // 默认选中镜像列表
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout className="main-layout">
|
||||
<Sider
|
||||
trigger={null}
|
||||
collapsible
|
||||
collapsed={collapsed}
|
||||
className="main-sider"
|
||||
>
|
||||
<div className="logo">
|
||||
{!collapsed && <span>VDI 系统</span>}
|
||||
</div>
|
||||
<Menu
|
||||
theme="dark"
|
||||
mode="inline"
|
||||
selectedKeys={[getSelectedKey()]}
|
||||
onClick={({ key }) => handleMenuClick(key)}
|
||||
>
|
||||
<Menu.Item key="images" icon={<AppstoreOutlined />}>
|
||||
镜像列表
|
||||
</Menu.Item>
|
||||
<Menu.Item key="profile" icon={<UserOutlined />}>
|
||||
我的
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
</Sider>
|
||||
|
||||
<Layout>
|
||||
<Header className="main-header">
|
||||
<Button
|
||||
type="text"
|
||||
icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
|
||||
onClick={() => setCollapsed(!collapsed)}
|
||||
className="trigger"
|
||||
/>
|
||||
|
||||
<div className="header-right">
|
||||
<span className="welcome-text">欢迎,{username}</span>
|
||||
<Dropdown overlay={userMenu} placement="bottomRight">
|
||||
<Avatar icon={<UserOutlined />} className="user-avatar" />
|
||||
</Dropdown>
|
||||
</div>
|
||||
</Header>
|
||||
|
||||
<Content className="main-content">
|
||||
<Outlet />
|
||||
</Content>
|
||||
</Layout>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default MainLayout;
|
|
@ -0,0 +1,2 @@
|
|||
import Guide from './Guide';
|
||||
export default Guide;
|
|
@ -0,0 +1 @@
|
|||
export const DEFAULT_NAME = 'Umi Max';
|
|
@ -0,0 +1,63 @@
|
|||
.main-layout {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.main-sider {
|
||||
.logo {
|
||||
height: 64px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
border-bottom: 1px solid #303030;
|
||||
}
|
||||
}
|
||||
|
||||
.main-header {
|
||||
background: #fff;
|
||||
padding: 0 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
|
||||
.trigger {
|
||||
font-size: 18px;
|
||||
color: #666;
|
||||
|
||||
&:hover {
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
.welcome-text {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
cursor: pointer;
|
||||
background: #1890ff;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.main-content {
|
||||
// margin: 24px;
|
||||
// padding: 24px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
min-height: calc(100vh - 112px);
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { Layout, Menu, Button, Avatar, Dropdown, message } from 'antd';
|
||||
import {
|
||||
AppstoreOutlined,
|
||||
UserOutlined,
|
||||
LogoutOutlined,
|
||||
MenuFoldOutlined,
|
||||
MenuUnfoldOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { history, useLocation, Outlet } from 'umi';
|
||||
import './index.less';
|
||||
|
||||
const { Header, Sider, Content } = Layout;
|
||||
|
||||
const MainLayout: React.FC = () => {
|
||||
|
||||
const location = useLocation();
|
||||
|
||||
|
||||
return (
|
||||
<Layout className="main-layout">
|
||||
<Content className="main-content">
|
||||
<Outlet />
|
||||
</Content>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default MainLayout;
|
|
@ -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;
|
|
@ -0,0 +1,64 @@
|
|||
.main-layout {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.main-sider {
|
||||
.logo {
|
||||
height: 64px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
border-bottom: 1px solid #303030;
|
||||
}
|
||||
}
|
||||
|
||||
.main-header {
|
||||
background: #fff;
|
||||
padding: 0 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
|
||||
.trigger {
|
||||
font-size: 18px;
|
||||
color: #666;
|
||||
|
||||
&:hover {
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
.welcome-text {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
cursor: pointer;
|
||||
background: #1890ff;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.main-content {
|
||||
// margin: 24px;
|
||||
// padding: 24px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
min-height: calc(100vh - 112px);
|
||||
}
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { Layout, Menu, Button, Avatar, Dropdown, message } from 'antd';
|
||||
import {
|
||||
AppstoreOutlined,
|
||||
UserOutlined,
|
||||
LogoutOutlined,
|
||||
MenuFoldOutlined,
|
||||
MenuUnfoldOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { history, useLocation, Outlet } from 'umi';
|
||||
import './index.less';
|
||||
|
||||
const { Header, Sider, Content } = Layout;
|
||||
|
||||
const MainLayout: React.FC = () => {
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const [username, setUsername] = useState('');
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
// 检查登录状态
|
||||
const isLoggedIn = localStorage.getItem('isLoggedIn');
|
||||
const currentUsername = localStorage.getItem('username');
|
||||
|
||||
if (!isLoggedIn) {
|
||||
message.error('请先登录!');
|
||||
history.push('/login');
|
||||
return;
|
||||
}
|
||||
|
||||
setUsername(currentUsername || '');
|
||||
}, []);
|
||||
|
||||
const handleMenuClick = (key: string) => {
|
||||
// 使用路由导航
|
||||
history.push(`/${key}`);
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
localStorage.removeItem('isLoggedIn');
|
||||
localStorage.removeItem('username');
|
||||
message.success('已退出登录');
|
||||
history.push('/login');
|
||||
};
|
||||
|
||||
const userMenu = (
|
||||
<Menu>
|
||||
<Menu.Item key="profile" icon={<UserOutlined />} onClick={() => history.push('/profile')}>
|
||||
个人资料
|
||||
</Menu.Item>
|
||||
<Menu.Divider />
|
||||
<Menu.Item key="logout" icon={<LogoutOutlined />} onClick={handleLogout}>
|
||||
退出登录
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
// 根据当前路径确定选中的菜单项
|
||||
const getSelectedKey = () => {
|
||||
const path = location.pathname;
|
||||
if (path === '/images') return 'images';
|
||||
if (path === '/profile') return 'profile';
|
||||
return 'images'; // 默认选中镜像列表
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout className="main-layout">
|
||||
<Sider
|
||||
trigger={null}
|
||||
collapsible
|
||||
collapsed={collapsed}
|
||||
className="main-sider"
|
||||
>
|
||||
<div className="logo">
|
||||
{!collapsed && <span>VDI管理平台</span>}
|
||||
</div>
|
||||
<Menu
|
||||
theme="dark"
|
||||
mode="inline"
|
||||
selectedKeys={[getSelectedKey()]}
|
||||
onClick={({ key }) => handleMenuClick(key)}
|
||||
>
|
||||
<Menu.Item key="images" icon={<AppstoreOutlined />}>
|
||||
镜像列表
|
||||
</Menu.Item>
|
||||
<Menu.Item key="profile" icon={<UserOutlined />}>
|
||||
我的
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
</Sider>
|
||||
|
||||
<Layout>
|
||||
<Header className="main-header">
|
||||
<Button
|
||||
type="text"
|
||||
icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
|
||||
onClick={() => setCollapsed(!collapsed)}
|
||||
className="trigger"
|
||||
/>
|
||||
|
||||
<div className="header-right">
|
||||
<span className="welcome-text">欢迎,{username}</span>
|
||||
<Dropdown overlay={userMenu} placement="bottomRight">
|
||||
<Avatar icon={<UserOutlined />} className="user-avatar" />
|
||||
</Dropdown>
|
||||
</div>
|
||||
</Header>
|
||||
|
||||
<Content className="main-content">
|
||||
<Outlet />
|
||||
</Content>
|
||||
</Layout>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default MainLayout;
|
|
@ -0,0 +1,9 @@
|
|||
const DocsPage = () => {
|
||||
return (
|
||||
<div>
|
||||
<p>This is umi docs.</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DocsPage;
|
|
@ -0,0 +1,93 @@
|
|||
// 页面头部样式
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
// 镜像列表样式
|
||||
.image-list {
|
||||
.image-detail {
|
||||
.detail-item {
|
||||
margin-bottom: 16px;
|
||||
|
||||
label {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
display: inline-block;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
span {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 8px 0 0 100px;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 个人资料样式
|
||||
.profile-page {
|
||||
.profile-content {
|
||||
.profile-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.profile-info {
|
||||
h3 {
|
||||
margin: 0 0 4px 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.quick-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.page-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.profile-content {
|
||||
.profile-header {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.quick-actions {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,235 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, Table, Tag, Button, Space, Modal, message, Popconfirm } from 'antd';
|
||||
import {
|
||||
PlusOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
EyeOutlined
|
||||
} from '@ant-design/icons';
|
||||
import './index.less';
|
||||
|
||||
interface ImageItem {
|
||||
id: string;
|
||||
name: string;
|
||||
version: string;
|
||||
size: string;
|
||||
status: 'active' | 'inactive' | 'building';
|
||||
createTime: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const ImageList: React.FC = () => {
|
||||
const [images, setImages] = useState<ImageItem[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedImage, setSelectedImage] = useState<ImageItem | null>(null);
|
||||
const [detailVisible, setDetailVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
loadImages();
|
||||
}, []);
|
||||
|
||||
const loadImages = () => {
|
||||
setLoading(true);
|
||||
// 模拟数据加载
|
||||
setTimeout(() => {
|
||||
const mockData: ImageItem[] = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Windows 10 专业版',
|
||||
version: 'v1.0.0',
|
||||
size: '15.2 GB',
|
||||
status: 'active',
|
||||
createTime: '2024-01-15 10:30:00',
|
||||
description: 'Windows 10 专业版镜像,包含常用办公软件'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Ubuntu 22.04 LTS',
|
||||
version: 'v2.1.0',
|
||||
size: '8.5 GB',
|
||||
status: 'active',
|
||||
createTime: '2024-01-10 14:20:00',
|
||||
description: 'Ubuntu 22.04 LTS 服务器版本,适用于开发环境'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'CentOS 8',
|
||||
version: 'v1.5.0',
|
||||
size: '12.1 GB',
|
||||
status: 'building',
|
||||
createTime: '2024-01-20 09:15:00',
|
||||
description: 'CentOS 8 企业级服务器操作系统'
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: 'macOS Monterey',
|
||||
version: 'v1.2.0',
|
||||
size: '18.7 GB',
|
||||
status: 'inactive',
|
||||
createTime: '2024-01-05 16:45:00',
|
||||
description: 'macOS Monterey 开发环境镜像'
|
||||
}
|
||||
];
|
||||
setImages(mockData);
|
||||
setLoading(false);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const getStatusTag = (status: string) => {
|
||||
const statusMap = {
|
||||
active: { color: 'green', text: '可用' },
|
||||
inactive: { color: 'red', text: '不可用' },
|
||||
building: { color: 'orange', text: '构建中' }
|
||||
};
|
||||
const config = statusMap[status as keyof typeof statusMap];
|
||||
return <Tag color={config.color}>{config.text}</Tag>;
|
||||
};
|
||||
|
||||
const handleViewDetail = (record: ImageItem) => {
|
||||
setSelectedImage(record);
|
||||
setDetailVisible(true);
|
||||
};
|
||||
|
||||
|
||||
|
||||
const handleEdit = (record: ImageItem) => {
|
||||
message.info(`编辑镜像:${record.name}`);
|
||||
};
|
||||
|
||||
const handleDelete = (record: ImageItem) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确定要删除镜像 "${record.name}" 吗?`,
|
||||
onOk: () => {
|
||||
setImages(images.filter(img => img.id !== record.id));
|
||||
message.success('删除成功');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '镜像名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
title: '版本',
|
||||
dataIndex: 'version',
|
||||
key: 'version',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '大小',
|
||||
dataIndex: 'size',
|
||||
key: 'size',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 100,
|
||||
render: (status: string) => getStatusTag(status),
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createTime',
|
||||
key: 'createTime',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 200,
|
||||
render: (_: any, record: ImageItem) => (
|
||||
<Space size="small">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={() => handleViewDetail(record)}
|
||||
title="查看详情"
|
||||
/>
|
||||
<Popconfirm
|
||||
title="确定要删除这个镜像吗?"
|
||||
description="删除后无法恢复,请谨慎操作。"
|
||||
onConfirm={() => handleDelete(record)}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<DeleteOutlined />}
|
||||
title="删除"
|
||||
danger
|
||||
/>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="image-list">
|
||||
<Card>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={images}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={{
|
||||
total: images.length,
|
||||
pageSize: 10,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total) => `共 ${total} 条记录`,
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Modal
|
||||
title="镜像详情"
|
||||
open={detailVisible}
|
||||
onCancel={() => setDetailVisible(false)}
|
||||
footer={[
|
||||
<Button key="close" onClick={() => setDetailVisible(false)}>
|
||||
关闭
|
||||
</Button>
|
||||
]}
|
||||
width={600}
|
||||
>
|
||||
{selectedImage && (
|
||||
<div className="image-detail">
|
||||
<div className="detail-item">
|
||||
<label>镜像名称:</label>
|
||||
<span>{selectedImage.name}</span>
|
||||
</div>
|
||||
<div className="detail-item">
|
||||
<label>版本:</label>
|
||||
<span>{selectedImage.version}</span>
|
||||
</div>
|
||||
<div className="detail-item">
|
||||
<label>大小:</label>
|
||||
<span>{selectedImage.size}</span>
|
||||
</div>
|
||||
<div className="detail-item">
|
||||
<label>状态:</label>
|
||||
<span>{getStatusTag(selectedImage.status)}</span>
|
||||
</div>
|
||||
<div className="detail-item">
|
||||
<label>创建时间:</label>
|
||||
<span>{selectedImage.createTime}</span>
|
||||
</div>
|
||||
<div className="detail-item">
|
||||
<label>描述:</label>
|
||||
<p>{selectedImage.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageList;
|
|
@ -0,0 +1,19 @@
|
|||
import { useEffect } from 'react';
|
||||
import { history } from 'umi';
|
||||
|
||||
export default function HomePage() {
|
||||
useEffect(() => {
|
||||
// 检查登录状态
|
||||
const isLoggedIn = localStorage.getItem('isLoggedIn');
|
||||
|
||||
if (isLoggedIn) {
|
||||
// 已登录,跳转到镜像列表页面
|
||||
history.push('/images');
|
||||
} else {
|
||||
// 未登录,跳转到登录页面
|
||||
history.push('/login');
|
||||
}
|
||||
}, []);
|
||||
|
||||
return null; // 不渲染任何内容,因为会立即跳转
|
||||
}
|
|
@ -0,0 +1,238 @@
|
|||
.login-container {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #1890ff 0%, #722ed1 100%);
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="grid" width="10" height="10" patternUnits="userSpaceOnUse"><path d="M 10 0 L 0 0 0 10" fill="none" stroke="rgba(255,255,255,0.1)" stroke-width="0.5"/></pattern></defs><rect width="100" height="100" fill="url(%23grid)"/></svg>');
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
|
||||
/* 左侧品牌区域 */
|
||||
.login-left {
|
||||
flex: 1;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
|
||||
}
|
||||
|
||||
.brand-content {
|
||||
text-align: center;
|
||||
color: white;
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
max-width: 400px;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.brand-logo {
|
||||
margin-bottom: 30px;
|
||||
|
||||
.logo-icon {
|
||||
font-size: 60px;
|
||||
margin-bottom: 20px;
|
||||
display: block;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.brand-title {
|
||||
font-size: 36px;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
color: white;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.brand-subtitle {
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 30px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.brand-description {
|
||||
margin-bottom: 40px;
|
||||
|
||||
p {
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
margin: 8px 0;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
.brand-features {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px;
|
||||
|
||||
.feature-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.feature-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
span:last-child {
|
||||
font-size: 14px;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 右侧登录区域 */
|
||||
.login-right {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.login-form-container {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.login-header {
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
|
||||
.login-title {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.login-subtitle {
|
||||
font-size: 16px;
|
||||
color: #fff;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.login-form {
|
||||
.ant-form-item {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.login-input {
|
||||
height: 48px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #d9d9d9;
|
||||
font-size: 16px;
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&.ant-input-focused {
|
||||
border-color: #1890ff;
|
||||
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
|
||||
}
|
||||
|
||||
.input-icon {
|
||||
color: #bfbfbf;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.login-button {
|
||||
height: 48px;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
background: linear-gradient(135deg, #1890ff 0%, #722ed1 100%);
|
||||
border: none;
|
||||
margin-top: 8px;
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(135deg, #40a9ff 0%, #9254de 100%);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.login-tips {
|
||||
text-align: center;
|
||||
margin-top: 24px;
|
||||
padding: 16px;
|
||||
background-color: #f6f8fa;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e8e8e8;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: #595959;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.login-footer {
|
||||
text-align: center;
|
||||
margin-top: 40px;
|
||||
|
||||
p {
|
||||
color: #8c8c8c;
|
||||
font-size: 12px;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.login-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.login-left {
|
||||
flex: none;
|
||||
height: 200px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.brand-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.brand-logo .logo-icon {
|
||||
font-size: 40px;
|
||||
}
|
||||
|
||||
.brand-title {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.brand-subtitle {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.brand-description p {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.login-right {
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,130 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Form, Input, Button, message } from 'antd';
|
||||
import { UserOutlined, LockOutlined, SafetyCertificateOutlined } from '@ant-design/icons';
|
||||
import { history } from '@umijs/max';
|
||||
import './index.less';
|
||||
|
||||
interface LoginForm {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
const LoginPage: React.FC = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const onFinish = async (values: LoginForm) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// 模拟登录验证
|
||||
if (values.username === 'admin' && values.password === '123456') {
|
||||
message.success('登录成功!');
|
||||
// 存储登录状态
|
||||
localStorage.setItem('isLoggedIn', 'true');
|
||||
localStorage.setItem('username', values.username);
|
||||
// 跳转到镜像列表页面
|
||||
history.push('/images');
|
||||
} else {
|
||||
message.error('用户名或密码错误!');
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('登录失败,请重试!');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="login-container">
|
||||
{/* 左侧品牌区域 */}
|
||||
<div className="login-left">
|
||||
<div className="brand-content">
|
||||
<div className="brand-logo">
|
||||
<SafetyCertificateOutlined className="logo-icon" />
|
||||
<h1 className="brand-title">紫光汇智</h1>
|
||||
</div>
|
||||
<div className="brand-subtitle">VDI 虚拟桌面管理平台</div>
|
||||
<div className="brand-description">
|
||||
<p>专业的虚拟桌面基础设施解决方案</p>
|
||||
<p>提供安全、高效、灵活的桌面云服务</p>
|
||||
</div>
|
||||
<div className="brand-features">
|
||||
<div className="feature-item">
|
||||
<span className="feature-icon">🔒</span>
|
||||
<span>安全可靠</span>
|
||||
</div>
|
||||
<div className="feature-item">
|
||||
<span className="feature-icon">⚡</span>
|
||||
<span>高效管理</span>
|
||||
</div>
|
||||
<div className="feature-item">
|
||||
<span className="feature-icon">🔄</span>
|
||||
<span>灵活部署</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 右侧登录区域 */}
|
||||
<div className="login-right">
|
||||
<div className="login-form-container">
|
||||
<div className="login-header">
|
||||
<h2 className="login-title">系统登录</h2>
|
||||
<p className="login-subtitle">欢迎使用VDI管理平台</p>
|
||||
</div>
|
||||
|
||||
<Form
|
||||
name="login"
|
||||
onFinish={onFinish}
|
||||
autoComplete="off"
|
||||
size="large"
|
||||
className="login-form"
|
||||
>
|
||||
<Form.Item
|
||||
name="username"
|
||||
rules={[{ required: true, message: '请输入用户名!' }]}
|
||||
>
|
||||
<Input
|
||||
prefix={<UserOutlined className="input-icon" />}
|
||||
placeholder="请输入用户名"
|
||||
className="login-input"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="password"
|
||||
rules={[{ required: true, message: '请输入密码!' }]}
|
||||
>
|
||||
<Input.Password
|
||||
prefix={<LockOutlined className="input-icon" />}
|
||||
placeholder="请输入密码"
|
||||
className="login-input"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
loading={loading}
|
||||
className="login-button"
|
||||
block
|
||||
>
|
||||
{loading ? '登录中...' : '登录'}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
<div className="login-tips">
|
||||
<p>演示账号:admin / 123456</p>
|
||||
</div>
|
||||
|
||||
<div className="login-footer">
|
||||
<p>© 2025 紫光汇智科技有限公司 版权所有</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginPage;
|
|
@ -0,0 +1,93 @@
|
|||
// 页面头部样式
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
// 镜像列表样式
|
||||
.image-list {
|
||||
.image-detail {
|
||||
.detail-item {
|
||||
margin-bottom: 16px;
|
||||
|
||||
label {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
display: inline-block;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
span {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 8px 0 0 100px;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 个人资料样式
|
||||
.profile-page {
|
||||
.profile-content {
|
||||
.profile-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.profile-info {
|
||||
h3 {
|
||||
margin: 0 0 4px 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.quick-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.page-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.profile-content {
|
||||
.profile-header {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.quick-actions {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,270 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, Avatar, Descriptions, Button, Form, Input, Modal, message, Divider, List, Tag } from 'antd';
|
||||
import {
|
||||
UserOutlined,
|
||||
EditOutlined,
|
||||
SaveOutlined,
|
||||
KeyOutlined,
|
||||
BellOutlined,
|
||||
SecurityScanOutlined,
|
||||
HistoryOutlined
|
||||
} from '@ant-design/icons';
|
||||
import './index.less';
|
||||
|
||||
interface UserInfo {
|
||||
username: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
department: string;
|
||||
role: string;
|
||||
lastLoginTime: string;
|
||||
createTime: string;
|
||||
}
|
||||
|
||||
interface LoginHistory {
|
||||
id: string;
|
||||
loginTime: string;
|
||||
ip: string;
|
||||
location: string;
|
||||
device: string;
|
||||
}
|
||||
|
||||
const Profile: React.FC = () => {
|
||||
const [userInfo, setUserInfo] = useState<UserInfo>({
|
||||
username: '',
|
||||
email: 'admin@example.com',
|
||||
phone: '138****8888',
|
||||
department: '技术部',
|
||||
role: '管理员',
|
||||
lastLoginTime: '2024-01-20 15:30:00',
|
||||
createTime: '2023-01-01 00:00:00'
|
||||
});
|
||||
const [editModalVisible, setEditModalVisible] = useState(false);
|
||||
const [passwordModalVisible, setPasswordModalVisible] = useState(false);
|
||||
const [loginHistory, setLoginHistory] = useState<LoginHistory[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const username = localStorage.getItem('username') || '';
|
||||
setUserInfo(prev => ({ ...prev, username }));
|
||||
loadLoginHistory();
|
||||
}, []);
|
||||
|
||||
const loadLoginHistory = () => {
|
||||
// 模拟登录历史数据
|
||||
const mockHistory: LoginHistory[] = [
|
||||
{
|
||||
id: '1',
|
||||
loginTime: '2024-01-20 15:30:00',
|
||||
ip: '192.168.1.100',
|
||||
location: '北京市',
|
||||
device: 'Chrome 120.0.0.0'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
loginTime: '2024-01-19 09:15:00',
|
||||
ip: '192.168.1.100',
|
||||
location: '北京市',
|
||||
device: 'Chrome 120.0.0.0'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
loginTime: '2024-01-18 14:20:00',
|
||||
ip: '192.168.1.100',
|
||||
location: '北京市',
|
||||
device: 'Chrome 120.0.0.0'
|
||||
}
|
||||
];
|
||||
setLoginHistory(mockHistory);
|
||||
};
|
||||
|
||||
const handleEditProfile = (values: any) => {
|
||||
setUserInfo(prev => ({ ...prev, ...values }));
|
||||
setEditModalVisible(false);
|
||||
message.success('个人信息更新成功');
|
||||
};
|
||||
|
||||
const handleChangePassword = (values: any) => {
|
||||
if (values.newPassword !== values.confirmPassword) {
|
||||
message.error('两次输入的密码不一致');
|
||||
return;
|
||||
}
|
||||
setPasswordModalVisible(false);
|
||||
message.success('密码修改成功');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="profile-page">
|
||||
<div className="page-header">
|
||||
<h2>个人中心</h2>
|
||||
</div>
|
||||
|
||||
<div className="profile-content">
|
||||
{/* 基本信息卡片 */}
|
||||
<Card
|
||||
title="基本信息"
|
||||
extra={
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => setEditModalVisible(true)}
|
||||
>
|
||||
编辑信息
|
||||
</Button>
|
||||
}
|
||||
style={{ marginBottom: 24 }}
|
||||
>
|
||||
<div className="profile-header">
|
||||
<Avatar size={80} icon={<UserOutlined />} />
|
||||
<div className="profile-info">
|
||||
<h3>{userInfo.username}</h3>
|
||||
<p>{userInfo.role}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Descriptions column={2}>
|
||||
<Descriptions.Item label="用户名">{userInfo.username}</Descriptions.Item>
|
||||
<Descriptions.Item label="邮箱">{userInfo.email}</Descriptions.Item>
|
||||
<Descriptions.Item label="手机号">{userInfo.phone}</Descriptions.Item>
|
||||
<Descriptions.Item label="部门">{userInfo.department}</Descriptions.Item>
|
||||
<Descriptions.Item label="角色">{userInfo.role}</Descriptions.Item>
|
||||
<Descriptions.Item label="最后登录">{userInfo.lastLoginTime}</Descriptions.Item>
|
||||
<Descriptions.Item label="注册时间">{userInfo.createTime}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Card>
|
||||
|
||||
{/* 快捷操作卡片 */}
|
||||
<Card title="快捷操作" style={{ marginBottom: 24 }}>
|
||||
<div className="quick-actions">
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<KeyOutlined />}
|
||||
onClick={() => setPasswordModalVisible(true)}
|
||||
>
|
||||
修改密码
|
||||
</Button>
|
||||
<Button icon={<BellOutlined />}>
|
||||
消息设置
|
||||
</Button>
|
||||
<Button icon={<SecurityScanOutlined />}>
|
||||
安全设置
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 登录历史卡片 */}
|
||||
<Card title="登录历史" extra={<HistoryOutlined />}>
|
||||
<List
|
||||
dataSource={loginHistory}
|
||||
renderItem={(item) => (
|
||||
<List.Item>
|
||||
<List.Item.Meta
|
||||
title={`登录时间:${item.loginTime}`}
|
||||
description={
|
||||
<div>
|
||||
<p>IP地址:{item.ip}</p>
|
||||
<p>登录地点:{item.location}</p>
|
||||
<p>设备信息:{item.device}</p>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<Tag color="green">成功</Tag>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 编辑个人信息模态框 */}
|
||||
<Modal
|
||||
title="编辑个人信息"
|
||||
open={editModalVisible}
|
||||
onCancel={() => setEditModalVisible(false)}
|
||||
footer={null}
|
||||
>
|
||||
<Form
|
||||
layout="vertical"
|
||||
initialValues={userInfo}
|
||||
onFinish={handleEditProfile}
|
||||
>
|
||||
<Form.Item
|
||||
name="email"
|
||||
label="邮箱"
|
||||
rules={[
|
||||
{ required: true, message: '请输入邮箱' },
|
||||
{ type: 'email', message: '请输入有效的邮箱地址' }
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="phone"
|
||||
label="手机号"
|
||||
rules={[
|
||||
{ required: true, message: '请输入手机号' },
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入有效的手机号' }
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit" icon={<SaveOutlined />} block>
|
||||
保存
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
{/* 修改密码模态框 */}
|
||||
<Modal
|
||||
title="修改密码"
|
||||
open={passwordModalVisible}
|
||||
onCancel={() => setPasswordModalVisible(false)}
|
||||
footer={null}
|
||||
>
|
||||
<Form
|
||||
layout="vertical"
|
||||
onFinish={handleChangePassword}
|
||||
>
|
||||
<Form.Item
|
||||
name="oldPassword"
|
||||
label="当前密码"
|
||||
rules={[{ required: true, message: '请输入当前密码' }]}
|
||||
>
|
||||
<Input.Password />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="newPassword"
|
||||
label="新密码"
|
||||
rules={[
|
||||
{ required: true, message: '请输入新密码' },
|
||||
{ min: 6, message: '密码长度不能少于6位' }
|
||||
]}
|
||||
>
|
||||
<Input.Password />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="confirmPassword"
|
||||
label="确认新密码"
|
||||
rules={[{ required: true, message: '请确认新密码' }]}
|
||||
>
|
||||
<Input.Password />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit" icon={<SaveOutlined />} block>
|
||||
确认修改
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Profile;
|
|
@ -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 || {}),
|
||||
});
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
/* eslint-disable */
|
||||
// 该文件由 OneAPI 自动生成,请勿手动修改!
|
||||
|
||||
import * as UserController from './UserController';
|
||||
export default {
|
||||
UserController,
|
||||
};
|
|
@ -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;
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
// 示例方法,没有实际意义
|
||||
export function trim(str: string) {
|
||||
return str.trim();
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"extends": "./src/.umi/tsconfig.json"
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
import '@umijs/max/typings';
|
Loading…
Reference in New Issue