add logger

This commit is contained in:
Julian Freeman
2026-03-14 20:55:46 -04:00
parent db20a31643
commit cdeb52c316
8 changed files with 348 additions and 54 deletions

View File

@@ -33,6 +33,20 @@
</span>
全部软件
</router-link>
<!-- 底部日志选项 -->
<router-link to="/logs" class="nav-item nav-logs">
<span class="nav-icon">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
<polyline points="14 2 14 8 20 8"></polyline>
<line x1="16" y1="13" x2="8" y2="13"></line>
<line x1="16" y1="17" x2="8" y2="17"></line>
<polyline points="10 9 9 9 8 9"></polyline>
</svg>
</span>
运行日志
</router-link>
</nav>
</div>
</template>
@@ -59,6 +73,7 @@
display: flex;
flex-direction: column;
gap: 8px;
flex: 1; /* 撑满空间 */
}
.nav-item {
@@ -90,4 +105,9 @@
align-items: center;
justify-content: center;
}
.nav-logs {
margin-top: auto; /* 将日志推到底部 */
margin-bottom: 20px;
}
</style>

View File

@@ -18,6 +18,10 @@ const router = createRouter({
{
path: '/all',
component: () => import('../views/AllSoftware.vue')
},
{
path: '/logs',
component: () => import('../views/Logs.vue')
}
]
})

View File

@@ -4,6 +4,13 @@ import { listen } from '@tauri-apps/api/event'
const sortByName = (a: any, b: any) => a.name.localeCompare(b.name, 'zh-CN', { sensitivity: 'accent' });
export interface LogEntry {
timestamp: string;
command: string;
output: string;
status: 'info' | 'success' | 'error';
}
export const useSoftwareStore = defineStore('software', {
state: () => ({
essentials: [] as any[],
@@ -11,6 +18,7 @@ export const useSoftwareStore = defineStore('software', {
allSoftware: [] as any[],
selectedEssentialIds: [] as string[],
selectedUpdateIds: [] as string[],
logs: [] as LogEntry[],
loading: false,
lastFetched: 0
}),
@@ -39,15 +47,11 @@ export const useSoftwareStore = defineStore('software', {
sortedAllSoftware: (state) => [...state.allSoftware].sort(sortByName)
},
actions: {
// 通用的勾选切换逻辑
toggleSelection(id: string, type: 'essential' | 'update') {
const list = type === 'essential' ? this.selectedEssentialIds : this.selectedUpdateIds;
const index = list.indexOf(id);
if (index === -1) {
list.push(id);
} else {
list.splice(index, 1);
}
if (index === -1) list.push(id);
else list.splice(index, 1);
},
selectAll(type: 'essential' | 'update') {
if (type === 'essential') {
@@ -79,7 +83,6 @@ export const useSoftwareStore = defineStore('software', {
try {
const res = await invoke('get_updates')
this.updates = res as any[]
// 刷新数据后,如果没选过,默认全选
if (this.selectedUpdateIds.length === 0) this.selectAll('update');
} finally {
this.loading = false
@@ -115,7 +118,6 @@ export const useSoftwareStore = defineStore('software', {
this.allSoftware = all as any[];
this.updates = updates as any[];
this.lastFetched = Date.now();
// 如果没选过,默认全选
if (this.selectedEssentialIds.length === 0) this.selectAll('essential');
} finally {
this.loading = false;
@@ -144,11 +146,17 @@ export const useSoftwareStore = defineStore('software', {
}
if (status === 'success') {
this.lastFetched = 0;
// 安装成功后从选择列表中移除
this.selectedEssentialIds = this.selectedEssentialIds.filter(i => i !== id);
this.selectedUpdateIds = this.selectedUpdateIds.filter(i => i !== id);
}
})
// 监听日志事件
listen('log-event', (event: any) => {
this.logs.unshift(event.payload as LogEntry);
// 限制日志条数,防止内存溢出
if (this.logs.length > 200) this.logs.pop();
})
}
}
})

183
src/views/Logs.vue Normal file
View File

@@ -0,0 +1,183 @@
<template>
<main class="content">
<header class="content-header">
<div class="header-left">
<h1>运行日志</h1>
<p class="count">记录应用最近执行的命令和结果</p>
</div>
<div class="header-actions">
<button @click="store.logs = []" class="secondary-btn action-btn">
清空日志
</button>
</div>
</header>
<div v-if="store.logs.length === 0" class="empty-state">
<span class="empty-icon">📝</span>
<p>暂无操作记录</p>
</div>
<div v-else class="log-list">
<div v-for="(log, index) in store.logs" :key="index" class="log-item" :class="log.status">
<div class="log-header">
<span class="timestamp">[{{ log.timestamp }}]</span>
<span class="command">{{ log.command }}</span>
<span class="status-badge">{{ log.status.toUpperCase() }}</span>
</div>
<pre class="log-output">{{ log.output }}</pre>
</div>
</div>
</main>
</template>
<script setup lang="ts">
import { useSoftwareStore } from '../store/software';
import { onMounted } from 'vue';
const store = useSoftwareStore();
onMounted(() => {
store.initListener();
});
</script>
<style scoped>
.content {
flex: 1;
padding: 40px 60px;
overflow-y: auto;
background-color: var(--bg-light);
}
.content-header {
display: flex;
justify-content: space-between;
align-items: flex-end;
margin-bottom: 30px;
}
.header-left h1 {
font-size: 32px;
font-weight: 700;
color: var(--text-main);
}
.header-left .count {
font-size: 14px;
color: var(--text-sec);
margin-top: 4px;
}
.header-actions {
display: flex;
gap: 12px;
}
.action-btn {
padding: 8px 16px;
border-radius: 10px;
font-weight: 600;
font-size: 13px;
cursor: pointer;
transition: all 0.2s ease;
border: 1px solid var(--border-color);
background-color: white;
color: var(--text-main);
}
.action-btn:hover {
background-color: #FFF5F5;
border-color: #FF3B30;
color: #FF3B30;
}
.log-list {
display: flex;
flex-direction: column;
gap: 16px;
padding-bottom: 40px;
}
.log-item {
background: white;
border-radius: 16px;
padding: 16px;
box-shadow: var(--card-shadow);
border: 1px solid transparent;
transition: all 0.2s ease;
}
.log-item.error {
border-left: 4px solid #FF3B30;
background-color: #FFF9F9;
}
.log-item.success {
border-left: 4px solid #34C759;
}
.log-item.info {
border-left: 4px solid var(--primary-color);
}
.log-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
.timestamp {
font-family: monospace;
color: var(--text-sec);
font-size: 13px;
}
.command {
font-weight: 700;
color: var(--text-main);
font-size: 14px;
flex: 1;
}
.status-badge {
font-size: 10px;
font-weight: 800;
padding: 2px 8px;
border-radius: 6px;
background: var(--bg-light);
}
.error .status-badge { color: #FF3B30; background: #FFE5E5; }
.success .status-badge { color: #34C759; background: #E5F9E5; }
.info .status-badge { color: var(--primary-color); background: #E5F0FF; }
.log-output {
margin: 0;
padding: 12px;
background: #1D1D1F;
color: #F5F5F7;
border-radius: 8px;
font-family: 'Cascadia Code', 'Fira Code', monospace;
font-size: 12px;
line-height: 1.5;
overflow-x: auto;
white-space: pre-wrap;
max-height: 200px;
overflow-y: auto;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 300px;
color: var(--text-sec);
}
.empty-icon {
font-size: 48px;
margin-bottom: 16px;
}
</style>