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