feat(桌面): 基础+欢迎界面

master
chenyt 2025-08-21 18:06:49 +08:00
parent 57444011dc
commit a4f2096c5d
16 changed files with 364 additions and 171 deletions

View File

@ -1,5 +1,7 @@
import { defineConfig } from 'umi';
import { Platform, Arch } from '@umijs/plugin-electron';
const path = require('path');
const rootdir = path.join(__dirname, ".");
export default defineConfig({
npmClient: 'yarn',
@ -13,29 +15,39 @@ export default defineConfig({
mfsu: false,
hash: true,
styles: ['src/global.less'],
alias: {
"@": path.resolve(rootdir, "src"),
"@assets": path.resolve(rootdir, "src/assets"),
"@components": path.resolve(rootdir, "src/components"),
"@utils": path.resolve(rootdir, "src/utils"),
},
// 路由配置
routes: [
{
path: '/login',
component: '@/pages/login',
},
// {
// path: '/',
// component: '@/pages/welcome',
// },
// {
// path: '/login',
// component: '@/pages/login',
// },
{
path: '/',
component: '@/pages/components/Layout/index',
routes: [
{
path: '/images',
component: '@/pages/images',
path: '/welcome',
component: '@/pages/welcome',
},
{
path: '/profile',
component: '@/pages/profile',
path: '/login',
component: '@/pages/login',
},
{
path: '/',
redirect: '/welcome',
},
],
},
{
path: '/',
redirect: '@/pages/login',
},
],
});

View File

@ -1,16 +1,17 @@
{
"name": "@umijs/electron-template",
"version": "1.0.2",
"name": "vdi-manager",
"version": "1.0.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@umijs/electron-template",
"version": "1.0.2",
"name": "vdi-manager",
"version": "1.0.5",
"hasInstallScript": true,
"dependencies": {
"@ant-design/icons": "^6.0.0",
"antd": "^5.26.6",
"axios": "^1.11.0",
"umi": "^4.0.42"
},
"devDependencies": {
@ -7352,7 +7353,6 @@
"version": "0.4.0",
"resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"dev": true,
"license": "MIT"
},
"node_modules/at-least-node": {
@ -7426,6 +7426,17 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/axios": {
"version": "1.11.0",
"resolved": "https://registry.npmmirror.com/axios/-/axios-1.11.0.tgz",
"integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/babel-jest": {
"version": "29.7.0",
"resolved": "https://registry.npmmirror.com/babel-jest/-/babel-jest-29.7.0.tgz",
@ -8528,7 +8539,6 @@
"version": "1.0.8",
"resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dev": true,
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
@ -9479,7 +9489,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.4.0"
@ -11359,6 +11368,26 @@
"license": "ISC",
"peer": true
},
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/for-each": {
"version": "0.3.5",
"resolved": "https://registry.npmmirror.com/for-each/-/for-each-0.3.5.tgz",
@ -11481,7 +11510,6 @@
"version": "4.0.4",
"resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"dev": true,
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
@ -16648,6 +16676,12 @@
"node": ">= 0.10"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/prr": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/prr/-/prr-1.0.1.tgz",

View File

@ -18,6 +18,7 @@
"dependencies": {
"@ant-design/icons": "^6.0.0",
"antd": "^5.26.6",
"axios": "^1.11.0",
"umi": "^4.0.42"
},
"devDependencies": {

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,15 @@
import { BrowserWindowConstructorOptions } from "electron";
export default{
browserWindow:{
kiosk: true,
frame: false,
titleBarStyle: 'hidden',
autoHideMenuBar: true,
// 禁止调整窗口大小
resizable: false,
// 禁止最大化和最小化按钮
maximizable: false,
minimizable: false,
closable: false,
} as BrowserWindowConstructorOptions,
}

View File

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

View File

@ -1,5 +1,23 @@
import { ipcMain } from 'electron';
import { ipcMain,app } from 'electron';
const window = getBrowserWindowRuntime();
// 监听渲染进程发送的消息
ipcMain.handle('getPlatform', () => {
return `hi, i'm from ${process.platform}`;
});
// 窗口控制:最小化,退出全屏
ipcMain.on('close-app', () => {
app.quit();
});
ipcMain.on('minimize-app', () => {
window?.minimize();
});
ipcMain.on('exit-kiosk', () => {
if (window) {
window.setFullScreen(false);
}
});

View File

@ -1,7 +1,31 @@
import { contextBridge, ipcRenderer } from 'electron';
contextBridge.exposeInMainWorld('$api', {
// 页面调用的方法
contextBridge.exposeInMainWorld('electronAPI', {
getPlatform: async () => {
return await ipcRenderer.invoke('getPlatform');
},
closeApp: () => ipcRenderer.send('close-app'),
minimizeApp: () => ipcRenderer.send('minimize-app'),
exitKiosk: () => ipcRenderer.send('exit-kiosk'),
// 事件监听
onMainProcessMessage: (callback: (data: string) => void) => {
ipcRenderer.on('main-process-message', (_, data) => callback(data));
},
on(...args: Parameters<typeof ipcRenderer.on>) {
const [channel, listener] = args
return ipcRenderer.on(channel, (event, ...args) => listener(event, ...args))
},
off(...args: Parameters<typeof ipcRenderer.off>) {
const [channel, ...omit] = args
return ipcRenderer.off(channel, ...omit)
},
send(...args: Parameters<typeof ipcRenderer.send>) {
const [channel, ...omit] = args
return ipcRenderer.send(channel, ...omit)
},
invoke(...args: Parameters<typeof ipcRenderer.invoke>) {
const [channel, ...omit] = args
return ipcRenderer.invoke(channel, ...omit)
}
});

View File

@ -1,64 +1,9 @@
.main-layout {
min-height: 100vh;
}
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);
}
.main-content {
min-height: 100vh;
}

View File

@ -1,34 +1,33 @@
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 { Layout, message } from 'antd';
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 || '');
// }, []);
useEffect(() => {
// 检查登录状态
const isLoggedIn = localStorage.getItem('isLoggedIn');
const currentUsername = localStorage.getItem('username');
if (!isLoggedIn) {
message.error('请先登录!');
history.push('/login');
return;
}
setUsername(currentUsername || '');
// TODO: 第一次来判断是否配置ip/DHCP、服务ip绑定终端 绑定:直接到版本更新页面 未绑定到配置ip/DHCP页面
setTimeout(() => {
history.push('/login');
},9000)
}, []);
const handleMenuClick = (key: string) => {
@ -43,73 +42,11 @@ const MainLayout: React.FC = () => {
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>
<Content className="main-content">
<Outlet />
</Content>
</Layout>
);
};

View File

@ -33,6 +33,24 @@ const LoginPage: React.FC = () => {
}
};
const closeApp = () => {
if (window.electronAPI) {
window.electronAPI.closeApp();
}
};
const minimizeApp = () => {
if (window.electronAPI) {
window.electronAPI.minimizeApp();
}
};
const exitKioskMode = () => {
if (window.electronAPI) {
window.electronAPI.exitKiosk();
}
};
return (
<div className="login-container">
<div className="login-content">
@ -74,7 +92,11 @@ const LoginPage: React.FC = () => {
</Button>
</Form.Item>
</Form>
<div>
<Button onClick={closeApp}></Button>
<Button onClick={minimizeApp}></Button>
<Button onClick={exitKioskMode}>退</Button>
</div>
<div className="login-tips">
<p> admin 123456</p>
</div>

View File

@ -0,0 +1,84 @@
.welcomeCon{
width: 100vw;
height: 100vh;
background-color: rgba(0, 9, 51, 1);
background-size: 100% 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.showTextCon{
display: flex;
flex-direction: column;
justify-self: center;
align-items: center;
.name{
height: 80px;
margin-bottom: 20px;
font-size: 36px;
color: #e5e5e5;
display: flex;
.welcomeIcon{
height: 66px;
width: 70px;
margin-right: 20px;
}
.textCon{
height: 120px;
display: flex;
flex-direction: column;
justify-content: space-between;
.text{
display: flex;
flex-direction: column;
.nameText{
font-size: 28px;
}
}
.loadingCon{
width: 96px;
height: 12px;
display: flex;
justify-content: space-between;
align-items: center;
.dot {
width: 10px;
height: 10px;
border-radius: 50%;
background-color: rgba(255, 255, 255, 0.3);
display: flex;
justify-content: space-between;
animation: loading 1.5s infinite ease-in-out;
}
.dot:nth-child(1) {
animation-delay: -0.32s;
}
.dot:nth-child(2) {
animation-delay: -0.16s;
}
@keyframes loading {
0%, 100% {
background-color: rgba(255, 255, 255, 0.3);
transform: scale(0.6);
}
25% {
background-color: rgba(255, 255, 255, 0.6);
transform: scale(0.8);
}
50% {
background-color: rgba(255, 255, 255, 1);
transform: scale(1);
}
75% {
background-color: rgba(255, 255, 255, 0.6);
transform: scale(0.8);
}
}
}
}
}
}
}

View File

@ -0,0 +1,29 @@
import React from 'react';
import styles from './index.less';
import WelcomeIcon from '../../assets/welcome-icon.jpg'
const Welcome = () => {
return (
<div className={styles.welcomeCon} >
<div className={styles.showTextCon}>
<div className={styles.name}>
<img src={WelcomeIcon} className={styles.welcomeIcon} />
<div className={styles.textCon}>
<div className={styles.text}>
<span></span>
<span className={styles.nameText}>UNISSENSE</span>
</div>
<div className={styles.loadingCon}>
<div className={styles.dot}></div>
<div className={styles.dot}></div>
<div className={styles.dot}></div>
</div>
</div>
</div>
</div>
</div>
);
}
export default Welcome;

View File

@ -0,0 +1,20 @@
import axios from '@/utils/axios';
export const BASE_URL = '/api/v1/test';
export const getPageConfig = async () => {
const res = await axios({
url: `${BASE_URL}/page-config/1`,
method: 'GET',
});
return res;
};
export const getTruckList = async (params) => {
const res = await axios({
url: `${BASE_URL}/listCarInfo`,
method: 'POST',
data: params,
});
return res;
};

View File

@ -0,0 +1,52 @@
import axios from 'axios';
import { message } from 'antd';
const HTTP_CODE_SUCCESS = '0000000000';
function getCookie(name: string): string {
let arr;
const reg = new RegExp(`(^| )${name}=([^;]*)(;|$)`);
if ((arr = document.cookie.match(reg))) return decodeURIComponent(arr[2]);
return '';
}
// headers
function getHeaders(headers = {}) {
// const userCode = getCookie('usercode') || 'admin';
// const userName = getCookie('username') || 'admin';
return {
'Content-Type': 'application/json',
// Authorization: encodeURIComponent(`username:${userName}&usercode:${userCode}`),
// User: encodeURIComponent(`username:${userName}&usercode:${userCode}`),
...headers,
};
}
const instance = axios.create({
timeout: 30000,
});
instance.interceptors.request.use(config => {
const _headers = getHeaders();
config.headers = { ..._headers, ...config.headers };
if (config.method === 'get' && config.params) {
config.params.t = +new Date;
}
return config;
}, error => {
return Promise.reject(error);
});
instance.interceptors.response.use(res => {
if (res?.data?.error_code === HTTP_CODE_SUCCESS) {
res.data.hzApiStatu = true;
}
return res.data;
}, rej => {
if (!rej?.config?.$refusedDefaultDealError && rej?.response?.data?.message) {
message.error(rej.response.data.message);
}
return Promise.reject(rej.response);
});
export default instance;

2
pc-fe/typings.d.ts vendored
View File

@ -2,6 +2,6 @@ import 'umi/typings';
declare global {
interface Window {
$api: any;
electronAPI: any;
}
}