refactor: 日志查看功能&优化日志功能

This commit is contained in:
2025-09-02 19:26:04 +08:00
parent 6b00c8a6be
commit baf8a642b8
9 changed files with 662 additions and 129 deletions

View File

@@ -13,7 +13,7 @@ import {
stopBackend,
} from './services/pythonService'
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 {
@@ -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 {
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)) {
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 {
const logFilePath = getLogPath()
let logFilePath: string
if (fileName) {
// 如果指定了文件名,清空指定的文件
const appRoot = getAppRoot()
logFilePath = path.join(appRoot, 'logs', fileName)
} else {
// 否则清空当前日志文件
logFilePath = getLogPath()
}
if (fs.existsSync(logFilePath)) {
fs.writeFileSync(logFilePath, '', 'utf8')
log.info('日志文件已清空')
log.info(`日志文件已清空: ${fileName || '当前文件'}`)
}
} catch (error) {
log.error('清空日志文件失败:', error)

View File

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

View File

@@ -15,6 +15,15 @@ function getLogDirectory(): string {
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() {
// 设置日志文件路径到软件安装目录
@@ -30,28 +39,21 @@ export function setupLogger() {
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.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.console.level = 'debug'
// 设置文件大小限制 (10MB)
log.transports.file.maxSize = 10 * 1024 * 1024
// 设置文件大小限制 (50MB,因为按日期分文件,可以设置更大)
log.transports.file.maxSize = 50 * 1024 * 1024
// 保留最近的5个日志文件
log.transports.file.archiveLog = (file: any) => {
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
}
}
// 禁用自动归档,因为我们按日期分文件
log.transports.file.archiveLog = null
// 捕获未处理的异常和Promise拒绝
log.catchErrors({
@@ -98,7 +100,7 @@ export function setupLogger() {
}
log.info('日志系统初始化完成')
log.info(`日志文件路径: ${path.join(logPath, 'main.log')}`)
log.info(`日志文件路径: ${path.join(logPath, getTodayLogFileName())}`)
return log
}
@@ -106,9 +108,25 @@ export function setupLogger() {
// 导出日志实例和工具函数
export { log, stripAnsiColors }
// 获取日志文件路径
// 获取当前日志文件路径
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 now = Date.now()
const maxAge = daysToKeep * 24 * 60 * 60 * 1000 // 转换为毫秒
const now = new Date()
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) => {
const filePath = path.join(logDir, file)
const stats = fs.statSync(filePath)
if (now - stats.mtime.getTime() > maxAge) {
try {
fs.unlinkSync(filePath)
log.info(`已删除旧日志文件: ${file}`)
} catch (error) {
log.error(`删除旧日志文件失败: ${file}`, error)
// 匹配日志文件名格式 frontendlog-YYYY-MM-DD.log
const match = file.match(/^frontendlog-(\d{4}-\d{2}-\d{2})\.log$/)
if (match) {
const fileDateStr = match[1]
if (fileDateStr < cutoffDateStr) {
const filePath = path.join(logDir, file)
try {
fs.unlinkSync(filePath)
log.info(`删除旧日志文件: ${file}`)
} catch (error) {
log.error(`删除旧日志文件失败: ${file}`, error)
}
}
}
})

View File

@@ -1,94 +1,218 @@
<template>
<div class="log-viewer">
<div class="log-controls">
<a-space wrap>
<a-button @click="refreshLogs" :loading="loading">
<template #icon>
<ReloadOutlined />
</template>
刷新日志
</a-button>
<!-- 工具栏 -->
<a-card size="small" class="toolbar-card">
<a-row :gutter="[12, 12]" align="middle" justify="space-between" class="toolbar-grid">
<!-- 左侧刷新 + 选择器 -->
<a-col :xs="24" :md="14">
<a-space :size="8" wrap>
<a-button @click="refreshLogs" :loading="loading" type="primary">
<template #icon>
<ReloadOutlined />
</template>
刷新日志
</a-button>
<a-select v-model:value="logLines" @change="refreshLogs" style="width: 120px">
<a-select-option :value="100">最近100行</a-select-option>
<a-select-option :value="500">最近500行</a-select-option>
<a-select-option :value="1000">最近1000行</a-select-option>
<a-select-option :value="0">全部日志</a-select-option>
</a-select>
<a-select v-model:value="selectedLogFile" @change="onLogFileChange" style="width: 220px"
placeholder="选择日志文件">
<a-select-option value="">今日日志</a-select-option>
<a-select-option v-for="file in logFiles" :key="file" :value="file">
{{ formatLogFileName(file) }}
</a-select-option>
</a-select>
<a-button @click="clearLogs" :loading="clearing" type="primary" danger>
<template #icon>
<DeleteOutlined />
</template>
清空日志
</a-button>
<a-select v-model:value="logLines" @change="refreshLogs" style="width: 140px"
placeholder="显示行数">
<a-select-option :value="100">最近100行</a-select-option>
<a-select-option :value="500">最近500行</a-select-option>
<a-select-option :value="1000">最近1000行</a-select-option>
<a-select-option :value="0">显示全部</a-select-option>
</a-select>
</a-space>
</a-col>
<a-button @click="cleanOldLogs" :loading="cleaning">
<template #icon>
<ClearOutlined />
</template>
清理旧日志
</a-button>
<!-- 右侧操作按钮 -->
<a-col :xs="24" :md="10">
<div class="toolbar-actions">
<a-space :size="8" wrap>
<a-button @click="openLogDirectory">
<template #icon>
<FolderOpenOutlined />
</template>
打开日志目录
</a-button>
</a-space>
</div>
<div class="log-info">
<a-space>
<span>日志文件: {{ logPath }}</span>
<span>总行数: {{ totalLines }}</span>
</a-space>
</div>
<a-popconfirm
:title="`确定要清空${selectedLogFile ? formatLogFileName(selectedLogFile) : '今日日志'}吗?`"
ok-text="确定" cancel-text="取消" @confirm="clearLogs">
<a-button :loading="clearing" danger>
<template #icon>
<DeleteOutlined />
</template>
清空当前日志
</a-button>
</a-popconfirm>
<div class="log-content">
<a-textarea v-model:value="logs" :rows="25" readonly class="log-textarea" placeholder="暂无日志内容" />
</div>
<a-button @click="cleanOldLogs" :loading="cleaning">
<template #icon>
<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>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
import { message, Empty } from 'ant-design-vue'
import {
ReloadOutlined,
DeleteOutlined,
ClearOutlined,
FolderOpenOutlined
FolderOpenOutlined,
ExportOutlined,
} from '@ant-design/icons-vue'
import { logger } from '@/utils/logger'
// 响应式数据
const logs = ref('')
const logPath = ref('')
const logFiles = ref<string[]>([])
const selectedLogFile = ref('')
const logLines = ref(500)
const totalLines = ref(0)
const loading = ref(false)
const clearing = 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 () => {
loading.value = true
try {
const logContent = await logger.getLogs(logLines.value || undefined)
logs.value = logContent
totalLines.value = logContent.split('\n').filter(line => line.trim()).length
console.log('开始获取日志,文件:', selectedLogFile.value, '行数限制:', logLines.value)
const logContent = await logger.getLogs(
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(() => {
const textarea = document.querySelector('.log-textarea textarea') as HTMLTextAreaElement
if (textarea) {
textarea.scrollTop = textarea.scrollHeight
}
}, 100)
await nextTick()
scrollToBottom()
} catch (error) {
message.error('获取日志失败: ' + error)
console.error('获取日志失败:', error)
message.error(`获取日志失败: ${error}`)
logger.error('获取日志失败:', error)
logs.value = ''
} finally {
loading.value = false
}
@@ -98,12 +222,12 @@ const refreshLogs = async () => {
const clearLogs = async () => {
clearing.value = true
try {
await logger.clearLogs()
await logger.clearLogs(selectedLogFile.value || undefined)
logs.value = ''
totalLines.value = 0
message.success('日志已清空')
const fileName = selectedLogFile.value ? formatLogFileName(selectedLogFile.value) : '今日日志'
message.success(`${fileName}已清空`)
} catch (error) {
message.error('清空日志失败: ' + error)
message.error(`清空日志失败: ${error}`)
logger.error('清空日志失败:', error)
} finally {
clearing.value = false
@@ -116,8 +240,11 @@ const cleanOldLogs = async () => {
try {
await logger.cleanOldLogs(7)
message.success('已清理7天前的旧日志文件')
// 清理后刷新日志文件列表和当前日志
await getLogFiles()
await refreshLogs()
} catch (error) {
message.error('清理旧日志失败: ' + error)
message.error(`清理旧日志失败: ${error}`)
logger.error('清理旧日志失败:', error)
} finally {
cleaning.value = false
@@ -132,58 +259,329 @@ const openLogDirectory = async () => {
const logDir = path.substring(0, path.lastIndexOf('\\') || path.lastIndexOf('/'))
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) {
message.error('打开日志目录失败: ' + error)
message.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 () => {
try {
logPath.value = await logger.getLogPath()
} catch (error) {
logger.error('获取日志路径失败:', error)
logPath.value = ''
}
}
onMounted(() => {
getLogPath()
refreshLogs()
// 生命周期
onMounted(async () => {
await getLogPath()
await getLogFiles()
await refreshLogs()
// 启动自动刷新
startAutoRefresh()
})
onUnmounted(() => {
if (autoRefreshTimer) {
clearInterval(autoRefreshTimer)
}
})
</script>
<style scoped>
.log-viewer {
padding: 16px;
height: 85vh;
/* 减少整体高度 */
display: flex;
flex-direction: column;
gap: 12px;
/* 减少间距 */
}
.log-controls {
margin-bottom: 16px;
.toolbar-card {
flex-shrink: 0;
}
.log-info {
margin-bottom: 12px;
font-size: 12px;
color: #666;
.toolbar-content {
display: flex;
justify-content: space-between;
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 {
border: 1px solid #d9d9d9;
flex: 1;
min-height: 0;
border: 1px solid var(--ant-color-border);
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-size: 12px;
line-height: 1.4;
border: none;
resize: none;
line-height: 1.5;
margin: 0;
white-space: pre;
color: var(--ant-color-text);
word-break: break-all;
}
.log-textarea :deep(.ant-input:focus) {
box-shadow: none;
.word-wrap .log-text {
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'),
meta: { title: '设置' },
},
{
path: '/logs',
name: 'Logs',
component: () => import('../views/Logs.vue'),
meta: { title: '日志查看' },
},
]
const router = createRouter({

View File

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

View File

@@ -1,8 +1,9 @@
// 渲染进程日志工具
interface ElectronAPI {
getLogPath: () => Promise<string>
getLogs: (lines?: number) => Promise<string>
clearLogs: () => Promise<void>
getLogFiles: () => Promise<string[]>
getLogs: (lines?: number, fileName?: string) => Promise<string>
clearLogs: (fileName?: string) => Promise<void>
cleanOldLogs: (daysToKeep?: number) => Promise<void>
}
@@ -45,19 +46,27 @@ class Logger {
throw new Error('Electron API not available')
}
// 获取日志内容
async getLogs(lines?: number): Promise<string> {
// 获取日志文件列表
async getLogFiles(): Promise<string[]> {
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')
}
// 清空日志
async clearLogs(): Promise<void> {
async clearLogs(fileName?: string): Promise<void> {
if (window.electronAPI) {
await window.electronAPI.clearLogs()
console.info('日志已清空')
await window.electronAPI.clearLogs(fileName)
console.info(`日志已清空: ${fileName || '当前文件'}`)
} else {
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">
import { ref, computed, onMounted, reactive } from 'vue'
import { useRouter } from 'vue-router'
import {
Button,
Card,
@@ -19,6 +20,7 @@ import { useSettingsApi } from '../composables/useSettingsApi'
import type { SelectValue } from 'ant-design-vue/es/select'
import type { SettingsData } from '../types/settings'
const router = useRouter()
const { themeMode, themeColor, themeColors, setThemeMode, setThemeColor, isDark } = useTheme()
const { loading, getSettings, updateSettings } = useSettingsApi()
@@ -182,6 +184,10 @@ const handleThemeColorChange = (value: SelectValue) => {
}
}
const goToLogs = () => {
router.push('/logs')
}
const openDevTools = () => {
if ((window as any).electronAPI) {
;(window as any).electronAPI.openDevTools()
@@ -679,6 +685,14 @@ onMounted(() => {
<Tabs.TabPane key="advanced" tab="高级设置">
<Card title="开发者选项" :bordered="false">
<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">
<h4>开发者工具</h4>
<p class="setting-description">打开浏览器开发者工具进行调试</p>