diff --git a/web/package-lock.json b/web/package-lock.json index a20ff22..3e17d32 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -16,6 +16,7 @@ "@vueuse/core": "^14.1.0", "@xterm/addon-fit": "^0.10.0", "@xterm/xterm": "^5.5.0", + "ansi-to-html": "^0.7.2", "apexcharts": "^5.3.6", "chart.js": "^4.5.1", "class-variance-authority": "^0.7.1", @@ -799,6 +800,30 @@ "dev": true, "license": "MIT" }, + "node_modules/ansi-to-html": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/ansi-to-html/-/ansi-to-html-0.7.2.tgz", + "integrity": "sha512-v6MqmEpNlxF+POuyhKkidusCHWWkaLcGRURzivcU3I9tv7k4JVhFcnukrM5Rlk2rUywdZuzYAZ+kbZqWCnfN3g==", + "license": "MIT", + "dependencies": { + "entities": "^2.2.0" + }, + "bin": { + "ansi-to-html": "bin/ansi-to-html" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/ansi-to-html/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", diff --git a/web/package.json b/web/package.json index d154b9d..9ccc756 100644 --- a/web/package.json +++ b/web/package.json @@ -17,6 +17,7 @@ "@vueuse/core": "^14.1.0", "@xterm/addon-fit": "^0.10.0", "@xterm/xterm": "^5.5.0", + "ansi-to-html": "^0.7.2", "apexcharts": "^5.3.6", "chart.js": "^4.5.1", "class-variance-authority": "^0.7.1", @@ -54,4 +55,4 @@ "vite": "$vite" } } -} \ No newline at end of file +} diff --git a/web/src/utils/ansi.ts b/web/src/utils/ansi.ts new file mode 100644 index 0000000..e0776fc --- /dev/null +++ b/web/src/utils/ansi.ts @@ -0,0 +1,21 @@ +import AnsiUp from 'ansi-to-html' + +const ansiUp = new AnsiUp({ + newline: false, + escapeXML: true, + stream: false +}) + +export function ansiToHtml(ansi: string): string { + if (!ansi) return '' + return ansiUp.toHtml(ansi) +} + +export function highlightHtml(html: string, keyword: string): string { + if (!keyword.trim()) return html + + const escaped = keyword.trim().replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + // Match keyword but not inside HTML tags + const regex = new RegExp(`(${escaped})(?![^<]*>)`, 'gi') + return html.replace(regex, '$1') +} diff --git a/web/src/views/dependencies/Dependencies.vue b/web/src/views/dependencies/Dependencies.vue index fde5065..b1bb470 100644 --- a/web/src/views/dependencies/Dependencies.vue +++ b/web/src/views/dependencies/Dependencies.vue @@ -133,6 +133,12 @@ async function uninstallPackage() { } } +import { ansiToHtml } from '@/utils/ansi' + +const renderedLog = computed(() => { + return ansiToHtml(logContent.value) +}) + function showLog(dep: Dependency) { logPkgName.value = dep.name logContent.value = dep.log || '暂无日志' @@ -357,7 +363,7 @@ onMounted(async () => { 查看依赖包的详细安装输出日志
-
{{ logContent }}
+

           
diff --git a/web/src/views/history/History.vue b/web/src/views/history/History.vue index da993c7..ae6a33d 100644 --- a/web/src/views/history/History.vue +++ b/web/src/views/history/History.vue @@ -56,10 +56,16 @@ const isWsLoading = ref(false) let logSocket: WebSocket | null = null +import { ansiToHtml } from '@/utils/ansi' + const decompressedOutput = computed(() => { return wsContent.value || '无输出' }) +const renderedOutput = computed(() => { + return ansiToHtml(decompressedOutput.value) +}) + async function loadLogs() { try { const params: { page: number; page_size: number; task_id?: string; task_name?: string; status?: string } = { @@ -566,7 +572,7 @@ watch(() => route.query, (newQuery) => {
{{ decompressedOutput }}
+ class="p-4 text-xs font-mono whitespace-pre-wrap break-all log-pre leading-relaxed" v-html="renderedOutput">
连接中...
diff --git a/web/src/views/history/LogViewer.vue b/web/src/views/history/LogViewer.vue index 0324763..a2ffb70 100644 --- a/web/src/views/history/LogViewer.vue +++ b/web/src/views/history/LogViewer.vue @@ -17,14 +17,12 @@ const emit = defineEmits<{ const searchKeyword = ref('') -// 高亮搜索结果 -const highlightedContent = computed(() => { - if (!searchKeyword.value.trim()) return props.content +import { ansiToHtml, highlightHtml } from '@/utils/ansi' - const keyword = searchKeyword.value.trim() - const escaped = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') - const regex = new RegExp(`(${escaped})`, 'gi') - return props.content.replace(regex, '$1') +// 高亮搜索结果并处理 ANSI 颜色 +const highlightedContent = computed(() => { + const html = ansiToHtml(props.content) + return highlightHtml(html, searchKeyword.value) }) function close() { diff --git a/web/src/views/notify/components/PushLog.vue b/web/src/views/notify/components/PushLog.vue index 9342855..069d436 100644 --- a/web/src/views/notify/components/PushLog.vue +++ b/web/src/views/notify/components/PushLog.vue @@ -150,6 +150,12 @@ function formatDate(dateStr: string) { } } +import { ansiToHtml } from '@/utils/ansi' + +const renderedContent = computed(() => { + return ansiToHtml(detailDialogProps.value.content) +}) + function onDialogClose(open: boolean) { if (!open) { selectedLogId.value = null @@ -291,8 +297,7 @@ function onDialogClose(open: boolean) {
- {{ detailDialogProps.content }} + class="text-sm text-foreground bg-muted/20 p-5 rounded-xl border border-border/50 whitespace-pre-wrap break-all leading-relaxed shadow-sm" v-html="renderedContent">
无推送内容