Kiosk 模式改造指南
大约 5 分钟
Kiosk 模式改造指南
📖 概述
本文档详细介绍如何将普通的 Vue + Electron 应用改造为专业的 Kiosk 模式应用。适用于公共展示终端、自助服务机、工业监控大屏等场景。
🎯 改造目标
将普通桌面应用改造为:
- 🔒 安全可控的 Kiosk 应用
- 🛡️ 防困死的安全启动模式
- 🔐 智能锁屏系统
- 🖥️ 全屏展示模式
- 🎨 工业级用户界面
🔧 核心改造步骤
1. 主进程安全改造
electron/main.js 关键配置
const { app, BrowserWindow, globalShortcut, ipcMain, Menu } = require('electron')
function createWindow() {
mainWindow = new BrowserWindow({
width: 1920,
height: 1080,
fullscreen: false, // 安全启动模式
frame: true, // 保持边框防困死
kiosk: false, // 不使用严格 Kiosk
autoHideMenuBar: true, // 隐藏菜单栏
alwaysOnTop: true, // 始终置顶
minimizable: true, // 允许最小化
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: path.join(__dirname, 'preload.js')
}
})
// 强制隐藏菜单栏
Menu.setApplicationMenu(null)
// 注册安全快捷键
registerKioskShortcuts()
}
// Kiosk 模式快捷键控制
function registerKioskShortcuts() {
const shortcuts = [
'Alt+F4', // 禁用关闭
'F11', // 禁用全屏切换
'Ctrl+Shift+I', // 禁用开发者工具
'Ctrl+R', // 禁用刷新
'F5' // 禁用刷新
]
shortcuts.forEach(shortcut => {
globalShortcut.register(shortcut, () => {
console.log(`Kiosk 模式禁用快捷键: ${shortcut}`)
return false
})
})
}
2. IPC 通信改造
electron/preload.js 扩展
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
// 基础标识
isElectron: true,
platform: process.platform,
// Kiosk 模式专用 API
toggleFullscreen: () => ipcRenderer.send('toggle-fullscreen'),
minimizeWindow: () => ipcRenderer.send('minimize-window'),
requestExit: () => ipcRenderer.send('request-exit'),
notifyMapLoaded: () => ipcRenderer.send('map-loaded'),
getFullscreenState: () => ipcRenderer.invoke('get-fullscreen-state'),
// 版本信息
versions: {
node: process.versions.node,
chrome: process.versions.chrome,
electron: process.versions.electron
}
})
3. 锁屏组件开发
src/components/KioskLockScreen/index.vue
<template>
<div v-if="isLocked" class="kiosk-lock-screen">
<!-- 四角高亮区域(吸引虫子) -->
<div class="corner-light top-left"></div>
<div class="corner-light top-right"></div>
<div class="corner-light bottom-left"></div>
<div class="corner-light bottom-right"></div>
<!-- 锁屏内容 -->
<div class="lock-content">
<div class="lock-title">系统已锁定</div>
<div class="lock-time">{{ currentTime }}</div>
<!-- 双按钮解锁 -->
<div class="unlock-buttons">
<div
class="unlock-button left"
@click="handleUnlockClick('left')"
>
<div class="button-label">解锁</div>
</div>
<div
class="unlock-button right"
@click="handleUnlockClick('right')"
>
<div class="button-label">解锁</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'KioskLockScreen',
props: {
autoLockTime: {
type: Number,
default: 300000 // 5分钟
},
enableAutoLock: {
type: Boolean,
default: true
}
},
data() {
return {
isLocked: false,
currentTime: '',
unlockClicks: { left: false, right: false },
autoLockTimer: null,
timeUpdateTimer: null
}
},
mounted() {
this.initLockScreen()
this.startTimeUpdate()
// 监听手动锁屏事件
this.$root.$on('kiosk-lock', this.lockScreen)
// ESC 键解锁
document.addEventListener('keydown', this.handleKeyDown)
},
methods: {
initLockScreen() {
if (this.enableAutoLock) {
this.startAutoLockTimer()
}
},
lockScreen(isManual = false) {
this.isLocked = true
console.log(isManual ? '手动锁屏' : '自动锁屏')
this.$emit('locked')
},
handleUnlockClick(side) {
this.unlockClicks[side] = true
// 检查是否双按钮都被点击
if (this.unlockClicks.left && this.unlockClicks.right) {
this.unlockScreen()
}
// 3秒后重置
setTimeout(() => {
this.unlockClicks[side] = false
}, 3000)
},
unlockScreen() {
this.isLocked = false
this.unlockClicks = { left: false, right: false }
this.startAutoLockTimer() // 重新开始计时
this.$emit('unlocked')
},
startAutoLockTimer() {
if (this.autoLockTimer) {
clearTimeout(this.autoLockTimer)
}
if (this.enableAutoLock && this.autoLockTime > 0) {
this.autoLockTimer = setTimeout(() => {
this.lockScreen(false)
}, this.autoLockTime)
}
},
startTimeUpdate() {
this.updateTime()
this.timeUpdateTimer = setInterval(this.updateTime, 1000)
},
updateTime() {
const now = new Date()
this.currentTime = now.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})
},
handleKeyDown(event) {
if (event.key === 'Escape' && this.isLocked) {
this.unlockScreen()
}
}
},
beforeDestroy() {
if (this.autoLockTimer) clearTimeout(this.autoLockTimer)
if (this.timeUpdateTimer) clearInterval(this.timeUpdateTimer)
document.removeEventListener('keydown', this.handleKeyDown)
this.$root.$off('kiosk-lock', this.lockScreen)
}
}
</script>
<style lang="scss" scoped>
.kiosk-lock-screen {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
// 四角高亮区域
.corner-light {
position: absolute;
width: 300px;
height: 300px;
z-index: 0;
pointer-events: none;
animation: cornerPulse 2s ease-in-out infinite;
&.top-left {
top: 0;
left: 0;
background: radial-gradient(circle at top left, rgba(255,255,255,0.8) 0%, transparent 70%);
}
&.top-right {
top: 0;
right: 0;
background: radial-gradient(circle at top right, rgba(255,255,255,0.8) 0%, transparent 70%);
}
&.bottom-left {
bottom: 0;
left: 0;
background: radial-gradient(circle at bottom left, rgba(255,255,255,0.8) 0%, transparent 70%);
}
&.bottom-right {
bottom: 0;
right: 0;
background: radial-gradient(circle at bottom right, rgba(255,255,255,0.8) 0%, transparent 70%);
}
}
@keyframes cornerPulse {
0%, 100% { opacity: 0.6; }
50% { opacity: 1; }
}
.lock-content {
text-align: center;
color: white;
z-index: 1;
}
.lock-title {
font-size: 48px;
font-weight: bold;
margin-bottom: 20px;
}
.lock-time {
font-size: 24px;
margin-bottom: 60px;
opacity: 0.8;
}
.unlock-buttons {
display: flex;
gap: 100px;
justify-content: center;
}
.unlock-button {
width: 150px;
height: 150px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
border: 3px solid rgba(255, 255, 255, 0.3);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
.button-label {
font-size: 28px;
font-weight: 600;
opacity: 0.4; // 淡化按钮文字
}
&:hover {
background: rgba(255, 255, 255, 0.2);
border-color: rgba(255, 255, 255, 0.5);
.button-label {
opacity: 0.6;
}
}
&:active {
transform: scale(0.95);
}
}
</style>
4. 控制面板改造
src/components/MapControls.vue
<template>
<div class="map-controls">
<!-- 全屏切换 -->
<div class="control-btn" @click="handleToggleFullscreen">
<i :class="isFullscreen ? 'el-icon-aim' : 'el-icon-full-screen'"></i>
<div class="btn-text">{{ isFullscreen ? '退出全屏' : '全屏' }}</div>
</div>
<!-- 回到桌面 -->
<div class="control-btn" @click="handleShowDesktop">
<i class="el-icon-monitor"></i>
<div class="btn-text">桌面</div>
</div>
<!-- 手动锁屏 -->
<div class="control-btn" @click="handleManualLock">
<i class="el-icon-lock"></i>
<div class="btn-text">锁屏</div>
</div>
<!-- 退出应用 -->
<div class="control-btn exit-btn" @click="handleExit">
<i class="el-icon-close"></i>
<div class="btn-text">退出</div>
</div>
</div>
</template>
<script>
export default {
name: 'MapControls',
data() {
return {
isFullscreen: false,
electronFullscreenCheckTimer: null
}
},
mounted() {
this.initFullscreen()
},
methods: {
initFullscreen() {
// Electron 环境下检查全屏状态
if (window.electronAPI && window.electronAPI.isElectron) {
this.checkElectronFullscreenState()
// 定期检查状态同步
this.electronFullscreenCheckTimer = setInterval(() => {
this.checkElectronFullscreenState()
}, 2000)
}
},
handleToggleFullscreen() {
if (window.electronAPI && window.electronAPI.isElectron) {
// 先更新 UI 状态
this.isFullscreen = !this.isFullscreen
// 调用 Electron API
window.electronAPI.toggleFullscreen()
// 延迟检查实际状态
setTimeout(() => {
this.checkElectronFullscreenState()
}, 500)
}
},
checkElectronFullscreenState() {
if (window.electronAPI && window.electronAPI.getFullscreenState) {
window.electronAPI.getFullscreenState().then(isFullscreen => {
if (this.isFullscreen !== isFullscreen) {
this.isFullscreen = isFullscreen
}
}).catch(err => {
console.warn('获取全屏状态失败:', err)
})
}
},
handleShowDesktop() {
if (window.electronAPI && window.electronAPI.minimizeWindow) {
window.electronAPI.minimizeWindow()
}
},
handleManualLock() {
// 触发锁屏事件
this.$root.$emit('kiosk-lock', true)
},
handleExit() {
this.$confirm('确定要退出应用吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
if (window.electronAPI && window.electronAPI.requestExit) {
window.electronAPI.requestExit()
}
})
}
},
beforeDestroy() {
if (this.electronFullscreenCheckTimer) {
clearInterval(this.electronFullscreenCheckTimer)
}
}
}
</script>
<style lang="scss" scoped>
.map-controls {
position: absolute;
left: 12px;
top: 12px;
display: flex;
flex-direction: column;
gap: 8px;
z-index: 1000;
}
.control-btn {
width: 80px;
height: 80px;
background: rgba(255, 255, 255, 0.9);
border-radius: 8px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
i {
font-size: 24px;
color: #409EFF;
margin-bottom: 4px;
}
.btn-text {
font-size: 12px;
color: #666;
font-weight: 500;
}
&:hover {
background: rgba(255, 255, 255, 1);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
&.exit-btn {
i {
color: #F56C6C;
}
}
}
</style>
🚀 改造完成验证
1. 功能测试清单
2. 部署测试
# 开发模式测试
npm run electron:dev
# 生产模式打包测试
npm run electron:build:win
📋 改造要点总结
- 安全第一:防困死设计,保持窗口边框和基本控制
- 用户体验:防虫子设计,智能锁屏机制
- 系统集成:快捷键拦截,全屏状态同步
- 工业级稳定性:异常恢复,状态监控
- 模块化设计:组件化开发,便于维护扩展
改造完成后,您的应用将具备完整的 Kiosk 模式功能,适用于各种公共展示和自助服务场景!