fix: 优化历史详情日志查看
This commit is contained in:
@@ -1,5 +1,12 @@
|
|||||||
# 项目上下文记录
|
# 项目上下文记录
|
||||||
|
|
||||||
|
## 2026-05-18:优化历史详情日志查看
|
||||||
|
|
||||||
|
- `frontend/src/components/HistoryPanel.vue` 的历史详情“处理日志”标题右侧新增复制按钮,点击后复制当前详情日志文本,优先使用 Clipboard API,失败时回退到 textarea 复制。
|
||||||
|
- 历史详情日志不再使用 `n-log`,改为自定义 `colored-log-panel`,日志框固定高度并同时支持横向和纵向滚动,不自动换行。
|
||||||
|
- 日志行按内容识别级别并着色:`INFO` 为蓝色,`SUCCESS/COMPLETED` 为绿色,`WARN/WARNING` 为黄色,`ERROR/FAILED` 为红色。
|
||||||
|
- 已执行 `npm run build`,构建通过;未启动浏览器或 headless Chrome。
|
||||||
|
|
||||||
## 2026-05-18:切换为圆形侧边栏收缩触发器
|
## 2026-05-18:切换为圆形侧边栏收缩触发器
|
||||||
|
|
||||||
- `frontend/src/AppShell.vue` 的 `n-layout-sider` 收缩触发器从 `show-trigger="bar"` 改为 `show-trigger="arrow-circle"`,恢复为 Naive UI 文档中侧栏右侧居中的圆形箭头按钮样式。
|
- `frontend/src/AppShell.vue` 的 `n-layout-sider` 收缩触发器从 `show-trigger="bar"` 改为 `show-trigger="arrow-circle"`,恢复为 Naive UI 文档中侧栏右侧居中的圆形箭头按钮样式。
|
||||||
|
|||||||
@@ -63,8 +63,20 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="section-label">处理日志</div>
|
<div class="section-label log-section-header">
|
||||||
<n-log :log="detail.logs.join('\n') || '暂无日志'" language="text" trim class="log-panel" />
|
<span>处理日志</span>
|
||||||
|
<n-button size="small" tertiary circle title="复制日志" :disabled="!detailLogText" @click="copyDetailLogs">
|
||||||
|
<template #icon><n-icon><CopyOutline /></n-icon></template>
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
<div class="colored-log-panel" role="log" aria-label="处理日志">
|
||||||
|
<pre class="colored-log-content"><span
|
||||||
|
v-for="(line, index) in detailLogLines"
|
||||||
|
:key="index"
|
||||||
|
class="colored-log-line"
|
||||||
|
:class="`log-level-${line.level}`"
|
||||||
|
>{{ line.text }}</span></pre>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</n-space>
|
</n-space>
|
||||||
</n-modal>
|
</n-modal>
|
||||||
@@ -74,12 +86,14 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
||||||
import { useDialog, useMessage } from 'naive-ui';
|
import { useDialog, useMessage } from 'naive-ui';
|
||||||
import { FileTrayOutline, RefreshOutline, TrashOutline } from '@vicons/ionicons5';
|
import { CopyOutline, FileTrayOutline, RefreshOutline, TrashOutline } from '@vicons/ionicons5';
|
||||||
|
|
||||||
import { apiGet, apiPost } from '../api/client';
|
import { apiGet, apiPost } from '../api/client';
|
||||||
import type { CacheSize, HistoryDetail, HistoryRecord } from '../types';
|
import type { CacheSize, HistoryDetail, HistoryRecord } from '../types';
|
||||||
import { resetPageHeader, setPageHeader } from '../composables/pageHeader';
|
import { resetPageHeader, setPageHeader } from '../composables/pageHeader';
|
||||||
|
|
||||||
|
type LogLevel = 'default' | 'info' | 'success' | 'warning' | 'error';
|
||||||
|
|
||||||
const message = useMessage();
|
const message = useMessage();
|
||||||
const dialog = useDialog();
|
const dialog = useDialog();
|
||||||
const records = ref<HistoryRecord[]>([]);
|
const records = ref<HistoryRecord[]>([]);
|
||||||
@@ -93,6 +107,14 @@ let recordSizeToken = 0;
|
|||||||
|
|
||||||
const totalSizeText = computed(() => `总占用: ${cacheSize.value?.size_formatted || '计算中...'}`);
|
const totalSizeText = computed(() => `总占用: ${cacheSize.value?.size_formatted || '计算中...'}`);
|
||||||
const hasRecords = computed(() => records.value.length > 0);
|
const hasRecords = computed(() => records.value.length > 0);
|
||||||
|
const detailLogText = computed(() => detail.value?.logs?.join('\n') || '');
|
||||||
|
const detailLogLines = computed(() => {
|
||||||
|
const lines = detailLogText.value ? detailLogText.value.split(/\r?\n/) : ['暂无日志'];
|
||||||
|
return lines.map(line => ({
|
||||||
|
text: line,
|
||||||
|
level: resolveLogLevel(line)
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
setPageHeader({
|
setPageHeader({
|
||||||
@@ -164,6 +186,52 @@ async function loadRecordSize(recordId: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function copyDetailLogs() {
|
||||||
|
if (!detailLogText.value) {
|
||||||
|
message.warning('暂无可复制日志');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await writeClipboardText(detailLogText.value);
|
||||||
|
message.success('日志已复制');
|
||||||
|
} catch {
|
||||||
|
message.error('复制日志失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeClipboardText(text: string) {
|
||||||
|
if (navigator.clipboard?.writeText) {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const textarea = document.createElement('textarea');
|
||||||
|
textarea.value = text;
|
||||||
|
textarea.setAttribute('readonly', 'true');
|
||||||
|
textarea.style.position = 'fixed';
|
||||||
|
textarea.style.top = '-1000px';
|
||||||
|
textarea.style.opacity = '0';
|
||||||
|
document.body.appendChild(textarea);
|
||||||
|
textarea.select();
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!document.execCommand('copy')) {
|
||||||
|
throw new Error('copy failed');
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
document.body.removeChild(textarea);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveLogLevel(line: string): LogLevel {
|
||||||
|
if (/\b(ERROR|FAILED|FAILURE)\b|错误|失败/i.test(line)) return 'error';
|
||||||
|
if (/\b(WARN|WARNING)\b|警告/i.test(line)) return 'warning';
|
||||||
|
if (/\b(SUCCESS|SUCCEEDED|COMPLETED)\b|成功|完成/i.test(line)) return 'success';
|
||||||
|
if (/\bINFO\b|信息/i.test(line)) return 'info';
|
||||||
|
return 'default';
|
||||||
|
}
|
||||||
|
|
||||||
function confirmDelete(recordId: string) {
|
function confirmDelete(recordId: string) {
|
||||||
dialog.warning({
|
dialog.warning({
|
||||||
title: '删除历史记录',
|
title: '删除历史记录',
|
||||||
|
|||||||
+48
-1
@@ -1002,7 +1002,54 @@ select {
|
|||||||
color: var(--td-error-color);
|
color: var(--td-error-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.log-panel,
|
.log-section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.colored-log-panel {
|
||||||
|
height: 360px;
|
||||||
|
overflow: scroll;
|
||||||
|
padding: 12px;
|
||||||
|
color: #d7dde7;
|
||||||
|
background: #111820;
|
||||||
|
border-radius: var(--td-radius-default);
|
||||||
|
}
|
||||||
|
|
||||||
|
.colored-log-content {
|
||||||
|
display: block;
|
||||||
|
min-width: max-content;
|
||||||
|
margin: 0;
|
||||||
|
font-family: var(--td-font-family-mono);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.7;
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
|
||||||
|
.colored-log-line {
|
||||||
|
display: block;
|
||||||
|
color: #d7dde7;
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
|
||||||
|
.colored-log-line.log-level-info {
|
||||||
|
color: #62a8ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.colored-log-line.log-level-success {
|
||||||
|
color: #38d996;
|
||||||
|
}
|
||||||
|
|
||||||
|
.colored-log-line.log-level-warning {
|
||||||
|
color: #f6c453;
|
||||||
|
}
|
||||||
|
|
||||||
|
.colored-log-line.log-level-error {
|
||||||
|
color: #ff6b7a;
|
||||||
|
}
|
||||||
|
|
||||||
.script-log {
|
.script-log {
|
||||||
min-height: 320px;
|
min-height: 320px;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
|
|||||||
Reference in New Issue
Block a user