789 lines
22 KiB
Vue
789 lines
22 KiB
Vue
|
|
<template>
|
||
|
|
<div class="debug-page">
|
||
|
|
<!-- 顶部标题栏 -->
|
||
|
|
<div class="page-header">
|
||
|
|
<div class="header-left">
|
||
|
|
<a-button type="text" @click="$router.back()">
|
||
|
|
<template #icon><ArrowLeftOutlined /></template>
|
||
|
|
</a-button>
|
||
|
|
<h1>AI接口调试</h1>
|
||
|
|
<span class="subtitle">在线测试AI模型接口</span>
|
||
|
|
</div>
|
||
|
|
<div class="header-right">
|
||
|
|
<a-button @click="handleClearAll">
|
||
|
|
<template #icon><ClearOutlined /></template>
|
||
|
|
重置
|
||
|
|
</a-button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- 主内容区 -->
|
||
|
|
<div class="main-content">
|
||
|
|
<!-- 模型选择区 -->
|
||
|
|
<div class="section">
|
||
|
|
<div class="section-title">选择模型</div>
|
||
|
|
<div class="model-selector">
|
||
|
|
<a-select
|
||
|
|
v-model:value="selectedModelId"
|
||
|
|
placeholder="请选择要调试的AI模型"
|
||
|
|
show-search
|
||
|
|
size="large"
|
||
|
|
style="width: 400px;"
|
||
|
|
:filter-option="filterOption"
|
||
|
|
@change="handleModelChange"
|
||
|
|
>
|
||
|
|
<a-select-opt-group v-for="group in groupedModels" :key="group.name">
|
||
|
|
<template #label>{{ group.name }}</template>
|
||
|
|
<a-select-option v-for="model in group.models" :key="model.id" :value="model.id">
|
||
|
|
<span>{{ model.name }}</span>
|
||
|
|
<span class="model-code">{{ model.code }}</span>
|
||
|
|
</a-select-option>
|
||
|
|
</a-select-opt-group>
|
||
|
|
</a-select>
|
||
|
|
|
||
|
|
<div v-if="selectedModel" class="model-meta">
|
||
|
|
<span class="meta-item">
|
||
|
|
<span class="meta-label">类型</span>
|
||
|
|
<span class="meta-value">{{ getTypeLabel(selectedModel.type) }}</span>
|
||
|
|
</span>
|
||
|
|
<span class="meta-divider">|</span>
|
||
|
|
<span class="meta-item">
|
||
|
|
<span class="meta-label">积分</span>
|
||
|
|
<span class="meta-value">{{ selectedModel.pointsCost }}/次</span>
|
||
|
|
</span>
|
||
|
|
<span v-if="selectedModel.workflowType !== 'direct'" class="meta-divider">|</span>
|
||
|
|
<span v-if="selectedModel.workflowType !== 'direct'" class="meta-item">
|
||
|
|
<span class="meta-label">工作流</span>
|
||
|
|
<span class="meta-value">{{ selectedModel.workflowType }}</span>
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- 参数配置区 -->
|
||
|
|
<div class="section">
|
||
|
|
<div class="section-title">输入参数</div>
|
||
|
|
<div v-if="selectedModel && inputParams.length > 0" class="params-form">
|
||
|
|
<a-form layout="vertical">
|
||
|
|
<a-row :gutter="24">
|
||
|
|
<template v-for="param in inputParams" :key="param.name">
|
||
|
|
<!-- 文本输入 -->
|
||
|
|
<a-col :span="param.type === 'textarea' ? 24 : 12" v-if="param.type === 'text' || param.type === 'input'">
|
||
|
|
<a-form-item :required="param.required">
|
||
|
|
<template #label><span class="param-label">{{ param.label }}</span></template>
|
||
|
|
<a-input v-model:value="formData[param.name]" :placeholder="param.placeholder || `请输入${param.label}`" />
|
||
|
|
</a-form-item>
|
||
|
|
</a-col>
|
||
|
|
|
||
|
|
<!-- 文本域 -->
|
||
|
|
<a-col :span="24" v-else-if="param.type === 'textarea'">
|
||
|
|
<a-form-item :required="param.required">
|
||
|
|
<template #label><span class="param-label">{{ param.label }}</span></template>
|
||
|
|
<a-textarea v-model:value="formData[param.name]" :placeholder="param.placeholder || `请输入${param.label}`" :rows="4" show-count :maxlength="2000" />
|
||
|
|
</a-form-item>
|
||
|
|
</a-col>
|
||
|
|
|
||
|
|
<!-- 下拉选择 -->
|
||
|
|
<a-col :span="12" v-else-if="param.type === 'select'">
|
||
|
|
<a-form-item :required="param.required">
|
||
|
|
<template #label><span class="param-label">{{ param.label }}</span></template>
|
||
|
|
<a-select v-model:value="formData[param.name]" :placeholder="param.placeholder || `请选择${param.label}`">
|
||
|
|
<a-select-option v-for="opt in param.options" :key="opt" :value="opt">{{ opt }}</a-select-option>
|
||
|
|
</a-select>
|
||
|
|
</a-form-item>
|
||
|
|
</a-col>
|
||
|
|
|
||
|
|
<!-- 数字输入 -->
|
||
|
|
<a-col :span="12" v-else-if="param.type === 'number'">
|
||
|
|
<a-form-item :required="param.required">
|
||
|
|
<template #label><span class="param-label">{{ param.label }}</span></template>
|
||
|
|
<a-input-number v-model:value="formData[param.name]" :min="param.min" :max="param.max" style="width: 100%;" />
|
||
|
|
</a-form-item>
|
||
|
|
</a-col>
|
||
|
|
|
||
|
|
<!-- 单图上传 -->
|
||
|
|
<a-col :span="12" v-else-if="param.type === 'image'">
|
||
|
|
<a-form-item :required="param.required">
|
||
|
|
<template #label><span class="param-label">{{ param.label }}</span></template>
|
||
|
|
<div class="image-upload-area">
|
||
|
|
<a-upload :file-list="getImageFileList(param.name)" list-type="picture-card" :before-upload="(file) => handleBeforeUpload(file, param.name, false)" @remove="() => handleRemoveImage(param.name)" :max-count="1">
|
||
|
|
<div v-if="!formData[param.name]" class="upload-placeholder"><PlusOutlined /><span>上传</span></div>
|
||
|
|
</a-upload>
|
||
|
|
<span class="upload-hint">JPG/PNG/GIF, 最大10MB</span>
|
||
|
|
</div>
|
||
|
|
</a-form-item>
|
||
|
|
</a-col>
|
||
|
|
|
||
|
|
<!-- 多图上传 -->
|
||
|
|
<a-col :span="24" v-else-if="param.type === 'images'">
|
||
|
|
<a-form-item :required="param.required">
|
||
|
|
<template #label><span class="param-label">{{ param.label }}</span></template>
|
||
|
|
<div class="image-upload-area">
|
||
|
|
<a-upload :file-list="getImagesFileList(param.name)" list-type="picture-card" :before-upload="(file) => handleBeforeUpload(file, param.name, true)" @remove="(file) => handleRemoveImages(param.name, file)" :max-count="param.maxCount || 5" multiple>
|
||
|
|
<div class="upload-placeholder"><PlusOutlined /><span>上传</span></div>
|
||
|
|
</a-upload>
|
||
|
|
<span class="upload-hint">最多{{ param.maxCount || 5 }}张</span>
|
||
|
|
</div>
|
||
|
|
</a-form-item>
|
||
|
|
</a-col>
|
||
|
|
</template>
|
||
|
|
</a-row>
|
||
|
|
</a-form>
|
||
|
|
</div>
|
||
|
|
<div v-else class="empty-params">
|
||
|
|
<span class="empty-text">请先选择模型</span>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- 发送按钮 -->
|
||
|
|
<div class="submit-area">
|
||
|
|
<a-button type="primary" size="large" :loading="debugging" @click="handleDebug" :disabled="!canSubmit">
|
||
|
|
<template #icon><SendOutlined /></template>
|
||
|
|
{{ debugging ? '请求中...' : '发送请求' }}
|
||
|
|
</a-button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- 结果展示区 - 始终显示 -->
|
||
|
|
<div class="section result-section">
|
||
|
|
<div class="section-header">
|
||
|
|
<div class="section-title">调试结果</div>
|
||
|
|
<div v-if="debugResult" class="section-actions">
|
||
|
|
<a-button size="small" @click="copyToClipboard(JSON.stringify(debugResult.response, null, 2))">
|
||
|
|
<template #icon><CopyOutlined /></template>复制
|
||
|
|
</a-button>
|
||
|
|
<a-button size="small" @click="debugResult = null">
|
||
|
|
<template #icon><DeleteOutlined /></template>清除
|
||
|
|
</a-button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- 有结果时显示 -->
|
||
|
|
<template v-if="debugResult">
|
||
|
|
<!-- 状态指示 -->
|
||
|
|
<div class="result-status" :class="debugResult.success ? 'success' : 'error'">
|
||
|
|
<span class="status-icon">
|
||
|
|
<CheckCircleOutlined v-if="debugResult.success" />
|
||
|
|
<CloseCircleOutlined v-else />
|
||
|
|
</span>
|
||
|
|
<span class="status-text">{{ debugResult.success ? '请求成功' : '请求失败' }}</span>
|
||
|
|
<span v-if="debugResult.duration" class="status-duration">耗时 {{ debugResult.duration }}</span>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- 提取的结果数据 -->
|
||
|
|
<div v-if="debugResult.success && extractedResult" class="extracted-result">
|
||
|
|
<div class="extracted-label">提取结果</div>
|
||
|
|
<div class="extracted-content">{{ extractedResult }}</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- 结果标签页 -->
|
||
|
|
<a-tabs v-model:activeKey="activeTab" class="result-tabs">
|
||
|
|
<a-tab-pane key="response" tab="响应数据">
|
||
|
|
<div class="code-block"><pre>{{ JSON.stringify(debugResult.response, null, 2) }}</pre></div>
|
||
|
|
</a-tab-pane>
|
||
|
|
|
||
|
|
<a-tab-pane key="request" tab="请求详情">
|
||
|
|
<div class="request-info">
|
||
|
|
<div class="info-row">
|
||
|
|
<span class="info-label">URL</span>
|
||
|
|
<span class="info-value url">{{ debugResult.request?.url }}</span>
|
||
|
|
</div>
|
||
|
|
<div class="info-row">
|
||
|
|
<span class="info-label">Method</span>
|
||
|
|
<span class="info-value method">{{ debugResult.request?.method }}</span>
|
||
|
|
</div>
|
||
|
|
<div class="info-row">
|
||
|
|
<span class="info-label">Headers</span>
|
||
|
|
<div class="code-block small"><pre>{{ JSON.stringify(debugResult.request?.headers, null, 2) }}</pre></div>
|
||
|
|
</div>
|
||
|
|
<div class="info-row">
|
||
|
|
<span class="info-label">Body</span>
|
||
|
|
<div class="code-block small"><pre>{{ JSON.stringify(debugResult.request?.body, null, 2) }}</pre></div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</a-tab-pane>
|
||
|
|
|
||
|
|
<a-tab-pane key="raw" tab="原始响应">
|
||
|
|
<div class="code-block"><pre>{{ debugResult.rawResponse }}</pre></div>
|
||
|
|
</a-tab-pane>
|
||
|
|
|
||
|
|
<a-tab-pane v-if="!debugResult.success" key="error" tab="错误信息">
|
||
|
|
<div class="error-info">
|
||
|
|
<div class="error-message">{{ debugResult.error }}</div>
|
||
|
|
<div v-if="debugResult.stackTrace" class="code-block"><pre>{{ debugResult.stackTrace }}</pre></div>
|
||
|
|
</div>
|
||
|
|
</a-tab-pane>
|
||
|
|
</a-tabs>
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<!-- 无结果时显示空状态 -->
|
||
|
|
<div v-else class="empty-result">
|
||
|
|
<div class="empty-icon"><ExperimentOutlined /></div>
|
||
|
|
<p>暂无调试结果</p>
|
||
|
|
<span class="empty-hint">配置参数后点击发送请求</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<script setup>
|
||
|
|
import { ref, computed, onMounted, reactive } from 'vue'
|
||
|
|
import { useRoute } from 'vue-router'
|
||
|
|
import { message } from 'ant-design-vue'
|
||
|
|
import { ArrowLeftOutlined, ClearOutlined, SendOutlined, PlusOutlined, DeleteOutlined, CopyOutlined, ExperimentOutlined, CheckCircleOutlined, CloseCircleOutlined } from '@ant-design/icons-vue'
|
||
|
|
import { getAiModels, debugAiModel, getAiModelTypes } from '@/api/ai'
|
||
|
|
import { uploadImage } from '@/api/upload'
|
||
|
|
|
||
|
|
const route = useRoute()
|
||
|
|
const selectedModelId = ref(null)
|
||
|
|
const selectedModel = ref(null)
|
||
|
|
const models = ref([])
|
||
|
|
const modelTypes = ref([])
|
||
|
|
const formData = reactive({})
|
||
|
|
const debugging = ref(false)
|
||
|
|
const debugResult = ref(null)
|
||
|
|
const activeTab = ref('response')
|
||
|
|
|
||
|
|
// 按厂商分组模型
|
||
|
|
const groupedModels = computed(() => {
|
||
|
|
const groups = {}
|
||
|
|
models.value.forEach(model => {
|
||
|
|
const providerName = model.providerName || '未知厂商'
|
||
|
|
if (!groups[providerName]) {
|
||
|
|
groups[providerName] = { name: providerName, models: [] }
|
||
|
|
}
|
||
|
|
groups[providerName].models.push(model)
|
||
|
|
})
|
||
|
|
return Object.values(groups)
|
||
|
|
})
|
||
|
|
|
||
|
|
// 解析输入参数配置
|
||
|
|
const inputParams = computed(() => {
|
||
|
|
if (!selectedModel.value || !selectedModel.value.inputParams) return []
|
||
|
|
try {
|
||
|
|
return JSON.parse(selectedModel.value.inputParams)
|
||
|
|
} catch (e) {
|
||
|
|
return []
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
// 检查是否可以提交
|
||
|
|
const canSubmit = computed(() => {
|
||
|
|
if (!selectedModel.value) return false
|
||
|
|
const missingParams = inputParams.value.filter(p => p.required && !formData[p.name])
|
||
|
|
return missingParams.length === 0
|
||
|
|
})
|
||
|
|
|
||
|
|
// 提取的结果数据
|
||
|
|
const extractedResult = computed(() => {
|
||
|
|
if (!debugResult.value || !debugResult.value.success || !debugResult.value.response) return null
|
||
|
|
if (!selectedModel.value || !selectedModel.value.responseMapping) return null
|
||
|
|
|
||
|
|
try {
|
||
|
|
const mapping = JSON.parse(selectedModel.value.responseMapping)
|
||
|
|
const resultPath = mapping.result
|
||
|
|
if (!resultPath) return null
|
||
|
|
|
||
|
|
// 解析路径提取数据,如 "data" 或 "choices[0].message.content"
|
||
|
|
const response = debugResult.value.response
|
||
|
|
return extractByPath(response, resultPath)
|
||
|
|
} catch (e) {
|
||
|
|
return null
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
// 根据路径提取数据
|
||
|
|
const extractByPath = (obj, path) => {
|
||
|
|
if (!obj || !path) return null
|
||
|
|
const parts = path.replace(/\[(\d+)\]/g, '.$1').split('.')
|
||
|
|
let result = obj
|
||
|
|
for (const part of parts) {
|
||
|
|
if (result === null || result === undefined) return null
|
||
|
|
result = result[part]
|
||
|
|
}
|
||
|
|
return typeof result === 'object' ? JSON.stringify(result, null, 2) : result
|
||
|
|
}
|
||
|
|
|
||
|
|
const getTypeLabel = (typeValue) => {
|
||
|
|
const type = modelTypes.value.find(t => t.value === typeValue)
|
||
|
|
return type ? type.label : typeValue
|
||
|
|
}
|
||
|
|
|
||
|
|
const loadModels = async () => {
|
||
|
|
try {
|
||
|
|
const res = await getAiModels({ status: 1 })
|
||
|
|
models.value = res.records || []
|
||
|
|
} catch (e) {
|
||
|
|
message.error('获取模型列表失败')
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const loadModelTypes = async () => {
|
||
|
|
try {
|
||
|
|
const res = await getAiModelTypes()
|
||
|
|
modelTypes.value = res || []
|
||
|
|
} catch (e) {}
|
||
|
|
}
|
||
|
|
|
||
|
|
const filterOption = (input, option) => {
|
||
|
|
const text = option.children?.[0]?.children || ''
|
||
|
|
return text.toLowerCase().includes(input.toLowerCase())
|
||
|
|
}
|
||
|
|
|
||
|
|
const handleModelChange = (modelId) => {
|
||
|
|
selectedModel.value = models.value.find(m => m.id === modelId)
|
||
|
|
Object.keys(formData).forEach(key => delete formData[key])
|
||
|
|
debugResult.value = null
|
||
|
|
inputParams.value.forEach(param => {
|
||
|
|
if (param.default) formData[param.name] = param.default
|
||
|
|
else if (param.type === 'images') formData[param.name] = []
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
const getImageFileList = (paramName) => {
|
||
|
|
const url = formData[paramName]
|
||
|
|
if (!url) return []
|
||
|
|
return [{ uid: '-1', name: 'image.png', status: 'done', url }]
|
||
|
|
}
|
||
|
|
|
||
|
|
const getImagesFileList = (paramName) => {
|
||
|
|
const urls = formData[paramName] || []
|
||
|
|
return urls.map((url, index) => ({ uid: `-${index}`, name: `image-${index}.png`, status: 'done', url }))
|
||
|
|
}
|
||
|
|
|
||
|
|
const handleBeforeUpload = async (file, paramName, isMultiple) => {
|
||
|
|
if (!file.type.startsWith('image/')) { message.error('只能上传图片'); return false }
|
||
|
|
if (file.size / 1024 / 1024 > 10) { message.error('图片不能超过10MB'); return false }
|
||
|
|
try {
|
||
|
|
message.loading({ content: '上传中...', key: 'upload' })
|
||
|
|
const url = await uploadImage(file)
|
||
|
|
if (isMultiple) {
|
||
|
|
if (!formData[paramName]) formData[paramName] = []
|
||
|
|
formData[paramName].push(url)
|
||
|
|
} else {
|
||
|
|
formData[paramName] = url
|
||
|
|
}
|
||
|
|
message.success({ content: '上传成功', key: 'upload' })
|
||
|
|
} catch (e) {
|
||
|
|
message.error({ content: '上传失败', key: 'upload' })
|
||
|
|
}
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
const handleRemoveImage = (paramName) => { delete formData[paramName] }
|
||
|
|
const handleRemoveImages = (paramName, file) => {
|
||
|
|
const urls = formData[paramName] || []
|
||
|
|
const index = urls.findIndex(url => url === file.url)
|
||
|
|
if (index > -1) urls.splice(index, 1)
|
||
|
|
}
|
||
|
|
|
||
|
|
const handleDebug = async () => {
|
||
|
|
const missingParams = inputParams.value.filter(p => p.required && !formData[p.name]).map(p => p.label)
|
||
|
|
if (missingParams.length > 0) { message.warning(`请填写: ${missingParams.join(', ')}`); return }
|
||
|
|
|
||
|
|
debugging.value = true
|
||
|
|
debugResult.value = null
|
||
|
|
try {
|
||
|
|
const result = await debugAiModel(selectedModelId.value, formData)
|
||
|
|
debugResult.value = result
|
||
|
|
activeTab.value = result.success ? 'response' : 'error'
|
||
|
|
message[result.success ? 'success' : 'error'](result.success ? '请求成功' : '请求失败')
|
||
|
|
} catch (e) {
|
||
|
|
message.error('请求失败: ' + e.message)
|
||
|
|
debugResult.value = { success: false, error: e.message, stackTrace: e.toString() }
|
||
|
|
} finally {
|
||
|
|
debugging.value = false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const copyToClipboard = (text) => {
|
||
|
|
navigator.clipboard.writeText(text).then(() => message.success('已复制')).catch(() => message.error('复制失败'))
|
||
|
|
}
|
||
|
|
|
||
|
|
const handleClearAll = () => {
|
||
|
|
selectedModelId.value = null
|
||
|
|
selectedModel.value = null
|
||
|
|
Object.keys(formData).forEach(key => delete formData[key])
|
||
|
|
debugResult.value = null
|
||
|
|
}
|
||
|
|
|
||
|
|
onMounted(async () => {
|
||
|
|
await Promise.all([loadModels(), loadModelTypes()])
|
||
|
|
const modelIdFromQuery = route.query.modelId
|
||
|
|
if (modelIdFromQuery) {
|
||
|
|
const modelId = parseInt(modelIdFromQuery)
|
||
|
|
if (models.value.find(m => m.id === modelId)) {
|
||
|
|
selectedModelId.value = modelId
|
||
|
|
handleModelChange(modelId)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
})
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<style scoped>
|
||
|
|
.debug-page {
|
||
|
|
min-height: 100vh;
|
||
|
|
background: #f7f8fa;
|
||
|
|
}
|
||
|
|
|
||
|
|
.page-header {
|
||
|
|
display: flex;
|
||
|
|
justify-content: space-between;
|
||
|
|
align-items: center;
|
||
|
|
padding: 16px 24px;
|
||
|
|
background: #fff;
|
||
|
|
border-bottom: 1px solid #e8e8e8;
|
||
|
|
}
|
||
|
|
|
||
|
|
.header-left {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 12px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.header-left h1 {
|
||
|
|
margin: 0;
|
||
|
|
font-size: 18px;
|
||
|
|
font-weight: 600;
|
||
|
|
color: #1a1a1a;
|
||
|
|
}
|
||
|
|
|
||
|
|
.subtitle {
|
||
|
|
color: #8c8c8c;
|
||
|
|
font-size: 14px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.main-content {
|
||
|
|
padding: 24px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.section {
|
||
|
|
background: #fff;
|
||
|
|
border-radius: 8px;
|
||
|
|
padding: 24px;
|
||
|
|
margin-bottom: 16px;
|
||
|
|
border: 1px solid #e8e8e8;
|
||
|
|
}
|
||
|
|
|
||
|
|
.section-title {
|
||
|
|
font-size: 15px;
|
||
|
|
font-weight: 600;
|
||
|
|
color: #1a1a1a;
|
||
|
|
margin-bottom: 16px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.section-header {
|
||
|
|
display: flex;
|
||
|
|
justify-content: space-between;
|
||
|
|
align-items: center;
|
||
|
|
margin-bottom: 16px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.section-actions {
|
||
|
|
display: flex;
|
||
|
|
gap: 8px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.model-selector {
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
gap: 12px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.model-code {
|
||
|
|
margin-left: 8px;
|
||
|
|
color: #8c8c8c;
|
||
|
|
font-size: 12px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.model-meta {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 8px;
|
||
|
|
padding: 12px 16px;
|
||
|
|
background: #fafafa;
|
||
|
|
border-radius: 6px;
|
||
|
|
font-size: 13px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.meta-item {
|
||
|
|
display: flex;
|
||
|
|
gap: 6px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.meta-label {
|
||
|
|
color: #8c8c8c;
|
||
|
|
}
|
||
|
|
|
||
|
|
.meta-value {
|
||
|
|
color: #1a1a1a;
|
||
|
|
font-weight: 500;
|
||
|
|
}
|
||
|
|
|
||
|
|
.meta-divider {
|
||
|
|
color: #d9d9d9;
|
||
|
|
}
|
||
|
|
|
||
|
|
.params-form {
|
||
|
|
margin-bottom: 20px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.param-label {
|
||
|
|
font-weight: 500;
|
||
|
|
color: #262626;
|
||
|
|
}
|
||
|
|
|
||
|
|
.empty-params {
|
||
|
|
padding: 40px 0;
|
||
|
|
text-align: center;
|
||
|
|
}
|
||
|
|
|
||
|
|
.empty-text {
|
||
|
|
color: #bfbfbf;
|
||
|
|
font-size: 14px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.image-upload-area {
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
gap: 8px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.upload-placeholder {
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
align-items: center;
|
||
|
|
gap: 4px;
|
||
|
|
color: #8c8c8c;
|
||
|
|
font-size: 12px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.upload-hint {
|
||
|
|
font-size: 12px;
|
||
|
|
color: #bfbfbf;
|
||
|
|
}
|
||
|
|
|
||
|
|
.submit-area {
|
||
|
|
display: flex;
|
||
|
|
justify-content: center;
|
||
|
|
padding-top: 8px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.result-status {
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 8px;
|
||
|
|
padding: 12px 16px;
|
||
|
|
border-radius: 6px;
|
||
|
|
margin-bottom: 16px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.result-status.success {
|
||
|
|
background: #f6ffed;
|
||
|
|
border: 1px solid #b7eb8f;
|
||
|
|
}
|
||
|
|
|
||
|
|
.result-status.error {
|
||
|
|
background: #fff2f0;
|
||
|
|
border: 1px solid #ffccc7;
|
||
|
|
}
|
||
|
|
|
||
|
|
.status-icon {
|
||
|
|
font-size: 18px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.result-status.success .status-icon {
|
||
|
|
color: #52c41a;
|
||
|
|
}
|
||
|
|
|
||
|
|
.result-status.error .status-icon {
|
||
|
|
color: #ff4d4f;
|
||
|
|
}
|
||
|
|
|
||
|
|
.status-text {
|
||
|
|
font-weight: 500;
|
||
|
|
color: #262626;
|
||
|
|
}
|
||
|
|
|
||
|
|
.status-duration {
|
||
|
|
margin-left: auto;
|
||
|
|
color: #8c8c8c;
|
||
|
|
font-size: 13px;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* 提取结果样式 */
|
||
|
|
.extracted-result {
|
||
|
|
background: #f0f9eb;
|
||
|
|
border: 1px solid #c2e7b0;
|
||
|
|
border-radius: 6px;
|
||
|
|
padding: 16px;
|
||
|
|
margin-bottom: 16px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.extracted-label {
|
||
|
|
font-size: 12px;
|
||
|
|
font-weight: 600;
|
||
|
|
color: #67c23a;
|
||
|
|
margin-bottom: 8px;
|
||
|
|
text-transform: uppercase;
|
||
|
|
}
|
||
|
|
|
||
|
|
.extracted-content {
|
||
|
|
font-size: 14px;
|
||
|
|
color: #262626;
|
||
|
|
line-height: 1.6;
|
||
|
|
white-space: pre-wrap;
|
||
|
|
word-break: break-all;
|
||
|
|
}
|
||
|
|
|
||
|
|
.empty-result {
|
||
|
|
text-align: center;
|
||
|
|
padding: 48px 0;
|
||
|
|
color: #bfbfbf;
|
||
|
|
}
|
||
|
|
|
||
|
|
.empty-icon {
|
||
|
|
font-size: 48px;
|
||
|
|
margin-bottom: 16px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.empty-hint {
|
||
|
|
font-size: 12px;
|
||
|
|
color: #d9d9d9;
|
||
|
|
}
|
||
|
|
|
||
|
|
.code-block {
|
||
|
|
background: #fafafa;
|
||
|
|
border: 1px solid #e8e8e8;
|
||
|
|
border-radius: 6px;
|
||
|
|
padding: 16px;
|
||
|
|
overflow: auto;
|
||
|
|
max-height: 400px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.code-block.small {
|
||
|
|
max-height: 200px;
|
||
|
|
padding: 12px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.code-block pre {
|
||
|
|
margin: 0;
|
||
|
|
font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
|
||
|
|
font-size: 13px;
|
||
|
|
line-height: 1.6;
|
||
|
|
color: #262626;
|
||
|
|
white-space: pre-wrap;
|
||
|
|
word-break: break-all;
|
||
|
|
}
|
||
|
|
|
||
|
|
.request-info {
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
gap: 16px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.info-row {
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
gap: 8px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.info-label {
|
||
|
|
font-size: 12px;
|
||
|
|
font-weight: 600;
|
||
|
|
color: #8c8c8c;
|
||
|
|
text-transform: uppercase;
|
||
|
|
}
|
||
|
|
|
||
|
|
.info-value {
|
||
|
|
font-size: 14px;
|
||
|
|
color: #262626;
|
||
|
|
}
|
||
|
|
|
||
|
|
.info-value.url {
|
||
|
|
font-family: 'SF Mono', monospace;
|
||
|
|
font-size: 13px;
|
||
|
|
word-break: break-all;
|
||
|
|
}
|
||
|
|
|
||
|
|
.info-value.method {
|
||
|
|
display: inline-block;
|
||
|
|
padding: 2px 8px;
|
||
|
|
background: #f0f0f0;
|
||
|
|
border-radius: 4px;
|
||
|
|
font-weight: 500;
|
||
|
|
}
|
||
|
|
|
||
|
|
.error-info {
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
gap: 16px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.error-message {
|
||
|
|
padding: 12px 16px;
|
||
|
|
background: #fff2f0;
|
||
|
|
border: 1px solid #ffccc7;
|
||
|
|
border-radius: 6px;
|
||
|
|
color: #cf1322;
|
||
|
|
}
|
||
|
|
|
||
|
|
:deep(.ant-tabs-nav) {
|
||
|
|
margin-bottom: 16px;
|
||
|
|
}
|
||
|
|
|
||
|
|
:deep(.ant-upload-list-picture-card-container),
|
||
|
|
:deep(.ant-upload-select-picture-card) {
|
||
|
|
width: 80px;
|
||
|
|
height: 80px;
|
||
|
|
}
|
||
|
|
|
||
|
|
:deep(.ant-form-item) {
|
||
|
|
margin-bottom: 16px;
|
||
|
|
}
|
||
|
|
|
||
|
|
:deep(.ant-input),
|
||
|
|
:deep(.ant-select-selector),
|
||
|
|
:deep(.ant-input-number) {
|
||
|
|
border-radius: 6px;
|
||
|
|
}
|
||
|
|
|
||
|
|
:deep(.ant-input:focus),
|
||
|
|
:deep(.ant-select-focused .ant-select-selector) {
|
||
|
|
border-color: #595959;
|
||
|
|
box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.05);
|
||
|
|
}
|
||
|
|
|
||
|
|
:deep(.ant-btn-primary) {
|
||
|
|
background: #262626;
|
||
|
|
border-color: #262626;
|
||
|
|
border-radius: 6px;
|
||
|
|
height: 40px;
|
||
|
|
padding: 0 32px;
|
||
|
|
}
|
||
|
|
|
||
|
|
:deep(.ant-btn-primary:hover) {
|
||
|
|
background: #434343;
|
||
|
|
border-color: #434343;
|
||
|
|
}
|
||
|
|
|
||
|
|
:deep(.ant-btn-primary:disabled) {
|
||
|
|
background: #d9d9d9;
|
||
|
|
border-color: #d9d9d9;
|
||
|
|
}
|
||
|
|
|
||
|
|
.code-block::-webkit-scrollbar {
|
||
|
|
width: 6px;
|
||
|
|
height: 6px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.code-block::-webkit-scrollbar-track {
|
||
|
|
background: #f0f0f0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.code-block::-webkit-scrollbar-thumb {
|
||
|
|
background: #d9d9d9;
|
||
|
|
border-radius: 3px;
|
||
|
|
}
|
||
|
|
</style>
|