feat: body rules 支持嵌套路径并增强前端验证体验

- 后端 apply_body_rules 支持点号分隔的嵌套路径(如 metadata.user.name)
- 支持 \. 转义字面量点号(如 config\.v1.enabled)
- 配置导出导入升级至 v2.2,新增 SystemConfig 支持
- 前端请求体规则编辑器改用 JSON 格式输入,增加实时验证指示
- 用量记录表格新增移动端卡片视图,优化筛选器响应式布局
This commit is contained in:
fawney19
2026-02-05 20:13:41 +08:00
parent a9b24c9161
commit e01dfee41d
7 changed files with 702 additions and 79 deletions

View File

@@ -120,17 +120,35 @@ export type HeaderRule = HeaderRuleSet | HeaderRuleDrop | HeaderRuleRename
* - drop: 删除字段
* - rename: 重命名字段(保留原值)
*/
/**
* 请求体规则 - 覆写字段
*
* - path 支持嵌套路径,如 "metadata.user.name"
* - 使用 "\." 转义字面量点号,如 "config\.v1.enabled"
*/
export interface BodyRuleSet {
action: 'set'
path: string
value: any
}
/**
* 请求体规则 - 删除字段
*
* - path 支持嵌套路径,如 "metadata.internal_flag"
* - 使用 "\." 转义字面量点号,如 "config\.v1.enabled"
*/
export interface BodyRuleDrop {
action: 'drop'
path: string
}
/**
* 请求体规则 - 重命名/移动字段
*
* - from/to 支持嵌套路径,如 "extra.old_config" -> "settings.new_config"
* - 使用 "\." 转义字面量点号,如 "config\.v1.enabled"
*/
export interface BodyRuleRename {
action: 'rename'
from: string

View File

@@ -25,7 +25,9 @@ const triggerClass = computed(() =>
:class="triggerClass"
:disabled="disabled"
>
<slot />
<ChevronDown class="h-4 w-4 opacity-50 pointer-events-none" />
<span class="truncate">
<slot />
</span>
<ChevronDown class="h-4 w-4 opacity-50 pointer-events-none flex-shrink-0" />
</SelectTriggerPrimitive>
</template>

View File

@@ -182,7 +182,7 @@
:disabled="savingEndpointId === endpoint.id"
@click="saveEndpoint(endpoint)"
>
<Check class="w-3.5 h-3.5" />
<Save class="w-3.5 h-3.5" />
</Button>
<Button
variant="ghost"
@@ -292,6 +292,13 @@
</Button>
</div>
<div
v-if="getEndpointEditBodyRules(endpoint.id).length > 0"
class="text-xs text-muted-foreground px-2"
>
用 <code class="bg-muted px-1 rounded">.</code> 访问嵌套字段;值为 JSON 格式,字符串需加引号如 <code class="bg-muted px-1 rounded">"text"</code>
</div>
<!-- 请求体规则列表 - 次要色边框 -->
<div
v-for="(rule, index) in getEndpointEditBodyRules(endpoint.id)"
@@ -326,7 +333,7 @@
<template v-if="rule.action === 'set'">
<Input
:model-value="rule.path"
placeholder="字段"
placeholder="字段路径 metadata.user_id"
size="sm"
class="flex-1 min-w-0 h-7 text-xs"
@update:model-value="(v) => updateEndpointBodyRuleField(endpoint.id, index, 'path', v)"
@@ -334,16 +341,21 @@
<span class="text-muted-foreground text-xs">=</span>
<Input
:model-value="rule.value"
placeholder=""
placeholder="123 / &quot;text&quot; / [1,2]"
size="sm"
class="flex-1 min-w-0 h-7 text-xs"
@update:model-value="(v) => updateEndpointBodyRuleField(endpoint.id, index, 'value', v)"
/>
<CheckCircle
class="w-4 h-4 shrink-0"
:class="getBodySetValueValidation(rule) === true ? 'text-green-600' : getBodySetValueValidation(rule) === false ? 'text-destructive' : 'text-muted-foreground/40'"
:title="getBodySetValueValidationTip(rule)"
/>
</template>
<template v-else-if="rule.action === 'drop'">
<Input
:model-value="rule.path"
placeholder="要删除的字段名"
placeholder="要删除的字段路径"
size="sm"
class="flex-1 min-w-0 h-7 text-xs"
@update:model-value="(v) => updateEndpointBodyRuleField(endpoint.id, index, 'path', v)"
@@ -352,7 +364,7 @@
<template v-else-if="rule.action === 'rename'">
<Input
:model-value="rule.from"
placeholder=""
placeholder="路径"
size="sm"
class="flex-1 min-w-0 h-7 text-xs"
@update:model-value="(v) => updateEndpointBodyRuleField(endpoint.id, index, 'from', v)"
@@ -360,7 +372,7 @@
<span class="text-muted-foreground text-xs">→</span>
<Input
:model-value="rule.to"
placeholder=""
placeholder="路径"
size="sm"
class="flex-1 min-w-0 h-7 text-xs"
@update:model-value="(v) => updateEndpointBodyRuleField(endpoint.id, index, 'to', v)"
@@ -494,7 +506,7 @@ import {
CollapsibleTrigger,
CollapsibleContent,
} from '@/components/ui'
import { Settings, Trash2, Check, X, Power, ChevronRight, Plus, Shuffle, RotateCcw, Radio } from 'lucide-vue-next'
import { Settings, Trash2, Check, X, Power, ChevronRight, Plus, Shuffle, RotateCcw, Radio, CheckCircle, Save } from 'lucide-vue-next'
import { useToast } from '@/composables/useToast'
import { log } from '@/utils/logger'
import AlertDialog from '@/components/common/AlertDialog.vue'
@@ -523,7 +535,7 @@ interface EditableRule {
interface EditableBodyRule {
action: 'set' | 'drop' | 'rename'
path: string // set/drop 用
value: string // set 用
value: string // set 用JSON 格式)
from: string // rename 用
to: string // rename 用
}
@@ -650,6 +662,44 @@ const RESERVED_BODY_FIELDS = new Set([
'stream',
])
function parseBodyRulePathParts(path: string): string[] | null {
const raw = path.trim()
if (!raw) return null
const parts: string[] = []
let current = ''
for (let i = 0; i < raw.length; i++) {
const ch = raw[i]
// 支持 \. 转义字面量点号;其他反斜杠组合按字面量保留
if (ch === '\\' && i + 1 < raw.length && raw[i + 1] === '.') {
current += '.'
i++
continue
}
if (ch === '.') {
if (!current) return null // 禁止空段:.a / a. / a..b
parts.push(current)
current = ''
continue
}
current += ch
}
if (!current) return null
parts.push(current)
return parts
}
function initBodyRuleSetValueForEditor(value: any): { value: string } {
if (value === undefined) return { value: '' }
// 所有值都用 JSON 格式回显
try {
return { value: JSON.stringify(value) }
} catch {
return { value: String(value) }
}
}
// 内部状态
const internalOpen = computed(() => props.modelValue)
@@ -733,7 +783,8 @@ function initEndpointEditState(endpoint: ProviderEndpoint): EndpointEditState {
if (endpoint.body_rules && endpoint.body_rules.length > 0) {
for (const rule of endpoint.body_rules) {
if (rule.action === 'set') {
bodyRules.push({ action: 'set', path: rule.path, value: rule.value || '', from: '', to: '' })
const { value } = initBodyRuleSetValueForEditor((rule as any).value)
bodyRules.push({ action: 'set', path: rule.path, value, from: '', to: '' })
} else if (rule.action === 'drop') {
bodyRules.push({ action: 'drop', path: rule.path, value: '', from: '', to: '' })
} else if (rule.action === 'rename') {
@@ -959,22 +1010,30 @@ function updateEndpointBodyRuleField(endpointId: string, index: number, field: '
// 验证请求体规则 path针对特定端点
function validateBodyRulePathForEndpoint(endpointId: string, path: string, index: number): string | null {
const trimmedPath = path.trim().toLowerCase()
if (!trimmedPath) return null
const raw = path.trim()
if (!raw) return null
if (RESERVED_BODY_FIELDS.has(trimmedPath)) {
return `"${path}" 是系统保留的字段`
const parts = parseBodyRulePathParts(raw)
if (!parts) {
return '路径格式无效(不允许 .a / a. / a..b'
}
const topKey = (parts[0] || '').trim().toLowerCase()
if (RESERVED_BODY_FIELDS.has(topKey)) {
return `"${parts[0]}" 是系统保留的顶层字段`
}
const normalizedPath = raw.toLowerCase()
const rules = getEndpointEditBodyRules(endpointId)
const duplicate = rules.findIndex(
(r, i) => i !== index && (
((r.action === 'set' || r.action === 'drop') && r.path.trim().toLowerCase() === trimmedPath) ||
(r.action === 'rename' && r.to.trim().toLowerCase() === trimmedPath)
((r.action === 'set' || r.action === 'drop') && r.path.trim().toLowerCase() === normalizedPath) ||
(r.action === 'rename' && r.to.trim().toLowerCase() === normalizedPath)
)
)
if (duplicate >= 0) {
return '字段重复'
return '字段路径重复'
}
return null
@@ -982,18 +1041,30 @@ function validateBodyRulePathForEndpoint(endpointId: string, path: string, index
// 验证请求体 rename from
function validateBodyRenameFromForEndpoint(endpointId: string, from: string, index: number): string | null {
const trimmedFrom = from.trim().toLowerCase()
if (!trimmedFrom) return null
const raw = from.trim()
if (!raw) return null
const parts = parseBodyRulePathParts(raw)
if (!parts) {
return '路径格式无效(不允许 .a / a. / a..b'
}
const topKey = (parts[0] || '').trim().toLowerCase()
if (RESERVED_BODY_FIELDS.has(topKey)) {
return `"${parts[0]}" 是系统保留的顶层字段`
}
const normalizedFrom = raw.toLowerCase()
const rules = getEndpointEditBodyRules(endpointId)
const duplicate = rules.findIndex(
(r, i) => i !== index &&
((r.action === 'set' && r.path.trim().toLowerCase() === trimmedFrom) ||
(r.action === 'drop' && r.path.trim().toLowerCase() === trimmedFrom) ||
(r.action === 'rename' && r.from.trim().toLowerCase() === trimmedFrom))
((r.action === 'set' && r.path.trim().toLowerCase() === normalizedFrom) ||
(r.action === 'drop' && r.path.trim().toLowerCase() === normalizedFrom) ||
(r.action === 'rename' && r.from.trim().toLowerCase() === normalizedFrom))
)
if (duplicate >= 0) {
return '该字段已被其他规则处理'
return '该路径已被其他规则处理'
}
return null
@@ -1001,26 +1072,78 @@ function validateBodyRenameFromForEndpoint(endpointId: string, from: string, ind
// 验证请求体 rename to
function validateBodyRenameToForEndpoint(endpointId: string, to: string, index: number): string | null {
const trimmedTo = to.trim().toLowerCase()
if (!trimmedTo) return null
const raw = to.trim()
if (!raw) return null
if (RESERVED_BODY_FIELDS.has(trimmedTo)) {
return `"${to}" 是系统保留的字段`
const parts = parseBodyRulePathParts(raw)
if (!parts) {
return '路径格式无效(不允许 .a / a. / a..b'
}
const topKey = (parts[0] || '').trim().toLowerCase()
if (RESERVED_BODY_FIELDS.has(topKey)) {
return `"${parts[0]}" 是系统保留的顶层字段`
}
const normalizedTo = raw.toLowerCase()
const rules = getEndpointEditBodyRules(endpointId)
const duplicate = rules.findIndex(
(r, i) => i !== index &&
((r.action === 'set' && r.path.trim().toLowerCase() === trimmedTo) ||
(r.action === 'rename' && r.to.trim().toLowerCase() === trimmedTo))
((r.action === 'set' && r.path.trim().toLowerCase() === normalizedTo) ||
(r.action === 'rename' && r.to.trim().toLowerCase() === normalizedTo))
)
if (duplicate >= 0) {
return '字段重复'
return '字段路径重复'
}
return null
}
function validateBodySetValue(rule: EditableBodyRule): string | null {
if (rule.action !== 'set') return null
const raw = rule.value.trim()
if (!raw) return '值不能为空'
try {
JSON.parse(raw)
} catch (err: any) {
const msg = err instanceof Error ? err.message : String(err)
return `JSON 格式错误${msg}`
}
return null
}
// 获取值验证状态true=有效, false=无效, null=空
function getBodySetValueValidation(rule: EditableBodyRule): boolean | null {
if (rule.action !== 'set') return null
const raw = rule.value.trim()
if (!raw) return null
try {
JSON.parse(raw)
return true
} catch {
return false
}
}
// 获取验证提示
function getBodySetValueValidationTip(rule: EditableBodyRule): string {
const validation = getBodySetValueValidation(rule)
if (validation === null) return '点击验证 JSON'
if (validation === true) {
const parsed = JSON.parse(rule.value.trim())
const type = Array.isArray(parsed) ? '数组' : typeof parsed === 'object' && parsed !== null ? '对象' : typeof parsed === 'string' ? '字符串' : typeof parsed === 'number' ? '数字' : typeof parsed === 'boolean' ? '布尔' : 'null'
return `有效的 JSON (${type})`
}
try {
JSON.parse(rule.value.trim())
return ''
} catch (err: any) {
return err instanceof Error ? err.message : String(err)
}
}
// 获取端点的请求体规则数量(有效的规则)
function getEndpointBodyRulesCount(endpoint: ProviderEndpoint): number {
const state = endpointEditStates.value[endpoint.id]
@@ -1096,7 +1219,9 @@ function hasBodyRulesChanges(endpoint: ProviderEndpoint): boolean {
if (!original) return true
if (edited.action !== original.action) return true
if (edited.action === 'set' && original.action === 'set') {
if (edited.path !== original.path || edited.value !== (original.value || '')) return true
const baseline = initBodyRuleSetValueForEditor((original as any).value)
if (edited.path !== original.path) return true
if (edited.value !== baseline.value) return true
} else if (edited.action === 'drop' && original.action === 'drop') {
if (edited.path !== original.path) return true
} else if (edited.action === 'rename' && original.action === 'rename') {
@@ -1112,7 +1237,14 @@ function rulesToBodyRules(rules: EditableBodyRule[]): BodyRule[] | null {
for (const rule of rules) {
if (rule.action === 'set' && rule.path.trim()) {
result.push({ action: 'set', path: rule.path.trim(), value: rule.value })
let value: any = rule.value
try {
value = JSON.parse(rule.value.trim())
} catch {
// 保存前会做校验;这里兜底避免 UI 崩溃
value = rule.value
}
result.push({ action: 'set', path: rule.path.trim(), value })
} else if (rule.action === 'drop' && rule.path.trim()) {
result.push({ action: 'drop', path: rule.path.trim() })
} else if (rule.action === 'rename' && rule.from.trim() && rule.to.trim()) {
@@ -1123,19 +1255,32 @@ function rulesToBodyRules(rules: EditableBodyRule[]): BodyRule[] | null {
return result.length > 0 ? result : null
}
// 检查请求体规则是否有验证错误
function hasBodyValidationErrorsForEndpoint(endpointId: string): boolean {
function getBodyValidationErrorForEndpoint(endpointId: string): string | null {
const rules = getEndpointEditBodyRules(endpointId)
for (let i = 0; i < rules.length; i++) {
const rule = rules[i]
const prefix = ` ${i + 1} 条请求体规则`
if (rule.action === 'set' || rule.action === 'drop') {
if (validateBodyRulePathForEndpoint(endpointId, rule.path, i)) return true
const pathErr = validateBodyRulePathForEndpoint(endpointId, rule.path, i)
if (pathErr) return `${prefix}${pathErr}`
if (rule.action === 'set') {
const valueErr = validateBodySetValue(rule)
if (valueErr) return `${prefix}${valueErr}`
}
} else if (rule.action === 'rename') {
if (validateBodyRenameFromForEndpoint(endpointId, rule.from, i)) return true
if (validateBodyRenameToForEndpoint(endpointId, rule.to, i)) return true
const fromErr = validateBodyRenameFromForEndpoint(endpointId, rule.from, i)
if (fromErr) return `${prefix}${fromErr}`
const toErr = validateBodyRenameToForEndpoint(endpointId, rule.to, i)
if (toErr) return `${prefix}${toErr}`
}
}
return false
return null
}
// 检查请求体规则是否有验证错误
function hasBodyValidationErrorsForEndpoint(endpointId: string): boolean {
return !!getBodyValidationErrorForEndpoint(endpointId)
}
// 检查端点 URL/路径是否有修改
@@ -1284,8 +1429,9 @@ async function saveEndpoint(endpoint: ProviderEndpoint) {
}
// 检查请求体规则是否有验证错误
if (hasBodyValidationErrorsForEndpoint(endpoint.id)) {
showError('请修正请求体规则中的错误')
const bodyErr = getBodyValidationErrorForEndpoint(endpoint.id)
if (bodyErr) {
showError(bodyErr)
return
}

View File

@@ -16,8 +16,8 @@
<Input
id="usage-records-search"
v-model="localSearch"
:placeholder="isAdmin ? '搜索用户/密钥/模型/提供商' : '搜索密钥/模型'"
class="w-32 sm:w-48 h-8 text-xs border-border/60 pl-8"
:placeholder="isAdmin ? '搜索用户/密钥' : '搜索密钥/模型'"
class="w-[7.5rem] sm:w-48 h-8 text-xs border-border/60 pl-8"
/>
</div>
@@ -28,8 +28,8 @@
:model-value="filterUser"
@update:model-value="$emit('update:filterUser', $event)"
>
<SelectTrigger class="w-24 sm:w-36 h-8 text-xs border-border/60">
<SelectValue placeholder="全部用户" />
<SelectTrigger class="flex-1 min-w-0 sm:flex-none sm:w-36 h-8 text-xs border-border/60">
<SelectValue placeholder="用户" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__all__">
@@ -51,8 +51,8 @@
:model-value="filterModel"
@update:model-value="$emit('update:filterModel', $event)"
>
<SelectTrigger class="w-24 sm:w-40 h-8 text-xs border-border/60">
<SelectValue placeholder="全部模型" />
<SelectTrigger class="flex-1 min-w-0 sm:flex-none sm:w-40 h-8 text-xs border-border/60">
<SelectValue placeholder="模型" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__all__">
@@ -75,8 +75,8 @@
:model-value="filterProvider"
@update:model-value="$emit('update:filterProvider', $event)"
>
<SelectTrigger class="w-24 sm:w-32 h-8 text-xs border-border/60">
<SelectValue placeholder="全部提供商" />
<SelectTrigger class="flex-1 min-w-0 sm:flex-none sm:w-32 h-8 text-xs border-border/60">
<SelectValue placeholder="提供商" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__all__">
@@ -98,8 +98,8 @@
:model-value="filterApiFormat"
@update:model-value="$emit('update:filterApiFormat', $event)"
>
<SelectTrigger class="w-24 sm:w-32 h-8 text-xs border-border/60">
<SelectValue placeholder="全部格式" />
<SelectTrigger class="flex-1 min-w-0 sm:flex-none sm:w-32 h-8 text-xs border-border/60">
<SelectValue placeholder="格式" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__all__">
@@ -121,8 +121,8 @@
:model-value="filterStatus"
@update:model-value="$emit('update:filterStatus', $event)"
>
<SelectTrigger class="w-20 sm:w-28 h-8 text-xs border-border/60">
<SelectValue placeholder="全部状态" />
<SelectTrigger class="flex-1 min-w-0 sm:flex-none sm:w-28 h-8 text-xs border-border/60">
<SelectValue placeholder="状态" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__all__">
@@ -165,7 +165,117 @@
</Button>
</template>
<Table>
<!-- 移动端卡片视图 -->
<div class="md:hidden">
<div
v-if="records.length === 0"
class="text-center py-12 text-muted-foreground"
>
暂无请求记录
</div>
<div
v-for="record in records"
v-else
:key="record.id"
class="border-b border-border/40 py-2.5 px-2"
:class="isAdmin ? 'cursor-pointer active:bg-muted/30 transition-colors' : ''"
@click="isAdmin && emit('showDetail', record.id)"
>
<!-- 第一行模型 + 费用 -->
<div class="flex items-center justify-between gap-2">
<div class="min-w-0 flex-1">
<span class="text-sm font-medium truncate block">{{ record.model }}</span>
<span
v-if="getActualModel(record)"
class="text-[11px] text-muted-foreground truncate block"
>-> {{ getActualModel(record) }}</span>
</div>
<div class="flex flex-col items-end flex-shrink-0">
<span class="text-xs text-primary font-medium">{{ formatCurrency(record.cost || 0) }}</span>
<span
v-if="showActualCost && record.actual_cost !== undefined"
class="text-[10px] text-muted-foreground"
>{{ formatCurrency(record.actual_cost) }}</span>
</div>
</div>
<!-- 第二行状态 | 时间 | API格式 | 耗时 | Tokens -->
<div class="flex items-center justify-between text-[11px] text-muted-foreground mt-1 leading-4">
<div class="flex items-center gap-1.5">
<!-- 状态 Badge -->
<Badge
v-if="record.status === 'failed' || (record.status_code && record.status_code >= 400) || record.error_message"
variant="destructive"
class="whitespace-nowrap text-[10px] px-1.5 h-4 leading-4 inline-flex items-center"
>
失败
</Badge>
<Badge
v-else-if="record.status === 'pending'"
variant="outline"
class="whitespace-nowrap animate-pulse border-muted-foreground/30 text-muted-foreground text-[10px] px-1.5 h-4 leading-4 inline-flex items-center"
>
等待
</Badge>
<Badge
v-else-if="record.status === 'streaming'"
variant="outline"
class="whitespace-nowrap animate-pulse border-primary/50 text-primary text-[10px] px-1.5 h-4 leading-4 inline-flex items-center"
>
传输
</Badge>
<Badge
v-else-if="record.status === 'cancelled'"
variant="outline"
class="whitespace-nowrap border-amber-500/50 text-amber-600 dark:text-amber-400 text-[10px] px-1.5 h-4 leading-4 inline-flex items-center"
>
取消
</Badge>
<Badge
v-else-if="record.is_stream"
variant="secondary"
class="whitespace-nowrap text-[10px] px-1.5 h-4 leading-4 inline-flex items-center"
>
流式
</Badge>
<Badge
v-else
variant="outline"
class="whitespace-nowrap border-border/60 text-muted-foreground text-[10px] px-1.5 h-4 leading-4 inline-flex items-center"
>
标准
</Badge>
<span class="text-muted-foreground/50">|</span>
<span>{{ formatDateTime(record.created_at) }}</span>
<template v-if="record.api_format">
<span class="text-muted-foreground/50">|</span>
<span>{{ formatApiFormat(record.api_format) }}</span>
</template>
</div>
<div class="flex items-center gap-1.5">
<!-- 耗时 -->
<span
v-if="record.status === 'pending' || record.status === 'streaming'"
class="text-primary tabular-nums"
>{{ getElapsedTime(record) }}</span>
<span
v-else-if="record.response_time_ms != null"
class="tabular-nums"
>{{ record.first_byte_time_ms != null ? (record.first_byte_time_ms / 1000).toFixed(1) + '/' : '' }}{{ (record.response_time_ms / 1000).toFixed(1) }}s</span>
<span
v-else
class="tabular-nums"
>-</span>
<span class="text-muted-foreground/50">|</span>
<!-- Tokens -->
<span>{{ formatTokens(record.input_tokens || 0) }}/{{ formatTokens(record.output_tokens || 0) }}</span>
</div>
</div>
</div>
</div>
<!-- 桌面端表格视图 -->
<Table class="hidden md:table">
<TableHeader>
<TableRow class="border-b border-border/60 hover:bg-transparent">
<TableHead class="h-12 font-semibold w-[70px]">
@@ -192,7 +302,7 @@
>
提供商
</TableHead>
<TableHead class="h-12 font-semibold w-[80px]">
<TableHead class="h-12 font-semibold w-[120px]">
API格式
</TableHead>
<TableHead class="h-12 font-semibold w-[70px] text-center">
@@ -354,7 +464,7 @@
</div>
</TableCell>
<TableCell
class="py-4 w-[80px]"
class="py-4 w-[120px]"
:title="getApiFormatTooltip(record)"
>
<!-- 有格式转换或同族格式差异两行显示 -->
@@ -362,7 +472,7 @@
v-if="shouldShowFormatConversion(record)"
class="flex flex-col text-xs gap-0.5"
>
<div class="flex items-center gap-1">
<div class="flex items-center gap-1 whitespace-nowrap">
<span>{{ formatApiFormat(record.api_format!) }}</span>
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -377,12 +487,12 @@
/>
</svg>
</div>
<span class="text-muted-foreground">{{ formatApiFormat(record.endpoint_api_format!) }}</span>
<span class="text-muted-foreground whitespace-nowrap">{{ formatApiFormat(record.endpoint_api_format!) }}</span>
</div>
<!-- 无格式转换单行显示 -->
<span
v-else-if="record.api_format"
class="text-xs"
class="text-xs whitespace-nowrap"
>{{ formatApiFormat(record.api_format) }}</span>
<span
v-else