feat: 分析模块、接口管理及其他功能优化
- 后端: WebMvcConfig/拦截器/AnalyticsService/Mapper/测试优化,新增 Knife4jConfig、AnalyticsDictionary、数据库迁移脚本 - 前端: 分析仪表盘 UI 优化、接口管理列表及详情测试面板 - 小程序: analytics 服务优化、request 增强 - 文档: 分析模块中文标签设计文档、品牌重命名设计文档 - 部署: conf 配置优化、deploy.py 脚本更新 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -13,13 +13,20 @@ export interface AnalyticsOverview {
|
||||
export interface AnalyticsTrendItem {
|
||||
bucket: string
|
||||
eventName: string
|
||||
eventLabel?: string
|
||||
count: number
|
||||
users: number
|
||||
}
|
||||
|
||||
export interface AnalyticsTopEvent {
|
||||
eventName: string
|
||||
eventLabel?: string
|
||||
eventType: string
|
||||
eventTypeLabel?: string
|
||||
pagePath?: string
|
||||
pageLabel?: string
|
||||
apiPath?: string
|
||||
apiLabel?: string
|
||||
count: number
|
||||
users: number
|
||||
}
|
||||
@@ -33,7 +40,9 @@ export interface AnalyticsFunnelItem {
|
||||
|
||||
export interface AnalyticsPreferenceItem {
|
||||
dimension: string
|
||||
dimensionLabel?: string
|
||||
value: string
|
||||
valueLabel?: string
|
||||
count: number
|
||||
users: number
|
||||
}
|
||||
|
||||
@@ -3,44 +3,44 @@
|
||||
<div class="dashboard-header">
|
||||
<div>
|
||||
<h2 class="page-title">行为分析</h2>
|
||||
<p class="page-subtitle">小程序访问、创作链路和朗读行为概览</p>
|
||||
<p class="page-subtitle">查看小程序访问、页面停留、点击操作、创作链路和接口调用情况</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
type="daterange"
|
||||
range-separator="-"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
value-format="YYYY-MM-DD"
|
||||
size="small"
|
||||
/>
|
||||
<el-button type="primary" size="small" :loading="loading" @click="fetchData">刷新</el-button>
|
||||
<el-button type="primary" size="small" :loading="loading" @click="fetchData">刷新数据</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-row :gutter="16" class="stats-row" v-loading="loading">
|
||||
<el-col v-for="card in cards" :key="card.label" :xs="12" :sm="8" :md="4">
|
||||
<el-card class="stat-card">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">{{ card.value }}</div>
|
||||
<div class="stat-label">{{ card.label }}</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="16" class="panel-row">
|
||||
<el-col :xs="24" :md="14">
|
||||
<el-card class="panel-card">
|
||||
<template #header>事件趋势</template>
|
||||
<el-card class="panel-card" shadow="never">
|
||||
<template #header>行为趋势</template>
|
||||
<div ref="trendChartRef" class="chart"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="24" :md="10">
|
||||
<el-card class="panel-card">
|
||||
<template #header>关键漏斗</template>
|
||||
<el-table :data="funnel" size="small" height="320">
|
||||
<el-card class="panel-card" shadow="never">
|
||||
<template #header>创作关键漏斗</template>
|
||||
<el-table :data="funnel" size="small" height="320" empty-text="暂无漏斗数据">
|
||||
<el-table-column prop="label" label="步骤" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column prop="users" label="用户" width="90" />
|
||||
<el-table-column prop="users" label="用户数" width="90" />
|
||||
<el-table-column label="转化率" width="100">
|
||||
<template #default="{ row }">{{ formatPercent(row.conversionRate) }}</template>
|
||||
</el-table-column>
|
||||
@@ -49,27 +49,53 @@
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="16" class="panel-row">
|
||||
<el-col :xs="24" :md="14">
|
||||
<el-card class="panel-card" shadow="never">
|
||||
<template #header>热门用户行为</template>
|
||||
<el-table :data="behaviorEvents" size="small" height="340" empty-text="暂无行为数据">
|
||||
<el-table-column prop="eventLabel" label="行为名称" min-width="170" show-overflow-tooltip />
|
||||
<el-table-column prop="pageLabel" label="所在页面" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column prop="eventTypeLabel" label="行为类型" width="110" />
|
||||
<el-table-column prop="count" label="次数" width="90" />
|
||||
<el-table-column prop="users" label="用户数" width="90" />
|
||||
</el-table>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="24" :md="10">
|
||||
<el-card class="panel-card" shadow="never">
|
||||
<template #header>接口调用情况</template>
|
||||
<el-table :data="apiEvents" size="small" height="340" empty-text="暂无接口调用数据">
|
||||
<el-table-column prop="apiLabel" label="接口说明" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column prop="eventLabel" label="调用结果" width="120" />
|
||||
<el-table-column prop="count" label="次数" width="90" />
|
||||
<el-table-column prop="users" label="用户数" width="90" />
|
||||
</el-table>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="16" class="panel-row">
|
||||
<el-col :xs="24" :md="12">
|
||||
<el-card class="panel-card">
|
||||
<template #header>热门事件</template>
|
||||
<el-table :data="topEvents" size="small" height="320">
|
||||
<el-table-column prop="eventName" label="事件" min-width="170" show-overflow-tooltip />
|
||||
<el-table-column prop="eventType" label="类型" width="110" />
|
||||
<el-table-column prop="count" label="次数" width="95" />
|
||||
<el-table-column prop="users" label="用户" width="95" />
|
||||
<el-card class="panel-card" shadow="never">
|
||||
<template #header>偏好分布</template>
|
||||
<el-table :data="preferences" size="small" height="320" empty-text="暂无偏好数据">
|
||||
<el-table-column prop="dimensionLabel" label="维度" width="130" show-overflow-tooltip />
|
||||
<el-table-column prop="valueLabel" label="偏好值" min-width="170" show-overflow-tooltip />
|
||||
<el-table-column prop="count" label="次数" width="90" />
|
||||
<el-table-column prop="users" label="用户数" width="90" />
|
||||
</el-table>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="24" :md="12">
|
||||
<el-card class="panel-card">
|
||||
<template #header>偏好分布</template>
|
||||
<el-table :data="preferences" size="small" height="320">
|
||||
<el-table-column prop="dimension" label="维度" width="110" show-overflow-tooltip />
|
||||
<el-table-column prop="value" label="值" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column prop="count" label="次数" width="95" />
|
||||
<el-table-column prop="users" label="用户" width="95" />
|
||||
</el-table>
|
||||
<el-card class="panel-card" shadow="never">
|
||||
<template #header>页面与操作说明</template>
|
||||
<div class="insight-list">
|
||||
<div v-for="item in insightItems" :key="item.title" class="insight-item">
|
||||
<div class="insight-title">{{ item.title }}</div>
|
||||
<div class="insight-desc">{{ item.desc }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
@@ -113,12 +139,30 @@ const trendChartRef = ref<HTMLDivElement>()
|
||||
let trendChart: echarts.ECharts | null = null
|
||||
|
||||
const cards = computed(() => [
|
||||
{ label: 'PV', value: overview.value.pv.toLocaleString() },
|
||||
{ label: 'UV', value: overview.value.uv.toLocaleString() },
|
||||
{ label: '事件数', value: overview.value.eventCount.toLocaleString() },
|
||||
{ label: '活跃用户', value: overview.value.activeUsers.toLocaleString() },
|
||||
{ label: '朗读请求', value: overview.value.ttsRequests.toLocaleString() },
|
||||
{ label: '平均停留', value: `${Math.round((overview.value.avgStayMs || 0) / 1000)}s` }
|
||||
{ label: '页面浏览量', value: overview.value.pv.toLocaleString() },
|
||||
{ label: '访问用户数', value: overview.value.uv.toLocaleString() },
|
||||
{ label: '行为事件数', value: overview.value.eventCount.toLocaleString() },
|
||||
{ label: '活跃用户数', value: overview.value.activeUsers.toLocaleString() },
|
||||
{ label: '朗读请求数', value: overview.value.ttsRequests.toLocaleString() },
|
||||
{ label: '平均停留时长', value: formatDuration(overview.value.avgStayMs || 0) }
|
||||
])
|
||||
|
||||
const behaviorEvents = computed(() => topEvents.value.filter(item => item.eventType !== 'api'))
|
||||
const apiEvents = computed(() => topEvents.value.filter(item => item.eventType === 'api' || item.eventName.startsWith('api_request_')))
|
||||
|
||||
const insightItems = computed(() => [
|
||||
{
|
||||
title: '行为名称',
|
||||
desc: '所有事件都显示为中文业务动作,例如“浏览页面”“提交创作愿望”“剧本生成成功”。'
|
||||
},
|
||||
{
|
||||
title: '所在页面',
|
||||
desc: '页面路径统一转换为小程序中的中文页面名称,例如“首页”“爽文生成页”“剧本详情页”。'
|
||||
},
|
||||
{
|
||||
title: '接口说明',
|
||||
desc: '接口调用按中文用途展示,例如“AI 流式生成”“创建朗读任务”“查询人生事件”。'
|
||||
}
|
||||
])
|
||||
|
||||
const queryParams = computed<AnalyticsQuery>(() => {
|
||||
@@ -132,6 +176,14 @@ const queryParams = computed<AnalyticsQuery>(() => {
|
||||
|
||||
const formatPercent = (value: number) => `${Math.round((value || 0) * 100)}%`
|
||||
|
||||
const formatDuration = (ms: number) => {
|
||||
const seconds = Math.round(ms / 1000)
|
||||
if (seconds < 60) return `${seconds}秒`
|
||||
const minutes = Math.floor(seconds / 60)
|
||||
const rest = seconds % 60
|
||||
return rest ? `${minutes}分${rest}秒` : `${minutes}分钟`
|
||||
}
|
||||
|
||||
const responseData = <T,>(response: unknown, fallback: T): T => {
|
||||
const payload = response as { data?: T }
|
||||
return payload.data ?? fallback
|
||||
@@ -143,8 +195,9 @@ const renderTrend = async () => {
|
||||
if (!trendChart) trendChart = echarts.init(trendChartRef.value)
|
||||
const buckets = [...new Set(trend.value.map(item => item.bucket))]
|
||||
const eventNames = [...new Set(trend.value.map(item => item.eventName))].slice(0, 5)
|
||||
const labelByEvent = new Map(trend.value.map(item => [item.eventName, item.eventLabel || item.eventName]))
|
||||
const series = eventNames.map(eventName => ({
|
||||
name: eventName,
|
||||
name: labelByEvent.get(eventName) || eventName,
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: buckets.map(bucket => trend.value.find(item => item.bucket === bucket && item.eventName === eventName)?.count || 0)
|
||||
@@ -167,7 +220,7 @@ const fetchData = async () => {
|
||||
const [overviewRes, trendRes, topRes, funnelRes, preferenceRes] = await Promise.all([
|
||||
getAnalyticsOverview(params),
|
||||
getAnalyticsTrend(params),
|
||||
getAnalyticsTopEvents({ ...params, limit: 20 }),
|
||||
getAnalyticsTopEvents({ ...params, limit: 60 }),
|
||||
getAnalyticsFunnel(params),
|
||||
getAnalyticsPreferences({ ...params, limit: 20 })
|
||||
])
|
||||
@@ -233,6 +286,12 @@ onBeforeUnmount(() => {
|
||||
|
||||
.stat-card {
|
||||
min-height: 96px;
|
||||
padding: 18px 20px;
|
||||
border: 1px solid var(--ls-glass-border);
|
||||
border-radius: var(--ls-radius-lg);
|
||||
background: rgba(15, 17, 26, 0.38);
|
||||
box-shadow: var(--ls-shadow);
|
||||
backdrop-filter: blur(20px) saturate(160%);
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
@@ -257,5 +316,31 @@ onBeforeUnmount(() => {
|
||||
height: 320px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.insight-list {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
min-height: 276px;
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
.insight-item {
|
||||
padding: 14px 16px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: var(--ls-radius-md);
|
||||
background: rgba(0, 0, 0, 0.16);
|
||||
}
|
||||
|
||||
.insight-title {
|
||||
color: var(--ls-text);
|
||||
font-weight: 700;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.insight-desc {
|
||||
color: rgba(226, 232, 240, 0.62);
|
||||
font-size: 13px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -65,15 +65,69 @@
|
||||
<el-form-item label="请求方法">
|
||||
<el-input v-model="testForm.method" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item v-for="param in (detail?.params || [])" :key="param.name" :label="param.name">
|
||||
<el-input v-model="testForm.params[param.name]" :placeholder="param.description || param.example || ''" />
|
||||
</el-form-item>
|
||||
<template v-for="param in queryParams" :key="param.name">
|
||||
<el-form-item :label="param.name">
|
||||
<!-- 枚举类型下拉 -->
|
||||
<el-select
|
||||
v-if="getParamInputType(param) === 'select'"
|
||||
v-model="testForm.params[param.name]"
|
||||
:placeholder="param.description || param.example || ''"
|
||||
clearable
|
||||
>
|
||||
<el-option
|
||||
v-for="opt in getEnumOptions(param)"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</el-select>
|
||||
<!-- 布尔类型开关 -->
|
||||
<el-switch
|
||||
v-else-if="getParamInputType(param) === 'switch'"
|
||||
v-model="testForm.params[param.name]"
|
||||
active-value="true"
|
||||
inactive-value="false"
|
||||
/>
|
||||
<!-- 数字类型 -->
|
||||
<el-input
|
||||
v-else-if="getParamInputType(param) === 'number'"
|
||||
v-model="testForm.params[param.name]"
|
||||
type="number"
|
||||
:placeholder="param.description || param.example || ''"
|
||||
/>
|
||||
<!-- 默认文本 -->
|
||||
<el-input
|
||||
v-else
|
||||
v-model="testForm.params[param.name]"
|
||||
:placeholder="param.description || param.example || ''"
|
||||
/>
|
||||
</el-form-item>
|
||||
</template>
|
||||
<el-form-item v-if="['POST', 'PUT', 'PATCH'].includes(testForm.method)" label="请求体">
|
||||
<el-input v-model="testForm.body" type="textarea" :rows="5" placeholder="JSON 格式" />
|
||||
</el-form-item>
|
||||
<div v-if="testForm.body" class="body-hint">
|
||||
<el-icon><InfoFilled /></el-icon>
|
||||
参数结构已预填充,修改值后点击"发送请求"
|
||||
</div>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<!-- 请求头 -->
|
||||
<el-card class="headers-card" style="margin-bottom: 16px">
|
||||
<template #header>请求头</template>
|
||||
<div class="headers-list">
|
||||
<div class="header-item">
|
||||
<span class="header-key">Authorization:</span>
|
||||
<span class="header-value">{{ currentToken ? `Bearer ${currentToken.slice(0, 20)}...` : '未设置' }}</span>
|
||||
</div>
|
||||
<div v-if="['POST', 'PUT', 'PATCH'].includes(testForm.method) && testForm.body" class="header-item">
|
||||
<span class="header-key">Content-Type:</span>
|
||||
<span class="header-value">application/json</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-button type="primary" @click="handleTest" :loading="testing">发送请求</el-button>
|
||||
|
||||
<!-- 响应结果 -->
|
||||
@@ -98,11 +152,13 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { InfoFilled } from '@element-plus/icons-vue'
|
||||
import { testEndpoint, type ApiEndpointDetail } from '@/api/endpoint'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
detail: ApiEndpointDetail | null
|
||||
defaultTab?: 'detail' | 'test'
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -127,6 +183,76 @@ const testForm = ref({
|
||||
body: ''
|
||||
})
|
||||
|
||||
const queryParams = computed(() => {
|
||||
return (props.detail?.params || []).filter(p => p.paramType === 'query' || p.paramType === 'path')
|
||||
})
|
||||
|
||||
const currentToken = computed(() => {
|
||||
if (tokenSource.value === 'admin') {
|
||||
return localStorage.getItem('adminToken') || ''
|
||||
}
|
||||
return manualToken.value
|
||||
})
|
||||
|
||||
const getParamInputType = (param: any): string => {
|
||||
if (param.enumValues) return 'select'
|
||||
const type = (param.paramTypeDef || '').toLowerCase()
|
||||
if (type === 'boolean') return 'switch'
|
||||
if (type === 'integer' || type === 'number') return 'number'
|
||||
return 'text'
|
||||
}
|
||||
|
||||
const getEnumOptions = (param: any): { label: string; value: string }[] => {
|
||||
if (!param.enumValues) return []
|
||||
return param.enumValues.split(',').map((v: string) => ({ label: v.trim(), value: v.trim() }))
|
||||
}
|
||||
|
||||
const generateJsonTemplate = (schema: any): any => {
|
||||
if (!schema || !schema.properties) return {}
|
||||
const result: any = {}
|
||||
for (const [key, prop] of Object.entries(schema.properties)) {
|
||||
const p = prop as any
|
||||
if (p.example && p.example !== '' && p.example !== 'null') {
|
||||
try { result[key] = JSON.parse(p.example) }
|
||||
catch { result[key] = p.example }
|
||||
} else if (p.type === 'string') {
|
||||
result[key] = ''
|
||||
} else if (p.type === 'integer') {
|
||||
result[key] = 0
|
||||
} else if (p.type === 'number') {
|
||||
result[key] = 0
|
||||
} else if (p.type === 'boolean') {
|
||||
result[key] = false
|
||||
} else if (p.type === 'array') {
|
||||
result[key] = []
|
||||
} else if (p.type === 'object' && p.properties) {
|
||||
result[key] = generateJsonTemplate(p)
|
||||
} else {
|
||||
result[key] = null
|
||||
}
|
||||
}
|
||||
const required = schema.required || []
|
||||
for (const k of Object.keys(result)) {
|
||||
if (!required.includes(k) && (result[k] === null || result[k] === '' || result[k] === 0 || result[k] === false || (Array.isArray(result[k]) && result[k].length === 0))) {
|
||||
delete result[k]
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
const generateRequestBody = () => {
|
||||
if (!props.detail?.requestSchema || props.detail.requestSchema === '{}') {
|
||||
testForm.value.body = ''
|
||||
return
|
||||
}
|
||||
try {
|
||||
const schema = JSON.parse(props.detail.requestSchema)
|
||||
testForm.value.body = JSON.stringify(generateJsonTemplate(schema), null, 2)
|
||||
} catch {
|
||||
testForm.value.body = ''
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props.detail, (ep) => {
|
||||
if (ep) {
|
||||
testForm.value.path = ep.path
|
||||
@@ -134,7 +260,8 @@ watch(() => props.detail, (ep) => {
|
||||
testForm.value.params = {}
|
||||
testForm.value.body = ''
|
||||
testResult.value = null
|
||||
activeTab.value = 'detail'
|
||||
generateRequestBody()
|
||||
activeTab.value = props.defaultTab || 'detail'
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
@@ -168,9 +295,9 @@ const handleTest = async () => {
|
||||
headers['Authorization'] = token.startsWith('Bearer ') ? token : `Bearer ${token}`
|
||||
}
|
||||
|
||||
const queryParams: Record<string, string> = {}
|
||||
const params: Record<string, string> = {}
|
||||
for (const [key, value] of Object.entries(testForm.value.params)) {
|
||||
if (value) queryParams[key] = value
|
||||
if (value) params[key] = value
|
||||
}
|
||||
|
||||
const res: any = await testEndpoint({
|
||||
@@ -178,7 +305,7 @@ const handleTest = async () => {
|
||||
path: '/api' + testForm.value.path,
|
||||
body: testForm.value.body || undefined,
|
||||
headers,
|
||||
params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
|
||||
params: Object.keys(params).length > 0 ? params : undefined,
|
||||
timeoutSeconds: 30
|
||||
})
|
||||
|
||||
@@ -232,4 +359,38 @@ const handleTest = async () => {
|
||||
font-family: 'Menlo', 'Monaco', monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.body-hint {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.headers-card {
|
||||
:deep(.el-card__header) {
|
||||
padding: 8px 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.headers-list {
|
||||
.header-item {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
font-family: 'Menlo', 'Monaco', monospace;
|
||||
|
||||
.header-key {
|
||||
color: var(--el-text-color-secondary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.header-value {
|
||||
color: var(--el-text-color-primary);
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -53,9 +53,10 @@
|
||||
<el-tag v-if="row.deprecated === 1" type="danger" size="small">废弃</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="100" align="center">
|
||||
<el-table-column label="操作" width="150" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link size="small" @click="showDetail(row)">详情</el-button>
|
||||
<el-button type="success" link size="small" @click="showTest(row)">测试</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -74,7 +75,7 @@
|
||||
</el-card>
|
||||
|
||||
<!-- 详情弹窗 -->
|
||||
<EndpointDetailDialog v-model="detailVisible" :detail="selectedDetail" />
|
||||
<EndpointDetailDialog v-model="detailVisible" :detail="selectedDetail" :default-tab="defaultTab" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -89,6 +90,7 @@ const loading = ref(false)
|
||||
const syncing = ref(false)
|
||||
const detailVisible = ref(false)
|
||||
const selectedDetail = ref<ApiEndpointDetail | null>(null)
|
||||
const defaultTab = ref<'detail' | 'test'>('detail')
|
||||
|
||||
const searchForm = reactive({
|
||||
keyword: '',
|
||||
@@ -149,6 +151,7 @@ const handleSync = async () => {
|
||||
|
||||
const showDetail = async (row: ApiEndpointItem) => {
|
||||
selectedDetail.value = null
|
||||
defaultTab.value = 'detail'
|
||||
detailVisible.value = true
|
||||
try {
|
||||
const res: any = await getEndpointDetail(row.operationId)
|
||||
@@ -158,6 +161,18 @@ const showDetail = async (row: ApiEndpointItem) => {
|
||||
}
|
||||
}
|
||||
|
||||
const showTest = async (row: ApiEndpointItem) => {
|
||||
selectedDetail.value = null
|
||||
detailVisible.value = true
|
||||
defaultTab.value = 'test'
|
||||
try {
|
||||
const res: any = await getEndpointDetail(row.operationId)
|
||||
selectedDetail.value = res.data
|
||||
} catch {
|
||||
ElMessage.error('加载详情失败')
|
||||
}
|
||||
}
|
||||
|
||||
const getMethodType = (method: string): string => {
|
||||
const types: Record<string, string> = {
|
||||
GET: 'success',
|
||||
|
||||
Reference in New Issue
Block a user