feat(initialization): 添加初始化页面和相关功能
- 新增初始化页面组件和路由 - 实现环境检查、Git下载、后端代码克隆等功能 - 添加下载服务和环境服务模块 - 更新类型定义,增加 Electron API 接口
This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
354
frontend/src/components/LogViewer.vue
Normal file
354
frontend/src/components/LogViewer.vue
Normal 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>
|
||||
@@ -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 })
|
||||
|
||||
@@ -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({
|
||||
|
||||
33
frontend/src/types/electron.d.ts
vendored
33
frontend/src/types/electron.d.ts
vendored
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
21
frontend/src/types/initialization.ts
Normal file
21
frontend/src/types/initialization.ts
Normal 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
|
||||
}
|
||||
185
frontend/src/utils/logger.ts
Normal file
185
frontend/src/utils/logger.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
967
frontend/src/views/Initialization.vue
Normal file
967
frontend/src/views/Initialization.vue
Normal 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>
|
||||
55
frontend/src/views/Logs.vue
Normal file
55
frontend/src/views/Logs.vue
Normal 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>
|
||||
Reference in New Issue
Block a user