feat: 新增 JsonViewer 和 AiCallLogDetailDialog 组件
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
<template>
|
||||
<div class="json-viewer">
|
||||
<div class="json-header">
|
||||
<span class="json-title">{{ title }}</span>
|
||||
<el-button v-if="showCopy" size="small" @click="handleCopy">
|
||||
<span class="copy-icon">📋</span> 复制
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="json-body" :style="{ maxHeight }">
|
||||
<pre v-if="isValidJson" class="json-code" v-html="highlightedJson" />
|
||||
<pre v-else class="json-code json-raw">{{ formattedJson }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
interface Props {
|
||||
data: string | object | null
|
||||
title?: string
|
||||
showCopy?: boolean
|
||||
maxHeight?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
title: '',
|
||||
showCopy: true,
|
||||
maxHeight: '300px'
|
||||
})
|
||||
|
||||
const formattedJson = computed(() => {
|
||||
if (!props.data) return ''
|
||||
if (typeof props.data === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(props.data)
|
||||
return JSON.stringify(parsed, null, 2)
|
||||
} catch {
|
||||
return props.data
|
||||
}
|
||||
}
|
||||
return JSON.stringify(props.data, null, 2)
|
||||
})
|
||||
|
||||
const isValidJson = computed(() => {
|
||||
if (!props.data) return false
|
||||
const str = typeof props.data === 'string' ? props.data : JSON.stringify(props.data)
|
||||
try {
|
||||
JSON.parse(str)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
const highlightedJson = computed(() => {
|
||||
if (!isValidJson.value) return formattedJson.value
|
||||
let json = formattedJson.value
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
json = json.replace(/("(?:[^"\\]|\\.)*")/g, '<span class="json-string">$1</span>')
|
||||
json = json.replace(/(<span class="json-string">"[^"]*"<\/span>)(\s*:)/g, '<span class="json-key">$1</span>$2')
|
||||
json = json.replace(/\b(true|false|null|\d+(?:\.\d+)?)\b/g, '<span class="json-number">$1</span>')
|
||||
json = json.replace(/([{}[\],:])/g, '<span class="json-punct">$1</span>')
|
||||
return json
|
||||
})
|
||||
|
||||
async function handleCopy() {
|
||||
try {
|
||||
await navigator.clipboard.writeText(formattedJson.value)
|
||||
ElMessage.success('已复制到剪贴板')
|
||||
} catch {
|
||||
ElMessage.error('复制失败')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.json-viewer {
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.json-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
background: rgba(30, 30, 30, 0.8);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
.json-title {
|
||||
color: rgba(226, 232, 240, 0.85);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.copy-icon {
|
||||
margin-right: 4px;
|
||||
}
|
||||
.json-body {
|
||||
overflow: auto;
|
||||
background: #1e1e1e;
|
||||
}
|
||||
.json-code {
|
||||
margin: 0;
|
||||
padding: 12px;
|
||||
color: rgba(226, 232, 240, 0.92);
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.7;
|
||||
white-space: pre;
|
||||
}
|
||||
.json-raw {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
:deep(.json-string) { color: #a5d6a7; }
|
||||
:deep(.json-key) { color: #ce93d8; }
|
||||
:deep(.json-number) { color: #90caf9; }
|
||||
:deep(.json-punct) { color: #b0bec5; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user