fix: logviewer style
This commit is contained in:
Generated
+38
@@ -17,6 +17,7 @@
|
|||||||
"@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",
|
"ansi-to-html": "^0.7.2",
|
||||||
|
"ansi-to-vue3": "^0.1.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",
|
||||||
@@ -800,6 +801,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/anser": {
|
||||||
|
"version": "2.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/anser/-/anser-2.3.5.tgz",
|
||||||
|
"integrity": "sha512-vcZjxvvVoxTeR5XBNJB38oTu/7eDCZlwdz32N1eNgpyPF7j/Z7Idf+CUwQOkKKpJ7RJyjxgLHCM7vdIK0iCNMQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/ansi-to-html": {
|
"node_modules/ansi-to-html": {
|
||||||
"version": "0.7.2",
|
"version": "0.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-to-html/-/ansi-to-html-0.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-to-html/-/ansi-to-html-0.7.2.tgz",
|
||||||
@@ -824,6 +831,31 @@
|
|||||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ansi-to-vue3": {
|
||||||
|
"version": "0.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-to-vue3/-/ansi-to-vue3-0.1.2.tgz",
|
||||||
|
"integrity": "sha512-mkfWeVNBKfmpoWLeLqmAeKQalqvOyAhhMnndqu1oJZEzz8tn56mbUqJC7OfvB8cOEg70ooKjmgeIZoI0i9cdxw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"workspaces": [
|
||||||
|
"playground"
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"anser": "^2.1.1",
|
||||||
|
"escape-carriage": "^1.3.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16",
|
||||||
|
"npm": ">=7"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"vue": "^3.2.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"vue": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"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",
|
||||||
@@ -1034,6 +1066,12 @@
|
|||||||
"@esbuild/win32-x64": "0.27.2"
|
"@esbuild/win32-x64": "0.27.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/escape-carriage": {
|
||||||
|
"version": "1.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/escape-carriage/-/escape-carriage-1.3.1.tgz",
|
||||||
|
"integrity": "sha512-GwBr6yViW3ttx1kb7/Oh+gKQ1/TrhYwxKqVmg5gS+BK+Qe2KrOa/Vh7w3HPBvgGf0LfcDGoY9I6NHKoA5Hozhw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/estree-walker": {
|
"node_modules/estree-walker": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
"@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",
|
"ansi-to-html": "^0.7.2",
|
||||||
|
"ansi-to-vue3": "^0.1.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,140 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, onUnmounted, watch } from 'vue'
|
||||||
|
import { Terminal } from '@xterm/xterm'
|
||||||
|
import { FitAddon } from '@xterm/addon-fit'
|
||||||
|
import '@xterm/xterm/css/xterm.css'
|
||||||
|
|
||||||
|
const lightTheme = {
|
||||||
|
background: '#f4f4f5', // zinc-100
|
||||||
|
foreground: '#18181b', // zinc-900
|
||||||
|
cursor: '#18181b',
|
||||||
|
selectionBackground: 'rgba(0, 0, 0, 0.15)',
|
||||||
|
}
|
||||||
|
|
||||||
|
const darkTheme = {
|
||||||
|
background: '#09090b', // zinc-950
|
||||||
|
foreground: '#e4e4e7', // zinc-200
|
||||||
|
cursor: '#e4e4e7',
|
||||||
|
selectionBackground: 'rgba(255, 255, 255, 0.2)',
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
content: string
|
||||||
|
fontSize?: number
|
||||||
|
theme?: 'dark' | 'light'
|
||||||
|
autoScroll?: boolean
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
fontSize: 12,
|
||||||
|
theme: 'dark',
|
||||||
|
autoScroll: true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const terminalRef = ref<HTMLDivElement | null>(null)
|
||||||
|
let terminal: Terminal | null = null
|
||||||
|
let fitAddon: FitAddon | null = null
|
||||||
|
const lightBackgroundClass = 'terminal-theme-light'
|
||||||
|
const darkBackgroundClass = 'terminal-theme-dark'
|
||||||
|
|
||||||
|
function getTheme() {
|
||||||
|
return props.theme === 'dark' ? darkTheme : lightTheme
|
||||||
|
}
|
||||||
|
|
||||||
|
function initTerminal() {
|
||||||
|
if (!terminalRef.value) return
|
||||||
|
|
||||||
|
terminal = new Terminal({
|
||||||
|
fontSize: props.fontSize,
|
||||||
|
fontFamily: "'JetBrains Mono', 'Fira Code', monospace",
|
||||||
|
theme: getTheme(),
|
||||||
|
allowProposedApi: true,
|
||||||
|
convertEol: true,
|
||||||
|
disableStdin: true,
|
||||||
|
cursorBlink: false,
|
||||||
|
rows: 10,
|
||||||
|
})
|
||||||
|
|
||||||
|
fitAddon = new FitAddon()
|
||||||
|
terminal.loadAddon(fitAddon)
|
||||||
|
terminal.open(terminalRef.value)
|
||||||
|
|
||||||
|
if (props.content) {
|
||||||
|
terminal.write(props.content)
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
fitAddon?.fit()
|
||||||
|
}, 50)
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => props.content, (newContent, oldContent) => {
|
||||||
|
if (!terminal) return
|
||||||
|
|
||||||
|
if (newContent.length < oldContent.length) {
|
||||||
|
terminal.clear()
|
||||||
|
terminal.write(newContent)
|
||||||
|
} else {
|
||||||
|
const appended = newContent.slice(oldContent.length)
|
||||||
|
terminal.write(appended)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.autoScroll) {
|
||||||
|
terminal.scrollToBottom()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(() => props.theme, () => {
|
||||||
|
if (terminal) {
|
||||||
|
terminal.options.theme = getTheme()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function handleResize() {
|
||||||
|
fitAddon?.fit()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener('resize', handleResize)
|
||||||
|
setTimeout(initTerminal, 100)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('resize', handleResize)
|
||||||
|
if (terminal) {
|
||||||
|
terminal.dispose()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
fit: () => fitAddon?.fit(),
|
||||||
|
clear: () => terminal?.clear()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
ref="terminalRef"
|
||||||
|
class="w-full h-full min-h-0"
|
||||||
|
:class="theme === 'dark' ? darkBackgroundClass : lightBackgroundClass"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.terminal-theme-light {
|
||||||
|
background-color: #f4f4f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-theme-dark {
|
||||||
|
background-color: #09090b;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.xterm) {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
:deep(.xterm-viewport),
|
||||||
|
:deep(.xterm-screen) {
|
||||||
|
background-color: inherit !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import { ref, watch, onMounted } from 'vue'
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
export type Theme = 'light' | 'dark' | 'system'
|
export type Theme = 'light' | 'dark' | 'system'
|
||||||
|
|
||||||
const theme = ref<Theme>('system')
|
const theme = ref<Theme>('system')
|
||||||
|
const systemTheme = ref<'light' | 'dark'>('light')
|
||||||
|
|
||||||
function getSystemTheme(): 'light' | 'dark' {
|
function getSystemTheme(): 'light' | 'dark' {
|
||||||
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
||||||
@@ -18,15 +19,16 @@ function applyTheme(t: Theme) {
|
|||||||
|
|
||||||
export function useTheme() {
|
export function useTheme() {
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// 从 localStorage 读取主题
|
systemTheme.value = getSystemTheme()
|
||||||
|
|
||||||
const saved = localStorage.getItem('theme') as Theme | null
|
const saved = localStorage.getItem('theme') as Theme | null
|
||||||
if (saved && ['light', 'dark', 'system'].includes(saved)) {
|
if (saved && ['light', 'dark', 'system'].includes(saved)) {
|
||||||
theme.value = saved
|
theme.value = saved
|
||||||
}
|
}
|
||||||
applyTheme(theme.value)
|
applyTheme(theme.value)
|
||||||
|
|
||||||
// 监听系统主题变化
|
|
||||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
|
||||||
|
systemTheme.value = getSystemTheme()
|
||||||
if (theme.value === 'system') {
|
if (theme.value === 'system') {
|
||||||
applyTheme('system')
|
applyTheme('system')
|
||||||
}
|
}
|
||||||
@@ -38,12 +40,17 @@ export function useTheme() {
|
|||||||
applyTheme(newTheme)
|
applyTheme(newTheme)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const resolvedTheme = computed<'light' | 'dark'>(() => {
|
||||||
|
return theme.value === 'system' ? systemTheme.value : theme.value
|
||||||
|
})
|
||||||
|
|
||||||
function setTheme(t: Theme) {
|
function setTheme(t: Theme) {
|
||||||
theme.value = t
|
theme.value = t
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
theme,
|
theme,
|
||||||
setTheme
|
resolvedTheme,
|
||||||
|
setTheme,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+55
-18
@@ -1,40 +1,77 @@
|
|||||||
import AnsiUp from 'ansi-to-html'
|
import AnsiUp from 'ansi-to-html'
|
||||||
|
|
||||||
|
const emojiMap: Record<string, string> = {
|
||||||
|
':success:': '✅',
|
||||||
|
':check:': '✅',
|
||||||
|
':done:': '✅',
|
||||||
|
':error:': '❌',
|
||||||
|
':fail:': '❌',
|
||||||
|
':x:': '❌',
|
||||||
|
':warn:': '⚠️',
|
||||||
|
':warning:': '⚠️',
|
||||||
|
':info:': 'ℹ️',
|
||||||
|
':rocket:': '🚀',
|
||||||
|
':sparkles:': '✨',
|
||||||
|
':fire:': '🔥',
|
||||||
|
':bug:': '🐛',
|
||||||
|
':lock:': '🔒',
|
||||||
|
':link:': '🔗',
|
||||||
|
':memo:': '📝',
|
||||||
|
':bulb:': '💡',
|
||||||
|
':clock:': '🕒',
|
||||||
|
':finish:': '🏁',
|
||||||
|
':start:': '🛫',
|
||||||
|
':cloud:': '☁️',
|
||||||
|
':bell:': '🔔',
|
||||||
|
}
|
||||||
|
|
||||||
const ansiUp = new AnsiUp({
|
const ansiUp = new AnsiUp({
|
||||||
newline: false,
|
newline: false,
|
||||||
escapeXML: true,
|
escapeXML: true,
|
||||||
stream: false,
|
stream: false,
|
||||||
colors: {
|
colors: {
|
||||||
// 基础 16 色优化,使其在深色背景下更鲜艳
|
// Optimized 16-color palette for dark terminal background
|
||||||
0: '#000000', // Black
|
0: '#09090b', // Black (Zinc-950)
|
||||||
1: '#ef4444', // Red (Tailwind red-500)
|
1: '#f87171', // Red (Tailwind red-400)
|
||||||
2: '#22c55e', // Green (Tailwind green-500)
|
2: '#4ade80', // Green (Tailwind green-400)
|
||||||
3: '#eab308', // Yellow (Tailwind yellow-500)
|
3: '#fbbf24', // Yellow (Tailwind amber-400)
|
||||||
4: '#3b82f6', // Blue (Tailwind blue-500)
|
4: '#60a5fa', // Blue (Tailwind blue-400)
|
||||||
5: '#a855f7', // Magenta (Tailwind purple-500)
|
5: '#c084fc', // Magenta (Tailwind purple-400)
|
||||||
6: '#06b6d4', // Cyan (Tailwind cyan-500)
|
6: '#22d3ee', // Cyan (Tailwind cyan-400)
|
||||||
7: '#d4d4d8', // White (Tailwind zinc-300)
|
7: '#e4e4e7', // White (Zinc-200)
|
||||||
8: '#71717a', // Bright Black
|
8: '#71717a', // Bright Black (Zinc-500)
|
||||||
9: '#f87171', // Bright Red
|
9: '#ef4444', // Bright Red (Tailwind red-500)
|
||||||
10: '#4ade80', // Bright Green
|
10: '#22c55e', // Bright Green (Tailwind green-500)
|
||||||
11: '#facc15', // Bright Yellow
|
11: '#f59e0b', // Bright Yellow (Tailwind amber-500)
|
||||||
12: '#60a5fa', // Bright Blue
|
12: '#3b82f6', // Bright Blue (Tailwind blue-500)
|
||||||
13: '#c084fc', // Bright Magenta
|
13: '#a855f7', // Bright Magenta (Tailwind purple-500)
|
||||||
14: '#22d3ee', // Bright Cyan
|
14: '#06b6d4', // Bright Cyan (Tailwind cyan-500)
|
||||||
15: '#ffffff' // Bright White
|
15: '#ffffff' // Bright White
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert ANSI escape codes to HTML and parse emoji shortcodes
|
||||||
|
*/
|
||||||
export function ansiToHtml(ansi: string): string {
|
export function ansiToHtml(ansi: string): string {
|
||||||
if (!ansi) return ''
|
if (!ansi) return ''
|
||||||
return ansiUp.toHtml(ansi)
|
|
||||||
|
// Parse emoji shortcodes
|
||||||
|
let processed = ansi.replace(/:([a-z0-9_-]+):/g, (match) => {
|
||||||
|
return emojiMap[match] || match
|
||||||
|
})
|
||||||
|
|
||||||
|
return ansiUp.toHtml(processed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Highlight keywords in HTML content while avoiding HTML tags
|
||||||
|
*/
|
||||||
export function highlightHtml(html: string, keyword: string): string {
|
export function highlightHtml(html: string, keyword: string): string {
|
||||||
if (!keyword.trim()) return html
|
if (!keyword.trim()) return html
|
||||||
|
|
||||||
const escaped = keyword.trim().replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
const escaped = keyword.trim().replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||||
// Match keyword but not inside HTML tags
|
// Match keyword but not inside HTML tags
|
||||||
const regex = new RegExp(`(${escaped})(?![^<]*>)`, 'gi')
|
const regex = new RegExp(`(${escaped})(?![^<]*>)`, 'gi')
|
||||||
return html.replace(regex, '<mark class="bg-yellow-300 text-black">$1</mark>')
|
return html.replace(regex, '<mark class="bg-yellow-400/30 text-yellow-100 border-b border-yellow-400/50 px-0.5 rounded-sm transition-colors">$1</mark>')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,11 +6,12 @@ import { Button } from '@/components/ui/button'
|
|||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import Pagination from '@/components/Pagination.vue'
|
import Pagination from '@/components/Pagination.vue'
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||||
import LogViewer from './LogViewer.vue'
|
|
||||||
import {
|
import {
|
||||||
RefreshCw, X, Search, Maximize2, GitBranch, Terminal,
|
RefreshCw, X, Search, Maximize2, GitBranch, Terminal,
|
||||||
CheckCircle2, XCircle, AlertCircle, Ban, Clock, Zap as ZapIcon, Check, Trash2
|
CheckCircle2, XCircle, AlertCircle, Ban, Clock, Zap as ZapIcon, Check, Trash2
|
||||||
} from 'lucide-vue-next'
|
} from 'lucide-vue-next'
|
||||||
|
import LogViewer from './LogViewer.vue'
|
||||||
|
import LogTerminal from '@/components/LogTerminal.vue'
|
||||||
import { api, type TaskLog } from '@/api'
|
import { api, type TaskLog } from '@/api'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import {
|
import {
|
||||||
@@ -26,6 +27,7 @@ import {
|
|||||||
import { toast } from 'vue-sonner'
|
import { toast } from 'vue-sonner'
|
||||||
import { useSiteSettings } from '@/composables/useSiteSettings'
|
import { useSiteSettings } from '@/composables/useSiteSettings'
|
||||||
import TextOverflow from '@/components/TextOverflow.vue'
|
import TextOverflow from '@/components/TextOverflow.vue'
|
||||||
|
import { useTheme } from '@/composables/useTheme'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const { pageSize } = useSiteSettings()
|
const { pageSize } = useSiteSettings()
|
||||||
@@ -55,15 +57,10 @@ const wsContent = ref('')
|
|||||||
const isWsLoading = ref(false)
|
const isWsLoading = ref(false)
|
||||||
let logSocket: WebSocket | null = null
|
let logSocket: WebSocket | null = null
|
||||||
|
|
||||||
|
const { resolvedTheme } = useTheme()
|
||||||
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() {
|
||||||
@@ -570,10 +567,18 @@ watch(() => route.query, (newQuery) => {
|
|||||||
<Maximize2 class="h-3.5 w-3.5" />
|
<Maximize2 class="h-3.5 w-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 overflow-auto bg-zinc-950 min-h-[160px]">
|
<div
|
||||||
<pre
|
class="flex-1 overflow-hidden min-h-[160px]"
|
||||||
class="p-4 text-xs font-mono whitespace-pre-wrap break-all log-pre leading-relaxed text-zinc-300" v-html="renderedOutput"></pre>
|
:class="resolvedTheme === 'dark' ? 'bg-zinc-950' : 'bg-zinc-100'"
|
||||||
<div v-if="isWsLoading" class="p-4 text-sm text-zinc-500 italic">连接中...</div>
|
ref="sideLogContainer"
|
||||||
|
>
|
||||||
|
<LogTerminal
|
||||||
|
:content="decompressedOutput"
|
||||||
|
:theme="resolvedTheme"
|
||||||
|
/>
|
||||||
|
<div v-if="isWsLoading" class="px-4 py-2 text-sm text-zinc-500 italic border-t border-zinc-200 dark:border-zinc-800">
|
||||||
|
连接中...
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch, onUnmounted } from 'vue'
|
import { watch, onUnmounted } from 'vue'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Input } from '@/components/ui/input'
|
import { X } from 'lucide-vue-next'
|
||||||
import { X, Search } from 'lucide-vue-next'
|
import LogTerminal from '@/components/LogTerminal.vue'
|
||||||
|
import { useTheme } from '@/composables/useTheme'
|
||||||
|
|
||||||
|
const { resolvedTheme } = useTheme()
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
open: boolean
|
open: boolean
|
||||||
@@ -15,15 +18,8 @@ const emit = defineEmits<{
|
|||||||
'update:open': [value: boolean]
|
'update:open': [value: boolean]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const searchKeyword = ref('')
|
const lightLogBackgroundClass = 'bg-zinc-100'
|
||||||
|
const darkLogBackgroundClass = 'bg-zinc-950'
|
||||||
import { ansiToHtml, highlightHtml } from '@/utils/ansi'
|
|
||||||
|
|
||||||
// 高亮搜索结果并处理 ANSI 颜色
|
|
||||||
const highlightedContent = computed(() => {
|
|
||||||
const html = ansiToHtml(props.content)
|
|
||||||
return highlightHtml(html, searchKeyword.value)
|
|
||||||
})
|
|
||||||
|
|
||||||
function close() {
|
function close() {
|
||||||
emit('update:open', false)
|
emit('update:open', false)
|
||||||
@@ -41,7 +37,6 @@ function toggleBodyScroll(lock: boolean) {
|
|||||||
// 监听打开状态
|
// 监听打开状态
|
||||||
watch(() => props.open, (val) => {
|
watch(() => props.open, (val) => {
|
||||||
if (val) {
|
if (val) {
|
||||||
searchKeyword.value = ''
|
|
||||||
toggleBodyScroll(true)
|
toggleBodyScroll(true)
|
||||||
} else {
|
} else {
|
||||||
toggleBodyScroll(false)
|
toggleBodyScroll(false)
|
||||||
@@ -78,19 +73,19 @@ onUnmounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<div class="relative flex-1 sm:flex-none">
|
|
||||||
<Search class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
||||||
<Input v-model="searchKeyword" placeholder="搜索内容..." class="h-8 pl-9 w-full sm:w-56 text-sm" />
|
|
||||||
</div>
|
|
||||||
<Button variant="ghost" size="icon" class="h-7 w-7 shrink-0" @click="close">
|
<Button variant="ghost" size="icon" class="h-7 w-7 shrink-0" @click="close">
|
||||||
<X class="h-4 w-4" />
|
<X class="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 overflow-auto bg-zinc-950">
|
<div class="flex-1 overflow-hidden" :class="resolvedTheme === 'dark' ? darkLogBackgroundClass : lightLogBackgroundClass">
|
||||||
<pre class="p-3 sm:p-4 text-xs font-mono whitespace-pre-wrap break-all text-zinc-300" v-html="highlightedContent"></pre>
|
<LogTerminal
|
||||||
|
:content="content"
|
||||||
|
:theme="resolvedTheme"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user