feat(electron): 添加系统级对话框功能

- 在 ElectronAPI 中新增对话框相关方法:showQuestionDialog、dialogResponse、resizeDialogWindow
- 实现独立的对话框窗口 dialog.html,支持自定义标题、消息和选项- 添加对话框窗口的键盘导航和焦点管理功能
- 在主进程中实现对话框窗口的创建、显示和响应处理
- 更新 WebSocket 消息监听器组件,使用系统级对话框替代应用内弹窗- 优化进程清理逻辑,增加多种清理方法并行执行
- 重构日志工具类,改进 Electron API 调用方式
- 调整 Git 更新检查逻辑,避免直接访问 GitHub
- 移除冗余的类型定义文件,统一 Electron API 接口定义
This commit is contained in:
MoeSnowyFox
2025-10-01 23:17:23 +08:00
parent 3d204980a2
commit 1d91204842
8 changed files with 799 additions and 409 deletions

View File

@@ -1,51 +1,26 @@
<template>
<div style="display: none">
<!-- 这是一个隐藏的监听组件不需要UI -->
</div>
<!-- 简单的自定义对话框 -->
<div v-if="showDialog" class="dialog-overlay" @click.self="showDialog = false">
<div class="dialog-container">
<div class="dialog-header">
<h3>{{ dialogData.title }}</h3>
</div>
<div class="dialog-content">
<p>{{ dialogData.message }}</p>
</div>
<div class="dialog-actions">
<button
v-for="(option, index) in dialogData.options"
:key="index"
class="dialog-button"
@click="handleChoice(index)"
>
{{ option }}
</button>
</div>
</div>
<!-- 现在使用系统级对话框窗口而不是应用内弹窗 -->
</div>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted, ref, nextTick } from 'vue'
import { onMounted, onUnmounted } from 'vue'
import { useWebSocket, type WebSocketBaseMessage } from '@/composables/useWebSocket'
import { logger } from '@/utils/logger'
// WebSocket hook
const { subscribe, unsubscribe, sendRaw } = useWebSocket()
// 对话框状态
const showDialog = ref(false)
const dialogData = ref({
title: '',
message: '',
options: ['确定', '取消'],
messageId: ''
})
// 存储订阅ID用于取消订阅
let subscriptionId: string
// 检查是否在 Electron 环境中
const isElectron = () => {
return typeof window !== 'undefined' && (window as any).electronAPI
}
// 发送用户选择结果到后端
const sendResponse = (messageId: string, choice: boolean) => {
const response = {
@@ -59,39 +34,40 @@ const sendResponse = (messageId: string, choice: boolean) => {
sendRaw('Response', response)
}
// 处理用户选择
const handleChoice = (choiceIndex: number) => {
const choice = choiceIndex === 0 // 第一个选项为true其他为false
sendResponse(dialogData.value.messageId, choice)
showDialog.value = false
}
// 显示问题对话框
const showQuestion = (questionData: any) => {
// 显示系统级问题对话框
const showQuestion = async (questionData: any) => {
const title = questionData.title || '操作提示'
const message = questionData.message || ''
const options = questionData.options || ['确定', '取消']
const messageId = questionData.message_id || 'fallback_' + Date.now()
logger.info('[WebSocket消息监听器] 显示自定义对话框:', questionData)
logger.info('[WebSocket消息监听器] 显示系统级对话框:', questionData)
// 设置对话框数据
dialogData.value = {
title,
message,
options,
messageId
if (!isElectron()) {
logger.error('[WebSocket消息监听器] 不在 Electron 环境中,无法显示系统级对话框')
// 在非 Electron 环境中,使用默认响应
sendResponse(messageId, false)
return
}
showDialog.value = true
// 在下一个tick自动聚焦第一个按钮
nextTick(() => {
const firstButton = document.querySelector('.dialog-button:first-child') as HTMLButtonElement
if (firstButton) {
firstButton.focus()
}
})
try {
// 调用 Electron API 显示系统级对话框
const result = await (window as any).electronAPI.showQuestionDialog({
title,
message,
options,
messageId
})
logger.info('[WebSocket消息监听器] 系统级对话框返回结果:', result)
// 发送结果到后端
sendResponse(messageId, result)
} catch (error) {
logger.error('[WebSocket消息监听器] 显示系统级对话框失败:', error)
// 出错时发送默认响应
sendResponse(messageId, false)
}
}
// 消息处理函数
@@ -137,14 +113,14 @@ const handleObjectMessage = (data: any) => {
logger.info('[WebSocket消息监听器] 发现Question类型消息')
if (data.message_id) {
logger.info('[WebSocket消息监听器] message_id存在显示选择弹窗')
logger.info('[WebSocket消息监听器] message_id存在显示系统级对话框')
showQuestion(data)
return
} else {
logger.warn('[WebSocket消息监听器] Question消息缺少message_id字段:', data)
// 即使缺少message_id也尝试显示弹窗使用当前时间戳作为ID
// 即使缺少message_id也尝试显示对话框使用当前时间戳作为ID
const fallbackId = 'fallback_' + Date.now()
logger.info('[WebSocket消息监听器] 使用备用ID显示弹窗:', fallbackId)
logger.info('[WebSocket消息监听器] 使用备用ID显示对话框:', fallbackId)
showQuestion({
...data,
message_id: fallbackId
@@ -210,150 +186,4 @@ onUnmounted(() => {
})
</script>
<style scoped>
/* 对话框遮罩层 */
.dialog-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
}
/* 对话框容器 */
.dialog-container {
background: white;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
min-width: 300px;
max-width: 500px;
width: 90%;
animation: dialogAppear 0.2s ease-out;
}
/* 对话框头部 */
.dialog-header {
padding: 16px 20px;
border-bottom: 1px solid #eee;
}
.dialog-header h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #333;
}
/* 对话框内容 */
.dialog-content {
padding: 20px;
}
.dialog-content p {
margin: 0;
font-size: 14px;
line-height: 1.5;
color: #666;
word-break: break-word;
}
/* 按钮区域 */
.dialog-actions {
padding: 12px 20px 20px;
display: flex;
justify-content: flex-end;
gap: 8px;
}
/* 按钮样式 */
.dialog-button {
padding: 8px 16px;
border: 1px solid #ddd;
border-radius: 4px;
background: white;
color: #333;
cursor: pointer;
font-size: 14px;
transition: all 0.2s ease;
min-width: 60px;
}
.dialog-button:hover {
background: #f5f5f5;
border-color: #999;
}
.dialog-button:focus {
outline: 2px solid #007bff;
outline-offset: 2px;
}
.dialog-button:first-child {
background: #007bff;
color: white;
border-color: #007bff;
}
.dialog-button:first-child:hover {
background: #0056b3;
border-color: #0056b3;
}
/* 出现动画 */
@keyframes dialogAppear {
from {
opacity: 0;
transform: scale(0.9);
}
to {
opacity: 1;
transform: scale(1);
}
}
/* 暗色主题适配 */
@media (prefers-color-scheme: dark) {
.dialog-container {
background: #2d2d2d;
color: #fff;
}
.dialog-header {
border-bottom-color: #444;
}
.dialog-header h3 {
color: #fff;
}
.dialog-content p {
color: #ccc;
}
.dialog-button {
background: #444;
color: #fff;
border-color: #555;
}
.dialog-button:hover {
background: #555;
border-color: #666;
}
.dialog-button:first-child {
background: #0d6efd;
border-color: #0d6efd;
}
.dialog-button:first-child:hover {
background: #0b5ed7;
border-color: #0b5ed7;
}
}
</style>

View File

@@ -1,64 +1,74 @@
export interface ElectronAPI {
openDevTools: () => Promise<void>
selectFolder: () => Promise<string | null>
selectFile: (filters?: any[]) => Promise<string[]>
openUrl: (url: string) => Promise<{ success: boolean; error?: string }>
// 窗口控制
windowMinimize: () => Promise<void>
windowMaximize: () => Promise<void>
windowClose: () => Promise<void>
windowIsMaximized: () => Promise<boolean>
appQuit: () => Promise<void>
// 进程管理
getRelatedProcesses: () => Promise<any[]>
killAllProcesses: () => Promise<{ success: boolean; error?: string }>
forceExit: () => Promise<{ success: boolean }>
// 初始化相关API
checkEnvironment: () => Promise<any>
checkCriticalFiles: () => Promise<{ pythonExists: boolean; gitExists: boolean; mainPyExists: boolean }>
checkGitUpdate: () => Promise<{ hasUpdate: boolean; error?: string }>
downloadPython: (mirror?: string) => Promise<any>
installPip: () => Promise<any>
downloadGit: () => Promise<any>
installDependencies: (mirror?: string) => Promise<any>
cloneBackend: (repoUrl?: string) => Promise<any>
updateBackend: (repoUrl?: string) => Promise<any>
startBackend: () => Promise<{ success: boolean; error?: string }>
stopBackend?: () => Promise<{ success: boolean; error?: string }>
// 管理员权限相关
checkAdmin: () => Promise<boolean>
restartAsAdmin: () => Promise<void>
// 配置文件操作
saveConfig: (config: any) => Promise<void>
loadConfig: () => Promise<any>
resetConfig: () => Promise<void>
// 日志文件操作
getLogPath: () => Promise<string>
getLogFiles: () => Promise<string[]>
getLogs: (lines?: number, fileName?: string) => Promise<string>
clearLogs: (fileName?: string) => Promise<void>
cleanOldLogs: (daysToKeep?: number) => Promise<void>
// 保留原有方法以兼容现有代码
saveLogsToFile: (logs: string) => Promise<void>
loadLogsFromFile: () => Promise<string | null>
// 文件系统操作
openFile: (filePath: string) => Promise<void>
showItemInFolder: (filePath: string) => Promise<void>
// 监听下载进度
onDownloadProgress: (callback: (progress: any) => void) => void
removeDownloadProgressListener: () => void
}
declare global {
interface ElectronAPI {
openDevTools: () => Promise<void>
selectFolder: () => Promise<string | null>
selectFile: (filters?: any[]) => Promise<string[]>
openUrl: (url: string) => Promise<{ success: boolean; error?: string }>
// 窗口控制
windowMinimize: () => Promise<void>
windowMaximize: () => Promise<void>
windowClose: () => Promise<void>
windowIsMaximized: () => Promise<boolean>
appQuit: () => Promise<void>
// 进程管理
getRelatedProcesses: () => Promise<any[]>
killAllProcesses: () => Promise<{ success: boolean; error?: string }>
forceExit: () => Promise<{ success: boolean }>
// 初始化相关API
checkEnvironment: () => Promise<any>
checkCriticalFiles: () => Promise<{ pythonExists: boolean; gitExists: boolean; mainPyExists: boolean }>
checkGitUpdate: () => Promise<{ hasUpdate: boolean; error?: string }>
downloadPython: (mirror?: string) => Promise<any>
installPip: () => Promise<any>
downloadGit: () => Promise<any>
installDependencies: (mirror?: string) => Promise<any>
cloneBackend: (repoUrl?: string) => Promise<any>
updateBackend: (repoUrl?: string) => Promise<any>
startBackend: () => Promise<{ success: boolean; error?: string }>
stopBackend?: () => Promise<{ success: boolean; error?: string }>
// 管理员权限相关
checkAdmin: () => Promise<boolean>
restartAsAdmin: () => Promise<void>
// 配置文件操作
saveConfig: (config: any) => Promise<void>
loadConfig: () => Promise<any>
resetConfig: () => Promise<void>
// 日志文件操作
getLogPath: () => Promise<string>
getLogFiles: () => Promise<string[]>
getLogs: (lines?: number, fileName?: string) => Promise<string>
clearLogs: (fileName?: string) => Promise<void>
cleanOldLogs: (daysToKeep?: number) => Promise<void>
// 保留原有方法以兼容现有代码
saveLogsToFile: (logs: string) => Promise<void>
loadLogsFromFile: () => Promise<string | null>
// 文件系统操作
openFile: (filePath: string) => Promise<void>
showItemInFolder: (filePath: string) => Promise<void>
// 对话框相关
showQuestionDialog: (questionData: {
title?: string
message?: string
options?: string[]
messageId?: string
}) => Promise<boolean>
dialogResponse: (messageId: string, choice: boolean) => Promise<boolean>
resizeDialogWindow: (height: number) => Promise<void>
// 监听下载进度
onDownloadProgress: (callback: (progress: any) => void) => void
removeDownloadProgressListener: () => void
}
interface Window {
electronAPI: ElectronAPI
}

View File

@@ -1,78 +0,0 @@
// Electron API 类型定义
export interface ElectronAPI {
// 开发工具
openDevTools: () => Promise<void>
selectFolder: () => Promise<string | null>
selectFile: (filters?: Array<{ name: string; extensions: string[] }>) => Promise<string | null>
// 窗口控制
windowMinimize: () => Promise<void>
windowMaximize: () => Promise<void>
windowClose: () => Promise<void>
windowIsMaximized: () => Promise<boolean>
// 管理员权限检查
checkAdmin: () => Promise<boolean>
// 重启为管理员
restartAsAdmin: () => Promise<void>
appQuit: () => Promise<void>
// 进程管理
getRelatedProcesses: () => Promise<any[]>
killAllProcesses: () => Promise<{ success: boolean; error?: string }>
forceExit: () => Promise<{ success: boolean }>
// 环境检查
checkEnvironment: () => Promise<{
pythonExists: boolean
gitExists: boolean
backendExists: boolean
dependenciesInstalled: boolean
isInitialized: boolean
}>
// 关键文件检查
checkCriticalFiles: () => Promise<{
pythonExists: boolean
pipExists: boolean
gitExists: boolean
mainPyExists: boolean
}>
// Python相关
downloadPython: (mirror: string) => Promise<{ success: boolean; error?: string }>
deletePython: () => Promise<{ success: boolean; error?: string }>
// pip相关
installPip: () => Promise<{ success: boolean; error?: string }>
deletePip: () => Promise<{ success: boolean; error?: string }>
// Git相关
downloadGit: () => Promise<{ success: boolean; error?: string }>
deleteGit: () => Promise<{ success: boolean; error?: string }>
checkGitUpdate: () => Promise<{ hasUpdate: boolean; error?: string }>
// 后端代码相关
cloneBackend: (gitUrl: string) => Promise<{ success: boolean; error?: string }>
updateBackend: (gitUrl: string) => Promise<{ success: boolean; error?: string }>
// 依赖安装
installDependencies: (mirror: string) => Promise<{ success: boolean; error?: string }>
// 后端服务
startBackend: () => Promise<{ success: boolean; error?: string }>
// 下载进度监听
onDownloadProgress: (
callback: (progress: { progress: number; status: string; message: string }) => void
) => void
removeDownloadProgressListener: () => void
}
declare global {
interface Window {
electronAPI: ElectronAPI
}
}
export {}

View File

@@ -1,24 +1,13 @@
// 渲染进程日志工具
interface ElectronAPI {
getLogPath: () => Promise<string>
getLogFiles: () => Promise<string[]>
getLogs: (lines?: number, fileName?: string) => Promise<string>
clearLogs: (fileName?: string) => Promise<void>
cleanOldLogs: (daysToKeep?: number) => Promise<void>
}
const LogLevel = {
DEBUG: 'DEBUG',
INFO: 'INFO',
WARN: 'WARN',
ERROR: 'ERROR'
} as const
declare global {
interface Window {
electronAPI: ElectronAPI
}
}
export enum LogLevel {
DEBUG = 'DEBUG',
INFO = 'INFO',
WARN = 'WARN',
ERROR = 'ERROR'
}
export type LogLevel = typeof LogLevel[keyof typeof LogLevel]
export { LogLevel }
class Logger {
// 直接使用原生console主进程会自动处理日志记录
@@ -40,32 +29,32 @@ class Logger {
// 获取日志文件路径
async getLogPath(): Promise<string> {
if (window.electronAPI) {
return await window.electronAPI.getLogPath()
if ((window as any).electronAPI) {
return await (window as any).electronAPI.getLogPath()
}
throw new Error('Electron API not available')
}
// 获取日志文件列表
async getLogFiles(): Promise<string[]> {
if (window.electronAPI) {
return await window.electronAPI.getLogFiles()
if ((window as any).electronAPI) {
return await (window as any).electronAPI.getLogFiles()
}
throw new Error('Electron API not available')
}
// 获取日志内容
async getLogs(lines?: number, fileName?: string): Promise<string> {
if (window.electronAPI) {
return await window.electronAPI.getLogs(lines, fileName)
if ((window as any).electronAPI) {
return await (window as any).electronAPI.getLogs(lines, fileName)
}
throw new Error('Electron API not available')
}
// 清空日志
async clearLogs(fileName?: string): Promise<void> {
if (window.electronAPI) {
await window.electronAPI.clearLogs(fileName)
if ((window as any).electronAPI) {
await (window as any).electronAPI.clearLogs(fileName)
console.info(`日志已清空: ${fileName || '当前文件'}`)
} else {
throw new Error('Electron API not available')
@@ -74,8 +63,8 @@ class Logger {
// 清理旧日志
async cleanOldLogs(daysToKeep: number = 7): Promise<void> {
if (window.electronAPI) {
await window.electronAPI.cleanOldLogs(daysToKeep)
if ((window as any).electronAPI) {
await (window as any).electronAPI.cleanOldLogs(daysToKeep)
console.info(`已清理${daysToKeep}天前的旧日志`)
} else {
throw new Error('Electron API not available')