今天开始配置一个完整的项目,如何搭建一个登录页面。登录页面采用ui部分为自定义,没有采用element-plus。该项目背景透明、CSS圆角、标题栏和菜单全部隐藏,采用自定义“关闭”和“最小化”按钮(主进程与渲染进程之间通信)、自定义桌面图标、r任务栏图标等。
Electron分为主进程和渲染进程:
主进程:每个 Electron 应用程序都有一个主进程,作为应用程序的入口点。主进程在 Node.js 环境中运行,这意味着它能够
require
模块化和使用所有 Node.js API。主要目的是使用 BrowserWindow模块创建和管理应用程序窗口。渲染进程:由于
Electron
使用Chromium
来展示页面。每个Electron
的页面都在运行着自己的进程,这样的进程我们称之为渲染进程。也可以理解为每创建一个web
页面都会创建一个渲染进程,每个web
页面都运行在它自己的渲染进程中,每个渲染进程是独立的,它只关心它所运行的页面。
一、基于vite初始化vue3+typescript项目
yarn create vite electron-project --template vue-ts
二、安装element-plus
yarn add element-plus
三、安装 concurrently cross-env electron-builder wait-on
yarn add --dev concurrently cross-env electron-builder wait-on
四、配置调试脚本
"scripts": {
"electron:serve": "concurrently -k \"yarn dev\" \"yarn electron\"",
"electron:build": "vite build && electron-builder",
"dev": "vite",
"build": "vue-tsc --noEmit && vite build",
"preview": "vite preview",
"electron": "wait-on tcp:4000 && cross-env NODE_ENV=development electron ."
},
五、登录页面-login.html
(1)、html
<template>
<div id="app">
<div class="title drag">
<div class="right-top">
<a class="a1 noDrag" @click="minimizeWin"></a>
<a class="a3 noDrag" @click="closeWin"></a>
</div>
</div>
<div class="drag">
<img src="../assets/login/images/hys.png" alt="" />
</div>
<div id="dl">
<h4>欢迎使用</h4>
<ul class="tabs noDrag">
<li
class="li-tab"
v-for="(item, index) in login.tabsParam"
:key="index"
@click="toggleTabs(index)"
:class="{ active: index != login.nowIndex }"
>
{{ item }}
</li>
</ul>
<div class="divTab noDrag" v-show="login.nowIndex === 0">
<div class="">
<form class="" id="">
<div class="">
<input
type="text"
class="input1"
v-model="login.username"
maxlength="10"
placeholder="请输入您的用户名"
id="username"
/>
</div>
<div class="">
<input
type="password"
class="input1"
maxlength="20"
v-model="login.userpwd"
placeholder="请输入密码"
id="userpwd"
/>
</div>
<div class="">
<div class="layui-input-block">
<button
type="button"
:disabled="btnState == false"
:class="btnState == false ? 'loginBtndisable' : 'loginBtn'"
@click="doLogin"
v-preventReClick
v-on:keydown.enter="doLogin"
>
登录
</button>
</div>
</div>
</form>
</div>
</div>
<div class="divTab noDrag" v-show="login.nowIndex === 1">
<div>
<form class="" id="">
<div class="">
<input
type="text"
class="input1"
v-model="login.mobile"
maxlength="11"
placeholder="请输入您的手机号"
id="mobile"
/>
</div>
<div class="put2">
<input
type="text"
name="userpwd"
v-model="login.code"
class="input2"
maxlength="4"
placeholder="请输入验证码"
id="code"
/>
<button
type="button"
:disabled="login.disabled"
:class="login.disabled == true ? 'codedisable' : 'code'"
@click="doCode"
v-preventReClick
>
{{
!login.codeTime
? "获取验证码"
: login.codeTime + "s" + "后获取"
}}
</button>
</div>
<div class="">
<div class="layui-input-block">
<button
type="button"
:disabled="codeState == false"
:class="codeState == false ? 'loginBtndisable' : 'loginBtn'"
@click="doCodeLogin"
v-preventReClick
@keyup.enter="doCodeLogin"
>
登录
</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</template>
(2)、typescript
<script lang="ts">
import router from "../router";
import { ElMessageBox } from "element-plus";
const { ipcRenderer: ipc } = require("electron");
import {
computed,
ComponentInternalInstance,
getCurrentInstance,
reactive,
} from "vue";
export default {
setup(props, context) {
const login = reactive({
username: "",
userpwd: "",
mobile: "",
code: "",
nowIndex: 0,
codeTime: 0,
buttondisabled: true,
disabled: false,
tabsParam: ["SE账号", "SE手机号"],
});
const btnState = computed(() => {
return login.username !== "" && login.userpwd !== "";
});
const codeState = computed(() => {
return login.mobile !== "" && login.code !== "";
});
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const doLogin = () => {
proxy?.$axios
.post("/admin/office/login", {
username: login.username,
userpwd: login.userpwd,
})
.then((res) => {
if (res.code == 1) {
localStorage.setItem("access-token", res.access_token);
localStorage.setItem("uid", res.uid);
localStorage.setItem("username", res.username);
localStorage.setItem("unid", res.units[0].unid);
localStorage.setItem("unitname", res.units[0].unitname);
ipc.send("login");
router.push({
path: "/main",
});
} else {
ElMessageBox.alert(res.msg, "提示",{});
}
});
};
const doCode = () => {
if (login.codeTime > 0) {
return;
} else {
proxy?.$axios
.post("/admin/office/sendSms", {
mobile: login.mobile,
})
.then((res) => {
if (res.code == 1) {
ElMessageBox.alert("短信发送成功!", "提示");
login.codeTime = 5;
let timer = setInterval(() => {
login.codeTime--;
if (login.codeTime < 1) {
clearInterval(timer);
login.codeTime = 0;
login.disabled = false;
}
}, 1000);
login.disabled = true;
} else {
ElMessageBox.alert(res.msg, "提示");
}
});
}
};
const doCodeLogin = () => {
proxy?.$axios
.post("/admin/office/smsLogin", {
mobile: login.mobile,
code: login.code,
})
.then((res) => {
if (res.code == 1) {
localStorage.setItem("access-token", res.access_token);
localStorage.setItem("uid", res.uid);
localStorage.setItem("username", res.username);
localStorage.setItem("unid", res.units[0].unid);
localStorage.setItem("unitname", res.units[0].unitname);
ipc.send("login");
router.push({
path: "/main",
});
} else {
ElMessageBox.alert(res.msg, "提示");
}
});
};
const toggleTabs = (index: any) => {
login.nowIndex = index;
};
const minimizeWin = () => {
ipc.send("min");
};
const maximizeWin = () => {
ipc.send("max");
};
const closeWin = () => {
ipc.send("close");
};
return {
doLogin,
toggleTabs,
minimizeWin,
maximizeWin,
closeWin,
login,
doCode,
btnState,
codeState,
doCodeLogin,
};
},
};
</script>
(3)、css
<style>
.el-overlay {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 2000;
height: 100%;
background-color: rgb(0 0 0 / 0%);
overflow: auto;
}
</style>
<style lang="scss" scoped>
ul li {
list-style: none;
}
a {
text-decoration: none;
}
#app {
width: 950px;
height: 630px;
display: flex;
margin-left: 5px;
margin-top: 5px;
justify-content: space-between;
align-items: center;
background: #f2f3f5;
box-sizing: border-box;
position: relative;
-moz-box-shadow:0px 0px 4px #6d6d6d;
-webkit-box-shadow:0px 0px 4px #6d6d6d;
box-shadow:0px 0px 4px #6d6d6d;
border-radius: 4px;
}
.drag{-webkit-app-region: drag;}
.noDrag {
-webkit-app-region: no-drag;
}
#app > div > img {
margin-left: 100px;
}
#dl {
margin-right: 40px;
}
.title {
position: absolute;
top: 0px;
right: 0px;
width: 100%;
height: 50px;
display: flex;
// background-color: #dcddee;
justify-content: space-between;
}
.right-top {
position: absolute;
top: 8px;
right: 8px;
width: 70px;
height: 34px;
display: flex;
justify-content: space-between;
}
.right-top a {
width: 34px;
height: 34px;
}
.right-top .a1 {
background: url(../assets/login/images/pick1.png) no-repeat center center #f2f3f5;
}
.right-top .a1:hover {
background: url(../assets/login/images/pick4.png) no-repeat center center #e6e8eb;
}
.right-top .a3 {
background: url(../assets/login/images/pick3.png) no-repeat center center #f2f3f5;
}
.right-top .a3:hover {
background: url(../assets/login/images/pick6.png) no-repeat center center #f44a45;
}
#dl {
width: 400px;
height: 512px;
background: #ffffff;
border-radius: 6px;
}
#dl > h4 {
font-size: 22px;
margin: 59px 0 0 29px;
}
#dl > ul {
margin: 30px 0 0 30px;
height: 40px;
}
#dl > ul li {
float: left;
margin-right: 29px;
font-size: 16px;
border-bottom: #1677ff 2px solid;
padding-bottom: 5px;
color: #1677ff;
cursor: pointer;
}
#dl > ul li.active {
border-bottom: 0;
color: #8e949d;
}
.bd > div {
display: none;
}
.bd > div.active {
display: block;
}
.bd .input1 {
width: 340px;
height: 50px;
border-radius: 6px;
background: #ffffff;
border: 1px solid #cfd2d5;
margin: 20px auto 0;
display: block;
font-size: 16px;
padding-left: 20px;
box-sizing: border-box;
}
.input1 {
width: 340px;
height: 50px;
border-radius: 6px;
background: #ffffff;
border: 1px solid #cfd2d5;
margin: 20px auto 0;
display: block;
font-size: 16px;
padding-left: 20px;
box-sizing: border-box;
}
.loginBtn {
display: block;
margin: 40px auto 30px;
width: 340px;
height: 50px;
background: #1677ff;
border-radius: 25px;
border: 0;
font-size: 16px;
color: #fff;
}
.loginBtndisable {
display: block;
margin: 40px auto 30px;
width: 340px;
height: 50px;
background: #bbbfc4;
border-radius: 25px;
border: 0;
font-size: 16px;
color: #fff;
cursor: not-allowed;
}
.put2 {
width: 340px;
margin: 20px auto 0px;
display: flex;
justify-content: space-between;
}
.input2 {
width: 200px;
height: 50px;
background: #ffffff;
border: 1px solid #cfd2d5;
border-radius: 6px;
font-size: 16px;
padding-left: 20px;
box-sizing: border-box;
}
$color-code-disable-false: #1677ff;
$color-code-disable-true: #c0c0c0;
.code {
width: 120px;
height: 50px;
background: $color-code-disable-false;
border-radius: 6px;
color: #fff;
border: 0;
cursor: pointer;
}
.codedisable {
width: 120px;
height: 50px;
background: $color-code-disable-true;
border-radius: 6px;
color: #fff;
border: 0;
cursor: not-allowed;
}
label {
display: block;
margin: 0 0 24px 30px;
color: #8e949d;
font-size: 14px;
}
label input {
margin-right: 10px;
}
label a {
color: #1677ff;
}
</style>
六、路由-router.js
import {createRouter, createWebHashHistory} from 'vue-router';
import login from "../view/login.vue"
const routes = [
{
path: '/',
redirect: '/login'
},
{
path: '/login',
name: 'login',
component: login
}
]
const router = createRouter({
history: createWebHashHistory(),
routes
})
const ipc = require('electron').ipcRenderer
router.beforeEach((to, from, next) => {
if (to.path === '/login') return next()
const token = localStorage.getItem('access-token')
if (!token) {
ipc.send("relaunch");
}
next()
})
export default router;
七、入口-main.js
const { app, BrowserWindow, dialog, globalShortcut } = require('electron')
const electron = require('electron');
const path = require('path')
const ipc = require('electron').ipcMain;
const Menu = electron.Menu;
const Tray = electron.Tray;
var appTray = null;
process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = 'true';
ipc.on('new-window', function () {
mainWindow.loadURL(url.format({
pathname: path.join(__dirname, '../dist/main.html'),
protocol: 'file:',
slashes: true
}))
})
const NODE_ENV = process.env.NODE_ENV;
const clearObj = {
storages: ['appcache', 'filesystem', 'localstorage', 'shadercache', 'websql', 'serviceworkers', 'cachestorage']
};
async function createWindow() {
const mainWindow = new BrowserWindow({
frame: false,
hasShadow: false,
transparent: true,
backgroundColor: '#00000000',
width: 960,
height: 640,
useContentSize: true,
resizable: false,
show: false,
icon: 'src/assets/icon/fav256.ico',
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
enableRemoteModule: true,
nodeIntegration: true,
webSecurity: false,
contextIsolation: false
}
})
mainWindow.setAppDetails({
appId: "com.successkaoyan",
appIconPath: "./src/assets/icon/fav256.ico",
appIconIndex: 0,
relaunchCommand: "Meeting Room",
relaunchDisplayName: "Meeting Room",
})
ipc.on('login', () => {
mainWindow.setSize(1260, 750);
mainWindow.center();
})
const gotTheLock = app.requestSingleInstanceLock()
if (!gotTheLock) {
app.quit()
} else {
app.on('second-instance', (event, commandLine, workingDirectory) => {
if (mainWindow) {
if (mainWindow.isMinimized()) mainWindow.restore()
mainWindow.focus()
mainWindow.show()
}
})
}
ipc.on('min', e => mainWindow.minimize());
ipc.on('max', e => {
if (mainWindow.isMaximized()) {
mainWindow.unmaximize()
} else {
mainWindow.maximize()
}
});
ipc.on('close', e => mainWindow.hide());
ipc.on('activate', e => {
mainWindow.setFullScreen(true);
mainWindow.maximize()
});
ipc.on('relaunch', e => {
app.relaunch(); app.exit();
})
var trayMenuTemplate = [
{
label: '退出',
click: function () {
mainWindow.webContents.session.clearStorageData(clearObj);
app.quit();
}
}
];
mainWindow.on("close",()=>{
mainWindow.webContents.session.clearStorageData(clearObj);
})
if (NODE_ENV === "development") {
trayIcon = path.join(app.getAppPath(), 'src/assets/icon/fav32.ico');
mainWindow.webContents.openDevTools()
} else {
trayIcon = path.join(__dirname, 'fav32.ico');
}
appTray = new Tray(trayIcon);
const contextMenu = Menu.buildFromTemplate(trayMenuTemplate);
appTray.setToolTip('Meeting Room');
appTray.setContextMenu(contextMenu);
appTray.on('click', function () {
mainWindow.show();
})
mainWindow.setMenu(null);
await mainWindow.loadURL(
NODE_ENV === "development" ?
"http://localhost:3000" :
`file://${path.join(__dirname, "../dist/index.html")}`
)
mainWindow.show()
globalShortcut.register('CommandOrControl+Shift+i', function () {
mainWindow.webContents.openDevTools()
})
globalShortcut.register('CommandOrControl+T', () => {
mainWindow.maximize()
})
globalShortcut.register('CommandOrControl+M', () => {
mainWindow.unmaximize()
})
globalShortcut.register('CommandOrControl+H', () => {
mainWindow.close()
})
}
app.whenReady().then(() => {
createWindow()
})
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
const gotTheLock = app.requestSingleInstanceLock()
if (!gotTheLock) {
app.quit()
}
app.disableHardwareAcceleration()
七、预加载-preload.js
window.addEventListener("DOMContentLoaded",() => {
const replaceText = (selector,text) =>{
const element = document.getElementById(selector);
if (element) element.innerText = text;
};
for(const dependency of ["chrome","node","electron"]){
replaceText(`${dependency}-version`,process.versions[dependency])
}
});
八、项目结构