This commit is contained in:
2025-09-25 16:12:31 +08:00
parent 6d7fbffdc1
commit 33a4aa5c30
5 changed files with 353 additions and 330 deletions

View File

@@ -12,187 +12,172 @@ let splashWindow: BrowserWindow | null = null;
let appOpened = false;
function openAppIfNotOpened() {
if (appOpened) return;
appOpened = true;
if (mainWindow) {
mainWindow.show();
mainWindow.focus();
}
if (splashWindow) { splashWindow.close(); splashWindow = null; }
if (appOpened) return;
appOpened = true;
if (mainWindow) {
mainWindow.show();
mainWindow.focus();
}
if (splashWindow) { splashWindow.close(); splashWindow = null; }
}
// 启动后端 Spring Boot使用你提供的绝对路径
function startSpringBoot() {
const jarPath = 'C:/Users/ZiJIe/Desktop/wox/RuoYi-Vue/ruoyi-admin/target/ruoyi-admin.jar';
const jarPath = 'C:/Users/ZiJIe/Desktop/wox/RuoYi-Vue/ruoyi-admin/target/ruoyi-admin.jar';
springProcess = spawn('java', ['-jar', jarPath], {
cwd: dirname(jarPath),
detached: false
});
springProcess = spawn('java', ['-jar', jarPath], {
cwd: dirname(jarPath),
detached: false
});
// 打印后端日志,监听启动成功标志
springProcess.stdout.on('data', (data) => {
console.log(`SpringBoot: ${data}`);
// 检测到启动成功日志立即进入主界面
if (data.toString().includes('Started RuoYiApplication')) {
openAppIfNotOpened();
}
});
// 打印后端日志,监听启动成功标志
springProcess.stdout.on('data', (data) => {
console.log(`SpringBoot: ${data}`);
// 检测到启动成功日志立即进入主界面
if (data.toString().includes('Started RuoYiApplication')) {
openAppIfNotOpened();
}
});
// 打印后端错误,检测启动失败
springProcess.stderr.on('data', (data) => {
console.error(`SpringBoot ERROR: ${data}`);
const errorStr = data.toString();
// 检测到关键错误信息,直接退出
if (errorStr.includes('APPLICATION FAILED TO START') ||
errorStr.includes('Port') && errorStr.includes('already in use') ||
errorStr.includes('Unable to start embedded Tomcat')) {
console.error('后端启动失败,程序即将退出');
app.quit();
}
});
// 打印后端错误,检测启动失败
springProcess.stderr.on('data', (data) => {
console.error(`SpringBoot ERROR: ${data}`);
const errorStr = data.toString();
// 检测到关键错误信息,直接退出
if (errorStr.includes('APPLICATION FAILED TO START') ||
errorStr.includes('Port') && errorStr.includes('already in use') ||
errorStr.includes('Unable to start embedded Tomcat')) {
console.error('后端启动失败,程序即将退出');
app.quit();
}
});
// 后端退出时,前端同步退出
springProcess.on('close', (code) => {
console.log(`SpringBoot exited with code ${code}`);
if (mainWindow) {
mainWindow.close();
} else {
app.quit();
}
});
// 后端退出时,前端同步退出
springProcess.on('close', (code) => {
console.log(`SpringBoot exited with code ${code}`);
if (mainWindow) {
mainWindow.close();
} else {
app.quit();
}
});
}
// 关闭后端进程Windows 使用 taskkill 结束整个进程树)
function stopSpringBoot() {
if (!springProcess) return;
try {
if (process.platform === 'win32') {
// Force kill the whole process tree on Windows
try {
const pid = springProcess.pid;
if (pid !== undefined && pid !== null) {
spawn('taskkill', ['/pid', String(pid), '/f', '/t']);
if (!springProcess) return;
try {
if (process.platform === 'win32') {
// Force kill the whole process tree on Windows
try {
const pid = springProcess.pid;
if (pid !== undefined && pid !== null) {
spawn('taskkill', ['/pid', String(pid), '/f', '/t']);
} else {
springProcess.kill();
}
} catch (e) {
// Fallback
springProcess.kill();
}
} else {
springProcess.kill();
springProcess.kill('SIGTERM');
}
} catch (e) {
// Fallback
springProcess.kill();
}
} else {
springProcess.kill('SIGTERM');
} catch (e) {
console.error('Failed to stop Spring Boot process:', e);
} finally {
springProcess = null;
}
} catch (e) {
console.error('Failed to stop Spring Boot process:', e);
} finally {
springProcess = null;
}
}
function createWindow () {
mainWindow = new BrowserWindow({
width: 1280,
height: 800,
show: false,
autoHideMenuBar: true,
webPreferences: {
preload: join(__dirname, 'preload.js'),
nodeIntegration: false,
contextIsolation: true,
mainWindow = new BrowserWindow({
width: 1280,
height: 800,
show: false,
autoHideMenuBar: true,
icon: join(__dirname, '../renderer/icon/icon.png'), // 添加窗口图标
webPreferences: {
preload: join(__dirname, 'preload.js'),
nodeIntegration: false,
contextIsolation: true,
}
});
// 彻底隐藏原生菜单栏
try {
Menu.setApplicationMenu(null);
mainWindow.setMenuBarVisibility(false);
if (typeof (mainWindow as any).setMenu === 'function') {
(mainWindow as any).setMenu(null);
}
} catch {}
// 生产环境加载本地文件
if (process.env.NODE_ENV === 'development') {
const rendererPort = process.argv[2] || 8083;
mainWindow.loadURL(`http://localhost:${rendererPort}`);
} else {
mainWindow.loadFile(join(__dirname, '../renderer/index.html'));
}
});
// 彻底隐藏原生菜单栏
try {
Menu.setApplicationMenu(null);
mainWindow.setMenuBarVisibility(false);
if (typeof (mainWindow as any).setMenu === 'function') {
(mainWindow as any).setMenu(null);
}
} catch {}
const rendererPort = process.argv[2];
mainWindow.loadURL(`http://localhost:${rendererPort}`);
mainWindow.on('closed', () => {
mainWindow = null;
});
}
app.whenReady().then(() => {
// 预创建主窗口(隐藏)
createWindow();
// 预创建主窗口(隐藏)
createWindow();
// 显示启动页
const { width: sw, height: sh } = screen.getPrimaryDisplay().workAreaSize;
const splashW = Math.min(Math.floor(sw * 0.8), 1800);
const splashH = Math.min(Math.floor(sh * 0.8), 1200);
splashWindow = new BrowserWindow({
width: splashW,
height: splashH,
frame: false,
transparent: false,
resizable: false,
alwaysOnTop: true,
show: true,
center: true,
});
// 显示启动页
const { width: sw, height: sh } = screen.getPrimaryDisplay().workAreaSize;
const splashW = Math.min(Math.floor(sw * 0.8), 1800);
const splashH = Math.min(Math.floor(sh * 0.8), 1200);
splashWindow = new BrowserWindow({
width: splashW,
height: splashH,
frame: false,
transparent: false,
resizable: false,
alwaysOnTop: true,
show: true,
center: true,
});
const candidateSplashPaths = [
join(__dirname, '../../public', 'splash.html'),
];
const foundSplash = candidateSplashPaths.find(p => existsSync(p));
if (foundSplash) {
splashWindow.loadFile(foundSplash);
}
// 注释掉后端启动,便于快速调试前端
// startSpringBoot();
// 快速调试模式 - 直接打开主窗口
setTimeout(() => {
openAppIfNotOpened();
}, 1000);
// 注释掉超时机制
/*
setTimeout(() => {
if (!appOpened) {
console.error('后端启动超时,程序即将退出');
app.quit();
const candidateSplashPaths = [
join(__dirname, '../../public', 'splash.html'),
];
const foundSplash = candidateSplashPaths.find(p => existsSync(p));
if (foundSplash) {
splashWindow.loadFile(foundSplash);
}
}, 30000);
*/
// 保守 CSP仅允许自身脚本避免引入不必要的外部脚本
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
callback({
responseHeaders: {
...details.responseHeaders,
'Content-Security-Policy': ['script-src \'self\'']
}
})
})
// 注释掉后端启动,便于快速调试前端
// startSpringBoot();
app.on('activate', function () {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
// 快速调试模式 - 直接打开主窗口
setTimeout(() => {
openAppIfNotOpened();
}, 1000);
app.on('activate', function () {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
});
app.on('window-all-closed', function () {
stopSpringBoot();
if (process.platform !== 'darwin') app.quit()
stopSpringBoot();
if (process.platform !== 'darwin') app.quit()
});
app.on('before-quit', () => {
stopSpringBoot();
stopSpringBoot();
});
ipcMain.on('message', (event, message) => {
console.log(message);
console.log(message);
})

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import {onMounted, ref, computed, defineAsyncComponent, type Component} from 'vue'
import {ElConfigProvider, ElMessage, ElMessageBox} from 'element-plus'
import {onMounted, ref, computed, defineAsyncComponent, type Component, onUnmounted} from 'vue'
import {ElMessage, ElMessageBox} from 'element-plus'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
// 图标已移至对应组件
import 'element-plus/dist/index.css'
@@ -11,6 +11,7 @@ import {deviceApi, type DeviceItem, type DeviceQuota} from './api/device'
const LoginDialog = defineAsyncComponent(() => import('./components/auth/LoginDialog.vue'))
const RegisterDialog = defineAsyncComponent(() => import('./components/auth/RegisterDialog.vue'))
const NavigationBar = defineAsyncComponent(() => import('./components/layout/NavigationBar.vue'))
// const UpdateDialog = defineAsyncComponent(() => import('./components/common/UpdateDialog.vue'))
const RakutenDashboard = defineAsyncComponent(() => import('./components/rakuten/RakutenDashboard.vue'))
const AmazonDashboard = defineAsyncComponent(() => import('./components/amazon/AmazonDashboard.vue'))
const ZebraDashboard = defineAsyncComponent(() => import('./components/zebra/ZebraDashboard.vue'))
@@ -47,6 +48,10 @@ const devices = ref<DeviceItem[]>([])
const deviceQuota = ref<DeviceQuota>({limit: 0, used: 0})
const userPermissions = ref<string>('')
// 更新状态
const updateStatus = ref<any>(null)
const showUpdateNotification = ref(false)
// 菜单配置 - 复刻ERP客户端格式
const menuConfig = [
{key: 'rakuten', name: 'Rakuten', index: 'rakuten', icon: 'R'},
@@ -150,11 +155,20 @@ async function handleLoginSuccess(data: { token: string; permissions?: string })
}
async function logout() {
const token = await authApi.getToken()
if (token) {
await authApi.logout(token)
// 主动设置设备离线
try {
const deviceId = await getClientIdFromToken()
if (deviceId) {
await deviceApi.offline({ deviceId })
}
} catch (error) {
console.warn('离线通知失败:', error)
}
const token = await authApi.getToken()
if (token) {
await authApi.logout(token)
}
await authApi.deleteTokenCache()
// 清理前端状态
@@ -164,8 +178,7 @@ async function logout() {
showAuthDialog.value = true
showDeviceDialog.value = false
// 关闭SSE连接`-+++++++
// 关闭SSE连接
SSEManager.disconnect()
}
@@ -181,7 +194,10 @@ async function handleUserClick() {
cancelButtonText: '取消'
})
await logout()
ElMessage.success('已退出登录')
ElMessage({
message: '已退出登录',
type: 'success'
})
} catch {
}
}
@@ -285,8 +301,12 @@ const SSEManager = {
handleMessage(e: MessageEvent) {
try {
console.log('SSE消息:', e.data)
// 处理ping心跳
if (e.type === 'ping') {
return // ping消息自动保持连接无需处理
}
console.log('SSE消息:', e.data)
const payload = JSON.parse(e.data)
switch (payload.type) {
case 'ready':
@@ -294,11 +314,17 @@ const SSEManager = {
break
case 'DEVICE_REMOVED':
logout()
ElMessage.warning('您的设备已被移除,请重新登录')
ElMessage({
message: '您的设备已被移除,请重新登录',
type: 'warning'
})
break
case 'FORCE_LOGOUT':
logout()
ElMessage.warning('会话已失效,请重新登录')
ElMessage({
message: '会话已失效,请重新登录',
type: 'warning'
})
break
case 'PERMISSIONS_UPDATED':
checkAuth()
@@ -336,7 +362,10 @@ async function openDeviceManager() {
async function fetchDeviceData() {
if (!currentUsername.value) {
ElMessage.warning('未获取到用户名,请重新登录')
ElMessage({
message: '未获取到用户名,请重新登录',
type: 'warning'
})
return
}
try {
@@ -349,7 +378,10 @@ async function fetchDeviceData() {
const clientId = await getClientIdFromToken()
devices.value = (list || []).map(d => ({...d, isCurrent: d.deviceId === clientId})) as any
} catch (e: any) {
ElMessage.error(e?.message || '获取设备列表失败')
ElMessage({
message: e?.message || '获取设备列表失败',
type: 'error'
})
} finally {
deviceLoading.value = false
}
@@ -373,9 +405,130 @@ async function confirmRemoveDevice(row: DeviceItem & { isCurrent?: boolean }) {
await logout()
}
ElMessage.success('已移除设备')
ElMessage({
message: '已移除设备',
type: 'success'
})
} catch (e: any) {
ElMessage.error('移除设备失败: ' + ((e as any)?.message || '未知错误'))
ElMessage({
message: '移除设备失败: ' + ((e as any)?.message || '未知错误'),
type: 'error'
})
}
}
// 更新相关功能
function setupUpdateHandler() {
if (!(window as any).electronAPI) return
(window as any).electronAPI.onUpdateStatus((status: any) => {
updateStatus.value = status
handleUpdateStatus(status)
})
}
function handleUpdateStatus(status: any) {
switch (status.type) {
case 'checking':
console.log('正在检查更新...')
break
case 'available':
showUpdateAvailableNotification(status)
break
case 'not-available':
console.log('当前已是最新版本')
break
case 'error':
ElMessage({
message: `更新检查失败: ${status.error}`,
type: 'error'
})
break
case 'download-progress':
// 可以在这里显示下载进度
console.log(`下载进度: ${status.progress}%`)
break
case 'downloaded':
showUpdateReadyNotification()
break
}
}
function showUpdateAvailableNotification(status: any) {
ElMessage({
message: `发现新版本 ${status.version},是否立即下载?`,
type: 'info'
})
// 可以在这里添加确认对话框
ElMessageBox.confirm(`新版本 ${status.version} 已发布,是否立即下载?`, '发现新版本', {
confirmButtonText: '立即下载',
cancelButtonText: '稍后提醒',
type: 'info'
}).then(() => {
downloadUpdate()
}).catch(() => {
// 用户取消
})
}
function showUpdateReadyNotification() {
ElMessageBox.confirm('更新已下载完成,是否重启应用以安装更新?', '更新准备就绪', {
confirmButtonText: '立即重启',
cancelButtonText: '稍后重启',
type: 'success'
}).then(() => {
installUpdate()
}).catch(() => {
// 用户取消
})
}
async function checkForUpdates() {
if (!(window as any).electronAPI) return
try {
const result = await (window as any).electronAPI.checkForUpdates()
if (!result.success) {
console.log('更新检查:', result.message || result.error)
}
} catch (error) {
console.error('检查更新失败:', error)
}
}
async function downloadUpdate() {
if (!(window as any).electronAPI) return
try {
ElMessage({
message: '开始下载更新...',
type: 'info'
})
const result = await (window as any).electronAPI.downloadUpdate()
if (!result.success) {
ElMessage({
message: `下载失败: ${result.error}`,
type: 'error'
})
}
} catch (error) {
ElMessage({
message: '下载更新失败',
type: 'error'
})
}
}
function installUpdate() {
if (!(window as any).electronAPI) return
try {
(window as any).electronAPI.installUpdate()
} catch (error) {
ElMessage({
message: '安装更新失败',
type: 'error'
})
}
}
@@ -383,11 +536,24 @@ onMounted(async () => {
showContent()
await checkAuth()
// 设置更新处理器
setupUpdateHandler()
// 监听页面关闭断开SSE连接会自动设置离线
window.addEventListener('beforeunload', () => {
SSEManager.disconnect()
})
})
onUnmounted(() => {
if ((window as any).electronAPI) {
(window as any).electronAPI.removeUpdateStatusListener()
}
})
</script>
<template>
<el-config-provider :locale="zhCnLocale">
<div>
<div id="app-root" class="root">
<div class="loading-container" id="loading">
<div class="loading-spinner"></div>
@@ -503,7 +669,7 @@ onMounted(async () => {
</div>
</div>
</div>
</el-config-provider>
</div>
</template>
<style scoped>
@@ -599,7 +765,7 @@ onMounted(async () => {
font-size: 12px;
color: #909399;
margin: 8px 6px 10px;
text-align: left; /* 电商平台四个字靠左 */
text-align: left; /* "电商平台"四个字靠左 */
}
.menu {
@@ -758,4 +924,19 @@ onMounted(async () => {
}
.device-count { color: #909399; font-weight: 500; }
.device-subtitle { font-size: 12px; color: #909399; }
</style>
/* 浮动版本信息 */
.version-info {
position: fixed;
right: 10px;
bottom: 10px;
background: rgba(255,255,255,0.9);
padding: 5px 10px;
border-radius: 4px;
font-size: 12px;
color: #909399;
z-index: 1000;
cursor: pointer;
user-select: none;
}
</style>

View File

@@ -1,9 +1,8 @@
import { createApp } from 'vue'
import './style.css';
import 'element-plus/dist/index.css'
import './style.css';
import ElementPlus from 'element-plus'
import App from './App.vue'
const app = createApp(App)
app.use(ElementPlus)
app.mount('#app')

View File

@@ -13,7 +13,6 @@
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
a {
font-weight: 500;
color: #646cff;