mirror of
https://github.com/fawney19/Aether.git
synced 2026-01-12 12:38:31 +08:00
Initial commit
This commit is contained in:
@@ -0,0 +1,505 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="!data || (typeof data === 'object' && Object.keys(data).length === 0)" class="text-sm text-muted-foreground">
|
||||
{{ emptyMessage }}
|
||||
</div>
|
||||
<!-- 纯字符串数据(非 JSON 对象) -->
|
||||
<Card v-else-if="typeof data === 'string'" class="bg-muted/30 overflow-hidden">
|
||||
<div class="p-4 overflow-x-auto max-h-[500px] overflow-y-auto">
|
||||
<pre class="text-xs font-mono whitespace-pre-wrap">{{ data }}</pre>
|
||||
</div>
|
||||
</Card>
|
||||
<!-- 非 JSON 响应(如 HTML 错误页面) -->
|
||||
<Card v-else-if="data.raw_response && data.metadata?.parse_error" class="bg-muted/30 overflow-hidden">
|
||||
<div class="p-3 bg-amber-50 dark:bg-amber-900/20 border-b border-amber-200 dark:border-amber-800">
|
||||
<div class="flex items-start gap-2">
|
||||
<span class="text-amber-600 dark:text-amber-400 text-sm font-medium">Warning: 响应解析失败</span>
|
||||
<span class="text-xs text-amber-700 dark:text-amber-300">{{ data.metadata.parse_error }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-4 overflow-x-auto max-h-[500px] overflow-y-auto">
|
||||
<pre class="text-xs font-mono whitespace-pre-wrap text-muted-foreground">{{ data.raw_response }}</pre>
|
||||
</div>
|
||||
</Card>
|
||||
<Card v-else class="bg-muted/30 overflow-hidden">
|
||||
<!-- JSON 查看器 -->
|
||||
<div class="json-viewer" :class="{ 'theme-dark': isDark }">
|
||||
<div class="json-lines">
|
||||
<template v-for="line in visibleLines" :key="line.displayId">
|
||||
<div class="json-line" :class="{ 'has-fold': line.canFold }">
|
||||
<!-- 行号区域(包含折叠按钮) -->
|
||||
<div class="line-number-area">
|
||||
<span
|
||||
v-if="line.canFold"
|
||||
class="fold-button"
|
||||
@click="toggleFold(line.blockId)"
|
||||
>
|
||||
<ChevronRight v-if="collapsedBlocks.has(line.blockId)" class="fold-icon" />
|
||||
<ChevronDown v-else class="fold-icon" />
|
||||
</span>
|
||||
<span class="line-number">{{ line.displayLineNumber }}</span>
|
||||
</div>
|
||||
<!-- 内容区域 -->
|
||||
<div class="line-content-area">
|
||||
<!-- 缩进 -->
|
||||
<span class="indent" :style="{ width: `${line.indent * 16}px` }"></span>
|
||||
<!-- 内容 -->
|
||||
<span
|
||||
class="line-content"
|
||||
:class="{ 'clickable-collapsed': line.canFold && collapsedBlocks.has(line.blockId) }"
|
||||
@click="line.canFold && collapsedBlocks.has(line.blockId) && toggleFold(line.blockId)"
|
||||
v-html="getDisplayHtml(line)"
|
||||
></span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { ChevronRight, ChevronDown } from 'lucide-vue-next'
|
||||
import Card from '@/components/ui/card.vue'
|
||||
|
||||
interface JsonLine {
|
||||
id: number
|
||||
lineNumber: number
|
||||
indent: number
|
||||
html: string
|
||||
canFold: boolean
|
||||
blockId: string
|
||||
blockEnd?: number
|
||||
collapsedInfo?: string
|
||||
closingBracket?: string
|
||||
trailingComma?: string
|
||||
}
|
||||
|
||||
interface DisplayLine extends JsonLine {
|
||||
displayId: string
|
||||
displayLineNumber: number
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
data: any
|
||||
viewMode: 'formatted' | 'raw' | 'compare'
|
||||
expandDepth: number
|
||||
isDark: boolean
|
||||
emptyMessage: string
|
||||
}>()
|
||||
|
||||
const collapsedBlocks = ref<Set<string>>(new Set())
|
||||
const lines = ref<JsonLine[]>([])
|
||||
|
||||
const getTokenHtml = (value: string, type: 'key' | 'string' | 'number' | 'boolean' | 'null' | 'bracket' | 'punctuation' | 'ellipsis'): string => {
|
||||
const classMap = {
|
||||
key: 'token-key',
|
||||
string: 'token-string',
|
||||
number: 'token-number',
|
||||
boolean: 'token-boolean',
|
||||
null: 'token-null',
|
||||
bracket: 'token-bracket',
|
||||
punctuation: 'token-punctuation',
|
||||
ellipsis: 'token-ellipsis',
|
||||
}
|
||||
return `<span class="${classMap[type]}">${escapeHtml(value)}</span>`
|
||||
}
|
||||
|
||||
const escapeHtml = (str: string): string => {
|
||||
return str
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
}
|
||||
|
||||
const parseJsonToLines = (data: any): JsonLine[] => {
|
||||
const result: JsonLine[] = []
|
||||
let lineNumber = 1
|
||||
let blockIdCounter = 0
|
||||
|
||||
const getBlockId = () => `block-${blockIdCounter++}`
|
||||
|
||||
const processValue = (value: any, indent: number, isLast: boolean, keyPrefix: string = ''): void => {
|
||||
const comma = isLast ? '' : ','
|
||||
|
||||
if (value === null) {
|
||||
result.push({
|
||||
id: result.length,
|
||||
lineNumber: lineNumber++,
|
||||
indent,
|
||||
html: keyPrefix + getTokenHtml('null', 'null') + comma,
|
||||
canFold: false,
|
||||
blockId: '',
|
||||
})
|
||||
} else if (typeof value === 'boolean') {
|
||||
result.push({
|
||||
id: result.length,
|
||||
lineNumber: lineNumber++,
|
||||
indent,
|
||||
html: keyPrefix + getTokenHtml(String(value), 'boolean') + comma,
|
||||
canFold: false,
|
||||
blockId: '',
|
||||
})
|
||||
} else if (typeof value === 'number') {
|
||||
result.push({
|
||||
id: result.length,
|
||||
lineNumber: lineNumber++,
|
||||
indent,
|
||||
html: keyPrefix + getTokenHtml(String(value), 'number') + comma,
|
||||
canFold: false,
|
||||
blockId: '',
|
||||
})
|
||||
} else if (typeof value === 'string') {
|
||||
result.push({
|
||||
id: result.length,
|
||||
lineNumber: lineNumber++,
|
||||
indent,
|
||||
html: keyPrefix + getTokenHtml(`"${escapeHtml(value)}"`, 'string') + comma,
|
||||
canFold: false,
|
||||
blockId: '',
|
||||
})
|
||||
} else if (Array.isArray(value)) {
|
||||
if (value.length === 0) {
|
||||
result.push({
|
||||
id: result.length,
|
||||
lineNumber: lineNumber++,
|
||||
indent,
|
||||
html: keyPrefix + getTokenHtml('[]', 'bracket') + comma,
|
||||
canFold: false,
|
||||
blockId: '',
|
||||
})
|
||||
} else {
|
||||
const blockId = getBlockId()
|
||||
const startLine = result.length
|
||||
result.push({
|
||||
id: result.length,
|
||||
lineNumber: lineNumber++,
|
||||
indent,
|
||||
html: keyPrefix + getTokenHtml('[', 'bracket'),
|
||||
canFold: true,
|
||||
blockId,
|
||||
collapsedInfo: `${value.length} items`,
|
||||
closingBracket: ']',
|
||||
trailingComma: comma,
|
||||
})
|
||||
|
||||
value.forEach((item, i) => {
|
||||
processValue(item, indent + 1, i === value.length - 1)
|
||||
})
|
||||
|
||||
result.push({
|
||||
id: result.length,
|
||||
lineNumber: lineNumber++,
|
||||
indent,
|
||||
html: getTokenHtml(']', 'bracket') + comma,
|
||||
canFold: false,
|
||||
blockId: '',
|
||||
})
|
||||
|
||||
result[startLine].blockEnd = result.length - 1
|
||||
}
|
||||
} else if (typeof value === 'object') {
|
||||
const keys = Object.keys(value)
|
||||
if (keys.length === 0) {
|
||||
result.push({
|
||||
id: result.length,
|
||||
lineNumber: lineNumber++,
|
||||
indent,
|
||||
html: keyPrefix + getTokenHtml('{}', 'bracket') + comma,
|
||||
canFold: false,
|
||||
blockId: '',
|
||||
})
|
||||
} else {
|
||||
const blockId = getBlockId()
|
||||
const startLine = result.length
|
||||
result.push({
|
||||
id: result.length,
|
||||
lineNumber: lineNumber++,
|
||||
indent,
|
||||
html: keyPrefix + getTokenHtml('{', 'bracket'),
|
||||
canFold: true,
|
||||
blockId,
|
||||
collapsedInfo: `${keys.length} keys`,
|
||||
closingBracket: '}',
|
||||
trailingComma: comma,
|
||||
})
|
||||
|
||||
keys.forEach((key, i) => {
|
||||
const keyHtml = getTokenHtml(`"${escapeHtml(key)}"`, 'key') + getTokenHtml(': ', 'punctuation')
|
||||
processValue(value[key], indent + 1, i === keys.length - 1, keyHtml)
|
||||
})
|
||||
|
||||
result.push({
|
||||
id: result.length,
|
||||
lineNumber: lineNumber++,
|
||||
indent,
|
||||
html: getTokenHtml('}', 'bracket') + comma,
|
||||
canFold: false,
|
||||
blockId: '',
|
||||
})
|
||||
|
||||
result[startLine].blockEnd = result.length - 1
|
||||
}
|
||||
} else {
|
||||
result.push({
|
||||
id: result.length,
|
||||
lineNumber: lineNumber++,
|
||||
indent,
|
||||
html: keyPrefix + getTokenHtml(String(value), 'string') + comma,
|
||||
canFold: false,
|
||||
blockId: '',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
processValue(data, 0, true)
|
||||
return result
|
||||
}
|
||||
|
||||
const visibleLines = computed((): DisplayLine[] => {
|
||||
const result: DisplayLine[] = []
|
||||
const hiddenRanges: Array<{ start: number; end: number }> = []
|
||||
|
||||
for (const line of lines.value) {
|
||||
if (line.canFold && collapsedBlocks.value.has(line.blockId) && line.blockEnd !== undefined) {
|
||||
hiddenRanges.push({ start: line.id + 1, end: line.blockEnd })
|
||||
}
|
||||
}
|
||||
|
||||
const isHidden = (id: number): boolean => {
|
||||
return hiddenRanges.some(range => id >= range.start && id <= range.end)
|
||||
}
|
||||
|
||||
let displayLineNumber = 1
|
||||
for (const line of lines.value) {
|
||||
if (!isHidden(line.id)) {
|
||||
result.push({
|
||||
...line,
|
||||
displayId: `display-${line.id}`,
|
||||
displayLineNumber: displayLineNumber++,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
const getDisplayHtml = (line: DisplayLine): string => {
|
||||
if (line.canFold && collapsedBlocks.value.has(line.blockId)) {
|
||||
const closingBracket = getTokenHtml(line.closingBracket || '}', 'bracket')
|
||||
const ellipsis = getTokenHtml('...', 'ellipsis')
|
||||
const comma = line.trailingComma || ''
|
||||
return `${line.html}${ellipsis}${closingBracket}${comma}<span class="collapsed-info">${line.collapsedInfo}</span>`
|
||||
}
|
||||
return line.html
|
||||
}
|
||||
|
||||
const toggleFold = (blockId: string) => {
|
||||
const newSet = new Set(collapsedBlocks.value)
|
||||
if (newSet.has(blockId)) {
|
||||
newSet.delete(blockId)
|
||||
} else {
|
||||
newSet.add(blockId)
|
||||
}
|
||||
collapsedBlocks.value = newSet
|
||||
}
|
||||
|
||||
const initCollapsedState = () => {
|
||||
const newSet = new Set<string>()
|
||||
|
||||
// 默认展开第一层(indent = 0),折叠更深层(indent >= 1)
|
||||
// expandDepth = 999 表示全部展开
|
||||
const depth = props.expandDepth === 0 ? 1 : props.expandDepth
|
||||
if (depth < 999) {
|
||||
for (const line of lines.value) {
|
||||
if (line.canFold && line.indent >= depth) {
|
||||
newSet.add(line.blockId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
collapsedBlocks.value = newSet
|
||||
}
|
||||
|
||||
watch(() => props.data, () => {
|
||||
if (props.data) {
|
||||
lines.value = parseJsonToLines(props.data)
|
||||
initCollapsedState()
|
||||
} else {
|
||||
lines.value = []
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
watch(() => props.expandDepth, () => {
|
||||
initCollapsedState()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.json-viewer {
|
||||
max-height: 500px;
|
||||
overflow: auto;
|
||||
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.json-lines {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.json-line {
|
||||
display: flex;
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
.json-line:hover {
|
||||
background: hsl(var(--muted) / 0.4);
|
||||
}
|
||||
|
||||
.line-number-area {
|
||||
flex-shrink: 0;
|
||||
width: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding-right: 8px;
|
||||
background: hsl(var(--muted) / 0.2);
|
||||
border-right: 1px solid hsl(var(--border));
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.fold-button {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
color: hsl(var(--muted-foreground) / 0.6);
|
||||
margin-right: 2px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.fold-button:hover {
|
||||
color: hsl(var(--foreground));
|
||||
background: hsl(var(--muted) / 0.8);
|
||||
}
|
||||
|
||||
.fold-icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.line-number {
|
||||
color: hsl(var(--muted-foreground) / 0.5);
|
||||
min-width: 20px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.line-content-area {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
||||
.indent {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.line-content {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.line-content.clickable-collapsed {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.line-content.clickable-collapsed:hover :deep(.token-ellipsis) {
|
||||
background: hsl(var(--primary) / 0.2);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* Token 颜色 - 亮色主题 */
|
||||
:deep(.token-key) {
|
||||
color: #0451a5;
|
||||
}
|
||||
|
||||
:deep(.token-string) {
|
||||
color: #a31515;
|
||||
}
|
||||
|
||||
:deep(.token-number) {
|
||||
color: #098658;
|
||||
}
|
||||
|
||||
:deep(.token-boolean) {
|
||||
color: #0000ff;
|
||||
}
|
||||
|
||||
:deep(.token-null) {
|
||||
color: #0000ff;
|
||||
}
|
||||
|
||||
:deep(.token-bracket) {
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
:deep(.token-punctuation) {
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
:deep(.token-ellipsis) {
|
||||
color: #0451a5;
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
:deep(.collapsed-info) {
|
||||
color: hsl(var(--muted-foreground));
|
||||
font-style: italic;
|
||||
margin-left: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* Token 颜色 - 暗色主题 */
|
||||
.theme-dark :deep(.token-key) {
|
||||
color: #9cdcfe;
|
||||
}
|
||||
|
||||
.theme-dark :deep(.token-string) {
|
||||
color: #ce9178;
|
||||
}
|
||||
|
||||
.theme-dark :deep(.token-number) {
|
||||
color: #b5cea8;
|
||||
}
|
||||
|
||||
.theme-dark :deep(.token-boolean) {
|
||||
color: #569cd6;
|
||||
}
|
||||
|
||||
.theme-dark :deep(.token-null) {
|
||||
color: #569cd6;
|
||||
}
|
||||
|
||||
.theme-dark :deep(.token-bracket) {
|
||||
color: #d4d4d4;
|
||||
}
|
||||
|
||||
.theme-dark :deep(.token-punctuation) {
|
||||
color: #d4d4d4;
|
||||
}
|
||||
|
||||
.theme-dark :deep(.token-ellipsis) {
|
||||
color: #9cdcfe;
|
||||
}
|
||||
|
||||
.theme-dark .line-number-area {
|
||||
background: hsl(var(--muted) / 0.3);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,161 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- 对比模式 - 并排 Diff -->
|
||||
<div v-show="viewMode === 'compare'">
|
||||
<div v-if="!detail.request_headers && !detail.provider_request_headers" class="text-sm text-muted-foreground">
|
||||
无请求头信息
|
||||
</div>
|
||||
<Card v-else class="bg-muted/30 overflow-hidden">
|
||||
<!-- Diff 头部 -->
|
||||
<div class="flex border-b bg-muted/50">
|
||||
<div class="flex-1 px-3 py-2 text-xs text-muted-foreground border-r flex items-center justify-between">
|
||||
<span class="font-medium">客户端请求头</span>
|
||||
<span class="text-destructive">-{{ headerStats.removed + headerStats.modified }}</span>
|
||||
</div>
|
||||
<div class="flex-1 px-3 py-2 text-xs text-muted-foreground flex items-center justify-between">
|
||||
<span class="font-medium">提供商请求头</span>
|
||||
<span class="text-green-600 dark:text-green-400">+{{ headerStats.added + headerStats.modified }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 并排 Diff 内容 -->
|
||||
<div class="overflow-x-auto max-h-[500px] overflow-y-auto">
|
||||
<div class="flex font-mono text-xs">
|
||||
<!-- 左侧:客户端 -->
|
||||
<div class="flex-1 border-r">
|
||||
<template v-for="entry in sortedEntries" :key="'left-' + entry.key">
|
||||
<!-- 删除的行 -->
|
||||
<div v-if="entry.status === 'removed'" class="flex items-start bg-destructive/10 px-3 py-0.5">
|
||||
<span class="text-destructive">
|
||||
"{{ entry.key }}": "{{ entry.clientValue }}"
|
||||
</span>
|
||||
</div>
|
||||
<!-- 修改的行 - 旧值 -->
|
||||
<div v-else-if="entry.status === 'modified'" class="flex items-start bg-amber-500/10 px-3 py-0.5">
|
||||
<span class="text-amber-600 dark:text-amber-400">
|
||||
"{{ entry.key }}": "{{ entry.clientValue }}"
|
||||
</span>
|
||||
</div>
|
||||
<!-- 新增的行 - 左侧空白占位 -->
|
||||
<div v-else-if="entry.status === 'added'" class="flex items-start bg-muted/30 px-3 py-0.5">
|
||||
<span class="text-muted-foreground/30 italic">(无)</span>
|
||||
</div>
|
||||
<!-- 未变化的行 -->
|
||||
<div v-else class="flex items-start px-3 py-0.5 hover:bg-muted/50">
|
||||
<span class="text-muted-foreground">
|
||||
"{{ entry.key }}": "{{ entry.clientValue }}"
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<!-- 右侧:提供商 -->
|
||||
<div class="flex-1">
|
||||
<template v-for="entry in sortedEntries" :key="'right-' + entry.key">
|
||||
<!-- 删除的行 - 右侧空白占位 -->
|
||||
<div v-if="entry.status === 'removed'" class="flex items-start bg-muted/30 px-3 py-0.5">
|
||||
<span class="text-muted-foreground/50 line-through">
|
||||
"{{ entry.key }}": "{{ entry.clientValue }}"
|
||||
</span>
|
||||
</div>
|
||||
<!-- 修改的行 - 新值 -->
|
||||
<div v-else-if="entry.status === 'modified'" class="flex items-start bg-amber-500/10 px-3 py-0.5">
|
||||
<span class="text-amber-600 dark:text-amber-400">
|
||||
"{{ entry.key }}": "{{ entry.providerValue }}"
|
||||
</span>
|
||||
</div>
|
||||
<!-- 新增的行 -->
|
||||
<div v-else-if="entry.status === 'added'" class="flex items-start bg-green-500/10 px-3 py-0.5">
|
||||
<span class="text-green-600 dark:text-green-400">
|
||||
"{{ entry.key }}": "{{ entry.providerValue }}"
|
||||
</span>
|
||||
</div>
|
||||
<!-- 未变化的行 -->
|
||||
<div v-else class="flex items-start px-3 py-0.5 hover:bg-muted/50">
|
||||
<span class="text-muted-foreground">
|
||||
"{{ entry.key }}": "{{ entry.providerValue }}"
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<!-- 格式化模式 - 直接使用 JsonContent -->
|
||||
<div v-show="viewMode === 'formatted'">
|
||||
<JsonContent
|
||||
:data="currentHeaderData"
|
||||
:view-mode="viewMode"
|
||||
:expand-depth="currentExpandDepth"
|
||||
:is-dark="isDark"
|
||||
empty-message="无请求头信息"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 原始模式 -->
|
||||
<div v-show="viewMode === 'raw'">
|
||||
<div v-if="!currentHeaderData || Object.keys(currentHeaderData).length === 0" class="text-sm text-muted-foreground">
|
||||
无请求头信息
|
||||
</div>
|
||||
<Card v-else class="bg-muted/30">
|
||||
<div class="p-4 overflow-x-auto">
|
||||
<pre class="text-xs font-mono whitespace-pre-wrap">{{ JSON.stringify(currentHeaderData, null, 2) }}</pre>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import Card from '@/components/ui/card.vue'
|
||||
import JsonContent from './JsonContent.vue'
|
||||
import type { RequestDetail } from '@/api/dashboard'
|
||||
|
||||
const props = defineProps<{
|
||||
detail: RequestDetail
|
||||
viewMode: 'compare' | 'formatted' | 'raw'
|
||||
dataSource: 'client' | 'provider'
|
||||
currentHeaderData: any
|
||||
currentExpandDepth: number
|
||||
hasProviderHeaders: boolean
|
||||
clientHeadersWithDiff: Array<{ key: string; value: any; status: string }>
|
||||
providerHeadersWithDiff: Array<{ key: string; value: any; status: string }>
|
||||
headerStats: { added: number; modified: number; removed: number; unchanged: number }
|
||||
isDark: boolean
|
||||
}>()
|
||||
|
||||
// 合并并排序的条目(用于并排显示)
|
||||
const sortedEntries = computed(() => {
|
||||
const clientHeaders = props.detail.request_headers || {}
|
||||
const providerHeaders = props.detail.provider_request_headers || {}
|
||||
|
||||
const clientKeys = new Set(Object.keys(clientHeaders))
|
||||
const providerKeys = new Set(Object.keys(providerHeaders))
|
||||
const allKeys = Array.from(new Set([...clientKeys, ...providerKeys])).sort()
|
||||
|
||||
return allKeys.map(key => {
|
||||
const inClient = clientKeys.has(key)
|
||||
const inProvider = providerKeys.has(key)
|
||||
const clientValue = clientHeaders[key]
|
||||
const providerValue = providerHeaders[key]
|
||||
|
||||
let status: 'added' | 'removed' | 'modified' | 'unchanged'
|
||||
if (inClient && inProvider) {
|
||||
status = clientValue === providerValue ? 'unchanged' : 'modified'
|
||||
} else if (inClient) {
|
||||
status = 'removed'
|
||||
} else {
|
||||
status = 'added'
|
||||
}
|
||||
|
||||
return {
|
||||
key,
|
||||
clientValue,
|
||||
providerValue,
|
||||
status
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user