feat(initialization): 新增配置文件操作和管理员权限检查

- 新增配置文件保存、加载和重置功能
- 添加管理员权限检查和重启为管理员的功能
- 实现 pip 包管理器安装功能
- 优化初始化流程,自动检测并安装依赖
This commit is contained in:
2025-08-07 20:18:53 +08:00
parent e7f898f357
commit 0171c3ca4d
15 changed files with 2397 additions and 8 deletions

View File

@@ -36,10 +36,48 @@ Object.defineProperty(exports, "__esModule", { value: true });
const electron_1 = require("electron");
const path = __importStar(require("path"));
const fs = __importStar(require("fs"));
const child_process_1 = require("child_process");
const environmentService_1 = require("./services/environmentService");
const downloadService_1 = require("./services/downloadService");
const pythonService_1 = require("./services/pythonService");
const gitService_1 = require("./services/gitService");
// 检查是否以管理员权限运行
function isRunningAsAdmin() {
try {
// 在Windows上尝试写入系统目录来检查管理员权限
if (process.platform === 'win32') {
const testPath = path.join(process.env.WINDIR || 'C:\\Windows', 'temp', 'admin-test.tmp');
try {
fs.writeFileSync(testPath, 'test');
fs.unlinkSync(testPath);
return true;
}
catch {
return false;
}
}
return true; // 非Windows系统暂时返回true
}
catch {
return false;
}
}
// 重新以管理员权限启动应用
function restartAsAdmin() {
if (process.platform === 'win32') {
const exePath = process.execPath;
const args = process.argv.slice(1);
// 使用PowerShell以管理员权限启动
(0, child_process_1.spawn)('powershell', [
'-Command',
`Start-Process -FilePath "${exePath}" -ArgumentList "${args.join(' ')}" -Verb RunAs`
], {
detached: true,
stdio: 'ignore'
});
electron_1.app.quit();
}
}
let mainWindow = null;
function createWindow() {
mainWindow = new electron_1.BrowserWindow({
@@ -109,6 +147,10 @@ electron_1.ipcMain.handle('download-python', async (event, mirror = 'tsinghua')
const appRoot = (0, environmentService_1.getAppRoot)();
return (0, pythonService_1.downloadPython)(appRoot, mirror);
});
electron_1.ipcMain.handle('install-pip', async () => {
const appRoot = (0, environmentService_1.getAppRoot)();
return (0, pythonService_1.installPipPackage)(appRoot);
});
electron_1.ipcMain.handle('install-dependencies', async (event, mirror = 'tsinghua') => {
const appRoot = (0, environmentService_1.getAppRoot)();
return (0, pythonService_1.installDependencies)(appRoot, mirror);
@@ -130,6 +172,54 @@ electron_1.ipcMain.handle('update-backend', async (event, repoUrl = 'https://git
const appRoot = (0, environmentService_1.getAppRoot)();
return (0, gitService_1.cloneBackend)(appRoot, repoUrl); // 使用相同的逻辑会自动判断是pull还是clone
});
// 配置文件操作
electron_1.ipcMain.handle('save-config', async (event, config) => {
try {
const appRoot = (0, environmentService_1.getAppRoot)();
const configDir = path.join(appRoot, 'config');
const configPath = path.join(configDir, 'frontend_config.json');
// 确保config目录存在
if (!fs.existsSync(configDir)) {
fs.mkdirSync(configDir, { recursive: true });
}
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
console.log(`配置已保存到: ${configPath}`);
}
catch (error) {
console.error('保存配置文件失败:', error);
throw error;
}
});
electron_1.ipcMain.handle('load-config', async () => {
try {
const appRoot = (0, environmentService_1.getAppRoot)();
const configPath = path.join(appRoot, 'config', 'frontend_config.json');
if (fs.existsSync(configPath)) {
const config = fs.readFileSync(configPath, 'utf8');
console.log(`从文件加载配置: ${configPath}`);
return JSON.parse(config);
}
return null;
}
catch (error) {
console.error('加载配置文件失败:', error);
return null;
}
});
electron_1.ipcMain.handle('reset-config', async () => {
try {
const appRoot = (0, environmentService_1.getAppRoot)();
const configPath = path.join(appRoot, 'config', 'frontend_config.json');
if (fs.existsSync(configPath)) {
fs.unlinkSync(configPath);
console.log(`配置文件已删除: ${configPath}`);
}
}
catch (error) {
console.error('重置配置文件失败:', error);
throw error;
}
});
// 日志文件操作
electron_1.ipcMain.handle('save-logs-to-file', async (event, logs) => {
try {
@@ -164,8 +254,23 @@ electron_1.ipcMain.handle('load-logs-from-file', async () => {
return null;
}
});
// 管理员权限相关
electron_1.ipcMain.handle('check-admin', () => {
return isRunningAsAdmin();
});
electron_1.ipcMain.handle('restart-as-admin', () => {
restartAsAdmin();
});
// 应用生命周期
electron_1.app.whenReady().then(createWindow);
electron_1.app.whenReady().then(() => {
// 检查管理员权限
if (!isRunningAsAdmin()) {
console.log('应用未以管理员权限运行');
// 在生产环境中,可以选择是否强制要求管理员权限
// 这里先创建窗口,让用户选择是否重新启动
}
createWindow();
});
electron_1.app.on('window-all-closed', () => {
if (process.platform !== 'darwin')
electron_1.app.quit();

View File

@@ -12,11 +12,19 @@ electron_1.contextBridge.exposeInMainWorld('electronAPI', {
// 初始化相关API
checkEnvironment: () => electron_1.ipcRenderer.invoke('check-environment'),
downloadPython: (mirror) => electron_1.ipcRenderer.invoke('download-python', mirror),
installPip: () => electron_1.ipcRenderer.invoke('install-pip'),
downloadGit: () => electron_1.ipcRenderer.invoke('download-git'),
installDependencies: (mirror) => electron_1.ipcRenderer.invoke('install-dependencies', mirror),
cloneBackend: (repoUrl) => electron_1.ipcRenderer.invoke('clone-backend', repoUrl),
updateBackend: (repoUrl) => electron_1.ipcRenderer.invoke('update-backend', repoUrl),
startBackend: () => electron_1.ipcRenderer.invoke('start-backend'),
// 管理员权限相关
checkAdmin: () => electron_1.ipcRenderer.invoke('check-admin'),
restartAsAdmin: () => electron_1.ipcRenderer.invoke('restart-as-admin'),
// 配置文件操作
saveConfig: (config) => electron_1.ipcRenderer.invoke('save-config', config),
loadConfig: () => electron_1.ipcRenderer.invoke('load-config'),
resetConfig: () => electron_1.ipcRenderer.invoke('reset-config'),
// 日志文件操作
saveLogsToFile: (logs) => electron_1.ipcRenderer.invoke('save-logs-to-file', logs),
loadLogsFromFile: () => electron_1.ipcRenderer.invoke('load-logs-from-file'),

View File

@@ -1,11 +1,51 @@
import { app, BrowserWindow, ipcMain, dialog } from 'electron'
import * as path from 'path'
import * as fs from 'fs'
import { spawn } from 'child_process'
import { getAppRoot, checkEnvironment } from './services/environmentService'
import { setMainWindow as setDownloadMainWindow } from './services/downloadService'
import { setMainWindow as setPythonMainWindow, downloadPython, installDependencies, startBackend } from './services/pythonService'
import { setMainWindow as setPythonMainWindow, downloadPython, installPipPackage, installDependencies, startBackend } from './services/pythonService'
import { setMainWindow as setGitMainWindow, downloadGit, cloneBackend } from './services/gitService'
// 检查是否以管理员权限运行
function isRunningAsAdmin(): boolean {
try {
// 在Windows上尝试写入系统目录来检查管理员权限
if (process.platform === 'win32') {
const testPath = path.join(process.env.WINDIR || 'C:\\Windows', 'temp', 'admin-test.tmp')
try {
fs.writeFileSync(testPath, 'test')
fs.unlinkSync(testPath)
return true
} catch {
return false
}
}
return true // 非Windows系统暂时返回true
} catch {
return false
}
}
// 重新以管理员权限启动应用
function restartAsAdmin(): void {
if (process.platform === 'win32') {
const exePath = process.execPath
const args = process.argv.slice(1)
// 使用PowerShell以管理员权限启动
spawn('powershell', [
'-Command',
`Start-Process -FilePath "${exePath}" -ArgumentList "${args.join(' ')}" -Verb RunAs`
], {
detached: true,
stdio: 'ignore'
})
app.quit()
}
}
let mainWindow: BrowserWindow | null = null
function createWindow() {
@@ -83,6 +123,11 @@ ipcMain.handle('download-python', async (event, mirror = 'tsinghua') => {
return downloadPython(appRoot, mirror)
})
ipcMain.handle('install-pip', async () => {
const appRoot = getAppRoot()
return installPipPackage(appRoot)
})
ipcMain.handle('install-dependencies', async (event, mirror = 'tsinghua') => {
const appRoot = getAppRoot()
return installDependencies(appRoot, mirror)
@@ -109,6 +154,59 @@ ipcMain.handle('update-backend', async (event, repoUrl = 'https://github.com/DLm
return cloneBackend(appRoot, repoUrl) // 使用相同的逻辑会自动判断是pull还是clone
})
// 配置文件操作
ipcMain.handle('save-config', async (event, config) => {
try {
const appRoot = getAppRoot()
const configDir = path.join(appRoot, 'config')
const configPath = path.join(configDir, 'frontend_config.json')
// 确保config目录存在
if (!fs.existsSync(configDir)) {
fs.mkdirSync(configDir, { recursive: true })
}
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8')
console.log(`配置已保存到: ${configPath}`)
} catch (error) {
console.error('保存配置文件失败:', error)
throw error
}
})
ipcMain.handle('load-config', async () => {
try {
const appRoot = getAppRoot()
const configPath = path.join(appRoot, 'config', 'frontend_config.json')
if (fs.existsSync(configPath)) {
const config = fs.readFileSync(configPath, 'utf8')
console.log(`从文件加载配置: ${configPath}`)
return JSON.parse(config)
}
return null
} catch (error) {
console.error('加载配置文件失败:', error)
return null
}
})
ipcMain.handle('reset-config', async () => {
try {
const appRoot = getAppRoot()
const configPath = path.join(appRoot, 'config', 'frontend_config.json')
if (fs.existsSync(configPath)) {
fs.unlinkSync(configPath)
console.log(`配置文件已删除: ${configPath}`)
}
} catch (error) {
console.error('重置配置文件失败:', error)
throw error
}
})
// 日志文件操作
ipcMain.handle('save-logs-to-file', async (event, logs: string) => {
try {
@@ -147,8 +245,25 @@ ipcMain.handle('load-logs-from-file', async () => {
}
})
// 管理员权限相关
ipcMain.handle('check-admin', () => {
return isRunningAsAdmin()
})
ipcMain.handle('restart-as-admin', () => {
restartAsAdmin()
})
// 应用生命周期
app.whenReady().then(createWindow)
app.whenReady().then(() => {
// 检查管理员权限
if (!isRunningAsAdmin()) {
console.log('应用未以管理员权限运行')
// 在生产环境中,可以选择是否强制要求管理员权限
// 这里先创建窗口,让用户选择是否重新启动
}
createWindow()
})
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit()

View File

@@ -13,12 +13,22 @@ contextBridge.exposeInMainWorld('electronAPI', {
// 初始化相关API
checkEnvironment: () => ipcRenderer.invoke('check-environment'),
downloadPython: (mirror?: string) => ipcRenderer.invoke('download-python', mirror),
installPip: () => ipcRenderer.invoke('install-pip'),
downloadGit: () => ipcRenderer.invoke('download-git'),
installDependencies: (mirror?: string) => ipcRenderer.invoke('install-dependencies', mirror),
cloneBackend: (repoUrl?: string) => ipcRenderer.invoke('clone-backend', repoUrl),
updateBackend: (repoUrl?: string) => ipcRenderer.invoke('update-backend', repoUrl),
startBackend: () => ipcRenderer.invoke('start-backend'),
// 管理员权限相关
checkAdmin: () => ipcRenderer.invoke('check-admin'),
restartAsAdmin: () => ipcRenderer.invoke('restart-as-admin'),
// 配置文件操作
saveConfig: (config: any) => ipcRenderer.invoke('save-config', config),
loadConfig: () => ipcRenderer.invoke('load-config'),
resetConfig: () => ipcRenderer.invoke('reset-config'),
// 日志文件操作
saveLogsToFile: (logs: string) => ipcRenderer.invoke('save-logs-to-file', logs),
loadLogsFromFile: () => ipcRenderer.invoke('load-logs-from-file'),

View File

@@ -399,6 +399,50 @@ export async function installDependencies(appRoot: string, mirror = 'tsinghua'):
}
}
// 导出pip安装函数
export async function installPipPackage(appRoot: string): Promise<{ success: boolean; error?: string }> {
try {
const pythonPath = path.join(appRoot, 'environment', 'python')
if (!fs.existsSync(pythonPath)) {
throw new Error('Python环境不存在请先安装Python')
}
if (mainWindow) {
mainWindow.webContents.send('download-progress', {
type: 'pip',
progress: 0,
status: 'installing',
message: '正在安装pip...'
})
}
await installPip(pythonPath, appRoot)
if (mainWindow) {
mainWindow.webContents.send('download-progress', {
type: 'pip',
progress: 100,
status: 'completed',
message: 'pip安装完成'
})
}
return { success: true }
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
if (mainWindow) {
mainWindow.webContents.send('download-progress', {
type: 'pip',
progress: 0,
status: 'error',
message: `pip安装失败: ${errorMessage}`
})
}
return { success: false, error: errorMessage }
}
}
// 启动后端
export async function startBackend(appRoot: string): Promise<{ success: boolean; error?: string }> {
try {

View File

@@ -0,0 +1,264 @@
<template>
<div class="step-panel">
<h3>获取后端源码</h3>
<div class="install-section">
<p>{{ backendExists ? '更新最新的后端代码' : '获取后端源代码' }}</p>
<div class="mirror-grid">
<div
v-for="mirror in gitMirrors"
:key="mirror.key"
class="mirror-card"
:class="{ active: selectedGitMirror === mirror.key }"
@click="selectedGitMirror = mirror.key"
>
<div class="mirror-header">
<h4>{{ mirror.name }}</h4>
<div class="speed-badge" :class="getSpeedClass(mirror.speed)">
<span v-if="mirror.speed === null && !testingGitSpeed">未测试</span>
<span v-else-if="testingGitSpeed">测试中...</span>
<span v-else-if="mirror.speed === 9999">超时</span>
<span v-else>{{ mirror.speed }}ms</span>
</div>
</div>
<div class="mirror-url">{{ mirror.url }}</div>
</div>
</div>
<div class="test-actions">
<a-button @click="testGitMirrorSpeed" :loading="testingGitSpeed" type="primary">
{{ testingGitSpeed ? '测速中...' : '开始测速' }}
</a-button>
<span class="test-note">3秒无响应视为超时</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { getConfig, saveConfig } from '@/utils/config'
interface Mirror {
key: string
name: string
url: string
speed: number | null
}
defineProps<{
backendExists: boolean
}>()
const gitMirrors = ref<Mirror[]>([
{ key: 'github', name: 'GitHub 官方', url: 'https://github.com/DLmaster361/AUTO_MAA.git', speed: null },
{ key: 'ghfast', name: 'ghfast 镜像', url: 'https://ghfast.top/https://github.com/DLmaster361/AUTO_MAA.git', speed: null }
])
const selectedGitMirror = ref('github')
const testingGitSpeed = ref(false)
// 加载配置中的镜像源选择
async function loadMirrorConfig() {
try {
const config = await getConfig()
selectedGitMirror.value = config.selectedGitMirror
console.log('Git镜像源配置已加载:', selectedGitMirror.value)
} catch (error) {
console.warn('加载Git镜像源配置失败:', error)
}
}
// 保存镜像源选择
async function saveMirrorConfig() {
try {
await saveConfig({ selectedGitMirror: selectedGitMirror.value })
console.log('Git镜像源配置已保存:', selectedGitMirror.value)
} catch (error) {
console.warn('保存Git镜像源配置失败:', error)
}
}
async function testMirrorWithTimeout(url: string, timeout = 3000): Promise<number> {
const startTime = Date.now()
try {
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), timeout)
await fetch(url.replace('.git', ''), {
method: 'HEAD',
mode: 'no-cors',
signal: controller.signal
})
clearTimeout(timeoutId)
return Date.now() - startTime
} catch (error) {
return 9999 // 超时或失败
}
}
async function testGitMirrorSpeed() {
testingGitSpeed.value = true
try {
const promises = gitMirrors.value.map(async (mirror) => {
mirror.speed = await testMirrorWithTimeout(mirror.url)
return mirror
})
await Promise.all(promises)
gitMirrors.value.sort((a, b) => (a.speed || 9999) - (b.speed || 9999))
const fastest = gitMirrors.value.find(m => m.speed !== 9999)
if (fastest) {
selectedGitMirror.value = fastest.key
await saveMirrorConfig() // 保存最快的镜像源选择
}
} finally {
testingGitSpeed.value = false
}
}
function getSpeedClass(speed: number | null) {
if (speed === null) return 'speed-unknown'
if (speed === 9999) return 'speed-timeout'
if (speed < 500) return 'speed-fast'
if (speed < 1500) return 'speed-medium'
return 'speed-slow'
}
defineExpose({
selectedGitMirror,
testGitMirrorSpeed,
gitMirrors
})
// 组件挂载时加载配置并自动开始测速
onMounted(async () => {
// 先加载配置
await loadMirrorConfig()
console.log('BackendStep 组件挂载,自动开始测速')
setTimeout(() => {
testGitMirrorSpeed()
}, 200) // 延迟200ms确保组件完全渲染
})
</script>
<style scoped>
.step-panel {
padding: 20px;
background: var(--ant-color-bg-elevated);
border-radius: 8px;
border: 1px solid var(--ant-color-border);
}
.step-panel h3 {
font-size: 20px;
font-weight: 600;
color: var(--ant-color-text);
margin-bottom: 20px;
}
.install-section {
display: flex;
flex-direction: column;
gap: 20px;
}
.install-section p {
color: var(--ant-color-text-secondary);
margin: 0;
}
.mirror-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 16px;
margin-bottom: 20px;
}
.mirror-card {
padding: 16px;
border: 2px solid var(--ant-color-border);
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
background: var(--ant-color-bg-container);
}
.mirror-card:hover {
border-color: var(--ant-color-primary);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.mirror-card.active {
border-color: var(--ant-color-primary);
background: var(--ant-color-primary-bg);
}
.mirror-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.mirror-header h4 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: var(--ant-color-text);
}
.speed-badge {
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.speed-badge.speed-unknown {
background: var(--ant-color-fill-tertiary);
color: var(--ant-color-text-tertiary);
}
.speed-badge.speed-fast {
background: var(--ant-color-success-bg);
color: var(--ant-color-success);
}
.speed-badge.speed-medium {
background: var(--ant-color-warning-bg);
color: var(--ant-color-warning);
}
.speed-badge.speed-slow {
background: var(--ant-color-error-bg);
color: var(--ant-color-error);
}
.speed-badge.speed-timeout {
background: var(--ant-color-error-bg);
color: var(--ant-color-error);
}
.mirror-url {
font-size: 12px;
color: var(--ant-color-text-tertiary);
word-break: break-all;
}
.test-actions {
display: flex;
align-items: center;
gap: 12px;
justify-content: center;
}
.test-note {
font-size: 12px;
color: var(--ant-color-text-tertiary);
}
</style>

View File

@@ -0,0 +1,263 @@
<template>
<div class="step-panel">
<h3>安装 Python 依赖包</h3>
<div class="install-section">
<p>通过 pip 安装项目所需的 Python 依赖包</p>
<div class="mirror-grid">
<div
v-for="mirror in pipMirrors"
:key="mirror.key"
class="mirror-card"
:class="{ active: selectedPipMirror === mirror.key }"
@click="selectedPipMirror = mirror.key"
>
<div class="mirror-header">
<h4>{{ mirror.name }}</h4>
<div class="speed-badge" :class="getSpeedClass(mirror.speed)">
<span v-if="mirror.speed === null && !testingPipSpeed">未测试</span>
<span v-else-if="testingPipSpeed">测试中...</span>
<span v-else-if="mirror.speed === 9999">超时</span>
<span v-else>{{ mirror.speed }}ms</span>
</div>
</div>
<div class="mirror-url">{{ mirror.url }}</div>
</div>
</div>
<div class="test-actions">
<a-button @click="testPipMirrorSpeed" :loading="testingPipSpeed" type="primary">
{{ testingPipSpeed ? '测速中...' : '重新测速' }}
</a-button>
<span class="test-note">3秒无响应视为超时</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { getConfig, saveConfig } from '@/utils/config'
interface Mirror {
key: string
name: string
url: string
speed: number | null
}
const pipMirrors = ref<Mirror[]>([
{ key: 'official', name: 'PyPI 官方', url: 'https://pypi.org/simple/', speed: null },
{ key: 'tsinghua', name: '清华大学', url: 'https://pypi.tuna.tsinghua.edu.cn/simple/', speed: null },
{ key: 'aliyun', name: '阿里云', url: 'https://mirrors.aliyun.com/pypi/simple/', speed: null },
{ key: 'douban', name: '豆瓣', url: 'https://pypi.douban.com/simple/', speed: null },
{ key: 'ustc', name: '中科大', url: 'https://pypi.mirrors.ustc.edu.cn/simple/', speed: null },
{ key: 'huawei', name: '华中科技大学', url: 'https://pypi.hustunique.com/simple/', speed: null }
])
const selectedPipMirror = ref('tsinghua')
const testingPipSpeed = ref(false)
// 加载配置中的镜像源选择
async function loadMirrorConfig() {
try {
const config = await getConfig()
selectedPipMirror.value = config.selectedPipMirror
console.log('pip镜像源配置已加载:', selectedPipMirror.value)
} catch (error) {
console.warn('加载pip镜像源配置失败:', error)
}
}
// 保存镜像源选择
async function saveMirrorConfig() {
try {
await saveConfig({ selectedPipMirror: selectedPipMirror.value })
console.log('pip镜像源配置已保存:', selectedPipMirror.value)
} catch (error) {
console.warn('保存pip镜像源配置失败:', error)
}
}
async function testMirrorWithTimeout(url: string, timeout = 3000): Promise<number> {
const startTime = Date.now()
try {
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), timeout)
await fetch(url, {
method: 'HEAD',
mode: 'no-cors',
signal: controller.signal
})
clearTimeout(timeoutId)
return Date.now() - startTime
} catch (error) {
return 9999 // 超时或失败
}
}
async function testPipMirrorSpeed() {
testingPipSpeed.value = true
try {
const promises = pipMirrors.value.map(async (mirror) => {
mirror.speed = await testMirrorWithTimeout(mirror.url)
return mirror
})
await Promise.all(promises)
pipMirrors.value.sort((a, b) => (a.speed || 9999) - (b.speed || 9999))
const fastest = pipMirrors.value.find(m => m.speed !== 9999)
if (fastest) {
selectedPipMirror.value = fastest.key
await saveMirrorConfig() // 保存最快的镜像源选择
}
} finally {
testingPipSpeed.value = false
}
}
function getSpeedClass(speed: number | null) {
if (speed === null) return 'speed-unknown'
if (speed === 9999) return 'speed-timeout'
if (speed < 500) return 'speed-fast'
if (speed < 1500) return 'speed-medium'
return 'speed-slow'
}
defineExpose({
selectedPipMirror,
testPipMirrorSpeed
})
// 组件挂载时加载配置并自动开始测速
onMounted(async () => {
// 先加载配置
await loadMirrorConfig()
console.log('DependenciesStep 组件挂载,自动开始测速')
setTimeout(() => {
testPipMirrorSpeed()
}, 200) // 延迟200ms确保组件完全渲染
})
</script>
<style scoped>
.step-panel {
padding: 20px;
background: var(--ant-color-bg-elevated);
border-radius: 8px;
border: 1px solid var(--ant-color-border);
}
.step-panel h3 {
font-size: 20px;
font-weight: 600;
color: var(--ant-color-text);
margin-bottom: 20px;
}
.install-section {
display: flex;
flex-direction: column;
gap: 20px;
}
.install-section p {
color: var(--ant-color-text-secondary);
margin: 0;
}
.mirror-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 16px;
margin-bottom: 20px;
}
.mirror-card {
padding: 16px;
border: 2px solid var(--ant-color-border);
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
background: var(--ant-color-bg-container);
}
.mirror-card:hover {
border-color: var(--ant-color-primary);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.mirror-card.active {
border-color: var(--ant-color-primary);
background: var(--ant-color-primary-bg);
}
.mirror-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.mirror-header h4 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: var(--ant-color-text);
}
.speed-badge {
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.speed-badge.speed-unknown {
background: var(--ant-color-fill-tertiary);
color: var(--ant-color-text-tertiary);
}
.speed-badge.speed-fast {
background: var(--ant-color-success-bg);
color: var(--ant-color-success);
}
.speed-badge.speed-medium {
background: var(--ant-color-warning-bg);
color: var(--ant-color-warning);
}
.speed-badge.speed-slow {
background: var(--ant-color-error-bg);
color: var(--ant-color-error);
}
.speed-badge.speed-timeout {
background: var(--ant-color-error-bg);
color: var(--ant-color-error);
}
.mirror-url {
font-size: 12px;
color: var(--ant-color-text-tertiary);
word-break: break-all;
}
.test-actions {
display: flex;
align-items: center;
gap: 12px;
justify-content: center;
}
.test-note {
font-size: 12px;
color: var(--ant-color-text-tertiary);
}
</style>

View File

@@ -0,0 +1,63 @@
<template>
<div class="step-panel">
<h3>Git 版本控制工具</h3>
<div v-if="!gitInstalled" class="install-section">
<p>需要安装 Git 工具来获取源代码</p>
<div class="git-info">
<a-alert
message="Git 工具信息"
description="将安装便携版 Git 工具,包含完整的版本控制功能,无需系统安装。"
type="info"
show-icon
/>
</div>
</div>
<div v-else class="already-installed">
<a-result status="success" title="Git 工具已安装" />
</div>
</div>
</template>
<script setup lang="ts">
defineProps<{
gitInstalled: boolean
}>()
</script>
<style scoped>
.step-panel {
padding: 20px;
background: var(--ant-color-bg-elevated);
border-radius: 8px;
border: 1px solid var(--ant-color-border);
}
.step-panel h3 {
font-size: 20px;
font-weight: 600;
color: var(--ant-color-text);
margin-bottom: 20px;
}
.install-section {
display: flex;
flex-direction: column;
gap: 20px;
}
.install-section p {
color: var(--ant-color-text-secondary);
margin: 0;
}
.git-info {
margin-top: 16px;
}
.already-installed {
display: flex;
justify-content: center;
align-items: center;
min-height: 200px;
}
</style>

View File

@@ -0,0 +1,64 @@
<template>
<div class="step-panel">
<h3>安装 pip 包管理器</h3>
<div v-if="!pipInstalled" class="install-section">
<p>pip Python 的包管理工具用于安装和管理 Python </p>
<div class="pip-info">
<a-alert
message="pip 安装信息"
description="将自动下载并安装 pip 包管理器,这是安装 Python 依赖包的必要工具。"
type="info"
show-icon
/>
</div>
</div>
<div v-else class="already-installed">
<a-result status="success" title="pip 已安装" />
</div>
</div>
</template>
<script setup lang="ts">
defineProps<{
pipInstalled: boolean
}>()
</script>
<style scoped>
.step-panel {
padding: 20px;
background: var(--ant-color-bg-elevated);
border-radius: 8px;
border: 1px solid var(--ant-color-border);
}
.step-panel h3 {
font-size: 20px;
font-weight: 600;
color: var(--ant-color-text);
margin-bottom: 20px;
}
.install-section {
display: flex;
flex-direction: column;
gap: 20px;
}
.install-section p {
color: var(--ant-color-text-secondary);
margin: 0;
}
.pip-info {
margin-top: 16px;
}
.already-installed {
display: flex;
justify-content: center;
align-items: center;
min-height: 200px;
}
</style>

View File

@@ -0,0 +1,274 @@
<template>
<div class="step-panel">
<h3>Python 运行环境</h3>
<div v-if="!pythonInstalled" class="install-section">
<p>需要安装 Python 3.13.0 运行环境64位嵌入式版本</p>
<div class="mirror-grid">
<div v-for="mirror in pythonMirrors" :key="mirror.key" class="mirror-card"
:class="{ active: selectedPythonMirror === mirror.key }" @click="selectedPythonMirror = mirror.key">
<div class="mirror-header">
<h4>{{ mirror.name }}</h4>
<div class="speed-badge" :class="getSpeedClass(mirror.speed)">
<span v-if="mirror.speed === null && !testingSpeed">未测试</span>
<span v-else-if="testingSpeed">测试中...</span>
<span v-else-if="mirror.speed === 9999">超时</span>
<span v-else>{{ mirror.speed }}ms</span>
</div>
</div>
<div class="mirror-url">{{ mirror.url }}</div>
</div>
</div>
<div class="test-actions">
<a-button @click="testPythonMirrorSpeed" :loading="testingSpeed" type="primary">
{{ testingSpeed ? '测速中...' : '重新测速' }}
</a-button>
<span class="test-note">3秒无响应视为超时</span>
</div>
</div>
<div v-else class="already-installed">
<a-result status="success" title="Python 环境已安装" />
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { getConfig, saveConfig } from '@/utils/config'
interface Mirror {
key: string
name: string
url: string
speed: number | null
}
const props = defineProps<{
pythonInstalled: boolean
}>()
const pythonMirrors = ref<Mirror[]>([
{ key: 'official', name: 'Python 官方', url: 'https://www.python.org/ftp/python/3.13.0/', speed: null },
{ key: 'tsinghua', name: '清华 TUNA 镜像', url: 'https://mirrors.tuna.tsinghua.edu.cn/python/3.13.0/', speed: null },
{ key: 'ustc', name: '中科大镜像', url: 'https://mirrors.ustc.edu.cn/python/3.13.0/', speed: null },
{ key: 'huawei', name: '华为云镜像', url: 'https://mirrors.huaweicloud.com/repository/toolkit/python/3.13.0/', speed: null },
{ key: 'aliyun', name: '阿里云镜像', url: 'https://mirrors.aliyun.com/python-release/windows/', speed: null }
])
const selectedPythonMirror = ref('tsinghua')
const testingSpeed = ref(false)
// 加载配置中的镜像源选择
async function loadMirrorConfig() {
try {
const config = await getConfig()
selectedPythonMirror.value = config.selectedPythonMirror
console.log('Python镜像源配置已加载:', selectedPythonMirror.value)
} catch (error) {
console.warn('加载Python镜像源配置失败:', error)
}
}
// 保存镜像源选择
async function saveMirrorConfig() {
try {
await saveConfig({ selectedPythonMirror: selectedPythonMirror.value })
console.log('Python镜像源配置已保存:', selectedPythonMirror.value)
} catch (error) {
console.warn('保存Python镜像源配置失败:', error)
}
}
async function testMirrorWithTimeout(url: string, timeout = 3000): Promise<number> {
const startTime = Date.now()
try {
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), timeout)
await fetch(url, {
method: 'HEAD',
mode: 'no-cors',
signal: controller.signal
})
clearTimeout(timeoutId)
return Date.now() - startTime
} catch (error) {
return 9999 // 超时或失败
}
}
async function testPythonMirrorSpeed() {
testingSpeed.value = true
try {
const promises = pythonMirrors.value.map(async (mirror) => {
mirror.speed = await testMirrorWithTimeout(mirror.url)
return mirror
})
await Promise.all(promises)
pythonMirrors.value.sort((a, b) => (a.speed || 9999) - (b.speed || 9999))
const fastest = pythonMirrors.value.find(m => m.speed !== 9999)
if (fastest) {
selectedPythonMirror.value = fastest.key
await saveMirrorConfig() // 保存最快的镜像源选择
}
} finally {
testingSpeed.value = false
}
}
function getSpeedClass(speed: number | null) {
if (speed === null) return 'speed-unknown'
if (speed === 9999) return 'speed-timeout'
if (speed < 500) return 'speed-fast'
if (speed < 1500) return 'speed-medium'
return 'speed-slow'
}
defineExpose({
selectedPythonMirror,
testPythonMirrorSpeed
})
// 组件挂载时加载配置并自动开始测速
onMounted(async () => {
// 先加载配置
await loadMirrorConfig()
if (!props.pythonInstalled) {
console.log('PythonStep 组件挂载,自动开始测速')
setTimeout(() => {
testPythonMirrorSpeed()
}, 200) // 延迟200ms确保组件完全渲染
}
})
</script>
<style scoped>
.step-panel {
padding: 20px;
background: var(--ant-color-bg-elevated);
border-radius: 8px;
border: 1px solid var(--ant-color-border);
}
.step-panel h3 {
font-size: 20px;
font-weight: 600;
color: var(--ant-color-text);
margin-bottom: 20px;
}
.install-section {
display: flex;
flex-direction: column;
gap: 20px;
}
.install-section p {
color: var(--ant-color-text-secondary);
margin: 0;
}
.mirror-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 16px;
margin-bottom: 20px;
}
.mirror-card {
padding: 16px;
border: 2px solid var(--ant-color-border);
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
background: var(--ant-color-bg-container);
}
.mirror-card:hover {
border-color: var(--ant-color-primary);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.mirror-card.active {
border-color: var(--ant-color-primary);
background: var(--ant-color-primary-bg);
}
.mirror-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.mirror-header h4 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: var(--ant-color-text);
}
.speed-badge {
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.speed-badge.speed-unknown {
background: var(--ant-color-fill-tertiary);
color: var(--ant-color-text-tertiary);
}
.speed-badge.speed-fast {
background: var(--ant-color-success-bg);
color: var(--ant-color-success);
}
.speed-badge.speed-medium {
background: var(--ant-color-warning-bg);
color: var(--ant-color-warning);
}
.speed-badge.speed-slow {
background: var(--ant-color-error-bg);
color: var(--ant-color-error);
}
.speed-badge.speed-timeout {
background: var(--ant-color-error-bg);
color: var(--ant-color-error);
}
.mirror-url {
font-size: 12px;
color: var(--ant-color-text-tertiary);
word-break: break-all;
}
.test-actions {
display: flex;
align-items: center;
gap: 12px;
justify-content: center;
}
.test-note {
font-size: 12px;
color: var(--ant-color-text-tertiary);
}
.already-installed {
display: flex;
justify-content: center;
align-items: center;
min-height: 200px;
}
</style>

View File

@@ -0,0 +1,63 @@
<template>
<div class="step-panel">
<h3>启动后端服务</h3>
<div class="service-status">
<a-spin :spinning="startingService">
<div class="status-info">
<p>{{ serviceStatus }}</p>
<a-progress v-if="showServiceProgress" :percent="serviceProgress" />
</div>
</a-spin>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const startingService = ref(false)
const showServiceProgress = ref(false)
const serviceProgress = ref(0)
const serviceStatus = ref('准备启动后端服务...')
defineExpose({
startingService,
showServiceProgress,
serviceProgress,
serviceStatus
})
</script>
<style scoped>
.step-panel {
padding: 20px;
background: var(--ant-color-bg-elevated);
border-radius: 8px;
border: 1px solid var(--ant-color-border);
}
.step-panel h3 {
font-size: 20px;
font-weight: 600;
color: var(--ant-color-text);
margin-bottom: 20px;
}
.service-status {
display: flex;
justify-content: center;
align-items: center;
min-height: 200px;
}
.status-info {
text-align: center;
width: 100%;
}
.status-info p {
font-size: 16px;
color: var(--ant-color-text);
margin-bottom: 16px;
}
</style>

View File

@@ -0,0 +1,145 @@
<template>
<div class="step-panel">
<h3>选择您的主题偏好</h3>
<div class="theme-settings">
<div class="setting-group">
<label>主题模式</label>
<a-radio-group v-model:value="selectedThemeMode" @change="onThemeModeChange">
<a-radio-button value="light">浅色模式</a-radio-button>
<a-radio-button value="dark">深色模式</a-radio-button>
<a-radio-button value="system">跟随系统</a-radio-button>
</a-radio-group>
</div>
<div class="setting-group">
<label>主题色彩</label>
<div class="color-picker">
<div
v-for="(color, key) in themeColors"
:key="key"
class="color-option"
:class="{ active: selectedThemeColor === key }"
:style="{ backgroundColor: color }"
@click="onThemeColorChange(key)"
/>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useTheme } from '@/composables/useTheme'
import { getConfig, saveThemeConfig } from '@/utils/config'
import type { ThemeMode, ThemeColor } from '@/composables/useTheme'
const { themeColors, setThemeMode, setThemeColor } = useTheme()
const selectedThemeMode = ref<ThemeMode>('system')
const selectedThemeColor = ref<ThemeColor>('blue')
async function onThemeModeChange() {
setThemeMode(selectedThemeMode.value)
await saveSettings()
}
async function onThemeColorChange(color: ThemeColor) {
selectedThemeColor.value = color
setThemeColor(color)
await saveSettings()
}
async function saveSettings() {
await saveThemeConfig(selectedThemeMode.value, selectedThemeColor.value)
console.log('主题设置已保存:', {
themeMode: selectedThemeMode.value,
themeColor: selectedThemeColor.value
})
}
async function loadSettings() {
try {
const config = await getConfig()
selectedThemeMode.value = config.themeMode
selectedThemeColor.value = config.themeColor
setThemeMode(selectedThemeMode.value)
setThemeColor(selectedThemeColor.value)
console.log('主题设置已加载:', {
themeMode: selectedThemeMode.value,
themeColor: selectedThemeColor.value
})
} catch (error) {
console.warn('Failed to load theme settings:', error)
}
}
// 暴露给父组件的方法
defineExpose({
loadSettings,
saveSettings,
selectedThemeMode,
selectedThemeColor
})
// 组件挂载时加载设置
onMounted(async () => {
await loadSettings()
})
</script>
<style scoped>
.step-panel {
padding: 20px;
background: var(--ant-color-bg-elevated);
border-radius: 8px;
border: 1px solid var(--ant-color-border);
}
.step-panel h3 {
font-size: 20px;
font-weight: 600;
color: var(--ant-color-text);
margin-bottom: 20px;
}
.theme-settings {
display: flex;
flex-direction: column;
gap: 24px;
}
.setting-group {
display: flex;
flex-direction: column;
gap: 12px;
}
.setting-group label {
font-weight: 500;
color: var(--ant-color-text);
}
.color-picker {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.color-option {
width: 40px;
height: 40px;
border-radius: 8px;
cursor: pointer;
border: 2px solid transparent;
transition: all 0.2s ease;
}
.color-option:hover {
transform: scale(1.1);
}
.color-option.active {
border-color: var(--ant-color-text);
transform: scale(1.1);
}
</style>

View File

@@ -11,7 +11,7 @@ const routes: RouteRecordRaw[] = [
{
path: '/initialization',
name: 'Initialization',
component: () => import('../views/Initialization.vue'),
component: () => import('../views/InitializationNew.vue'),
meta: { title: '初始化' },
},
{
@@ -87,16 +87,18 @@ const router = createRouter({
routes,
})
import { isAppInitialized } from '@/utils/config'
// 添加路由守卫,确保在生产环境中也能正确进入初始化页面
router.beforeEach((to, from, next) => {
router.beforeEach(async (to, from, next) => {
console.log('路由守卫:', { to: to.path, from: from.path })
// 如果访问的不是初始化页面,且没有初始化标记,则重定向到初始化页面
if (to.path !== '/initialization') {
const isInitialized = localStorage.getItem('app-initialized')
console.log('检查初始化状态:', isInitialized)
const initialized = await isAppInitialized()
console.log('检查初始化状态:', initialized)
if (!isInitialized) {
if (!initialized) {
console.log('应用未初始化,重定向到初始化页面')
next('/initialization')
return

View File

@@ -0,0 +1,163 @@
import { createComponentLogger } from './logger'
import type { ThemeMode, ThemeColor } from '@/composables/useTheme'
const logger = createComponentLogger('Config')
export interface FrontendConfig {
// 基础配置
isFirstLaunch: boolean
init: boolean
lastUpdateCheck?: string
// 主题设置
themeMode: ThemeMode
themeColor: ThemeColor
// 镜像源设置
selectedGitMirror: string
selectedPythonMirror: string
selectedPipMirror: string
// 安装状态
pythonInstalled?: boolean
gitInstalled?: boolean
backendExists?: boolean
dependenciesInstalled?: boolean
pipInstalled?: boolean
}
const DEFAULT_CONFIG: FrontendConfig = {
isFirstLaunch: true,
init: false,
themeMode: 'system',
themeColor: 'blue',
selectedGitMirror: 'github',
selectedPythonMirror: 'tsinghua',
selectedPipMirror: 'tsinghua',
pythonInstalled: false,
gitInstalled: false,
backendExists: false,
dependenciesInstalled: false,
pipInstalled: false
}
// 读取配置(内部使用,不触发保存)
async function getConfigInternal(): Promise<FrontendConfig> {
try {
// 优先从文件读取配置
const fileConfig = await window.electronAPI.loadConfig()
if (fileConfig) {
console.log('从文件加载配置:', fileConfig)
return { ...DEFAULT_CONFIG, ...fileConfig }
}
// 如果文件不存在尝试从localStorage迁移
const localConfig = localStorage.getItem('app-config')
const themeConfig = localStorage.getItem('theme-settings')
let config = { ...DEFAULT_CONFIG }
if (localConfig) {
const parsed = JSON.parse(localConfig)
config = { ...config, ...parsed }
console.log('从localStorage迁移配置:', parsed)
}
if (themeConfig) {
const parsed = JSON.parse(themeConfig)
config.themeMode = parsed.themeMode || 'system'
config.themeColor = parsed.themeColor || 'blue'
console.log('从localStorage迁移主题配置:', parsed)
}
return config
} catch (error) {
console.error('读取配置失败:', error)
logger.error('读取配置失败', error)
return { ...DEFAULT_CONFIG }
}
}
// 读取配置(公共接口)
export async function getConfig(): Promise<FrontendConfig> {
const config = await getConfigInternal()
// 如果是从localStorage迁移的配置保存到文件并清理localStorage
const hasLocalStorage = localStorage.getItem('app-config') || localStorage.getItem('theme-settings')
if (hasLocalStorage) {
try {
await window.electronAPI.saveConfig(config)
localStorage.removeItem('app-config')
localStorage.removeItem('theme-settings')
localStorage.removeItem('app-initialized')
console.log('配置已从localStorage迁移到文件')
} catch (error) {
console.error('迁移配置失败:', error)
}
}
return config
}
// 保存配置
export async function saveConfig(config: Partial<FrontendConfig>): Promise<void> {
try {
console.log('开始保存配置:', config)
const currentConfig = await getConfigInternal() // 使用内部函数避免递归
const newConfig = { ...currentConfig, ...config }
console.log('合并后的配置:', newConfig)
await window.electronAPI.saveConfig(newConfig)
console.log('配置保存成功')
logger.info('配置已保存', newConfig)
} catch (error) {
console.error('保存配置失败:', error)
logger.error('保存配置失败', error)
throw error
}
}
// 重置配置
export async function resetConfig(): Promise<void> {
try {
await window.electronAPI.resetConfig()
localStorage.removeItem('app-config')
localStorage.removeItem('theme-settings')
localStorage.removeItem('app-initialized')
logger.info('配置已重置')
} catch (error) {
logger.error('重置配置失败', error)
}
}
// 检查是否已初始化
export async function isAppInitialized(): Promise<boolean> {
const config = await getConfig()
console.log('isAppInitialized 检查配置:', config)
console.log('init 字段值:', config.init)
console.log('init === true:', config.init === true)
return config.init === true
}
// 检查是否第一次启动
export async function isFirstLaunch(): Promise<boolean> {
const config = await getConfig()
return config.isFirstLaunch === true
}
// 设置初始化完成
export async function setInitialized(value: boolean = true): Promise<void> {
await saveConfig({ init: value, isFirstLaunch: false })
}
// 保存主题设置
export async function saveThemeConfig(themeMode: ThemeMode, themeColor: ThemeColor): Promise<void> {
await saveConfig({ themeMode, themeColor })
}
// 保存镜像源设置
export async function saveMirrorConfig(gitMirror: string, pythonMirror?: string, pipMirror?: string): Promise<void> {
const config: Partial<FrontendConfig> = { selectedGitMirror: gitMirror }
if (pythonMirror) config.selectedPythonMirror = pythonMirror
if (pipMirror) config.selectedPipMirror = pipMirror
await saveConfig(config)
}

View File

@@ -0,0 +1,806 @@
<template>
<div class="initialization-container">
<!-- 管理员权限检查 -->
<div v-if="!isAdmin" class="admin-check">
<a-result
status="warning"
title="需要管理员权限"
sub-title="为了正常安装和配置环境请以管理员权限运行此应用"
>
<template #extra>
<a-button type="primary" @click="restartAsAdmin">
重新以管理员权限启动
</a-button>
</template>
</a-result>
</div>
<!-- 自动初始化模式 -->
<div v-else-if="autoMode" class="auto-mode">
<div class="header">
<h1>AUTO MAA 自动初始化</h1>
<p>检测到环境已配置正在自动启动...</p>
</div>
<div class="auto-progress">
<a-spin size="large" />
<div class="progress-text">{{ autoProgressText }}</div>
<a-progress :percent="autoProgress" :status="autoProgressStatus" />
</div>
<div class="auto-actions">
<a-button @click="switchToManualMode">切换到手动模式</a-button>
</div>
</div>
<!-- 手动初始化模式 -->
<div v-else class="manual-mode">
<div class="header">
<h1>AUTO MAA 初始化向导</h1>
<p>欢迎使用 AUTO MAA让我们来配置您的运行环境</p>
<div class="header-actions">
<a-button size="large" type="primary" @click="skipToHome">
跳转至首页仅开发用
</a-button>
<a-button size="large" type="default" @click="jumpToStep(6)" style="margin-left: 16px;">
跳到启动服务第七步
</a-button>
</div>
</div>
<a-steps
:current="currentStep"
:status="stepStatus"
class="init-steps"
>
<a-step title="主题设置" description="选择您喜欢的主题" />
<a-step title="Python 环境" description="安装 Python 运行环境" />
<a-step title="pip 安装" description="安装 Python 包管理器" />
<a-step title="Git 工具" description="安装 Git 版本控制工具" />
<a-step title="源码获取" description="获取最新的后端代码" />
<a-step title="依赖安装" description="安装 Python 依赖包" />
<a-step title="启动服务" description="启动后端服务" />
</a-steps>
<!-- 全局进度条 -->
<div v-if="isProcessing" class="global-progress">
<a-progress
:percent="globalProgress"
:status="globalProgressStatus"
:show-info="true"
/>
<div class="progress-text">{{ progressText }}</div>
</div>
<div class="step-content">
<!-- 步骤 0: 主题设置 -->
<ThemeStep v-if="currentStep === 0" ref="themeStepRef" />
<!-- 步骤 1: Python 环境 -->
<PythonStep v-if="currentStep === 1" :python-installed="pythonInstalled" ref="pythonStepRef" />
<!-- 步骤 2: pip 安装 -->
<PipStep v-if="currentStep === 2" :pip-installed="pipInstalled" />
<!-- 步骤 3: Git 工具 -->
<GitStep v-if="currentStep === 3" :git-installed="gitInstalled" />
<!-- 步骤 4: 源码获取 -->
<BackendStep v-if="currentStep === 4" :backend-exists="backendExists" ref="backendStepRef" />
<!-- 步骤 5: 依赖安装 -->
<DependenciesStep v-if="currentStep === 5" ref="dependenciesStepRef" />
<!-- 步骤 6: 启动服务 -->
<ServiceStep v-if="currentStep === 6" ref="serviceStepRef" />
</div>
<div class="step-actions">
<a-button
v-if="currentStep > 0"
@click="prevStep"
:disabled="isProcessing"
>
上一步
</a-button>
<a-button
v-if="currentStep < 6"
type="primary"
@click="nextStep"
:loading="isProcessing"
>
{{ getNextButtonText() }}
</a-button>
<!-- 第7步启动服务按钮 -->
<a-button
v-if="currentStep === 6 && !serviceStarted"
type="primary"
@click="nextStep"
:loading="isProcessing"
>
启动服务
</a-button>
<!-- 服务启动完成后的进入应用按钮 -->
<a-button
v-if="currentStep === 6 && serviceStarted"
type="primary"
@click="enterApp"
>
进入应用
</a-button>
</div>
<div v-if="errorMessage" class="error-message">
<a-alert
:message="errorMessage"
type="error"
show-icon
closable
@close="errorMessage = ''"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import { createComponentLogger } from '@/utils/logger'
import { getConfig, saveConfig, setInitialized } from '@/utils/config'
import ThemeStep from '@/components/initialization/ThemeStep.vue'
import PythonStep from '@/components/initialization/PythonStep.vue'
import PipStep from '@/components/initialization/PipStep.vue'
import GitStep from '@/components/initialization/GitStep.vue'
import BackendStep from '@/components/initialization/BackendStep.vue'
import DependenciesStep from '@/components/initialization/DependenciesStep.vue'
import ServiceStep from '@/components/initialization/ServiceStep.vue'
import type { DownloadProgress } from '@/types/initialization'
const router = useRouter()
const logger = createComponentLogger('InitializationNew')
// 基础状态
const currentStep = ref(0)
const stepStatus = ref<'wait' | 'process' | 'finish' | 'error'>('process')
const errorMessage = ref('')
const isProcessing = ref(false)
const isAdmin = ref(true)
// 模式控制
const autoMode = ref(false)
const autoProgress = ref(0)
const autoProgressText = ref('')
const autoProgressStatus = ref<'normal' | 'exception' | 'success'>('normal')
// 安装状态
const pythonInstalled = ref(false)
const pipInstalled = ref(false)
const gitInstalled = ref(false)
const backendExists = ref(false)
const dependenciesInstalled = ref(false)
const serviceStarted = ref(false)
// 全局进度条状态
const globalProgress = ref(0)
const globalProgressStatus = ref<'normal' | 'exception' | 'success'>('normal')
const progressText = ref('')
// 组件引用
const themeStepRef = ref()
const pythonStepRef = ref()
const backendStepRef = ref()
const dependenciesStepRef = ref()
const serviceStepRef = ref()
// 计算属性
const allCompleted = computed(() =>
pythonInstalled.value && pipInstalled.value && gitInstalled.value &&
backendExists.value && dependenciesInstalled.value && serviceStarted.value
)
// 基础功能函数
function skipToHome() {
router.push('/home')
}
function jumpToStep(step: number) {
currentStep.value = step
}
function switchToManualMode() {
autoMode.value = false
}
async function restartAsAdmin() {
try {
await window.electronAPI.restartAsAdmin()
} catch (error) {
logger.error('重启为管理员失败', error)
}
}
// 步骤控制
function prevStep() {
if (currentStep.value > 0) {
currentStep.value--
}
}
async function nextStep() {
console.log('nextStep 被调用,当前步骤:', currentStep.value)
isProcessing.value = true
errorMessage.value = ''
try {
switch (currentStep.value) {
case 0: // 主题设置
console.log('执行主题设置')
themeStepRef.value?.saveSettings()
break
case 1: // Python 环境
console.log('执行Python环境安装')
if (!pythonInstalled.value) {
await installPython()
}
break
case 2: // pip 安装
console.log('执行pip安装')
if (!pipInstalled.value) {
await installPip()
}
break
case 3: // Git 工具
console.log('执行Git工具安装')
if (!gitInstalled.value) {
await installGit()
}
break
case 4: // 源码获取
console.log('执行源码获取')
if (!backendExists.value) {
await cloneBackend()
} else {
await updateBackend()
}
break
case 5: // 依赖安装
console.log('执行依赖安装')
if (!dependenciesInstalled.value) {
await installDependencies()
}
break
case 6: // 启动服务
console.log('执行启动服务')
await startBackendService()
break
}
if (currentStep.value < 6) {
currentStep.value++
// 进入新步骤时自动开始测速
await autoStartSpeedTest()
}
} catch (error) {
console.error('nextStep 执行出错:', error)
errorMessage.value = error instanceof Error ? error.message : String(error)
stepStatus.value = 'error'
} finally {
isProcessing.value = false
}
}
function getNextButtonText() {
switch (currentStep.value) {
case 0: return '下一步'
case 1: return pythonInstalled.value ? '下一步' : '安装 Python'
case 2: return pipInstalled.value ? '下一步' : '安装 pip'
case 3: return gitInstalled.value ? '下一步' : '安装 Git'
case 4: return backendExists.value ? '更新代码' : '获取代码'
case 5: return '安装依赖'
case 6: return '启动服务'
default: return '下一步'
}
}
// 自动开始测速
async function autoStartSpeedTest() {
// 延迟一下确保组件已经挂载
setTimeout(async () => {
switch (currentStep.value) {
case 1: // Python 环境
if (!pythonInstalled.value && pythonStepRef.value?.testPythonMirrorSpeed) {
console.log('自动开始Python镜像测速')
await pythonStepRef.value.testPythonMirrorSpeed()
}
break
case 4: // 源码获取
if (backendStepRef.value?.testGitMirrorSpeed) {
console.log('自动开始Git镜像测速')
await backendStepRef.value.testGitMirrorSpeed()
}
break
case 5: // 依赖安装
if (!dependenciesInstalled.value && dependenciesStepRef.value?.testPipMirrorSpeed) {
console.log('自动开始pip镜像测速')
await dependenciesStepRef.value.testPipMirrorSpeed()
}
break
}
}, 500) // 延迟500ms确保组件完全加载
}
// 安装函数
async function installPython() {
logger.info('开始安装Python')
const mirror = pythonStepRef.value?.selectedPythonMirror || 'tsinghua'
const result = await window.electronAPI.downloadPython(mirror)
if (result.success) {
logger.info('Python安装成功')
pythonInstalled.value = true
saveConfig({ pythonInstalled: true })
} else {
logger.error('Python安装失败', result.error)
throw new Error(result.error)
}
}
async function installPip() {
logger.info('开始安装pip')
const result = await window.electronAPI.installPip()
if (result.success) {
logger.info('pip安装成功')
pipInstalled.value = true
saveConfig({ pipInstalled: true })
} else {
logger.error('pip安装失败', result.error)
throw new Error(result.error)
}
}
async function installGit() {
logger.info('开始安装Git工具')
const result = await window.electronAPI.downloadGit()
if (result.success) {
logger.info('Git工具安装成功')
gitInstalled.value = true
saveConfig({ gitInstalled: true })
} else {
logger.error('Git工具安装失败', result.error)
throw new Error(result.error)
}
}
async function cloneBackend() {
const selectedMirror = backendStepRef.value?.selectedGitMirror || 'github'
const mirror = backendStepRef.value?.gitMirrors?.find((m: any) => m.key === selectedMirror)
logger.info('开始克隆后端代码', { mirror: mirror?.name, url: mirror?.url })
const result = await window.electronAPI.cloneBackend(mirror?.url)
if (result.success) {
logger.info('后端代码克隆成功')
backendExists.value = true
saveConfig({ backendExists: true })
} else {
logger.error('后端代码克隆失败', result.error)
throw new Error(result.error)
}
}
async function updateBackend() {
const selectedMirror = backendStepRef.value?.selectedGitMirror || 'github'
const mirror = backendStepRef.value?.gitMirrors?.find((m: any) => m.key === selectedMirror)
logger.info('开始更新后端代码', { mirror: mirror?.name, url: mirror?.url })
const result = await window.electronAPI.updateBackend(mirror?.url)
if (!result.success) {
logger.error('后端代码更新失败', result.error)
throw new Error(result.error)
}
logger.info('后端代码更新成功')
}
async function installDependencies() {
logger.info('开始安装Python依赖')
const mirror = dependenciesStepRef.value?.selectedPipMirror || 'tsinghua'
const result = await window.electronAPI.installDependencies(mirror)
if (result.success) {
logger.info('Python依赖安装成功')
dependenciesInstalled.value = true
saveConfig({ dependenciesInstalled: true })
} else {
logger.error('Python依赖安装失败', result.error)
throw new Error(result.error)
}
}
async function startBackendService() {
logger.info('开始启动后端服务')
if (serviceStepRef.value) {
serviceStepRef.value.startingService = true
serviceStepRef.value.showServiceProgress = true
serviceStepRef.value.serviceStatus = '正在启动后端服务...'
}
try {
const result = await window.electronAPI.startBackend()
if (result.success) {
if (serviceStepRef.value) {
serviceStepRef.value.serviceProgress = 100
serviceStepRef.value.serviceStatus = '后端服务启动成功'
}
serviceStarted.value = true
stepStatus.value = 'finish'
logger.info('后端服务启动成功')
} else {
logger.error('后端服务启动失败', result.error)
throw new Error(result.error)
}
} catch (error) {
if (serviceStepRef.value) {
serviceStepRef.value.serviceStatus = '后端服务启动失败'
}
logger.error('后端服务启动异常', error)
throw error
} finally {
if (serviceStepRef.value) {
serviceStepRef.value.startingService = false
}
}
}
// 进入应用
async function enterApp() {
try {
// 设置初始化完成标记
await setInitialized(true)
console.log('设置初始化完成标记,跳转到首页')
router.push('/home')
} catch (error) {
console.error('进入应用失败:', error)
errorMessage.value = '保存配置失败,请重试'
}
}
// 检查环境状态
async function checkEnvironment() {
try {
logger.info('开始检查环境状态')
const status = await window.electronAPI.checkEnvironment()
logger.info('环境检查结果', status)
console.log('环境检查结果:', status)
pythonInstalled.value = status.pythonExists
gitInstalled.value = status.gitExists
backendExists.value = status.backendExists
dependenciesInstalled.value = status.dependenciesInstalled
// 检查配置文件中的状态
const config = await getConfig()
pipInstalled.value = config.pipInstalled || false
// 更新配置文件中的状态,确保与实际环境一致
const needsUpdate =
config.pythonInstalled !== status.pythonExists ||
config.gitInstalled !== status.gitExists ||
config.backendExists !== status.backendExists ||
config.dependenciesInstalled !== status.dependenciesInstalled
if (needsUpdate) {
console.log('更新配置文件中的环境状态')
await saveConfig({
pythonInstalled: status.pythonExists,
gitInstalled: status.gitExists,
backendExists: status.backendExists,
dependenciesInstalled: status.dependenciesInstalled
})
}
// 检查是否第一次启动
const isFirst = config.isFirstLaunch
console.log('是否第一次启动:', isFirst)
// 检查是否应该进入自动模式
console.log('自动模式判断条件:')
console.log('- 不是第一次启动:', !isFirst)
console.log('- 配置显示已初始化:', config.init)
console.log('- 环境检查结果:', status.isInitialized)
// 如果配置显示已初始化且不是第一次启动,进入自动模式
// 不再依赖环境检查结果,因为配置文件更准确
if (!isFirst && config.init) {
logger.info('非首次启动且配置显示已初始化,进入自动模式')
console.log('进入自动模式,开始自动启动流程')
autoMode.value = true
await autoStartProcess()
} else {
logger.info('首次启动或配置显示未初始化,进入手动模式')
console.log('进入手动模式,当前步骤:', currentStep.value)
console.log('原因: isFirst =', isFirst, ', config.init =', config.init)
// 如果是首次启动,从主题设置开始
if (isFirst) {
currentStep.value = 0
console.log('首次启动,从主题设置开始')
}
}
} catch (error) {
const errorMsg = `环境检查失败: ${error instanceof Error ? error.message : String(error)}`
logger.error('环境检查失败', error)
console.error('环境检查失败:', error)
errorMessage.value = errorMsg
}
}
// 自动启动流程
async function autoStartProcess() {
try {
// 获取配置中保存的镜像源设置
const config = await getConfig()
autoProgressText.value = '检查Git仓库更新...'
autoProgress.value = 20
// 检查Git仓库是否有更新
const hasUpdate = await checkGitUpdate()
if (hasUpdate) {
autoProgressText.value = '发现更新,正在更新代码...'
autoProgress.value = 40
// 使用配置中保存的Git镜像源
const gitMirrorUrl = getGitMirrorUrl(config.selectedGitMirror)
const result = await window.electronAPI.updateBackend(gitMirrorUrl)
if (!result.success) {
throw new Error(`代码更新失败: ${result.error}`)
}
autoProgressText.value = '更新依赖包...'
autoProgress.value = 60
// 使用配置中保存的pip镜像源
const pipResult = await window.electronAPI.installDependencies(config.selectedPipMirror)
if (!pipResult.success) {
throw new Error(`依赖更新失败: ${pipResult.error}`)
}
}
autoProgressText.value = '启动后端服务...'
autoProgress.value = 80
await startBackendService()
autoProgressText.value = '启动完成!'
autoProgress.value = 100
autoProgressStatus.value = 'success'
logger.info('自动启动流程完成,即将进入应用')
// 延迟2秒后自动进入应用
setTimeout(() => {
enterApp()
}, 2000)
} catch (error) {
logger.error('自动启动流程失败', error)
autoProgressText.value = `自动启动失败: ${error instanceof Error ? error.message : String(error)}`
autoProgressStatus.value = 'exception'
// 5秒后提供切换到手动模式的选项
setTimeout(() => {
if (autoProgressStatus.value === 'exception') {
autoProgressText.value = '自动启动失败,请点击下方按钮切换到手动模式'
}
}, 5000)
}
}
// 根据镜像源key获取对应的URL
function getGitMirrorUrl(mirrorKey: string): string {
const mirrors = {
github: 'https://github.com/DLmaster361/AUTO_MAA.git',
ghfast: 'https://ghfast.top/https://github.com/DLmaster361/AUTO_MAA.git'
}
return mirrors[mirrorKey as keyof typeof mirrors] || mirrors.github
}
// 检查Git更新简化版本实际可以调用Git API
async function checkGitUpdate(): Promise<boolean> {
// 这里可以实现更复杂的Git更新检查逻辑
// 暂时返回false表示没有更新
return false
}
// 检查管理员权限
async function checkAdminPermission() {
try {
const adminStatus = await window.electronAPI.checkAdmin()
isAdmin.value = adminStatus
console.log('管理员权限检查结果:', adminStatus)
} catch (error) {
logger.error('检查管理员权限失败', error)
isAdmin.value = false
}
}
// 监听下载进度
function handleDownloadProgress(progress: DownloadProgress) {
// 更新全局进度条
globalProgress.value = progress.progress
progressText.value = progress.message
if (progress.status === 'error') {
globalProgressStatus.value = 'exception'
} else if (progress.status === 'completed') {
globalProgressStatus.value = 'success'
} else {
globalProgressStatus.value = 'normal'
}
// 更新自动模式进度
if (autoMode.value) {
autoProgress.value = progress.progress
autoProgressText.value = progress.message
}
}
onMounted(async () => {
console.log('初始化页面 onMounted 开始')
// 测试配置系统
try {
console.log('测试配置系统...')
const testConfig = await getConfig()
console.log('当前配置:', testConfig)
// 测试保存配置
await saveConfig({ isFirstLaunch: false })
console.log('测试配置保存成功')
// 重新读取配置验证
const updatedConfig = await getConfig()
console.log('更新后的配置:', updatedConfig)
} catch (error) {
console.error('配置系统测试失败:', error)
}
// 检查管理员权限
await checkAdminPermission()
if (isAdmin.value) {
// 延迟检查环境,确保页面完全加载
setTimeout(async () => {
console.log('开始环境检查')
await checkEnvironment()
}, 100)
}
window.electronAPI.onDownloadProgress(handleDownloadProgress)
console.log('初始化页面 onMounted 完成')
})
onUnmounted(() => {
window.electronAPI.removeDownloadProgressListener()
})
</script>
<style scoped>
.initialization-container {
min-height: 100vh;
background: var(--ant-color-bg-layout);
padding: 50px 100px;
margin: 0 auto;
}
.admin-check {
display: flex;
justify-content: center;
align-items: center;
min-height: 60vh;
}
.auto-mode {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 60vh;
}
.auto-progress {
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
margin: 40px 0;
width: 400px;
}
.progress-text {
font-size: 16px;
color: var(--ant-color-text);
text-align: center;
}
.auto-actions {
margin-top: 20px;
}
.manual-mode .header {
text-align: center;
margin-bottom: 40px;
}
.header h1 {
font-size: 28px;
font-weight: 600;
color: var(--ant-color-text);
margin-bottom: 8px;
}
.header p {
font-size: 16px;
color: var(--ant-color-text-secondary);
margin: 0 0 20px 0;
}
.header-actions {
display: flex;
justify-content: center;
gap: 16px;
}
.init-steps {
margin-bottom: 40px;
}
.step-content {
min-height: 300px;
margin-bottom: 40px;
}
.step-actions {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.error-message {
margin-top: 20px;
}
.global-progress {
margin: 20px 0;
padding: 20px;
background: var(--ant-color-bg-container);
border-radius: 8px;
border: 1px solid var(--ant-color-border);
}
.global-progress .progress-text {
text-align: center;
margin-top: 8px;
font-size: 14px;
color: var(--ant-color-text-secondary);
}
@media (max-width: 768px) {
.initialization-container {
padding: 20px;
}
.header-actions {
flex-direction: column;
gap: 8px;
}
.step-actions {
flex-direction: column;
gap: 12px;
}
}
</style>