Merge branch 'feature/refactor' of github.com:DLmaster361/AUTO_MAA into feature/refactor

This commit is contained in:
DLmaster361
2025-09-02 19:26:47 +08:00
9 changed files with 662 additions and 129 deletions

View File

@@ -13,7 +13,7 @@ import {
stopBackend, stopBackend,
} from './services/pythonService' } from './services/pythonService'
import { setMainWindow as setGitMainWindow, downloadGit, cloneBackend } from './services/gitService' import { setMainWindow as setGitMainWindow, downloadGit, cloneBackend } from './services/gitService'
import { setupLogger, log, getLogPath, cleanOldLogs } from './services/logService' import { setupLogger, log, getLogPath, getLogFiles, cleanOldLogs } from './services/logService'
// 检查是否以管理员权限运行 // 检查是否以管理员权限运行
function isRunningAsAdmin(): boolean { function isRunningAsAdmin(): boolean {
@@ -256,9 +256,27 @@ ipcMain.handle('get-log-path', async () => {
} }
}) })
ipcMain.handle('get-logs', async (event, lines?: number) => { ipcMain.handle('get-log-files', async () => {
try { try {
const logFilePath = getLogPath() return getLogFiles()
} catch (error) {
log.error('获取日志文件列表失败:', error)
throw error
}
})
ipcMain.handle('get-logs', async (event, lines?: number, fileName?: string) => {
try {
let logFilePath: string
if (fileName) {
// 如果指定了文件名,使用指定的文件
const appRoot = getAppRoot()
logFilePath = path.join(appRoot, 'logs', fileName)
} else {
// 否则使用当前日志文件
logFilePath = getLogPath()
}
if (!fs.existsSync(logFilePath)) { if (!fs.existsSync(logFilePath)) {
return '' return ''
@@ -278,13 +296,22 @@ ipcMain.handle('get-logs', async (event, lines?: number) => {
} }
}) })
ipcMain.handle('clear-logs', async () => { ipcMain.handle('clear-logs', async (event, fileName?: string) => {
try { try {
const logFilePath = getLogPath() let logFilePath: string
if (fileName) {
// 如果指定了文件名,清空指定的文件
const appRoot = getAppRoot()
logFilePath = path.join(appRoot, 'logs', fileName)
} else {
// 否则清空当前日志文件
logFilePath = getLogPath()
}
if (fs.existsSync(logFilePath)) { if (fs.existsSync(logFilePath)) {
fs.writeFileSync(logFilePath, '', 'utf8') fs.writeFileSync(logFilePath, '', 'utf8')
log.info('日志文件已清空') log.info(`日志文件已清空: ${fileName || '当前文件'}`)
} }
} catch (error) { } catch (error) {
log.error('清空日志文件失败:', error) log.error('清空日志文件失败:', error)

View File

@@ -32,8 +32,9 @@ contextBridge.exposeInMainWorld('electronAPI', {
// 日志文件操作 // 日志文件操作
getLogPath: () => ipcRenderer.invoke('get-log-path'), getLogPath: () => ipcRenderer.invoke('get-log-path'),
getLogs: (lines?: number) => ipcRenderer.invoke('get-logs', lines), getLogFiles: () => ipcRenderer.invoke('get-log-files'),
clearLogs: () => ipcRenderer.invoke('clear-logs'), getLogs: (lines?: number, fileName?: string) => ipcRenderer.invoke('get-logs', lines, fileName),
clearLogs: (fileName?: string) => ipcRenderer.invoke('clear-logs', fileName),
cleanOldLogs: (daysToKeep?: number) => ipcRenderer.invoke('clean-old-logs', daysToKeep), cleanOldLogs: (daysToKeep?: number) => ipcRenderer.invoke('clean-old-logs', daysToKeep),
// 保留原有方法以兼容现有代码 // 保留原有方法以兼容现有代码

View File

@@ -15,6 +15,15 @@ function getLogDirectory(): string {
return path.join(appRoot, 'logs') return path.join(appRoot, 'logs')
} }
// 获取当前日期的日志文件名 - 使用ISO 8601格式
function getTodayLogFileName(): string {
const today = new Date()
const year = today.getFullYear()
const month = String(today.getMonth() + 1).padStart(2, '0')
const day = String(today.getDate()).padStart(2, '0')
return `frontendlog-${year}-${month}-${day}.log`
}
// 配置日志系统 // 配置日志系统
export function setupLogger() { export function setupLogger() {
// 设置日志文件路径到软件安装目录 // 设置日志文件路径到软件安装目录
@@ -30,28 +39,21 @@ export function setupLogger() {
log.transports.file.format = '[{y}-{m}-{d} {h}:{i}:{s}.{ms}] [{level}] {text}' log.transports.file.format = '[{y}-{m}-{d} {h}:{i}:{s}.{ms}] [{level}] {text}'
log.transports.console.format = '[{y}-{m}-{d} {h}:{i}:{s}.{ms}] [{level}] {text}' log.transports.console.format = '[{y}-{m}-{d} {h}:{i}:{s}.{ms}] [{level}] {text}'
// 设置主进程日志文件路径和名称 // 设置主进程日志文件路径和名称 - 按日期分文件
log.transports.file.resolvePathFn = () => path.join(logPath, 'app.log') log.transports.file.resolvePathFn = () => {
const fileName = getTodayLogFileName()
return path.join(logPath, fileName)
}
// 设置日志级别 // 设置日志级别
log.transports.file.level = 'debug' log.transports.file.level = 'debug'
log.transports.console.level = 'debug' log.transports.console.level = 'debug'
// 设置文件大小限制 (10MB) // 设置文件大小限制 (50MB,因为按日期分文件,可以设置更大)
log.transports.file.maxSize = 10 * 1024 * 1024 log.transports.file.maxSize = 50 * 1024 * 1024
// 保留最近的5个日志文件 // 禁用自动归档,因为我们按日期分文件
log.transports.file.archiveLog = (file: any) => { log.transports.file.archiveLog = null
const filePath = file.toString()
const info = path.parse(filePath)
try {
return path.join(info.dir, `${info.name}.old${info.ext}`)
} catch (e) {
console.warn('Could not archive log file', e)
return null
}
}
// 捕获未处理的异常和Promise拒绝 // 捕获未处理的异常和Promise拒绝
log.catchErrors({ log.catchErrors({
@@ -98,7 +100,7 @@ export function setupLogger() {
} }
log.info('日志系统初始化完成') log.info('日志系统初始化完成')
log.info(`日志文件路径: ${path.join(logPath, 'main.log')}`) log.info(`日志文件路径: ${path.join(logPath, getTodayLogFileName())}`)
return log return log
} }
@@ -106,9 +108,25 @@ export function setupLogger() {
// 导出日志实例和工具函数 // 导出日志实例和工具函数
export { log, stripAnsiColors } export { log, stripAnsiColors }
// 获取日志文件路径 // 获取当前日志文件路径
export function getLogPath(): string { export function getLogPath(): string {
return path.join(getLogDirectory(), 'main.log') return path.join(getLogDirectory(), getTodayLogFileName())
}
// 获取所有日志文件列表
export function getLogFiles(): string[] {
const fs = require('fs')
const logDir = getLogDirectory()
if (!fs.existsSync(logDir)) {
return []
}
const files = fs.readdirSync(logDir)
return files
.filter((file: string) => file.match(/^frontendlog-\d{4}-\d{2}-\d{2}\.log$/))
.sort()
.reverse() // 最新的在前面
} }
// 清理旧日志文件 // 清理旧日志文件
@@ -121,19 +139,27 @@ export function cleanOldLogs(daysToKeep: number = 7) {
} }
const files = fs.readdirSync(logDir) const files = fs.readdirSync(logDir)
const now = Date.now() const now = new Date()
const maxAge = daysToKeep * 24 * 60 * 60 * 1000 // 转换为毫秒 const cutoffDate = new Date(now.getTime() - daysToKeep * 24 * 60 * 60 * 1000)
// 格式化截止日期为YYYY-MM-DD
const cutoffDateStr = cutoffDate.getFullYear() + '-' +
String(cutoffDate.getMonth() + 1).padStart(2, '0') + '-' +
String(cutoffDate.getDate()).padStart(2, '0')
files.forEach((file: string) => { files.forEach((file: string) => {
const filePath = path.join(logDir, file) // 匹配日志文件名格式 frontendlog-YYYY-MM-DD.log
const stats = fs.statSync(filePath) const match = file.match(/^frontendlog-(\d{4}-\d{2}-\d{2})\.log$/)
if (match) {
if (now - stats.mtime.getTime() > maxAge) { const fileDateStr = match[1]
try { if (fileDateStr < cutoffDateStr) {
fs.unlinkSync(filePath) const filePath = path.join(logDir, file)
log.info(`已删除旧日志文件: ${file}`) try {
} catch (error) { fs.unlinkSync(filePath)
log.error(`删除旧日志文件失败: ${file}`, error) log.info(`删除旧日志文件: ${file}`)
} catch (error) {
log.error(`删除旧日志文件失败: ${file}`, error)
}
} }
} }
}) })

View File

@@ -1,94 +1,218 @@
<template> <template>
<div class="log-viewer"> <div class="log-viewer">
<div class="log-controls"> <!-- 工具栏 -->
<a-space wrap> <a-card size="small" class="toolbar-card">
<a-button @click="refreshLogs" :loading="loading"> <a-row :gutter="[12, 12]" align="middle" justify="space-between" class="toolbar-grid">
<template #icon> <!-- 左侧刷新 + 选择器 -->
<ReloadOutlined /> <a-col :xs="24" :md="14">
</template> <a-space :size="8" wrap>
刷新日志 <a-button @click="refreshLogs" :loading="loading" type="primary">
</a-button> <template #icon>
<ReloadOutlined />
</template>
刷新日志
</a-button>
<a-select v-model:value="logLines" @change="refreshLogs" style="width: 120px"> <a-select v-model:value="selectedLogFile" @change="onLogFileChange" style="width: 220px"
<a-select-option :value="100">最近100行</a-select-option> placeholder="选择日志文件">
<a-select-option :value="500">最近500行</a-select-option> <a-select-option value="">今日日志</a-select-option>
<a-select-option :value="1000">最近1000行</a-select-option> <a-select-option v-for="file in logFiles" :key="file" :value="file">
<a-select-option :value="0">全部日志</a-select-option> {{ formatLogFileName(file) }}
</a-select> </a-select-option>
</a-select>
<a-button @click="clearLogs" :loading="clearing" type="primary" danger> <a-select v-model:value="logLines" @change="refreshLogs" style="width: 140px"
<template #icon> placeholder="显示行数">
<DeleteOutlined /> <a-select-option :value="100">最近100行</a-select-option>
</template> <a-select-option :value="500">最近500行</a-select-option>
清空日志 <a-select-option :value="1000">最近1000行</a-select-option>
</a-button> <a-select-option :value="0">显示全部</a-select-option>
</a-select>
</a-space>
</a-col>
<a-button @click="cleanOldLogs" :loading="cleaning"> <!-- 右侧操作按钮 -->
<template #icon> <a-col :xs="24" :md="10">
<ClearOutlined /> <div class="toolbar-actions">
</template> <a-space :size="8" wrap>
清理旧日志
</a-button>
<a-button @click="openLogDirectory">
<template #icon>
<FolderOpenOutlined />
</template>
打开日志目录
</a-button>
</a-space>
</div>
<div class="log-info"> <a-popconfirm
<a-space> :title="`确定要清空${selectedLogFile ? formatLogFileName(selectedLogFile) : '今日日志'}吗?`"
<span>日志文件: {{ logPath }}</span> ok-text="确定" cancel-text="取消" @confirm="clearLogs">
<span>总行数: {{ totalLines }}</span> <a-button :loading="clearing" danger>
</a-space> <template #icon>
</div> <DeleteOutlined />
</template>
清空当前日志
</a-button>
</a-popconfirm>
<div class="log-content"> <a-button @click="cleanOldLogs" :loading="cleaning">
<a-textarea v-model:value="logs" :rows="25" readonly class="log-textarea" placeholder="暂无日志内容" /> <template #icon>
</div> <ClearOutlined />
</template>
清理7日前的旧日志
</a-button>
<a-button @click="openLogDirectory">
<template #icon>
<FolderOpenOutlined />
</template>
打开日志所在目录
</a-button>
<a-button @click="exportLogs">
<template #icon>
<ExportOutlined />
</template>
导出日志txt格式
</a-button>
<!-- <a-button @click="scrollToBottom" :disabled="!logs">-->
<!-- <template #icon><DownOutlined /></template>-->
<!-- 跳转底部-->
<!-- </a-button>-->
</a-space>
</div>
</a-col>
</a-row>
</a-card>
<!-- 日志内容 -->
<a-card class="log-content-card">
<template #title>
<span>日志内容</span>
</template>
<div class="log-content" :class="{ 'word-wrap': wordWrap }">
<a-spin :spinning="loading" tip="加载日志中..." class="log-spin">
<div ref="logContainer" class="log-container" v-if="displayLogs">
<pre class="log-text" v-html="displayContent"></pre>
</div>
<a-empty v-else description="暂无日志内容" :image="Empty.PRESENTED_IMAGE_SIMPLE" class="log-empty" />
</a-spin>
</div>
</a-card>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue' import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
import { message } from 'ant-design-vue' import { message, Empty } from 'ant-design-vue'
import { import {
ReloadOutlined, ReloadOutlined,
DeleteOutlined, DeleteOutlined,
ClearOutlined, ClearOutlined,
FolderOpenOutlined FolderOpenOutlined,
ExportOutlined,
} from '@ant-design/icons-vue' } from '@ant-design/icons-vue'
import { logger } from '@/utils/logger' import { logger } from '@/utils/logger'
// 响应式数据
const logs = ref('') const logs = ref('')
const logPath = ref('') const logPath = ref('')
const logFiles = ref<string[]>([])
const selectedLogFile = ref('')
const logLines = ref(500) const logLines = ref(500)
const totalLines = ref(0)
const loading = ref(false) const loading = ref(false)
const clearing = ref(false) const clearing = ref(false)
const cleaning = ref(false) const cleaning = ref(false)
const autoRefresh = ref(true)
const wordWrap = ref(true)
const logContainer = ref<HTMLElement>()
// 自动刷新定时器
let autoRefreshTimer: NodeJS.Timeout | null = null
// 计算属性
const displayLogs = computed(() => {
const hasContent = logs.value && logs.value.trim().length > 0
console.log('displayLogs computed:', {
hasLogs: !!logs.value,
logsLength: logs.value?.length || 0,
trimmedLength: logs.value?.trim().length || 0,
hasContent,
firstChars: logs.value?.substring(0, 100) || 'empty',
})
return hasContent
})
const displayContent = computed(() => {
if (!logs.value) return ''
// 转义HTML特殊字符
return logs.value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
})
// 格式化日志文件名显示
const formatLogFileName = (fileName: string) => {
const match = fileName.match(/^frontendlog-(\d{4}-\d{2}-\d{2})\.log$/)
if (match) {
const [, dateStr] = match
// 转换为更友好的中文显示
const date = new Date(dateStr + 'T00:00:00')
const options: Intl.DateTimeFormatOptions = {
year: 'numeric',
month: 'long',
day: 'numeric',
weekday: 'short',
}
return date.toLocaleDateString('zh-CN', options)
}
return fileName
}
// 获取日志文件列表
const getLogFiles = async () => {
try {
const files = await logger.getLogFiles()
logFiles.value = files
} catch (error) {
logger.error('获取日志文件列表失败:', error)
logFiles.value = []
}
}
// 日志文件选择变化
const onLogFileChange = () => {
refreshLogs()
}
// 刷新日志 // 刷新日志
const refreshLogs = async () => { const refreshLogs = async () => {
loading.value = true loading.value = true
try { try {
const logContent = await logger.getLogs(logLines.value || undefined) console.log('开始获取日志,文件:', selectedLogFile.value, '行数限制:', logLines.value)
logs.value = logContent const logContent = await logger.getLogs(
totalLines.value = logContent.split('\n').filter(line => line.trim()).length logLines.value || undefined,
selectedLogFile.value || undefined
)
console.log('获取到的日志内容:', {
type: typeof logContent,
length: logContent?.length || 0,
isNull: logContent === null,
isUndefined: logContent === undefined,
isEmpty: logContent === '',
preview: logContent?.substring(0, 200) || 'no content',
})
logs.value = logContent || ''
// 日志内容已更新
// 自动滚动到底部 // 自动滚动到底部
setTimeout(() => { await nextTick()
const textarea = document.querySelector('.log-textarea textarea') as HTMLTextAreaElement scrollToBottom()
if (textarea) {
textarea.scrollTop = textarea.scrollHeight
}
}, 100)
} catch (error) { } catch (error) {
message.error('获取日志失败: ' + error) console.error('获取日志失败:', error)
message.error(`获取日志失败: ${error}`)
logger.error('获取日志失败:', error) logger.error('获取日志失败:', error)
logs.value = ''
} finally { } finally {
loading.value = false loading.value = false
} }
@@ -98,12 +222,12 @@ const refreshLogs = async () => {
const clearLogs = async () => { const clearLogs = async () => {
clearing.value = true clearing.value = true
try { try {
await logger.clearLogs() await logger.clearLogs(selectedLogFile.value || undefined)
logs.value = '' logs.value = ''
totalLines.value = 0 const fileName = selectedLogFile.value ? formatLogFileName(selectedLogFile.value) : '今日日志'
message.success('日志已清空') message.success(`${fileName}已清空`)
} catch (error) { } catch (error) {
message.error('清空日志失败: ' + error) message.error(`清空日志失败: ${error}`)
logger.error('清空日志失败:', error) logger.error('清空日志失败:', error)
} finally { } finally {
clearing.value = false clearing.value = false
@@ -116,8 +240,11 @@ const cleanOldLogs = async () => {
try { try {
await logger.cleanOldLogs(7) await logger.cleanOldLogs(7)
message.success('已清理7天前的旧日志文件') message.success('已清理7天前的旧日志文件')
// 清理后刷新日志文件列表和当前日志
await getLogFiles()
await refreshLogs()
} catch (error) { } catch (error) {
message.error('清理旧日志失败: ' + error) message.error(`清理旧日志失败: ${error}`)
logger.error('清理旧日志失败:', error) logger.error('清理旧日志失败:', error)
} finally { } finally {
cleaning.value = false cleaning.value = false
@@ -132,58 +259,329 @@ const openLogDirectory = async () => {
const logDir = path.substring(0, path.lastIndexOf('\\') || path.lastIndexOf('/')) const logDir = path.substring(0, path.lastIndexOf('\\') || path.lastIndexOf('/'))
if (window.electronAPI?.openUrl) { if (window.electronAPI?.openUrl) {
await window.electronAPI.openUrl(`file://${logDir}`) const result = await window.electronAPI.openUrl(`file://${logDir}`)
if (!result.success) {
throw new Error(result.error || '打开目录失败')
}
} else {
throw new Error('Electron API 不可用')
} }
} catch (error) { } catch (error) {
message.error('打开日志目录失败: ' + error) message.error(`打开日志目录失败: ${error}`)
logger.error('打开日志目录失败:', error) logger.error('打开日志目录失败:', error)
} }
} }
// 导出日志
const exportLogs = async () => {
try {
if (!logs.value) {
message.warning('没有日志内容可导出')
return
}
const blob = new Blob([logs.value], { type: 'text/plain;charset=utf-8' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
// 使用当前选择的日志文件名或默认名称
let fileName = 'logs'
if (selectedLogFile.value) {
fileName = selectedLogFile.value.replace('.log', '')
} else {
fileName = `logs_${new Date().toISOString().slice(0, 10)}`
}
a.download = `${fileName}.txt`
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
message.success('日志导出成功')
} catch (error) {
message.error(`导出日志失败: ${error}`)
logger.error('导出日志失败:', error)
}
}
// 滚动到底部
const scrollToBottom = () => {
if (logContainer.value) {
// 使用 nextTick 确保DOM已更新
nextTick(() => {
if (logContainer.value) {
logContainer.value.scrollTop = logContainer.value.scrollHeight
// 添加平滑滚动效果
logContainer.value.scrollTo({
top: logContainer.value.scrollHeight,
behavior: 'smooth',
})
}
})
}
}
// 启动自动刷新
const startAutoRefresh = () => {
if (autoRefreshTimer) {
clearInterval(autoRefreshTimer)
}
autoRefreshTimer = setInterval(() => {
refreshLogs()
}, 2000) // 每2秒刷新一次
}
// 获取日志文件路径 // 获取日志文件路径
const getLogPath = async () => { const getLogPath = async () => {
try { try {
logPath.value = await logger.getLogPath() logPath.value = await logger.getLogPath()
} catch (error) { } catch (error) {
logger.error('获取日志路径失败:', error) logger.error('获取日志路径失败:', error)
logPath.value = ''
} }
} }
onMounted(() => { // 生命周期
getLogPath() onMounted(async () => {
refreshLogs() await getLogPath()
await getLogFiles()
await refreshLogs()
// 启动自动刷新
startAutoRefresh()
})
onUnmounted(() => {
if (autoRefreshTimer) {
clearInterval(autoRefreshTimer)
}
}) })
</script> </script>
<style scoped> <style scoped>
.log-viewer { .log-viewer {
padding: 16px; padding: 16px;
height: 85vh;
/* 减少整体高度 */
display: flex;
flex-direction: column;
gap: 12px;
/* 减少间距 */
} }
.log-controls { .toolbar-card {
margin-bottom: 16px; flex-shrink: 0;
} }
.log-info { .toolbar-content {
margin-bottom: 12px; display: flex;
font-size: 12px; justify-content: space-between;
color: #666; align-items: center;
flex-wrap: wrap;
gap: 16px;
}
.toolbar-left,
.toolbar-right {
flex-wrap: wrap;
}
/* 响应式处理 */
@media (max-width: 1400px) {
.toolbar-content {
flex-direction: column;
align-items: stretch;
}
.toolbar-left,
.toolbar-right {
justify-content: center;
}
}
.log-content-card {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
.log-content-card :deep(.ant-card-body) {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
padding: 12px;
/* 减少内边距 */
} }
.log-content { .log-content {
border: 1px solid #d9d9d9; flex: 1;
min-height: 0;
border: 1px solid var(--ant-color-border);
border-radius: 6px; border-radius: 6px;
background: var(--ant-color-bg-container);
display: flex;
flex-direction: column;
} }
.log-textarea :deep(.ant-input) { .log-spin {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
.log-container {
flex: 1;
overflow: auto;
padding: 12px;
background: var(--ant-color-bg-elevated);
border-radius: 4px;
min-height: 0;
}
.log-empty {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
}
.log-text {
font-family: 'Consolas', 'Monaco', 'Courier New', monospace; font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 12px; font-size: 12px;
line-height: 1.4; line-height: 1.5;
border: none; margin: 0;
resize: none; white-space: pre;
color: var(--ant-color-text);
word-break: break-all;
} }
.log-textarea :deep(.ant-input:focus) { .word-wrap .log-text {
box-shadow: none; white-space: pre-wrap;
word-break: break-word;
} }
</style>
/* 滚动条样式 - 适配深色模式 */
.log-container::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.log-container::-webkit-scrollbar-track {
background: var(--ant-color-bg-container);
border-radius: 4px;
}
.log-container::-webkit-scrollbar-thumb {
background: var(--ant-color-border);
border-radius: 4px;
}
.log-container::-webkit-scrollbar-thumb:hover {
background: var(--ant-color-border-secondary);
}
/* 空状态样式 */
:deep(.ant-empty) {
padding: 40px 20px;
}
:deep(.ant-empty-description) {
color: var(--ant-color-text-secondary);
}
/* 加载状态 */
:deep(.ant-spin-container) {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
:deep(.ant-spin-nested-loading) {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
/* 深色模式特定样式 */
[data-theme='dark'] .log-content {
border-color: #434343;
background: #1f1f1f;
}
[data-theme='dark'] .log-container {
background: #141414;
}
[data-theme='dark'] .log-text {
color: #e6e6e6;
}
[data-theme='dark'] .log-container::-webkit-scrollbar-track {
background: #262626;
}
[data-theme='dark'] .log-container::-webkit-scrollbar-thumb {
background: #434343;
}
[data-theme='dark'] .log-container::-webkit-scrollbar-thumb:hover {
background: #595959;
}
/* 响应式调整 */
@media (max-width: 768px) {
.log-viewer {
padding: 8px;
gap: 8px;
}
.log-text {
font-size: 11px;
}
}
/* 日志级别颜色 - 适配深色模式 */
.log-text {
/* ERROR 级别 - 红色 */
--log-error-color: #ff4d4f;
--log-error-bg: rgba(255, 77, 79, 0.1);
/* WARN 级别 - 橙色 */
--log-warn-color: #fa8c16;
--log-warn-bg: rgba(250, 140, 22, 0.1);
/* INFO 级别 - 蓝色 */
--log-info-color: #1890ff;
--log-info-bg: rgba(24, 144, 255, 0.1);
/* DEBUG 级别 - 绿色 */
--log-debug-color: #52c41a;
--log-debug-bg: rgba(82, 196, 26, 0.1);
}
[data-theme='dark'] .log-text {
--log-error-color: #ff7875;
--log-error-bg: rgba(255, 120, 117, 0.15);
--log-warn-color: #ffa940;
--log-warn-bg: rgba(255, 169, 64, 0.15);
--log-info-color: #40a9ff;
--log-info-bg: rgba(64, 169, 255, 0.15);
--log-debug-color: #73d13d;
--log-debug-bg: rgba(115, 209, 61, 0.15);
}
</style>

View File

@@ -76,6 +76,12 @@ const routes: RouteRecordRaw[] = [
component: () => import('../views/Settings.vue'), component: () => import('../views/Settings.vue'),
meta: { title: '设置' }, meta: { title: '设置' },
}, },
{
path: '/logs',
name: 'Logs',
component: () => import('../views/Logs.vue'),
meta: { title: '日志查看' },
},
] ]
const router = createRouter({ const router = createRouter({

View File

@@ -25,8 +25,9 @@ export interface ElectronAPI {
// 日志文件操作 // 日志文件操作
getLogPath: () => Promise<string> getLogPath: () => Promise<string>
getLogs: (lines?: number) => Promise<string> getLogFiles: () => Promise<string[]>
clearLogs: () => Promise<void> getLogs: (lines?: number, fileName?: string) => Promise<string>
clearLogs: (fileName?: string) => Promise<void>
cleanOldLogs: (daysToKeep?: number) => Promise<void> cleanOldLogs: (daysToKeep?: number) => Promise<void>
// 保留原有方法以兼容现有代码 // 保留原有方法以兼容现有代码

View File

@@ -1,8 +1,9 @@
// 渲染进程日志工具 // 渲染进程日志工具
interface ElectronAPI { interface ElectronAPI {
getLogPath: () => Promise<string> getLogPath: () => Promise<string>
getLogs: (lines?: number) => Promise<string> getLogFiles: () => Promise<string[]>
clearLogs: () => Promise<void> getLogs: (lines?: number, fileName?: string) => Promise<string>
clearLogs: (fileName?: string) => Promise<void>
cleanOldLogs: (daysToKeep?: number) => Promise<void> cleanOldLogs: (daysToKeep?: number) => Promise<void>
} }
@@ -45,19 +46,27 @@ class Logger {
throw new Error('Electron API not available') throw new Error('Electron API not available')
} }
// 获取日志内容 // 获取日志文件列表
async getLogs(lines?: number): Promise<string> { async getLogFiles(): Promise<string[]> {
if (window.electronAPI) { if (window.electronAPI) {
return await window.electronAPI.getLogs(lines) return await window.electronAPI.getLogFiles()
}
throw new Error('Electron API not available')
}
// 获取日志内容
async getLogs(lines?: number, fileName?: string): Promise<string> {
if (window.electronAPI) {
return await window.electronAPI.getLogs(lines, fileName)
} }
throw new Error('Electron API not available') throw new Error('Electron API not available')
} }
// 清空日志 // 清空日志
async clearLogs(): Promise<void> { async clearLogs(fileName?: string): Promise<void> {
if (window.electronAPI) { if (window.electronAPI) {
await window.electronAPI.clearLogs() await window.electronAPI.clearLogs(fileName)
console.info('日志已清空') console.info(`日志已清空: ${fileName || '当前文件'}`)
} else { } else {
throw new Error('Electron API not available') throw new Error('Electron API not available')
} }

View File

@@ -0,0 +1,51 @@
<template>
<div class="logs-container">
<div class="logs-header">
<h1>日志查看</h1>
<p class="logs-description">查看和管理应用程序日志</p>
</div>
<LogViewer />
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import LogViewer from '@/components/LogViewer.vue'
import { useTheme } from '@/composables/useTheme'
const { isDark } = useTheme()
const textColor = computed(() =>
isDark.value ? 'rgba(255, 255, 255, 0.88)' : 'rgba(0, 0, 0, 0.88)'
)
const textSecondaryColor = computed(() =>
isDark.value ? 'rgba(255, 255, 255, 0.65)' : 'rgba(0, 0, 0, 0.65)'
)
</script>
<style scoped>
.logs-container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.logs-header {
margin-bottom: 24px;
}
.logs-header h1 {
margin: 0 0 8px 0;
font-size: 24px;
font-weight: 600;
color: v-bind(textColor);
}
.logs-description {
margin: 0;
color: v-bind(textSecondaryColor);
font-size: 14px;
line-height: 1.5;
}
</style>

View File

@@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted, reactive } from 'vue' import { ref, computed, onMounted, reactive } from 'vue'
import { useRouter } from 'vue-router'
import { import {
Button, Button,
Card, Card,
@@ -19,6 +20,7 @@ import { useSettingsApi } from '../composables/useSettingsApi'
import type { SelectValue } from 'ant-design-vue/es/select' import type { SelectValue } from 'ant-design-vue/es/select'
import type { SettingsData } from '../types/settings' import type { SettingsData } from '../types/settings'
const router = useRouter()
const { themeMode, themeColor, themeColors, setThemeMode, setThemeColor, isDark } = useTheme() const { themeMode, themeColor, themeColors, setThemeMode, setThemeColor, isDark } = useTheme()
const { loading, getSettings, updateSettings } = useSettingsApi() const { loading, getSettings, updateSettings } = useSettingsApi()
@@ -182,6 +184,10 @@ const handleThemeColorChange = (value: SelectValue) => {
} }
} }
const goToLogs = () => {
router.push('/logs')
}
const openDevTools = () => { const openDevTools = () => {
if ((window as any).electronAPI) { if ((window as any).electronAPI) {
;(window as any).electronAPI.openDevTools() ;(window as any).electronAPI.openDevTools()
@@ -679,6 +685,14 @@ onMounted(() => {
<Tabs.TabPane key="advanced" tab="高级设置"> <Tabs.TabPane key="advanced" tab="高级设置">
<Card title="开发者选项" :bordered="false"> <Card title="开发者选项" :bordered="false">
<Space direction="vertical" size="middle" style="width: 100%"> <Space direction="vertical" size="middle" style="width: 100%">
<div class="setting-item">
<h4>日志管理</h4>
<p class="setting-description">查看和管理应用程序日志文件</p>
<Button type="primary" @click="goToLogs">查看日志</Button>
</div>
<Divider />
<div class="setting-item"> <div class="setting-item">
<h4>开发者工具</h4> <h4>开发者工具</h4>
<p class="setting-description">打开浏览器开发者工具进行调试</p> <p class="setting-description">打开浏览器开发者工具进行调试</p>