feat(PC): 初始化项目
commit
2b39922d58
|
@ -0,0 +1,12 @@
|
||||||
|
/node_modules
|
||||||
|
/.env.local
|
||||||
|
/.umirc.local.ts
|
||||||
|
/config/config.local.ts
|
||||||
|
/src/.umi
|
||||||
|
/src/.umi-production
|
||||||
|
/src/.umi-test
|
||||||
|
/dist
|
||||||
|
.swc
|
||||||
|
.electron
|
||||||
|
.electron-production
|
||||||
|
yarn-error.log
|
|
@ -0,0 +1,4 @@
|
||||||
|
registry=https://registry.npmmirror.com
|
||||||
|
strict-peer-dependencies=false
|
||||||
|
electron-mirror=https://registry.npmmirror.com/-/binary/electron/
|
||||||
|
electron-builder-binaries-mirror=https://registry.npmmirror.com/binary.html?path=electron-builder-binaries/
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { defineConfig } from 'umi';
|
||||||
|
import { Platform, Arch } from '@umijs/plugin-electron';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
npmClient: 'yarn',
|
||||||
|
plugins: ['@umijs/plugin-electron'],
|
||||||
|
electron: {
|
||||||
|
builder: {
|
||||||
|
targets: Platform.LINUX.createTarget(['AppImage'], Arch.x64),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 配置 Ant Design
|
||||||
|
mfsu: false,
|
||||||
|
hash: true,
|
||||||
|
styles: ['src/global.less'],
|
||||||
|
// 路由配置
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: '/login',
|
||||||
|
component: '@/pages/login',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
component: '@/pages/components/Layout/index',
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: '/images',
|
||||||
|
component: '@/pages/images',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/profile',
|
||||||
|
component: '@/pages/profile',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
redirect: '@/pages/login',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
|
@ -0,0 +1,110 @@
|
||||||
|
# VDI 系统前端
|
||||||
|
|
||||||
|
这是一个基于 UmiJS 和 Electron 的 VDI(虚拟桌面基础设施)系统前端应用。
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
- 🔐 用户登录认证
|
||||||
|
- 🖥️ 镜像列表管理
|
||||||
|
- 👤 个人中心
|
||||||
|
- 📱 响应式设计
|
||||||
|
- 🎨 现代化 UI 界面
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
- **框架**: UmiJS 4
|
||||||
|
- **UI 组件**: Ant Design
|
||||||
|
- **桌面应用**: Electron
|
||||||
|
- **语言**: TypeScript
|
||||||
|
- **样式**: Less
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
### 安装依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### 开发模式
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### 构建应用
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── pages/ # 页面组件
|
||||||
|
│ ├── index.tsx # 首页(自动跳转)
|
||||||
|
│ ├── login.tsx # 登录页面
|
||||||
|
│ ├── images.tsx # 镜像列表页面
|
||||||
|
│ ├── profile.tsx # 个人资料页面
|
||||||
|
│ ├── components/ # 页面组件
|
||||||
|
│ │ ├── ImageList.tsx # 镜像列表组件
|
||||||
|
│ │ └── Profile.tsx # 个人资料组件
|
||||||
|
│ └── ...
|
||||||
|
├── layouts/ # 布局组件
|
||||||
|
│ └── index.tsx # 主布局(包含侧边栏和顶部导航)
|
||||||
|
├── assets/ # 静态资源
|
||||||
|
└── main/ # Electron 主进程
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用说明
|
||||||
|
|
||||||
|
### 登录
|
||||||
|
|
||||||
|
1. 启动应用后会自动跳转到登录页面
|
||||||
|
2. 使用以下测试账号登录:
|
||||||
|
- 用户名:`admin`
|
||||||
|
- 密码:`123456`
|
||||||
|
|
||||||
|
### 功能导航
|
||||||
|
|
||||||
|
- **镜像列表** (`/images`): 查看和管理系统镜像
|
||||||
|
- **我的** (`/profile`): 个人资料和设置
|
||||||
|
|
||||||
|
|
||||||
|
### 镜像管理
|
||||||
|
|
||||||
|
- 查看镜像详情
|
||||||
|
- 下载镜像
|
||||||
|
- 编辑镜像信息
|
||||||
|
- 删除镜像
|
||||||
|
|
||||||
|
### 个人中心
|
||||||
|
|
||||||
|
- 查看个人信息
|
||||||
|
- 修改密码
|
||||||
|
- 查看登录历史
|
||||||
|
|
||||||
|
## 开发说明
|
||||||
|
|
||||||
|
### 添加新页面
|
||||||
|
|
||||||
|
1. 在 `src/pages/` 目录下创建新的页面组件
|
||||||
|
2. 在路由中注册新页面
|
||||||
|
3. 在菜单中添加对应的导航项
|
||||||
|
|
||||||
|
### 样式开发
|
||||||
|
|
||||||
|
- 使用 Less 编写样式
|
||||||
|
- 遵循 BEM 命名规范
|
||||||
|
- 支持响应式设计
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
- 当前版本使用 localStorage 进行登录状态管理
|
||||||
|
- 镜像数据为模拟数据,实际使用时需要连接后端 API
|
||||||
|
- 建议在生产环境中使用更安全的认证方式
|
||||||
|
|
||||||
|
## 许可证
|
||||||
|
|
||||||
|
MIT License
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,58 @@
|
||||||
|
{
|
||||||
|
"name": "vdi-manager",
|
||||||
|
"version": "1.0.5",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "umi dev",
|
||||||
|
"build": "umi build",
|
||||||
|
"postinstall": "umi setup",
|
||||||
|
"setup": "umi setup",
|
||||||
|
"start": "npm run dev",
|
||||||
|
"electron:dev": "umi dev electron",
|
||||||
|
"electron:build": "umi build electron"
|
||||||
|
},
|
||||||
|
"author": {
|
||||||
|
"name": "may505",
|
||||||
|
"email": "13658239852@163.com"
|
||||||
|
},
|
||||||
|
"homepage": "http://10.209.8.11/users/sign_in",
|
||||||
|
"dependencies": {
|
||||||
|
"@ant-design/icons": "^6.0.0",
|
||||||
|
"antd": "^5.26.6",
|
||||||
|
"umi": "^4.0.42"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tsconfig/node14": "^1.0.3",
|
||||||
|
"@types/react": "^18.0.0",
|
||||||
|
"@types/react-dom": "^18.0.0",
|
||||||
|
"@umijs/plugin-electron": "^0.2.0",
|
||||||
|
"electron": "23.1.1",
|
||||||
|
"typescript": "^4.1.2"
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public",
|
||||||
|
"registry": "https://registry.npmjs.org"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"src/**/*",
|
||||||
|
"tsconfig.json",
|
||||||
|
".npmrc",
|
||||||
|
".gitignore",
|
||||||
|
"README.md",
|
||||||
|
".umirc.ts",
|
||||||
|
"package.json",
|
||||||
|
"typings.d.ts"
|
||||||
|
],
|
||||||
|
"main": "src/main/index.js",
|
||||||
|
"build": {
|
||||||
|
"appId": "com.vdi.app",
|
||||||
|
"productName": "VDI",
|
||||||
|
"directories": {
|
||||||
|
"output": "dist-electron"
|
||||||
|
},
|
||||||
|
"linux": {
|
||||||
|
"target": "AppImage",
|
||||||
|
"category": "Utility",
|
||||||
|
"icon": "src/assets/unis.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,10 @@
|
||||||
|
import { history } from 'umi';
|
||||||
|
|
||||||
|
export function onRouteChange({ location }: { location: any }) {
|
||||||
|
// 路由变化时的处理逻辑
|
||||||
|
console.log('Route changed to:', location.pathname);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function render(oldRender: () => void) {
|
||||||
|
oldRender();
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 1.1 MiB |
Binary file not shown.
After Width: | Height: | Size: 177 KiB |
|
@ -0,0 +1,36 @@
|
||||||
|
@import 'antd/dist/reset.css';
|
||||||
|
|
||||||
|
// 全局样式
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB',
|
||||||
|
'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif, 'Apple Color Emoji',
|
||||||
|
'Segoe UI Emoji', 'Segoe UI Symbol';
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 滚动条样式
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: #f1f1f1;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: #c1c1c1;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #a8a8a8;
|
||||||
|
}
|
|
@ -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 @@
|
||||||
|
// getBrowserWindowRuntime().webContents.openDevTools();
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { ipcMain } from 'electron';
|
||||||
|
|
||||||
|
ipcMain.handle('getPlatform', () => {
|
||||||
|
return `hi, i'm from ${process.platform}`;
|
||||||
|
});
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { contextBridge, ipcRenderer } from 'electron';
|
||||||
|
|
||||||
|
contextBridge.exposeInMainWorld('$api', {
|
||||||
|
getPlatform: async () => {
|
||||||
|
return await ipcRenderer.invoke('getPlatform');
|
||||||
|
},
|
||||||
|
});
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"extends": "@tsconfig/node14"
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { BrowserWindow } from 'electron';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
export function getBrowserWindowRuntime(): BrowserWindow;
|
||||||
|
}
|
|
@ -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,230 @@
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Card, Table, Tag, Button, Space, Modal, message } from 'antd';
|
||||||
|
import {
|
||||||
|
PlusOutlined,
|
||||||
|
EditOutlined,
|
||||||
|
DeleteOutlined,
|
||||||
|
EyeOutlined,
|
||||||
|
DownloadOutlined
|
||||||
|
} 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 handleDownload = (record: ImageItem) => {
|
||||||
|
message.success(`开始下载镜像:${record.name}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
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="查看详情"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={<DownloadOutlined />}
|
||||||
|
onClick={() => handleDownload(record)}
|
||||||
|
title="下载"
|
||||||
|
/>
|
||||||
|
</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,78 @@
|
||||||
|
.login-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-content {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card {
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||||
|
border-radius: 12px;
|
||||||
|
|
||||||
|
.ant-card-head {
|
||||||
|
text-align: center;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
|
||||||
|
.ant-card-head-title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card-body {
|
||||||
|
padding: 32px 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-tips {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 16px;
|
||||||
|
padding: 12px;
|
||||||
|
background-color: #f6f8fa;
|
||||||
|
border-radius: 6px;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-form-item {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-input-affix-wrapper {
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #d9d9d9;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus,
|
||||||
|
&.ant-input-affix-wrapper-focused {
|
||||||
|
border-color: #667eea;
|
||||||
|
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-btn-primary {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
height: 44px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,87 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Form, Input, Button, Card, message } from 'antd';
|
||||||
|
import { UserOutlined, LockOutlined } from '@ant-design/icons';
|
||||||
|
import { history } from 'umi';
|
||||||
|
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-content">
|
||||||
|
<Card className="login-card" title="系统登录" bordered={false}>
|
||||||
|
<Form
|
||||||
|
name="login"
|
||||||
|
onFinish={onFinish}
|
||||||
|
autoComplete="off"
|
||||||
|
size="large"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
htmlType="submit"
|
||||||
|
loading={loading}
|
||||||
|
block
|
||||||
|
>
|
||||||
|
登录
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
|
||||||
|
<div className="login-tips">
|
||||||
|
<p>提示:用户名 admin,密码 123456</p>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</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,3 @@
|
||||||
|
{
|
||||||
|
"extends": "./src/.umi/tsconfig.json"
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
import 'umi/typings';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
$api: any;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue