feat(initialization): 添加初始化页面和相关功能

- 新增初始化页面组件和路由
- 实现环境检查、Git下载、后端代码克隆等功能
- 添加下载服务和环境服务模块
- 更新类型定义,增加 Electron API 接口
This commit is contained in:
2025-08-07 00:11:29 +08:00
parent 18202045bf
commit ae151a9311
20 changed files with 2777 additions and 490 deletions

View File

@@ -1,11 +1,16 @@
<script setup lang="ts">
import { onMounted } from 'vue'
import { onMounted, computed } from 'vue'
import { useRoute } from 'vue-router'
import { ConfigProvider } from 'ant-design-vue'
import { useTheme } from './composables/useTheme.ts'
import AppLayout from './components/AppLayout.vue'
const route = useRoute()
const { antdTheme, initTheme } = useTheme()
// 判断是否为初始化页面
const isInitializationPage = computed(() => route.name === 'Initialization')
onMounted(() => {
initTheme()
})
@@ -13,7 +18,10 @@ onMounted(() => {
<template>
<ConfigProvider :theme="antdTheme">
<AppLayout />
<!-- 初始化页面使用全屏布局 -->
<router-view v-if="isInitializationPage" />
<!-- 其他页面使用应用布局 -->
<AppLayout v-else />
</ConfigProvider>
</template>

View File

@@ -0,0 +1,354 @@
<template>
<div class="log-viewer">
<div class="log-header">
<div class="log-controls">
<a-select
v-model:value="selectedLevel"
style="width: 120px"
@change="filterLogs"
>
<a-select-option value="all">所有级别</a-select-option>
<a-select-option value="debug">Debug</a-select-option>
<a-select-option value="info">Info</a-select-option>
<a-select-option value="warn">Warn</a-select-option>
<a-select-option value="error">Error</a-select-option>
</a-select>
<a-input-search
v-model:value="searchText"
placeholder="搜索日志..."
style="width: 200px"
@search="filterLogs"
@change="filterLogs"
/>
<a-button @click="clearLogs" danger>
<template #icon>
<DeleteOutlined />
</template>
清空日志
</a-button>
<a-button @click="downloadLogs" type="primary">
<template #icon>
<DownloadOutlined />
</template>
导出日志
</a-button>
<a-button @click="toggleAutoScroll" :type="autoScroll ? 'primary' : 'default'">
<template #icon>
<VerticalAlignBottomOutlined />
</template>
自动滚动
</a-button>
</div>
<div class="log-stats">
总计: {{ filteredLogs.length }} 条日志
</div>
</div>
<div
ref="logContainer"
class="log-container"
@scroll="handleScroll"
>
<div
v-for="(log, index) in filteredLogs"
:key="index"
class="log-entry"
:class="[`log-${log.level}`, { 'log-highlight': highlightedIndex === index }]"
>
<div class="log-timestamp">{{ log.timestamp }}</div>
<div class="log-level">{{ log.level.toUpperCase() }}</div>
<div v-if="log.component" class="log-component">[{{ log.component }}]</div>
<div class="log-message">{{ log.message }}</div>
<div v-if="log.data" class="log-data">
<a-button
size="small"
type="link"
@click="toggleDataVisibility(index)"
>
{{ expandedData.has(index) ? '隐藏数据' : '显示数据' }}
</a-button>
<pre v-if="expandedData.has(index)" class="log-data-content">{{ JSON.stringify(log.data, null, 2) }}</pre>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, nextTick, onMounted, onUnmounted } from 'vue'
import {
DeleteOutlined,
DownloadOutlined,
VerticalAlignBottomOutlined
} from '@ant-design/icons-vue'
import { logger, type LogEntry, type LogLevel } from '@/utils/logger'
const logContainer = ref<HTMLElement>()
const selectedLevel = ref<LogLevel | 'all'>('all')
const searchText = ref('')
const autoScroll = ref(true)
const expandedData = ref(new Set<number>())
const highlightedIndex = ref(-1)
const logs = logger.getLogs()
const filteredLogs = computed(() => {
let filtered = logs.value
// 按级别过滤
if (selectedLevel.value !== 'all') {
filtered = filtered.filter(log => log.level === selectedLevel.value)
}
// 按搜索文本过滤
if (searchText.value) {
const search = searchText.value.toLowerCase()
filtered = filtered.filter(log =>
log.message.toLowerCase().includes(search) ||
log.component?.toLowerCase().includes(search) ||
(log.data && JSON.stringify(log.data).toLowerCase().includes(search))
)
}
return filtered
})
function filterLogs() {
// 过滤逻辑已在computed中处理
nextTick(() => {
if (autoScroll.value) {
scrollToBottom()
}
})
}
function clearLogs() {
logger.clearLogs()
expandedData.value.clear()
}
function downloadLogs() {
logger.downloadLogs()
}
function toggleAutoScroll() {
autoScroll.value = !autoScroll.value
if (autoScroll.value) {
scrollToBottom()
}
}
function toggleDataVisibility(index: number) {
if (expandedData.value.has(index)) {
expandedData.value.delete(index)
} else {
expandedData.value.add(index)
}
}
function scrollToBottom() {
if (logContainer.value) {
logContainer.value.scrollTop = logContainer.value.scrollHeight
}
}
function handleScroll() {
if (!logContainer.value) return
const { scrollTop, scrollHeight, clientHeight } = logContainer.value
const isAtBottom = scrollTop + clientHeight >= scrollHeight - 10
if (!isAtBottom) {
autoScroll.value = false
}
}
// 监听新日志添加
let unwatchLogs: (() => void) | null = null
onMounted(() => {
// 初始滚动到底部
nextTick(() => {
scrollToBottom()
})
// 监听日志变化
unwatchLogs = logs.value && typeof logs.value === 'object' && 'length' in logs.value
? () => {} // 如果logs是响应式的Vue会自动处理
: null
})
onUnmounted(() => {
if (unwatchLogs) {
unwatchLogs()
}
})
// 监听日志变化,自动滚动
const prevLogsLength = ref(logs.value.length)
const checkForNewLogs = () => {
if (logs.value.length > prevLogsLength.value) {
prevLogsLength.value = logs.value.length
if (autoScroll.value) {
nextTick(() => {
scrollToBottom()
})
}
}
}
// 定期检查新日志
const logCheckInterval = setInterval(checkForNewLogs, 100)
onUnmounted(() => {
clearInterval(logCheckInterval)
})
</script>
<style scoped>
.log-viewer {
height: 100%;
display: flex;
flex-direction: column;
background: var(--ant-color-bg-container);
border-radius: 8px;
overflow: hidden;
}
.log-header {
padding: 16px;
border-bottom: 1px solid var(--ant-color-border);
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 12px;
}
.log-controls {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.log-stats {
font-size: 14px;
color: var(--ant-color-text-secondary);
}
.log-container {
flex: 1;
overflow-y: auto;
padding: 8px;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 12px;
line-height: 1.4;
}
.log-entry {
display: flex;
align-items: flex-start;
gap: 8px;
padding: 4px 8px;
border-radius: 4px;
margin-bottom: 2px;
word-break: break-all;
}
.log-entry:hover {
background: var(--ant-color-fill-quaternary);
}
.log-highlight {
background: var(--ant-color-primary-bg) !important;
}
.log-timestamp {
color: var(--ant-color-text-tertiary);
white-space: nowrap;
min-width: 140px;
}
.log-level {
font-weight: bold;
min-width: 50px;
text-align: center;
padding: 2px 6px;
border-radius: 3px;
font-size: 10px;
}
.log-debug .log-level {
background: var(--ant-color-fill-secondary);
color: var(--ant-color-text-secondary);
}
.log-info .log-level {
background: var(--ant-color-info-bg);
color: var(--ant-color-info);
}
.log-warn .log-level {
background: var(--ant-color-warning-bg);
color: var(--ant-color-warning);
}
.log-error .log-level {
background: var(--ant-color-error-bg);
color: var(--ant-color-error);
}
.log-component {
color: var(--ant-color-primary);
font-weight: 500;
white-space: nowrap;
}
.log-message {
flex: 1;
color: var(--ant-color-text);
}
.log-data {
margin-top: 4px;
width: 100%;
}
.log-data-content {
background: var(--ant-color-fill-quaternary);
padding: 8px;
border-radius: 4px;
margin-top: 4px;
font-size: 11px;
overflow-x: auto;
}
@media (max-width: 768px) {
.log-header {
flex-direction: column;
align-items: stretch;
}
.log-controls {
justify-content: center;
}
.log-entry {
flex-direction: column;
align-items: stretch;
gap: 4px;
}
.log-timestamp,
.log-level,
.log-component {
min-width: auto;
}
}
</style>

View File

@@ -2,6 +2,7 @@ import { createApp } from 'vue'
import App from './App.vue'
import router from './router/index.ts'
import { OpenAPI } from '@/api'
import LoggerPlugin, { logger } from '@/utils/logger'
import Antd from 'ant-design-vue'
import 'ant-design-vue/dist/reset.css'
@@ -9,4 +10,16 @@ import 'ant-design-vue/dist/reset.css'
// 配置API基础URL
OpenAPI.BASE = 'http://localhost:8000'
createApp(App).use(Antd).use(router).mount('#app')
// 创建应用实例
const app = createApp(App)
// 注册插件
app.use(Antd)
app.use(router)
app.use(LoggerPlugin)
// 挂载应用
app.mount('#app')
// 记录应用启动日志
logger.info('应用启动', { version: '1.0.0', environment: process.env.NODE_ENV })

View File

@@ -4,7 +4,13 @@ import type { RouteRecordRaw } from 'vue-router'
const routes: RouteRecordRaw[] = [
{
path: '/',
redirect: '/home',
redirect: '/initialization',
},
{
path: '/initialization',
name: 'Initialization',
component: () => import('../views/Initialization.vue'),
meta: { title: '初始化' },
},
{
path: '/home',
@@ -66,6 +72,12 @@ const routes: RouteRecordRaw[] = [
component: () => import('../views/Settings.vue'),
meta: { title: '设置' },
},
{
path: '/logs',
name: 'Logs',
component: () => import('../views/Logs.vue'),
meta: { title: '系统日志' },
},
]
const router = createRouter({

View File

@@ -1,11 +1,30 @@
export {}
export interface ElectronAPI {
openDevTools: () => Promise<void>
selectFolder: () => Promise<string | null>
selectFile: (filters?: any[]) => Promise<string | null>
// 初始化相关API
checkEnvironment: () => Promise<{
pythonExists: boolean
gitExists: boolean
backendExists: boolean
dependenciesInstalled: boolean
isInitialized: boolean
}>
downloadPython: (mirror?: string) => Promise<{ success: boolean; error?: string }>
downloadGit: () => Promise<{ success: boolean; error?: string }>
installDependencies: (mirror?: string) => Promise<{ success: boolean; error?: string }>
cloneBackend: (repoUrl?: string) => Promise<{ success: boolean; error?: string }>
updateBackend: (repoUrl?: string) => Promise<{ success: boolean; error?: string }>
startBackend: () => Promise<{ success: boolean; error?: string }>
// 监听下载进度
onDownloadProgress: (callback: (progress: any) => void) => void
removeDownloadProgressListener: () => void
}
declare global {
interface Window {
electronAPI: {
openDevTools: () => void,
selectFolder: () => Promise<string | null>
selectFile: (filters?: Array<{ name: string; extensions: string[] }>) => Promise<string | null>
}
electronAPI: ElectronAPI
}
}
}

View File

@@ -0,0 +1,21 @@
export interface InitializationStatus {
pythonExists: boolean
gitExists: boolean
backendExists: boolean
dependenciesInstalled: boolean
isInitialized: boolean
}
export interface DownloadProgress {
type: 'python' | 'git' | 'backend' | 'dependencies' | 'service'
progress: number
status: 'downloading' | 'extracting' | 'installing' | 'completed' | 'error'
message: string
}
export interface MirrorSource {
key: string
name: string
url: string
speed: number | null
}

View File

@@ -0,0 +1,185 @@
import { ref } from 'vue'
export type LogLevel = 'debug' | 'info' | 'warn' | 'error'
export interface LogEntry {
timestamp: string
level: LogLevel
message: string
data?: any
component?: string
}
class Logger {
private logs = ref<LogEntry[]>([])
private maxLogs = 1000 // 最大日志条数
private logToConsole = true
private logToStorage = true
constructor() {
this.loadLogsFromStorage()
}
private formatTimestamp(): string {
const now = new Date()
return now.toISOString().replace('T', ' ').substring(0, 19)
}
private addLog(level: LogLevel, message: string, data?: any, component?: string) {
const logEntry: LogEntry = {
timestamp: this.formatTimestamp(),
level,
message,
data,
component
}
// 添加到内存日志
this.logs.value.push(logEntry)
// 限制日志数量
if (this.logs.value.length > this.maxLogs) {
this.logs.value.shift()
}
// 输出到控制台
if (this.logToConsole) {
const consoleMessage = `[${logEntry.timestamp}] [${level.toUpperCase()}] ${component ? `[${component}] ` : ''}${message}`
switch (level) {
case 'debug':
console.debug(consoleMessage, data)
break
case 'info':
console.info(consoleMessage, data)
break
case 'warn':
console.warn(consoleMessage, data)
break
case 'error':
console.error(consoleMessage, data)
break
}
}
// 保存到本地存储
if (this.logToStorage) {
this.saveLogsToStorage()
}
}
private saveLogsToStorage() {
try {
const logsToSave = this.logs.value.slice(-500) // 只保存最近500条日志
localStorage.setItem('app-logs', JSON.stringify(logsToSave))
} catch (error) {
console.error('保存日志到本地存储失败:', error)
}
}
private loadLogsFromStorage() {
try {
const savedLogs = localStorage.getItem('app-logs')
if (savedLogs) {
const parsedLogs = JSON.parse(savedLogs) as LogEntry[]
this.logs.value = parsedLogs
}
} catch (error) {
console.error('从本地存储加载日志失败:', error)
}
}
// 公共方法
debug(message: string, data?: any, component?: string) {
this.addLog('debug', message, data, component)
}
info(message: string, data?: any, component?: string) {
this.addLog('info', message, data, component)
}
warn(message: string, data?: any, component?: string) {
this.addLog('warn', message, data, component)
}
error(message: string, data?: any, component?: string) {
this.addLog('error', message, data, component)
}
// 获取日志
getLogs() {
return this.logs
}
// 清空日志
clearLogs() {
this.logs.value = []
localStorage.removeItem('app-logs')
}
// 导出日志到文件
exportLogs(): string {
const logText = this.logs.value
.map(log => {
const dataStr = log.data ? ` | Data: ${JSON.stringify(log.data)}` : ''
const componentStr = log.component ? ` | Component: ${log.component}` : ''
return `[${log.timestamp}] [${log.level.toUpperCase()}]${componentStr} ${log.message}${dataStr}`
})
.join('\n')
return logText
}
// 下载日志文件
downloadLogs() {
const logText = this.exportLogs()
const blob = new Blob([logText], { type: 'text/plain;charset=utf-8' })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `auto-maa-logs-${new Date().toISOString().split('T')[0]}.txt`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
}
// 配置选项
setLogToConsole(enabled: boolean) {
this.logToConsole = enabled
}
setLogToStorage(enabled: boolean) {
this.logToStorage = enabled
}
setMaxLogs(max: number) {
this.maxLogs = max
if (this.logs.value.length > max) {
this.logs.value = this.logs.value.slice(-max)
}
}
}
// 创建全局日志实例
export const logger = new Logger()
// 创建组件专用的日志器
export function createComponentLogger(componentName: string) {
return {
debug: (message: string, data?: any) => logger.debug(message, data, componentName),
info: (message: string, data?: any) => logger.info(message, data, componentName),
warn: (message: string, data?: any) => logger.warn(message, data, componentName),
error: (message: string, data?: any) => logger.error(message, data, componentName),
}
}
// Vue插件
export default {
install(app: any) {
app.config.globalProperties.$logger = logger
app.provide('logger', logger)
}
}

View File

@@ -0,0 +1,967 @@
<template>
<div class="initialization-container">
<div class="initialization-content">
<div class="header">
<h1>AUTO MAA 初始化向导</h1>
<p>欢迎使用 AUTO MAA让我们来配置您的运行环境</p>
</div>
<a-steps
:current="currentStep"
:status="stepStatus"
class="init-steps"
>
<a-step title="主题设置" description="选择您喜欢的主题" />
<a-step title="Python 环境" description="安装 Python 运行环境" />
<a-step title="Git 工具" description="安装 Git 版本控制工具" />
<a-step title="源码获取" description="获取最新的后端代码" />
<a-step title="依赖安装" description="安装 Python 依赖包" />
<a-step title="启动服务" description="启动后端服务" />
</a-steps>
<div class="step-content">
<!-- 步骤 0: 主题设置 -->
<div v-if="currentStep === 0" 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>
<!-- 步骤 1: Python 环境 -->
<div v-if="currentStep === 1" 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>
<!-- 步骤 2: Git 工具 -->
<div v-if="currentStep === 2" 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>
<!-- 步骤 3: 源码获取 -->
<div v-if="currentStep === 3" 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>
<!-- 步骤 4: 依赖安装 -->
<div v-if="currentStep === 4" 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>
<!-- 步骤 5: 启动服务 -->
<div v-if="currentStep === 5" 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>
</div>
<div class="step-actions">
<a-button
v-if="currentStep > 0"
@click="prevStep"
:disabled="isProcessing"
>
上一步
</a-button>
<a-button
v-if="currentStep < 5"
type="primary"
@click="nextStep"
:loading="isProcessing"
>
{{ getNextButtonText() }}
</a-button>
<a-button
v-if="currentStep === 5 && allCompleted"
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 { useTheme } from '@/composables/useTheme'
import { createComponentLogger } from '@/utils/logger'
import type { InitializationStatus, DownloadProgress } from '@/types/initialization'
import type { ThemeMode, ThemeColor } from '@/composables/useTheme'
const router = useRouter()
const { themeColors, setThemeMode, setThemeColor } = useTheme()
const logger = createComponentLogger('Initialization')
// 基础状态
const currentStep = ref(0)
const stepStatus = ref<'wait' | 'process' | 'finish' | 'error'>('process')
const errorMessage = ref('')
const isProcessing = ref(false)
// 主题设置
const selectedThemeMode = ref<ThemeMode>('system')
const selectedThemeColor = ref<ThemeColor>('blue')
// 安装状态
const pythonInstalled = ref(false)
const gitInstalled = ref(false)
const backendExists = ref(false)
const dependenciesInstalled = ref(false)
// 镜像源配置
const pythonMirrors = ref([
{ key: 'official', name: 'Python 官方', url: 'https://www.python.org/ftp/python/3.13.0/', speed: null as number | null },
{ key: 'tsinghua', name: '清华 TUNA 镜像', url: 'https://mirrors.tuna.tsinghua.edu.cn/python/3.13.0/', speed: null as number | null },
{ key: 'ustc', name: '中科大镜像', url: 'https://mirrors.ustc.edu.cn/python/3.13.0/', speed: null as number | null },
{ key: 'huawei', name: '华为云镜像', url: 'https://mirrors.huaweicloud.com/repository/toolkit/python/3.13.0/', speed: null as number | null },
{ key: 'aliyun', name: '阿里云镜像', url: 'https://mirrors.aliyun.com/python-release/windows/', speed: null as number | null }
])
const pipMirrors = ref([
{ key: 'official', name: 'PyPI 官方', url: 'https://pypi.org/simple/', speed: null as number | null },
{ key: 'tsinghua', name: '清华大学', url: 'https://pypi.tuna.tsinghua.edu.cn/simple/', speed: null as number | null },
{ key: 'aliyun', name: '阿里云', url: 'https://mirrors.aliyun.com/pypi/simple/', speed: null as number | null },
{ key: 'douban', name: '豆瓣', url: 'https://pypi.douban.com/simple/', speed: null as number | null },
{ key: 'ustc', name: '中科大', url: 'https://pypi.mirrors.ustc.edu.cn/simple/', speed: null as number | null },
{ key: 'huawei', name: '华中科技大学', url: 'https://pypi.hustunique.com/simple/', speed: null as number | null }
])
const gitMirrors = ref([
{ key: 'github', name: 'GitHub 官方', url: 'https://github.com/DLmaster361/AUTO_MAA.git', speed: null as number | null },
{ key: 'fastgit', name: 'FastGit 镜像', url: 'https://ghfast.top/https://github.com/DLmaster361/AUTO_MAA.git', speed: null as number | null }
])
// 选中的镜像源
const selectedPythonMirror = ref('tsinghua')
const selectedPipMirror = ref('tsinghua')
const selectedGitMirror = ref('github')
// 测速状态
const testingSpeed = ref(false)
const testingPipSpeed = ref(false)
const testingGitSpeed = ref(false)
// 服务状态
const startingService = ref(false)
const showServiceProgress = ref(false)
const serviceProgress = ref(0)
const serviceStatus = ref('准备启动后端服务...')
const allCompleted = computed(() =>
pythonInstalled.value && gitInstalled.value && backendExists.value && dependenciesInstalled.value
)
// 主题设置相关
function onThemeModeChange() {
setThemeMode(selectedThemeMode.value)
saveSettings()
}
function onThemeColorChange(color: ThemeColor) {
selectedThemeColor.value = color
setThemeColor(color)
saveSettings()
}
// 保存设置到本地存储
function saveSettings() {
const settings = {
themeMode: selectedThemeMode.value,
themeColor: selectedThemeColor.value,
pythonMirror: selectedPythonMirror.value,
pipMirror: selectedPipMirror.value,
gitMirror: selectedGitMirror.value
}
localStorage.setItem('init-settings', JSON.stringify(settings))
}
// 加载设置
function loadSettings() {
const saved = localStorage.getItem('init-settings')
if (saved) {
try {
const settings = JSON.parse(saved)
selectedThemeMode.value = settings.themeMode || 'system'
selectedThemeColor.value = settings.themeColor || 'blue'
selectedPythonMirror.value = settings.pythonMirror || 'tsinghua'
selectedPipMirror.value = settings.pipMirror || 'tsinghua'
selectedGitMirror.value = settings.gitMirror || 'github'
} catch (error) {
console.warn('Failed to load settings:', error)
}
}
}
// 测速功能 - 带3秒超时
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
}
} finally {
testingSpeed.value = false
}
}
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
}
} finally {
testingPipSpeed.value = false
}
}
async function testGitMirrorSpeed() {
testingGitSpeed.value = true
try {
const promises = gitMirrors.value.map(async (mirror) => {
const url = mirror.url.replace('.git', '')
mirror.speed = await testMirrorWithTimeout(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
}
} finally {
testingGitSpeed.value = false
}
}
// 步骤控制
function prevStep() {
if (currentStep.value > 0) {
currentStep.value--
}
}
async function nextStep() {
isProcessing.value = true
errorMessage.value = ''
try {
switch (currentStep.value) {
case 0: // 主题设置
saveSettings()
break
case 1: // Python 环境
if (!pythonInstalled.value) {
await installPython()
}
break
case 2: // Git 工具
if (!gitInstalled.value) {
await installGit()
}
break
case 3: // 源码获取
if (!backendExists.value) {
await cloneBackend()
} else {
await updateBackend()
}
break
case 4: // 依赖安装
if (!dependenciesInstalled.value) {
await installDependencies()
}
break
case 5: // 启动服务
await startBackendService()
break
}
if (currentStep.value < 5) {
currentStep.value++
// 进入新步骤时自动开始测速
await autoStartSpeedTest()
}
} catch (error) {
errorMessage.value = error instanceof Error ? error.message : String(error)
stepStatus.value = 'error'
} finally {
isProcessing.value = false
}
}
// 自动开始测速
async function autoStartSpeedTest() {
switch (currentStep.value) {
case 1: // Python 环境
if (!pythonInstalled.value) {
await testPythonMirrorSpeed()
}
break
case 3: // 源码获取
await testGitMirrorSpeed()
break
case 4: // 依赖安装
if (!dependenciesInstalled.value) {
await testPipMirrorSpeed()
}
break
}
}
function getNextButtonText() {
switch (currentStep.value) {
case 0: return '下一步'
case 1: return pythonInstalled.value ? '下一步' : '安装 Python'
case 2: return gitInstalled.value ? '下一步' : '安装 Git'
case 3: return backendExists.value ? '更新代码' : '获取代码'
case 4: return '安装依赖'
case 5: return '启动服务'
default: return '下一步'
}
}
// 检查环境状态
async function checkEnvironment() {
try {
logger.info('开始检查环境状态')
const status = await window.electronAPI.checkEnvironment()
logger.info('环境检查结果', status)
pythonInstalled.value = status.pythonExists
gitInstalled.value = status.gitExists
backendExists.value = status.backendExists
dependenciesInstalled.value = status.dependenciesInstalled
// 如果所有环境都已准备好,跳到最后一步
if (status.isInitialized) {
logger.info('环境已初始化完成,跳转到启动服务步骤')
currentStep.value = 5
await startBackendService()
}
} catch (error) {
const errorMsg = `环境检查失败: ${error instanceof Error ? error.message : String(error)}`
logger.error('环境检查失败', error)
errorMessage.value = errorMsg
}
}
// 安装 Python
async function installPython() {
logger.info('开始安装Python', { mirror: selectedPythonMirror.value })
const result = await window.electronAPI.downloadPython(selectedPythonMirror.value)
if (result.success) {
logger.info('Python安装成功')
pythonInstalled.value = true
} else {
logger.error('Python安装失败', result.error)
throw new Error(result.error)
}
}
// 安装依赖
async function installDependencies() {
logger.info('开始安装Python依赖', { mirror: selectedPipMirror.value })
const result = await window.electronAPI.installDependencies(selectedPipMirror.value)
if (result.success) {
logger.info('Python依赖安装成功')
dependenciesInstalled.value = true
} else {
logger.error('Python依赖安装失败', result.error)
throw new Error(result.error)
}
}
// 安装 Git
async function installGit() {
logger.info('开始安装Git工具')
const result = await window.electronAPI.downloadGit()
if (result.success) {
logger.info('Git工具安装成功')
gitInstalled.value = true
} else {
logger.error('Git工具安装失败', result.error)
throw new Error(result.error)
}
}
// 克隆后端代码
async function cloneBackend() {
const selectedMirror = gitMirrors.value.find(m => m.key === selectedGitMirror.value)
logger.info('开始克隆后端代码', { mirror: selectedMirror?.name, url: selectedMirror?.url })
const result = await window.electronAPI.cloneBackend(selectedMirror?.url)
if (result.success) {
logger.info('后端代码克隆成功')
backendExists.value = true
} else {
logger.error('后端代码克隆失败', result.error)
throw new Error(result.error)
}
}
// 更新后端代码
async function updateBackend() {
const selectedMirror = gitMirrors.value.find(m => m.key === selectedGitMirror.value)
logger.info('开始更新后端代码', { mirror: selectedMirror?.name, url: selectedMirror?.url })
const result = await window.electronAPI.updateBackend(selectedMirror?.url)
if (!result.success) {
logger.error('后端代码更新失败', result.error)
throw new Error(result.error)
}
logger.info('后端代码更新成功')
}
// 启动后端服务
async function startBackendService() {
startingService.value = true
showServiceProgress.value = true
serviceStatus.value = '正在启动后端服务...'
logger.info('开始启动后端服务')
try {
const result = await window.electronAPI.startBackend()
if (result.success) {
serviceProgress.value = 100
serviceStatus.value = '后端服务启动成功'
stepStatus.value = 'finish'
logger.info('后端服务启动成功')
} else {
logger.error('后端服务启动失败', result.error)
throw new Error(result.error)
}
} catch (error) {
serviceStatus.value = '后端服务启动失败'
logger.error('后端服务启动异常', error)
throw error
} finally {
startingService.value = false
}
}
// 进入应用
function enterApp() {
router.push('/home')
}
// 获取速度样式类
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'
}
// 监听下载进度
function handleDownloadProgress(progress: DownloadProgress) {
if (progress.type === 'service') {
serviceProgress.value = progress.progress
serviceStatus.value = progress.message
}
}
onMounted(async () => {
loadSettings()
await checkEnvironment()
window.electronAPI.onDownloadProgress(handleDownloadProgress)
// 如果当前步骤需要测速,自动开始测速
await autoStartSpeedTest()
})
onUnmounted(() => {
window.electronAPI.removeDownloadProgressListener()
})
</script>
<style scoped>
.initialization-container {
min-height: 100vh;
background: var(--ant-color-bg-layout);
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.initialization-content {
background: var(--ant-color-bg-container);
border-radius: 16px;
padding: 40px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
max-width: 900px;
width: 100%;
}
.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;
}
.init-steps {
margin-bottom: 40px;
}
.step-content {
min-height: 300px;
margin-bottom: 40px;
}
.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);
}
.install-section {
display: flex;
flex-direction: column;
gap: 20px;
}
.install-section p {
color: var(--ant-color-text-secondary);
margin: 0;
}
.mirror-selection {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.mirror-selection label {
font-weight: 500;
color: var(--ant-color-text);
white-space: nowrap;
}
.speed-info {
color: var(--ant-color-text-tertiary);
font-size: 12px;
}
.already-installed {
display: flex;
justify-content: center;
align-items: center;
min-height: 200px;
}
.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;
}
.step-actions {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.error-message {
margin-top: 20px;
}
/* 镜像卡片样式 */
.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);
}
.git-info {
margin-top: 16px;
}
@media (max-width: 768px) {
.initialization-content {
padding: 20px;
margin: 10px;
}
.mirror-grid {
grid-template-columns: 1fr;
}
.mirror-selection {
flex-direction: column;
align-items: stretch;
}
.mirror-selection label {
text-align: left;
}
.step-actions {
flex-direction: column;
gap: 12px;
}
.test-actions {
flex-direction: column;
gap: 8px;
}
}
</style>

View File

@@ -0,0 +1,55 @@
<template>
<div class="logs-page">
<div class="page-header">
<h2>系统日志</h2>
<p>查看应用运行日志支持搜索过滤和导出功能</p>
</div>
<div class="logs-content">
<LogViewer />
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted } from 'vue'
import LogViewer from '@/components/LogViewer.vue'
import { createComponentLogger } from '@/utils/logger'
const logger = createComponentLogger('LogsPage')
onMounted(() => {
logger.info('进入日志查看页面')
})
</script>
<style scoped>
.logs-page {
height: 100%;
display: flex;
flex-direction: column;
padding: 24px;
}
.page-header {
margin-bottom: 24px;
}
.page-header h2 {
margin: 0 0 8px 0;
color: var(--ant-color-text);
font-size: 24px;
font-weight: 600;
}
.page-header p {
margin: 0;
color: var(--ant-color-text-secondary);
font-size: 14px;
}
.logs-content {
flex: 1;
min-height: 0;
}
</style>