feat(web/logs): 日志管理页与 JSON 折叠组件
新增日志管理页(实时诊断/文件浏览/日志分析三区,echarts 按需引入绘制标签分布、日志量、字段时序图, 图表实例与定时器在卸载时释放);新增 api/logs 封装后端日志接口;新增通用 JsonFold 折叠查看组件 (折叠子树惰性渲染 v-if,避免大 JSON 一次性铺满 DOM)。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
<template>
|
||||
<div class="jfn" :class="{ 'jfn--root': depth === 0 }">
|
||||
<template v-if="isCollection">
|
||||
<div class="jfn-line" @click="toggle">
|
||||
<span class="jfn-toggle">{{ expanded ? '▼' : '▶' }}</span>
|
||||
<span v-if="label" class="jfn-key">{{ label }}: </span>
|
||||
<span class="jfn-brace">{{ openBrace }}</span>
|
||||
<span v-if="!expanded" class="jfn-ellipsis">{{ collapsedHint }}</span>
|
||||
<span v-if="!expanded" class="jfn-brace">{{ closeBrace }}</span>
|
||||
</div>
|
||||
<div v-if="expanded" class="jfn-children">
|
||||
<JsonFoldNode
|
||||
v-for="(entry, idx) in entries"
|
||||
:key="entry.key"
|
||||
:label="entry.label"
|
||||
:value="entry.value"
|
||||
:depth="depth + 1"
|
||||
:comma="idx < entries.length - 1"
|
||||
/>
|
||||
<div class="jfn-line jfn-close">
|
||||
<span class="jfn-brace">{{ closeBrace }}</span>
|
||||
<span v-if="comma" class="jfn-comma">,</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="jfn-line jfn-primitive">
|
||||
<span v-if="label" class="jfn-key">{{ label }}: </span>
|
||||
<span :class="primitiveClass">{{ primitiveText }}</span>
|
||||
<span v-if="comma" class="jfn-comma">,</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import JsonFoldNode from './JsonFoldNode.vue'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
value: unknown
|
||||
label?: string
|
||||
depth?: number
|
||||
comma?: boolean
|
||||
}>(),
|
||||
{
|
||||
depth: 0,
|
||||
comma: false
|
||||
}
|
||||
)
|
||||
|
||||
const expanded = ref(props.depth < 1)
|
||||
|
||||
const isArray = computed(() => Array.isArray(props.value))
|
||||
const isObject = computed(
|
||||
() => props.value !== null && typeof props.value === 'object' && !isArray.value
|
||||
)
|
||||
const isCollection = computed(() => isArray.value || isObject.value)
|
||||
|
||||
const openBrace = computed(() => (isArray.value ? '[' : '{'))
|
||||
const closeBrace = computed(() => (isArray.value ? ']' : '}'))
|
||||
|
||||
const entries = computed(() => {
|
||||
if (isArray.value) {
|
||||
return (props.value as unknown[]).map((v, i) => ({
|
||||
key: String(i),
|
||||
label: String(i),
|
||||
value: v
|
||||
}))
|
||||
}
|
||||
if (isObject.value) {
|
||||
return Object.entries(props.value as Record<string, unknown>).map(([k, v]) => ({
|
||||
key: k,
|
||||
label: JSON.stringify(k),
|
||||
value: v
|
||||
}))
|
||||
}
|
||||
return []
|
||||
})
|
||||
|
||||
const collapsedHint = computed(() => {
|
||||
const n = entries.value.length
|
||||
if (isArray.value) return ` … ${n} items `
|
||||
return ` … ${n} keys `
|
||||
})
|
||||
|
||||
const primitiveClass = computed(() => {
|
||||
const t = typeof props.value
|
||||
if (t === 'string') return 'jfn-val jfn-val--str'
|
||||
if (t === 'number') return 'jfn-val jfn-val--num'
|
||||
if (t === 'boolean') return 'jfn-val jfn-val--bool'
|
||||
return 'jfn-val jfn-val--null'
|
||||
})
|
||||
|
||||
const primitiveText = computed(() => {
|
||||
if (props.value === null) return 'null'
|
||||
if (props.value === undefined) return 'undefined'
|
||||
if (typeof props.value === 'string') return JSON.stringify(props.value)
|
||||
return String(props.value)
|
||||
})
|
||||
|
||||
function toggle() {
|
||||
expanded.value = !expanded.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.jfn {
|
||||
font-family: var(--mg-font-mono, Consolas, monospace);
|
||||
font-size: 12px;
|
||||
line-height: 1.55;
|
||||
color: var(--mg-text-light, #e8e0f0);
|
||||
}
|
||||
|
||||
.jfn-line {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
flex-wrap: wrap;
|
||||
gap: 2px;
|
||||
cursor: default;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.jfn-line:not(.jfn-primitive):not(.jfn-close) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.jfn-toggle {
|
||||
display: inline-block;
|
||||
width: 14px;
|
||||
flex-shrink: 0;
|
||||
color: var(--mg-text-muted, #9a8fb0);
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.jfn-children {
|
||||
padding-left: 16px;
|
||||
border-left: 1px solid rgba(255, 255, 255, 0.06);
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.jfn-key {
|
||||
color: #c792ea;
|
||||
}
|
||||
|
||||
.jfn-brace {
|
||||
color: #89ddff;
|
||||
}
|
||||
|
||||
.jfn-ellipsis {
|
||||
color: var(--mg-text-muted, #9a8fb0);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.jfn-comma {
|
||||
color: var(--mg-text-muted, #9a8fb0);
|
||||
}
|
||||
|
||||
.jfn-val--str {
|
||||
color: #c3e88d;
|
||||
}
|
||||
|
||||
.jfn-val--num {
|
||||
color: #f78c6c;
|
||||
}
|
||||
|
||||
.jfn-val--bool {
|
||||
color: #ffcb6b;
|
||||
}
|
||||
|
||||
.jfn-val--null {
|
||||
color: #89ddff;
|
||||
font-style: italic;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<div class="json-fold-viewer">
|
||||
<JsonFoldNode v-if="parsed !== undefined" :value="parsed" :depth="0" />
|
||||
<pre v-else-if="raw" class="json-fold-fallback">{{ raw }}</pre>
|
||||
<el-empty v-else description="无 JSON 内容" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import JsonFoldNode from './JsonFoldNode.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
raw?: string
|
||||
data?: unknown
|
||||
}>()
|
||||
|
||||
const parsed = computed(() => {
|
||||
if (props.data !== undefined) return props.data
|
||||
if (!props.raw?.trim()) return undefined
|
||||
try {
|
||||
return JSON.parse(props.raw) as unknown
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.json-fold-viewer {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding: 4px 2px;
|
||||
}
|
||||
|
||||
.json-fold-fallback {
|
||||
margin: 0;
|
||||
font-family: var(--mg-font-mono, Consolas, monospace);
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
color: var(--mg-text-light);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user