refactor: 适配校验页重构为顶栏选配置+自动批量解析+URL卡片内展示提取数据
This commit is contained in:
@@ -88,6 +88,7 @@ export default {
|
||||
},
|
||||
adapter: {
|
||||
parse: (configId, accessResultId) => api.post('/admin/adapter/parse', { configId, accessResultId }),
|
||||
parseBatch: (configId, siteId) => api.post('/admin/adapter/parse-batch', { configId, siteId }),
|
||||
},
|
||||
health: () => api.get('/health', { baseURL: '' })
|
||||
}
|
||||
|
||||
@@ -1,473 +1,314 @@
|
||||
<template>
|
||||
<div>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
|
||||
<!-- 顶栏 -->
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
|
||||
<h2 style="margin:0">适配校验:{{ siteId }}</h2>
|
||||
<el-button @click="$router.push('/access-results')">← 返回列表</el-button>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;gap:16px;height:calc(100vh - 200px)">
|
||||
<!-- 左侧:适配配置面板 -->
|
||||
<el-card style="flex:0 0 380px;overflow:auto" shadow="never">
|
||||
<el-card shadow="never" style="margin-bottom:12px">
|
||||
<div style="display:flex;gap:16px;align-items:center;flex-wrap:wrap">
|
||||
<!-- 配置版本选择 -->
|
||||
<el-select v-model="selectedConfigId" placeholder="选择配置版本" @change="onConfigChange" style="width:260px">
|
||||
<el-option v-for="v in allConfigVersions" :key="v.id" :label="'v'+v.version + (v.status==='pending'?' [待审]':'') + (v.isLatest?' [当前]':'')" :value="v.id" />
|
||||
</el-select>
|
||||
|
||||
<el-divider direction="vertical" />
|
||||
|
||||
<span class="text-xs"><strong>{{ pages.length }}</strong> 页面</span>
|
||||
<span class="text-xs"><strong>{{ totalResults }}</strong> 次提交</span>
|
||||
<el-tag size="small" type="success">{{ validCount }} 有效</el-tag>
|
||||
<el-tag size="small" type="danger">{{ invalidCount }} 无效</el-tag>
|
||||
<el-tag size="small" type="warning">{{ uncheckedCount }} 待验</el-tag>
|
||||
|
||||
<el-divider direction="vertical" />
|
||||
|
||||
<el-button size="small" @click="refresh" :loading="loading">刷新</el-button>
|
||||
|
||||
<el-button size="small" type="primary" @click="toggleConfigPanel" v-if="selectedConfigId">
|
||||
{{ showConfig ? '收起配置' : '查看配置' }}
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 配置编辑面板(可折叠) -->
|
||||
<div v-if="showConfig && selectedConfig" style="margin-top:12px;padding-top:12px;border-top:1px solid #ebeef5">
|
||||
<div style="display:flex;gap:8px;align-items:center;margin-bottom:8px">
|
||||
<span class="text-xs">v{{ selectedConfig.version }}</span>
|
||||
<el-tag size="small" :type="selectedConfig.status==='approved'?'success':'warning'">
|
||||
{{ selectedConfig.status==='approved'?'已批准':'待审核' }}
|
||||
</el-tag>
|
||||
<template v-if="!editing">
|
||||
<el-button size="small" type="primary" text @click="reloadBatchParse" :loading="parseLoading">重新解析</el-button>
|
||||
<template v-if="selectedConfig.status==='pending'">
|
||||
<el-button size="small" type="success" @click="approveConfig" :loading="approving">审批通过</el-button>
|
||||
<el-button size="small" type="danger" @click="rejectConfig" :loading="rejecting">拒绝</el-button>
|
||||
<el-button size="small" @click="startEdit">编辑</el-button>
|
||||
</template>
|
||||
</template>
|
||||
<div v-else style="display:flex;gap:6px">
|
||||
<el-button size="small" type="primary" @click="saveEdit" :loading="saving">保存</el-button>
|
||||
<el-button size="small" @click="cancelEdit">取消</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-input v-if="editing" v-model="editContent" type="textarea" :rows="10" style="font-family:monospace;font-size:11px" />
|
||||
<pre v-else class="config-json" v-html="highlightedConfig"></pre>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 解析加载中 -->
|
||||
<div v-if="parseLoading" style="text-align:center;padding:40px;color:#909399">
|
||||
<el-icon class="is-loading" :size="24"><Loading /></el-icon>
|
||||
<div style="margin-top:8px">正在解析各页面适配结果...</div>
|
||||
</div>
|
||||
|
||||
<!-- 页面卡片列表 -->
|
||||
<div v-else style="display:flex;flex-direction:column;gap:8px">
|
||||
<el-card v-for="page in pages" :key="page.url" shadow="hover" size="small">
|
||||
<template #header>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:8px">
|
||||
<span><strong>适配配置</strong></span>
|
||||
<span v-if="selectedConfig" class="text-xs" style="color:#909399">
|
||||
v{{ selectedConfig.version }}
|
||||
<el-tag size="small" :type="selectedConfig.status === 'approved' ? 'success' : 'warning'">
|
||||
{{ selectedConfig.status === 'approved' ? '已批准' : '待审核' }}
|
||||
<div style="display:flex;align-items:center;gap:8px">
|
||||
<strong style="font-size:14px;word-break:break-all">{{ page.url }}</strong>
|
||||
<el-tag v-if="page.statusCode" size="small" :type="statusTag(page.statusCode)">{{ page.statusCode }}</el-tag>
|
||||
<span v-if="page.responseTimeMs!=null" class="text-xs" style="color:#909399">{{ page.responseTimeMs }}ms</span>
|
||||
<el-tag size="small" type="info">×{{ page.totalCount }}</el-tag>
|
||||
</div>
|
||||
<div style="display:flex;gap:6px;align-items:center">
|
||||
<!-- 解析状态 -->
|
||||
<el-tag v-if="page.parsed?.matched" size="small" type="success">
|
||||
{{ page.parsed.pageName }} · {{ page.parsed.rowCount }}行
|
||||
</el-tag>
|
||||
<el-tag v-else-if="page.parsed" size="small" type="warning">未匹配</el-tag>
|
||||
<el-tag v-else size="small" type="info">未解析</el-tag>
|
||||
|
||||
<el-button v-if="page.uncheckedCount>0" size="small" type="success" @click="batchValidate(page.url,true)">全部有效</el-button>
|
||||
<el-button v-if="page.uncheckedCount>0" size="small" type="danger" @click="batchValidate(page.url,false)">全部无效</el-button>
|
||||
<el-button size="small" @click="toggleView(page)">查看</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 展开内容 -->
|
||||
<template v-if="expandedSet.has(page.url)">
|
||||
<!-- 解析结果表格 -->
|
||||
<div v-if="page.parsed?.matched && page.parsed.extracted?.length" style="margin-bottom:8px">
|
||||
<el-table :data="page.parsed.extracted" border stripe size="small" max-height="300">
|
||||
<el-table-column type="index" label="#" width="45" />
|
||||
<el-table-column v-for="col in Object.keys(page.parsed.extracted[0])" :key="col"
|
||||
:prop="col" :label="col" min-width="100" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
<span :style="scope.row[col]==null?'color:#c0c4cc':''">{{ scope.row[col] ?? '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div v-else style="color:#e6a23c;font-size:12px;margin-bottom:8px">
|
||||
{{ page.parsed?.message || '暂无提取数据' }}
|
||||
</div>
|
||||
|
||||
<!-- 历史记录 -->
|
||||
<div v-if="page.history?.length > 1" style="margin-top:8px">
|
||||
<span class="text-xs" style="color:#909399">历史记录:</span>
|
||||
<span v-for="(h,i) in page.history" :key="h.id"
|
||||
class="text-xs" style="color:#909399;margin-left:6px">
|
||||
{{ formatTime(h.createdAt) }}
|
||||
<el-tag size="small" :type="h.isValid===true?'success':h.isValid===false?'danger':'info'">
|
||||
{{ h.isValid===true?'✓':h.isValid===false?'✗':'?' }}
|
||||
</el-tag>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 版本选择 -->
|
||||
<div style="margin-bottom:12px">
|
||||
<el-tabs v-model="configTab" @tab-change="onConfigTabChange" type="card" size="small">
|
||||
<el-tab-pane v-for="v in allConfigVersions" :key="v.id"
|
||||
:label="'v' + v.version" :name="String(v.id)">
|
||||
<template #label>
|
||||
<span style="display:flex;align-items:center;gap:4px">
|
||||
v{{ v.version }}
|
||||
<el-tag v-if="v.status === 'pending'" size="small" type="warning" style="margin-left:2px">待审</el-tag>
|
||||
<el-tag v-else-if="v.isLatest" size="small" type="success" style="margin-left:2px">当前</el-tag>
|
||||
</span>
|
||||
</template>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
|
||||
<div v-if="configLoading" style="text-align:center;padding:20px;color:#909399">加载配置中...</div>
|
||||
<div v-else-if="!selectedConfig" style="text-align:center;padding:20px;color:#909399">暂无配置</div>
|
||||
<template v-else>
|
||||
<!-- 编辑模式 -->
|
||||
<div v-if="editing">
|
||||
<el-input v-model="editContent" type="textarea" :rows="18" style="font-family:monospace;font-size:11px" />
|
||||
<div style="display:flex;gap:8px;margin-top:8px;flex-wrap:wrap">
|
||||
<el-button size="small" type="primary" @click="saveEdit" :loading="saving">保存</el-button>
|
||||
<el-button size="small" @click="cancelEdit">取消</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 只读模式 -->
|
||||
<div v-else>
|
||||
<pre v-if="selectedConfigJson" class="config-json" v-html="highlightedConfig"></pre>
|
||||
<!-- 待审核配置操作栏 -->
|
||||
<div v-if="selectedConfig.status === 'pending'" style="display:flex;gap:8px;margin-top:10px;flex-wrap:wrap">
|
||||
<el-button size="small" type="success" @click="approveConfig" :loading="approving">审批通过</el-button>
|
||||
<el-button size="small" type="danger" @click="rejectConfig" :loading="rejecting">拒绝</el-button>
|
||||
<el-button size="small" @click="startEdit">编辑</el-button>
|
||||
</div>
|
||||
<!-- 已批准配置操作栏 -->
|
||||
<div v-else style="display:flex;gap:8px;margin-top:10px">
|
||||
<el-button size="small" @click="startEdit" v-if="selectedConfig.status === 'pending'">编辑</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-card>
|
||||
|
||||
<!-- 右侧:访问结果 -->
|
||||
<div style="flex:1;overflow:auto;display:flex;flex-direction:column;gap:8px">
|
||||
<!-- 统计 -->
|
||||
<el-card shadow="never" size="small">
|
||||
<div style="display:flex;gap:16px;align-items:center;flex-wrap:wrap">
|
||||
<span class="text-xs"><strong>页面数:</strong>{{ groupedPages.length }}</span>
|
||||
<span class="text-xs"><strong>总次数:</strong>{{ allResults.length }}</span>
|
||||
<el-tag size="small" type="success">{{ validCount }} 有效</el-tag>
|
||||
<el-tag size="small" type="danger">{{ invalidCount }} 无效</el-tag>
|
||||
<el-tag size="small" type="warning">{{ uncheckedCount }} 待验</el-tag>
|
||||
<el-button size="small" @click="refresh" :loading="loading">刷新</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 页面列表 -->
|
||||
<el-card v-for="page in groupedPages" :key="page.url" shadow="never" size="small">
|
||||
<template #header>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:8px">
|
||||
<div style="display:flex;align-items:center;gap:10px">
|
||||
<strong style="font-size:14px;word-break:break-all">{{ page.url }}</strong>
|
||||
<el-tag size="small" :type="page.hasUnchecked ? 'warning' : 'info'">
|
||||
{{ page.visibleCount }}/{{ page.totalCount }}
|
||||
</el-tag>
|
||||
<el-tag v-if="page.allValid" size="small" type="success">全部有效</el-tag>
|
||||
<el-tag v-else-if="page.allInvalid" size="small" type="danger">全部无效</el-tag>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px">
|
||||
<el-button v-if="page.totalCount > 1" size="small" text @click="toggleExpand(page.url)">
|
||||
{{ expandedUrls.has(page.url) ? '收起' : `显示全部(${page.totalCount}条)` }}
|
||||
</el-button>
|
||||
<el-button v-if="page.hasUnchecked" size="small" type="success" @click="batchValidate(page.url, true)">全部有效</el-button>
|
||||
<el-button v-if="page.hasUnchecked" size="small" type="danger" @click="batchValidate(page.url, false)">全部无效</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-for="(r, ri) in page.visibleResults" :key="r.id"
|
||||
style="padding:6px 0;display:flex;justify-content:space-between;align-items:center"
|
||||
:style="ri < page.visibleResults.length - 1 ? 'border-bottom:1px solid #ebeef5' : ''">
|
||||
<div style="display:flex;align-items:center;gap:10px;flex:1;min-width:0">
|
||||
<el-tag v-if="r.statusCode" size="small" :type="statusTag(r.statusCode)">{{ r.statusCode }}</el-tag>
|
||||
<span v-if="r.responseTimeMs != null" class="text-xs" style="color:#909399">{{ r.responseTimeMs }}ms</span>
|
||||
<span class="text-xs" style="color:#909399">{{ formatTime(r.createdAt) }}</span>
|
||||
<span v-if="r.clientId" class="text-xs" style="color:#909399;max-width:80px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{{ r.clientId }}</span>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:6px">
|
||||
<el-tag v-if="r.isValid === true" size="small" type="success">有效</el-tag>
|
||||
<el-tag v-else-if="r.isValid === false" size="small" type="danger">无效</el-tag>
|
||||
<el-button size="small" @click="viewContent(r)">查看</el-button>
|
||||
<el-button size="small" type="primary" :disabled="!selectedConfig" @click="parseResult(r)">解析</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!expandedUrls.has(page.url) && page.totalCount > 5" style="padding:2px 0;color:#909399;font-size:12px;text-align:center">
|
||||
还有 {{ page.totalCount - 5 }} 条更早记录
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<div v-if="groupedPages.length === 0 && !loading" style="text-align:center;padding:40px;color:#909399">
|
||||
暂无访问结果
|
||||
</div>
|
||||
<div v-if="pages.length===0 && !loading && !parseLoading" style="text-align:center;padding:60px;color:#909399">
|
||||
暂无访问结果
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 查看内容弹窗 -->
|
||||
<el-dialog v-model="contentVisible" title="访问内容" width="900px" :close-on-click-modal="false">
|
||||
<el-descriptions :column="2" border size="small" style="margin-bottom:12px">
|
||||
<el-descriptions-item label="URL">{{ viewingResult.url || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="HTTP状态">
|
||||
<el-tag v-if="viewingResult.statusCode" size="small" :type="statusTag(viewingResult.statusCode)">{{ viewingResult.statusCode }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="响应耗时">{{ viewingResult.responseTimeMs != null ? viewingResult.responseTimeMs + 'ms' : '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="时间">{{ viewingResult.createdAt }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<div v-if="contentLoading" style="text-align:center;padding:40px">加载中...</div>
|
||||
<pre v-else-if="viewingContentType === 'json'" class="content-json" v-html="highlightedViewJson"></pre>
|
||||
<pre v-else class="content-html" v-html="highlightedViewHtml"></pre>
|
||||
<template #footer>
|
||||
<el-button @click="contentVisible = false">关闭</el-button>
|
||||
<el-button v-if="viewingResult.isValid == null" type="success" size="small" @click="singleValidate(viewingResult.id, true)">有效</el-button>
|
||||
<el-button v-if="viewingResult.isValid == null" type="danger" size="small" @click="singleValidate(viewingResult.id, false)">无效</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 适配解析结果弹窗 -->
|
||||
<el-dialog v-model="parseVisible" title="适配解析结果" width="850px" :close-on-click-modal="false">
|
||||
<div v-if="parseLoading" style="text-align:center;padding:40px">解析中...</div>
|
||||
<div v-else-if="parseError" style="color:#f56c6c;padding:10px">{{ parseError }}</div>
|
||||
<template v-else>
|
||||
<el-descriptions :column="3" border size="small" style="margin-bottom:12px">
|
||||
<el-descriptions-item label="匹配页面">{{ parseData.pageName || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="提取行数">{{ parseData.rowCount ?? 0 }}</el-descriptions-item>
|
||||
<el-descriptions-item label="字段数">{{ parseData.fieldCount ?? 0 }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<div v-if="!parseData.matched" style="color:#e6a23c;margin-bottom:8px">
|
||||
未匹配到页面规则。可用页面: {{ (parseData.availablePages || []).join(', ') || '无' }}
|
||||
</div>
|
||||
<el-table v-if="parseData.extracted?.length" :data="parseData.extracted" border stripe size="small" max-height="400">
|
||||
<el-table-column type="index" label="#" width="50" />
|
||||
<el-table-column v-for="col in parseColumns" :key="col" :prop="col" :label="col" min-width="120" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
<span :style="scope.row[col] == null ? 'color:#909399' : ''">{{ scope.row[col] ?? 'null' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div v-else-if="parseData.matched" style="text-align:center;padding:20px;color:#909399">
|
||||
无提取结果(HTML 内容可能不包含匹配元素)
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
<el-button @click="parseVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useRoute } from 'vue-router'
|
||||
import api from '../api'
|
||||
import { Loading } from '@element-plus/icons-vue'
|
||||
|
||||
const route = useRoute()
|
||||
const siteId = route.params.siteId
|
||||
|
||||
const loading = ref(false)
|
||||
const allResults = ref([])
|
||||
|
||||
const allConfigVersions = ref([])
|
||||
const parseLoading = ref(false)
|
||||
const selectedConfigId = ref(null)
|
||||
const selectedConfig = ref(null)
|
||||
const selectedConfigJson = ref('')
|
||||
const configTab = ref('')
|
||||
const configLoading = ref(true)
|
||||
const showConfig = ref(false)
|
||||
const editing = ref(false)
|
||||
const editContent = ref('')
|
||||
const saving = ref(false)
|
||||
const approving = ref(false)
|
||||
const rejecting = ref(false)
|
||||
|
||||
const contentVisible = ref(false)
|
||||
const contentLoading = ref(false)
|
||||
const viewingResult = ref({})
|
||||
const viewingContentType = ref('html')
|
||||
const viewingContent = ref('')
|
||||
const allConfigVersions = ref([])
|
||||
const allResults = ref([])
|
||||
const pages = ref([])
|
||||
const expandedSet = reactive(new Set())
|
||||
|
||||
const parseVisible = ref(false)
|
||||
const parseLoading = ref(false)
|
||||
const parseError = ref('')
|
||||
const parseData = ref({})
|
||||
const totalResults = computed(() => allResults.value.length)
|
||||
const validCount = computed(() => allResults.value.filter(r => r.isValid===true).length)
|
||||
const invalidCount = computed(() => allResults.value.filter(r => r.isValid===false).length)
|
||||
const uncheckedCount = computed(() => allResults.value.filter(r => r.isValid==null).length)
|
||||
|
||||
const expandedUrls = reactive(new Set())
|
||||
const MAX_VISIBLE = 5
|
||||
|
||||
const toggleExpand = (url) => {
|
||||
if (expandedUrls.has(url)) expandedUrls.delete(url)
|
||||
else expandedUrls.add(url)
|
||||
}
|
||||
|
||||
const groupedPages = computed(() => {
|
||||
const map = new Map()
|
||||
for (const r of allResults.value) {
|
||||
const key = r.url || '(无URL)'
|
||||
if (!map.has(key)) map.set(key, [])
|
||||
map.get(key).push(r)
|
||||
}
|
||||
return Array.from(map.entries()).map(([url, results]) => {
|
||||
const allValid = results.length > 0 && results.every(r => r.isValid === true)
|
||||
const allInvalid = results.length > 0 && results.every(r => r.isValid === false)
|
||||
const hasUnchecked = results.some(r => r.isValid == null)
|
||||
const totalCount = results.length
|
||||
const visibleResults = expandedUrls.has(url) ? results.slice(0, MAX_VISIBLE) : results.slice(0, 1)
|
||||
return { url, results, totalCount, visibleResults, visibleCount: visibleResults.length, allValid, allInvalid, hasUnchecked }
|
||||
})
|
||||
})
|
||||
|
||||
const validCount = computed(() => allResults.value.filter(r => r.isValid === true).length)
|
||||
const invalidCount = computed(() => allResults.value.filter(r => r.isValid === false).length)
|
||||
const uncheckedCount = computed(() => allResults.value.filter(r => r.isValid == null).length)
|
||||
|
||||
const refresh = async () => {
|
||||
loading.value = true
|
||||
try { const r = await api.accessResults.bySite(siteId); allResults.value = r.data || [] }
|
||||
catch { ElMessage.error('获取访问结果失败') }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
|
||||
// 加载所有配置版本(已批准 + 待审核)
|
||||
// 加载配置版本列表
|
||||
const loadConfigs = async () => {
|
||||
configLoading.value = true
|
||||
try {
|
||||
const [approvedRes, pendingRes] = await Promise.allSettled([
|
||||
const [ar, pr] = await Promise.allSettled([
|
||||
api.configs.bySite(siteId),
|
||||
api.adminConfigs.pendingBySite(siteId)
|
||||
])
|
||||
const approved = approvedRes.status === 'fulfilled' ? (approvedRes.value.data || []) : []
|
||||
const pending = pendingRes.status === 'fulfilled' ? (pendingRes.value.data || []) : []
|
||||
// 合并:待审核在前,已批准在后
|
||||
const approved = ar.status==='fulfilled' ? (ar.value.data||[]) : []
|
||||
const pending = pr.status==='fulfilled' ? (pr.value.data||[]) : []
|
||||
allConfigVersions.value = [...pending, ...approved]
|
||||
} catch {
|
||||
allConfigVersions.value = []
|
||||
} finally {
|
||||
configLoading.value = false
|
||||
// 默认选中第一个待审核,否则第一个已批准
|
||||
if (allConfigVersions.value.length) {
|
||||
const first = allConfigVersions.value[0]
|
||||
configTab.value = String(first.id)
|
||||
selectConfig(first)
|
||||
}
|
||||
}
|
||||
if (allConfigVersions.value.length) selectedConfigId.value = allConfigVersions.value[0].id
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const onConfigTabChange = (tabName) => {
|
||||
const cfg = allConfigVersions.value.find(v => String(v.id) === tabName)
|
||||
if (cfg) selectConfig(cfg)
|
||||
}
|
||||
|
||||
const selectConfig = async (cfg) => {
|
||||
// 选中配置 → 更新 selectedConfig + 自动批量解析
|
||||
const onConfigChange = async () => {
|
||||
const cfg = allConfigVersions.value.find(v => v.id===selectedConfigId.value)
|
||||
if (!cfg) return
|
||||
selectedConfig.value = cfg
|
||||
editing.value = false
|
||||
|
||||
// 加载配置内容
|
||||
try {
|
||||
if (cfg.status === 'pending') {
|
||||
if (cfg.status==='pending') {
|
||||
const r = await api.adminConfigs.getContent(cfg.id)
|
||||
selectedConfigJson.value = r.data?.content || ''
|
||||
selectedConfigJson.value = r.data?.content||''
|
||||
} else {
|
||||
const r = await api.configs.content(siteId, cfg.version)
|
||||
selectedConfigJson.value = typeof r.data === 'string' ? r.data : JSON.stringify(r.data || {}, null, 2)
|
||||
selectedConfigJson.value = typeof r.data==='string' ? r.data : JSON.stringify(r.data||{},null,2)
|
||||
}
|
||||
} catch {
|
||||
selectedConfigJson.value = ''
|
||||
}
|
||||
} catch { selectedConfigJson.value = '' }
|
||||
|
||||
// 自动批量解析
|
||||
await doBatchParse()
|
||||
}
|
||||
|
||||
const startEdit = () => {
|
||||
editContent.value = selectedConfigJson.value
|
||||
editing.value = true
|
||||
const doBatchParse = async () => {
|
||||
if (!selectedConfigId.value) return
|
||||
parseLoading.value = true
|
||||
try {
|
||||
const r = await api.adapter.parseBatch(selectedConfigId.value, siteId)
|
||||
const data = r.data || {}
|
||||
pages.value = (data.pages || []).map(p => ({
|
||||
...p,
|
||||
history: allResults.value.filter(r => r.url===p.url).slice(0, 5)
|
||||
}))
|
||||
} catch { ElMessage.error('批量解析失败') }
|
||||
finally { parseLoading.value = false }
|
||||
}
|
||||
|
||||
const cancelEdit = () => { editing.value = false }
|
||||
const reloadBatchParse = () => doBatchParse()
|
||||
|
||||
const refresh = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const r = await api.accessResults.bySite(siteId)
|
||||
allResults.value = r.data||[]
|
||||
// 刷新解析数据
|
||||
if (selectedConfigId.value) {
|
||||
const data = (await api.adapter.parseBatch(selectedConfigId.value, siteId)).data||{}
|
||||
pages.value = (data.pages||[]).map(p => ({
|
||||
...p,
|
||||
history: allResults.value.filter(r => r.url===p.url).slice(0,5)
|
||||
}))
|
||||
}
|
||||
} catch { ElMessage.error('获取访问结果失败') }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
|
||||
// 编辑配置
|
||||
const startEdit = () => { editContent.value=selectedConfigJson.value; editing.value=true }
|
||||
const cancelEdit = () => { editing.value=false }
|
||||
const saveEdit = async () => {
|
||||
if (!selectedConfig.value) return
|
||||
saving.value = true
|
||||
try {
|
||||
await api.adminConfigs.updateContent(selectedConfig.value.id, editContent.value)
|
||||
selectedConfigJson.value = editContent.value
|
||||
editing.value = false
|
||||
ElMessage.success('配置已保存')
|
||||
} catch (e) { ElMessage.error(e.response?.data?.message || '保存失败') }
|
||||
selectedConfigJson.value = editContent.value; editing.value = false
|
||||
ElMessage.success('已保存'); doBatchParse()
|
||||
} catch(e) { ElMessage.error(e.response?.data?.message||'保存失败') }
|
||||
finally { saving.value = false }
|
||||
}
|
||||
|
||||
const approveConfig = async () => {
|
||||
if (!selectedConfig.value) return
|
||||
try {
|
||||
await ElMessageBox.confirm('确认审批通过?通过后版本号将自动加1。', '审批配置', { confirmButtonText: '确认' })
|
||||
} catch { return }
|
||||
try { await ElMessageBox.confirm('确认审批?版本号将自动+1','审批配置',{confirmButtonText:'确认'}) } catch{return}
|
||||
approving.value = true
|
||||
try {
|
||||
await api.adminConfigs.approve(selectedConfig.value.id)
|
||||
ElMessage.success('配置已审批通过')
|
||||
loadConfigs()
|
||||
} catch (e) { ElMessage.error(e.response?.data?.message || '审批失败') }
|
||||
try { await api.adminConfigs.approve(selectedConfig.value.id); ElMessage.success('已审批'); loadConfigs() }
|
||||
catch(e) { ElMessage.error(e.response?.data?.message||'审批失败') }
|
||||
finally { approving.value = false }
|
||||
}
|
||||
|
||||
const rejectConfig = async () => {
|
||||
if (!selectedConfig.value) return
|
||||
try {
|
||||
await ElMessageBox.confirm('确认拒绝该配置?', '拒绝配置', { confirmButtonText: '确认', type: 'warning' })
|
||||
} catch { return }
|
||||
try { await ElMessageBox.confirm('确认拒绝?','拒绝配置',{confirmButtonText:'确认',type:'warning'}) } catch{return}
|
||||
rejecting.value = true
|
||||
try {
|
||||
await api.adminConfigs.reject(selectedConfig.value.id, '')
|
||||
ElMessage.success('配置已拒绝')
|
||||
loadConfigs()
|
||||
} catch (e) { ElMessage.error(e.response?.data?.message || '拒绝失败') }
|
||||
try { await api.adminConfigs.reject(selectedConfig.value.id,''); ElMessage.success('已拒绝'); loadConfigs() }
|
||||
catch(e) { ElMessage.error(e.response?.data?.message||'拒绝失败') }
|
||||
finally { rejecting.value = false }
|
||||
}
|
||||
|
||||
const toggleConfigPanel = () => showConfig.value = !showConfig.value
|
||||
const toggleView = (page) => {
|
||||
if (expandedSet.has(page.url)) expandedSet.delete(page.url)
|
||||
else expandedSet.add(page.url)
|
||||
}
|
||||
|
||||
const batchValidate = async (url, isValid) => {
|
||||
try {
|
||||
const r = await api.accessResults.batchValidate(siteId, url, isValid)
|
||||
ElMessage.success(`已标记 ${r.data?.affected || 0} 条为${isValid ? '有效' : '无效'}`)
|
||||
ElMessage.success(`已标记 ${r.data?.affected||0} 条为${isValid?'有效':'无效'}`)
|
||||
refresh()
|
||||
} catch { ElMessage.error('批量校验失败') }
|
||||
}
|
||||
|
||||
const singleValidate = async (id, isValid) => {
|
||||
try {
|
||||
await api.accessResults.validate(id, isValid)
|
||||
ElMessage.success(`已标记为${isValid ? '有效' : '无效'}`)
|
||||
contentVisible.value = false
|
||||
refresh()
|
||||
} catch { ElMessage.error('校验失败') }
|
||||
}
|
||||
|
||||
const parseColumns = computed(() => {
|
||||
if (!parseData.value.extracted?.length) return []
|
||||
return Object.keys(parseData.value.extracted[0])
|
||||
})
|
||||
|
||||
const parseResult = async (row) => {
|
||||
if (!selectedConfig.value) { ElMessage.warning('请先选择适配配置'); return }
|
||||
parseVisible.value = true
|
||||
parseLoading.value = true
|
||||
parseError.value = ''
|
||||
parseData.value = {}
|
||||
try {
|
||||
const r = await api.adapter.parse(selectedConfig.value.id, row.id)
|
||||
parseData.value = r.data || {}
|
||||
} catch (e) {
|
||||
parseError.value = e.response?.data?.message || '解析失败'
|
||||
} finally { parseLoading.value = false }
|
||||
}
|
||||
|
||||
const viewContent = async (row) => {
|
||||
viewingResult.value = { ...row }
|
||||
viewingContentType.value = row.contentType || 'html'
|
||||
contentVisible.value = true
|
||||
contentLoading.value = true
|
||||
viewingContent.value = ''
|
||||
try {
|
||||
const r = await api.accessResults.get(row.id)
|
||||
viewingResult.value = r.data || {}
|
||||
viewingContent.value = r.data?.content || ''
|
||||
} catch { ElMessage.error('获取内容失败'); viewingContent.value = '加载失败' }
|
||||
finally { contentLoading.value = false }
|
||||
}
|
||||
|
||||
const formatTime = (t) => t ? t.substring(5, 16) : '-'
|
||||
const formatTime = (t) => t ? t.substring(5,16) : '-'
|
||||
const statusTag = (code) => {
|
||||
if (code >= 200 && code < 300) return 'success'
|
||||
if (code >= 300 && code < 400) return 'warning'
|
||||
if (code>=200&&code<300) return 'success'
|
||||
if (code>=300&&code<400) return 'warning'
|
||||
return 'danger'
|
||||
}
|
||||
|
||||
// JSON 语法高亮
|
||||
// 配置语法高亮
|
||||
const highlightedConfig = computed(() => {
|
||||
try { return syntaxHighlight(JSON.stringify(JSON.parse(selectedConfigJson.value), null, 2)) }
|
||||
catch { return escapeHtml(selectedConfigJson.value || '') }
|
||||
try { return syntaxHighlight(JSON.stringify(JSON.parse(selectedConfigJson.value),null,2)) }
|
||||
catch { return escapeHtml(selectedConfigJson.value||'') }
|
||||
})
|
||||
const highlightedViewJson = computed(() => {
|
||||
try { return syntaxHighlight(JSON.stringify(typeof viewingContent.value === 'string' ? JSON.parse(viewingContent.value) : viewingContent.value, null, 2)) }
|
||||
catch { return escapeHtml(viewingContent.value || '') }
|
||||
})
|
||||
const highlightedViewHtml = computed(() => {
|
||||
return escapeHtml(viewingContent.value || '')
|
||||
.replace(/(<\/?)(\w+)([\s\S]*?)(\/?>)/g, '<span class="html-tag">$1</span><span class="html-name">$2</span><span class="html-tag">$3$4</span>')
|
||||
.replace(/(\w+)=(".*?")/g, '<span class="html-attr">$1</span>=<span class="html-value">$2</span>')
|
||||
})
|
||||
|
||||
const escapeHtml = (str) => {
|
||||
if (!str) return ''
|
||||
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
}
|
||||
const escapeHtml = (s) => { if(!s)return''; return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>') }
|
||||
const syntaxHighlight = (json) => escapeHtml(json).replace(
|
||||
/("(\\u[a-fA-F0-9]{4}|\\[^u]|[^"\\])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g,
|
||||
(match) => {
|
||||
let cls = 'number'
|
||||
if (/^"/.test(match)) cls = /:$/.test(match) ? 'key' : 'string'
|
||||
else if (/true|false/.test(match)) cls = 'boolean'
|
||||
else if (/null/.test(match)) cls = 'null'
|
||||
return `<span class="${cls}">${match}</span>`
|
||||
(m) => {
|
||||
let cls='number'
|
||||
if(/^"/.test(m)) cls=/:$/.test(m)?'key':'string'
|
||||
else if(/true|false/.test(m)) cls='boolean'
|
||||
else if(/null/.test(m)) cls='null'
|
||||
return `<span class="${cls}">${m}</span>`
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
refresh()
|
||||
loadConfigs()
|
||||
onMounted(async () => {
|
||||
await refresh()
|
||||
await loadConfigs()
|
||||
// 默认自动解析
|
||||
if (selectedConfigId.value) await onConfigChange()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.config-json {
|
||||
background: #1e1e1e;
|
||||
color: #d4d4d4;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
max-height: calc(100vh - 440px);
|
||||
overflow: auto;
|
||||
background:#1e1e1e; color:#d4d4d4; padding:10px; border-radius:4px;
|
||||
font-size:11px; line-height:1.5; margin:0; white-space:pre-wrap; word-break:break-all;
|
||||
max-height:300px; overflow:auto;
|
||||
}
|
||||
.content-json, .content-html {
|
||||
background: #1e1e1e;
|
||||
color: #d4d4d4;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
max-height: 500px;
|
||||
overflow: auto;
|
||||
}
|
||||
.config-json :deep(.key), .content-json :deep(.key) { color: #9cdcfe; }
|
||||
.config-json :deep(.string), .content-json :deep(.string) { color: #ce9178; }
|
||||
.config-json :deep(.number), .content-json :deep(.number) { color: #b5cea8; }
|
||||
.config-json :deep(.boolean), .content-json :deep(.boolean) { color: #569cd6; }
|
||||
.config-json :deep(.null), .content-json :deep(.null) { color: #569cd6; }
|
||||
.content-html :deep(.html-tag) { color: #808080; }
|
||||
.content-html :deep(.html-name) { color: #569cd6; }
|
||||
.content-html :deep(.html-attr) { color: #9cdcfe; }
|
||||
.content-html :deep(.html-value) { color: #ce9178; }
|
||||
.config-json :deep(.key) { color:#9cdcfe }
|
||||
.config-json :deep(.string) { color:#ce9178 }
|
||||
.config-json :deep(.number) { color:#b5cea8 }
|
||||
.config-json :deep(.boolean) { color:#569cd6 }
|
||||
.config-json :deep(.null) { color:#569cd6 }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user