refactor: 适配校验页审批/编辑提至顶栏,新增全局解析摘要,URL卡片纯展示

This commit is contained in:
mediabot-pt
2026-07-01 08:59:42 +08:00
parent fd4301b365
commit 535988bebe

View File

@@ -6,89 +6,106 @@
<el-button @click="$router.push('/access-results')">← 返回列表</el-button> <el-button @click="$router.push('/access-results')">← 返回列表</el-button>
</div> </div>
<!-- 控制栏 -->
<el-card shadow="never" style="margin-bottom:12px"> <el-card shadow="never" style="margin-bottom:12px">
<div style="display:flex;gap:16px;align-items:center;flex-wrap:wrap"> <div style="display:flex;gap:12px;align-items:center;flex-wrap:wrap">
<!-- 配置版本选择 -->
<el-select v-model="selectedConfigId" placeholder="选择配置版本" @change="onConfigChange" style="width:260px"> <el-select v-model="selectedConfigId" placeholder="选择配置版本" @change="onConfigChange" style="width:240px">
<el-option v-for="v in allConfigVersions" :key="v.id" :label="'v'+v.version + (v.status==='pending'?' [待审]':'') + (v.isLatest?' [当前]':'')" :value="v.id" /> <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-select>
<el-divider direction="vertical" /> <el-divider direction="vertical" />
<span class="text-xs"><strong>{{ pages.length }}</strong> 页面</span> <span class="text-xs"><strong>{{ pages.length }}</strong> 页面</span>
<span class="text-xs"><strong>{{ totalResults }}</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-divider direction="vertical" />
<el-button size="small" @click="refresh" :loading="loading">刷新</el-button> <!-- 解析摘要 -->
<template v-if="selectedConfig">
<el-button size="small" type="primary" @click="toggleConfigPanel" v-if="selectedConfigId"> <el-tag v-if="parseSummary.matched>0" size="small" type="success">
{{ showConfig ? '收起配置' : '查看配置' }} ✓ {{ parseSummary.matched }} 页匹配
</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> </el-tag>
<template v-if="!editing"> <el-tag v-if="parseSummary.unmatched>0" size="small" type="warning">
<el-button size="small" type="primary" text @click="reloadBatchParse" :loading="parseLoading">重新解析</el-button> ⚠ {{ parseSummary.unmatched }} 页未匹配
<template v-if="selectedConfig.status==='pending'"> </el-tag>
<el-button size="small" type="success" @click="approveConfig" :loading="approving">审批通过</el-button> <el-tag v-if="!parseSummary.hasParsed" size="small" type="info">未解析</el-tag>
<el-button size="small" type="danger" @click="rejectConfig" :loading="rejecting">拒绝</el-button> </template>
<el-button size="small" @click="startEdit">编辑</el-button>
</template> <el-button size="small" @click="refresh" :loading="loading">刷新</el-button>
</template> <el-button size="small" @click="doBatchParse" :loading="parseLoading" v-if="selectedConfigId">重新解析</el-button>
<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> <template v-if="selectedConfig?.status==='pending'">
</div> <el-divider direction="vertical" />
</div> <el-button size="small" type="success" @click="approveConfig" :loading="approving">审批通过</el-button>
<el-input v-if="editing" v-model="editContent" type="textarea" :rows="10" style="font-family:monospace;font-size:11px" /> <el-button size="small" type="danger" @click="rejectConfig" :loading="rejecting">拒绝</el-button>
<pre v-else class="config-json" v-html="highlightedConfig"></pre> <el-button size="small" @click="startEdit">编辑</el-button>
</template>
<el-button size="small" text @click="configVisible=true" v-if="selectedConfigId">查看配置JSON</el-button>
</div> </div>
</el-card> </el-card>
<!-- 解析加载中提示(不隐藏已有列表) --> <!-- 配置编辑弹窗 -->
<el-dialog v-model="configVisible" title="适配配置" width="700px" :close-on-click-modal="false">
<template v-if="editing">
<el-input v-model="editContent" type="textarea" :rows="20" style="font-family:monospace;font-size:12px" />
<div style="margin-top:10px;display:flex;gap:8px">
<el-button type="primary" size="small" @click="saveEdit" :loading="saving">保存</el-button>
<el-button size="small" @click="cancelEdit">取消</el-button>
</div>
</template>
<pre v-else class="config-json" v-html="highlightedConfig"></pre>
<template #footer>
<el-button @click="configVisible=false">关闭</el-button>
<template v-if="!editing && selectedConfig?.status==='pending'">
<el-button type="primary" size="small" @click="startEdit">编辑</el-button>
</template>
</template>
</el-dialog>
<!-- 解析中 -->
<div v-if="parseLoading" style="text-align:center;padding:12px;color:#909399"> <div v-if="parseLoading" style="text-align:center;padding:12px;color:#909399">
<el-icon class="is-loading"><Loading /></el-icon> 正在解析各页面适配结果... <el-icon class="is-loading"><Loading /></el-icon> 正在解析各页面适配结果...
</div> </div>
<!-- 页面卡片列表 --> <!-- URL 卡片列表 -->
<div style="display:flex;flex-direction:column;gap:8px"> <div style="display:flex;flex-direction:column;gap:8px">
<el-card v-for="page in pages" :key="page.url" shadow="hover" size="small"> <el-card v-for="page in pages" :key="page.url" shadow="hover" size="small">
<template #header> <template #header>
<div style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:8px"> <div style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:8px">
<div style="display:flex;align-items:center;gap:8px"> <div style="display:flex;align-items:center;gap:8px">
<!-- 解析状态图标 -->
<span v-if="page.parsed?.matched" style="color:#67c23a;font-size:16px">✓</span>
<span v-else-if="page.parsed" style="color:#e6a23c;font-size:16px">⚠</span>
<span v-else style="color:#c0c4cc;font-size:16px">○</span>
<strong style="font-size:14px;word-break:break-all">{{ page.url }}</strong> <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> <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> <span v-if="page.responseTimeMs!=null" class="text-xs" style="color:#909399">{{ page.responseTimeMs }}ms</span>
<!-- 解析信息 -->
<template v-if="page.parsed?.matched">
<el-tag size="small" type="success">{{ page.parsed.pageName }}</el-tag>
<span class="text-xs" style="color:#909399">{{ page.parsed.rowCount }}行 {{ Object.keys(page.parsed.extracted?.[0]||{}).length }}字段</span>
</template>
<el-tag v-else-if="page.parsed" size="small" type="warning">未匹配规则</el-tag>
<el-tag size="small" type="info">×{{ page.totalCount }}</el-tag> <el-tag size="small" type="info">×{{ page.totalCount }}</el-tag>
</div> </div>
<div style="display:flex;gap:6px;align-items:center"> <div style="display:flex;gap:6px">
<!-- 解析状态 --> <el-button size="small" @click="toggleView(page)">
<el-tag v-if="page.parsed?.matched" size="small" type="success"> {{ expandedSet.has(page.url) ? '收起' : '查看' }}
{{ page.parsed.pageName }} · {{ page.parsed.rowCount }}行 </el-button>
</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>
</div> </div>
</template> </template>
<!-- 展开内容 --> <!-- 展开内容 -->
<template v-if="expandedSet.has(page.url)"> <template v-if="expandedSet.has(page.url)">
<!-- 解析结果表格 --> <!-- 解析结果 -->
<div v-if="page.parsed?.matched && page.parsed.extracted?.length" style="margin-bottom:8px"> <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 :data="page.parsed.extracted" border stripe size="small" max-height="300">
<el-table-column type="index" label="#" width="45" /> <el-table-column type="index" label="#" width="45" />
@@ -100,19 +117,16 @@
</el-table-column> </el-table-column>
</el-table> </el-table>
</div> </div>
<div v-else style="color:#e6a23c;font-size:12px;margin-bottom:8px"> <div v-else style="color:#909399;font-size:12px;margin-bottom:8px">
{{ page.parsed?.message || '暂无提取数据' }} {{ page.parsed?.message || '暂无提取数据' }}
</div> </div>
<!-- 历史记录 --> <!-- 历史记录 -->
<div v-if="page.history?.length > 1" style="margin-top:8px"> <div v-if="page.history?.length > 1" style="font-size:12px;color:#909399">
<span class="text-xs" style="color:#909399">历史记录:</span> 历史提交:
<span v-for="(h,i) in page.history" :key="h.id" <span v-for="h in page.history" :key="h.id" style="margin-left:6px">
class="text-xs" style="color:#909399;margin-left:6px">
{{ formatTime(h.createdAt) }} {{ formatTime(h.createdAt) }}
<el-tag size="small" :type="h.isValid===true?'success':h.isValid===false?'danger':'info'"> <el-tag size="small" :type="h.statusCode>=200&&h.statusCode<300?'success':'danger'">{{ h.statusCode }}</el-tag>
{{ h.isValid===true?'✓':h.isValid===false?'✗':'?' }}
</el-tag>
</span> </span>
</div> </div>
</template> </template>
@@ -138,7 +152,7 @@ const parseLoading = ref(false)
const selectedConfigId = ref(null) const selectedConfigId = ref(null)
const selectedConfig = ref(null) const selectedConfig = ref(null)
const selectedConfigJson = ref('') const selectedConfigJson = ref('')
const showConfig = ref(false) const configVisible = ref(false)
const editing = ref(false) const editing = ref(false)
const editContent = ref('') const editContent = ref('')
const saving = ref(false) const saving = ref(false)
@@ -147,44 +161,47 @@ const rejecting = ref(false)
const allConfigVersions = ref([]) const allConfigVersions = ref([])
const allResults = ref([]) const allResults = ref([])
const pageParsed = ref({}) // url → parsed data const pageParsed = ref({})
const expandedSet = reactive(new Set()) const expandedSet = reactive(new Set())
const totalResults = computed(() => allResults.value.length) 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)
// 始终从 allResults 按 URL 分组生成 pages,解析数据注入 // 始终从 allResults 按 URL 分组
const pages = computed(() => { const pages = computed(() => {
const map = new Map() const map = new Map()
for (const r of allResults.value) { for (const r of allResults.value) {
const key = r.url || '(无URL)' const key = r.url || '(无URL)'
if (!map.has(key)) { if (!map.has(key)) {
map.set(key, { url: key, statusCode: r.statusCode, responseTimeMs: r.responseTimeMs, map.set(key, { url: key, statusCode: r.statusCode, responseTimeMs: r.responseTimeMs,
createdAt: r.createdAt, totalCount: 0, validCount: 0, invalidCount: 0, createdAt: r.createdAt, totalCount: 0, history: [], parsed: null })
uncheckedCount: 0, history: [], parsed: null })
} }
const p = map.get(key) const p = map.get(key)
p.totalCount++ p.totalCount++
if (r.isValid===true) p.validCount++
else if (r.isValid===false) p.invalidCount++
else p.uncheckedCount++
if (p.history.length < 5) p.history.push(r) if (p.history.length < 5) p.history.push(r)
} }
// 合并解析数据
for (const [url, parsed] of Object.entries(pageParsed.value)) { for (const [url, parsed] of Object.entries(pageParsed.value)) {
if (map.has(url)) map.get(url).parsed = parsed if (map.has(url)) map.get(url).parsed = parsed
} }
return Array.from(map.values()) return Array.from(map.values())
}) })
// 加载配置版本列表 // 全局解析摘要
const parseSummary = computed(() => {
let matched = 0, unmatched = 0
for (const p of pages.value) {
if (p.parsed) {
if (p.parsed.matched) matched++
else unmatched++
}
}
return { matched, unmatched, hasParsed: matched + unmatched > 0 }
})
// 加载配置版本
const loadConfigs = async () => { const loadConfigs = async () => {
try { try {
const [ar, pr] = await Promise.allSettled([ const [ar, pr] = await Promise.allSettled([
api.configs.bySite(siteId), api.configs.bySite(siteId), api.adminConfigs.pendingBySite(siteId)
api.adminConfigs.pendingBySite(siteId)
]) ])
const approved = ar.status==='fulfilled' ? (ar.value.data||[]) : [] const approved = ar.status==='fulfilled' ? (ar.value.data||[]) : []
const pending = pr.status==='fulfilled' ? (pr.value.data||[]) : [] const pending = pr.status==='fulfilled' ? (pr.value.data||[]) : []
@@ -193,14 +210,11 @@ const loadConfigs = async () => {
} catch {} } catch {}
} }
// 选中配置 → 更新 selectedConfig + 自动批量解析
const onConfigChange = async () => { const onConfigChange = async () => {
const cfg = allConfigVersions.value.find(v => v.id===selectedConfigId.value) const cfg = allConfigVersions.value.find(v => v.id===selectedConfigId.value)
if (!cfg) return if (!cfg) return
selectedConfig.value = cfg selectedConfig.value = cfg
editing.value = false editing.value = false
// 加载配置内容
try { try {
if (cfg.status==='pending') { if (cfg.status==='pending') {
const r = await api.adminConfigs.getContent(cfg.id) const r = await api.adminConfigs.getContent(cfg.id)
@@ -210,7 +224,6 @@ const onConfigChange = async () => {
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() await doBatchParse()
} }
@@ -228,20 +241,17 @@ const doBatchParse = async () => {
finally { parseLoading.value = false } finally { parseLoading.value = false }
} }
const reloadBatchParse = () => doBatchParse()
const refresh = async () => { const refresh = async () => {
loading.value = true loading.value = true
try { try {
const r = await api.accessResults.bySite(siteId) const r = await api.accessResults.bySite(siteId)
allResults.value = r.data||[] allResults.value = r.data||[]
// 刷新解析数据
if (selectedConfigId.value) await doBatchParse() if (selectedConfigId.value) await doBatchParse()
} catch { ElMessage.error('获取访问结果失败') } } catch { ElMessage.error('获取访问结果失败') }
finally { loading.value = false } finally { loading.value = false }
} }
// 编辑配置 // 编辑
const startEdit = () => { editContent.value=selectedConfigJson.value; editing.value=true } const startEdit = () => { editContent.value=selectedConfigJson.value; editing.value=true }
const cancelEdit = () => { editing.value=false } const cancelEdit = () => { editing.value=false }
const saveEdit = async () => { const saveEdit = async () => {
@@ -270,20 +280,11 @@ const rejectConfig = async () => {
finally { rejecting.value = false } finally { rejecting.value = false }
} }
const toggleConfigPanel = () => showConfig.value = !showConfig.value
const toggleView = (page) => { const toggleView = (page) => {
if (expandedSet.has(page.url)) expandedSet.delete(page.url) if (expandedSet.has(page.url)) expandedSet.delete(page.url)
else expandedSet.add(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?'有效':'无效'}`)
refresh()
} catch { ElMessage.error('批量校验失败') }
}
const formatTime = (t) => t ? t.substring(5,16) : '-' const formatTime = (t) => t ? t.substring(5,16) : '-'
const statusTag = (code) => { const statusTag = (code) => {
if (code>=200&&code<300) return 'success' if (code>=200&&code<300) return 'success'
@@ -291,7 +292,6 @@ const statusTag = (code) => {
return 'danger' return 'danger'
} }
// 配置语法高亮
const highlightedConfig = computed(() => { const highlightedConfig = computed(() => {
try { return syntaxHighlight(JSON.stringify(JSON.parse(selectedConfigJson.value),null,2)) } try { return syntaxHighlight(JSON.stringify(JSON.parse(selectedConfigJson.value),null,2)) }
catch { return escapeHtml(selectedConfigJson.value||'') } catch { return escapeHtml(selectedConfigJson.value||'') }
@@ -311,7 +311,6 @@ const syntaxHighlight = (json) => escapeHtml(json).replace(
onMounted(async () => { onMounted(async () => {
await refresh() await refresh()
await loadConfigs() await loadConfigs()
// 默认自动解析
if (selectedConfigId.value) await onConfigChange() if (selectedConfigId.value) await onConfigChange()
}) })
</script> </script>
@@ -320,7 +319,7 @@ onMounted(async () => {
.config-json { .config-json {
background:#1e1e1e; color:#d4d4d4; padding:10px; border-radius:4px; 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; font-size:11px; line-height:1.5; margin:0; white-space:pre-wrap; word-break:break-all;
max-height:300px; overflow:auto; max-height:500px; overflow:auto;
} }
.config-json :deep(.key) { color:#9cdcfe } .config-json :deep(.key) { color:#9cdcfe }
.config-json :deep(.string) { color:#ce9178 } .config-json :deep(.string) { color:#ce9178 }