feat: add logs ansi render
This commit is contained in:
Generated
+25
@@ -16,6 +16,7 @@
|
|||||||
"@vueuse/core": "^14.1.0",
|
"@vueuse/core": "^14.1.0",
|
||||||
"@xterm/addon-fit": "^0.10.0",
|
"@xterm/addon-fit": "^0.10.0",
|
||||||
"@xterm/xterm": "^5.5.0",
|
"@xterm/xterm": "^5.5.0",
|
||||||
|
"ansi-to-html": "^0.7.2",
|
||||||
"apexcharts": "^5.3.6",
|
"apexcharts": "^5.3.6",
|
||||||
"chart.js": "^4.5.1",
|
"chart.js": "^4.5.1",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
@@ -799,6 +800,30 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/anymatch": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
"@vueuse/core": "^14.1.0",
|
"@vueuse/core": "^14.1.0",
|
||||||
"@xterm/addon-fit": "^0.10.0",
|
"@xterm/addon-fit": "^0.10.0",
|
||||||
"@xterm/xterm": "^5.5.0",
|
"@xterm/xterm": "^5.5.0",
|
||||||
|
"ansi-to-html": "^0.7.2",
|
||||||
"apexcharts": "^5.3.6",
|
"apexcharts": "^5.3.6",
|
||||||
"chart.js": "^4.5.1",
|
"chart.js": "^4.5.1",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
|
|||||||
@@ -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, '<mark class="bg-yellow-300 text-black">$1</mark>')
|
||||||
|
}
|
||||||
@@ -133,6 +133,12 @@ async function uninstallPackage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
import { ansiToHtml } from '@/utils/ansi'
|
||||||
|
|
||||||
|
const renderedLog = computed(() => {
|
||||||
|
return ansiToHtml(logContent.value)
|
||||||
|
})
|
||||||
|
|
||||||
function showLog(dep: Dependency) {
|
function showLog(dep: Dependency) {
|
||||||
logPkgName.value = dep.name
|
logPkgName.value = dep.name
|
||||||
logContent.value = dep.log || '暂无日志'
|
logContent.value = dep.log || '暂无日志'
|
||||||
@@ -357,7 +363,7 @@ onMounted(async () => {
|
|||||||
<DialogDescription class="sr-only">查看依赖包的详细安装输出日志</DialogDescription>
|
<DialogDescription class="sr-only">查看依赖包的详细安装输出日志</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div class="max-h-[400px] overflow-y-auto">
|
<div class="max-h-[400px] overflow-y-auto">
|
||||||
<pre class="text-xs bg-muted p-3 rounded-lg whitespace-pre-wrap break-all font-mono">{{ logContent }}</pre>
|
<pre class="text-xs bg-muted p-3 rounded-lg whitespace-pre-wrap break-all font-mono" v-html="renderedLog"></pre>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" @click="showLogDialog = false">关闭</Button>
|
<Button variant="outline" @click="showLogDialog = false">关闭</Button>
|
||||||
|
|||||||
@@ -56,10 +56,16 @@ const isWsLoading = ref(false)
|
|||||||
let logSocket: WebSocket | null = null
|
let logSocket: WebSocket | null = null
|
||||||
|
|
||||||
|
|
||||||
|
import { ansiToHtml } from '@/utils/ansi'
|
||||||
|
|
||||||
const decompressedOutput = computed(() => {
|
const decompressedOutput = computed(() => {
|
||||||
return wsContent.value || '无输出'
|
return wsContent.value || '无输出'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const renderedOutput = computed(() => {
|
||||||
|
return ansiToHtml(decompressedOutput.value)
|
||||||
|
})
|
||||||
|
|
||||||
async function loadLogs() {
|
async function loadLogs() {
|
||||||
try {
|
try {
|
||||||
const params: { page: number; page_size: number; task_id?: string; task_name?: string; status?: string } = {
|
const params: { page: number; page_size: number; task_id?: string; task_name?: string; status?: string } = {
|
||||||
@@ -566,7 +572,7 @@ watch(() => route.query, (newQuery) => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex-1 overflow-auto bg-muted/5 min-h-[160px]">
|
<div class="flex-1 overflow-auto bg-muted/5 min-h-[160px]">
|
||||||
<pre
|
<pre
|
||||||
class="p-4 text-xs font-mono whitespace-pre-wrap break-all log-pre leading-relaxed">{{ decompressedOutput }}</pre>
|
class="p-4 text-xs font-mono whitespace-pre-wrap break-all log-pre leading-relaxed" v-html="renderedOutput"></pre>
|
||||||
<div v-if="isWsLoading" class="p-4 text-sm text-muted-foreground italic">连接中...</div>
|
<div v-if="isWsLoading" class="p-4 text-sm text-muted-foreground italic">连接中...</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,14 +17,12 @@ const emit = defineEmits<{
|
|||||||
|
|
||||||
const searchKeyword = ref('')
|
const searchKeyword = ref('')
|
||||||
|
|
||||||
// 高亮搜索结果
|
import { ansiToHtml, highlightHtml } from '@/utils/ansi'
|
||||||
const highlightedContent = computed(() => {
|
|
||||||
if (!searchKeyword.value.trim()) return props.content
|
|
||||||
|
|
||||||
const keyword = searchKeyword.value.trim()
|
// 高亮搜索结果并处理 ANSI 颜色
|
||||||
const escaped = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
const highlightedContent = computed(() => {
|
||||||
const regex = new RegExp(`(${escaped})`, 'gi')
|
const html = ansiToHtml(props.content)
|
||||||
return props.content.replace(regex, '<mark class="bg-yellow-300 text-black">$1</mark>')
|
return highlightHtml(html, searchKeyword.value)
|
||||||
})
|
})
|
||||||
|
|
||||||
function close() {
|
function close() {
|
||||||
|
|||||||
@@ -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) {
|
function onDialogClose(open: boolean) {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
selectedLogId.value = null
|
selectedLogId.value = null
|
||||||
@@ -291,8 +297,7 @@ function onDialogClose(open: boolean) {
|
|||||||
</div>
|
</div>
|
||||||
<div class="p-6">
|
<div class="p-6">
|
||||||
<div v-if="detailDialogProps.content"
|
<div v-if="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">
|
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">
|
||||||
{{ detailDialogProps.content }}
|
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="text-sm text-muted-foreground italic py-2">无推送内容</div>
|
<div v-else class="text-sm text-muted-foreground italic py-2">无推送内容</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user