feat(PC): 初始化项目

master
may505 2025-07-30 09:07:36 +08:00
commit 2b39922d58
31 changed files with 36201 additions and 0 deletions

12
pc-fe/.gitignore vendored 100644
View File

@ -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

4
pc-fe/.npmrc 100644
View File

@ -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/

41
pc-fe/.umirc.ts 100644
View File

@ -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',
},
],
});

110
pc-fe/README.md 100644
View File

@ -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

21424
pc-fe/package-lock.json generated 100644

File diff suppressed because it is too large Load Diff

58
pc-fe/package.json 100644
View File

@ -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"
}
}
}

13322
pc-fe/pnpm-lock.yaml 100644

File diff suppressed because it is too large Load Diff

10
pc-fe/src/app.tsx 100644
View File

@ -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();
}

View File

@ -0,0 +1 @@

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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;

View File

@ -0,0 +1 @@
// getBrowserWindowRuntime().webContents.openDevTools();

View File

@ -0,0 +1,5 @@
import { ipcMain } from 'electron';
ipcMain.handle('getPlatform', () => {
return `hi, i'm from ${process.platform}`;
});

View File

@ -0,0 +1,7 @@
import { contextBridge, ipcRenderer } from 'electron';
contextBridge.exposeInMainWorld('$api', {
getPlatform: async () => {
return await ipcRenderer.invoke('getPlatform');
},
});

View File

@ -0,0 +1,3 @@
{
"extends": "@tsconfig/node14"
}

5
pc-fe/src/main/typing.d.ts vendored 100644
View File

@ -0,0 +1,5 @@
import { BrowserWindow } from 'electron';
declare global {
export function getBrowserWindowRuntime(): BrowserWindow;
}

View File

@ -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);
}

View File

@ -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;

View File

@ -0,0 +1,9 @@
const DocsPage = () => {
return (
<div>
<p>This is umi docs.</p>
</div>
);
};
export default DocsPage;

View File

@ -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;
}
}
}

View File

@ -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;

View File

@ -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; // 不渲染任何内容,因为会立即跳转
}

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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;
}
}
}

View File

@ -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;

View File

@ -0,0 +1,3 @@
{
"extends": "./src/.umi/tsconfig.json"
}

7
pc-fe/typings.d.ts vendored 100644
View File

@ -0,0 +1,7 @@
import 'umi/typings';
declare global {
interface Window {
$api: any;
}
}