Files
par/web/src/views/AccessResultCheck.vue

328 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div>
<!-- 顶栏 -->
<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>
<!-- 控制栏 -->
<el-card shadow="never" style="margin-bottom:12px">
<div style="display:flex;gap:12px;align-items:center;flex-wrap:wrap">
<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-select>
<el-divider direction="vertical" />
<span class="text-xs"><strong>{{ pages.length }}</strong> 页面</span>
<span class="text-xs"><strong>{{ totalResults }}</strong> 次提交</span>
<el-divider direction="vertical" />
<!-- 解析摘要 -->
<template v-if="selectedConfig">
<el-tag v-if="parseSummary.matched>0" size="small" type="success">
✓ {{ parseSummary.matched }} 页匹配
</el-tag>
<el-tag v-if="parseSummary.unmatched>0" size="small" type="warning">
⚠ {{ parseSummary.unmatched }} 页未匹配
</el-tag>
<el-tag v-if="!parseSummary.hasParsed" size="small" type="info">未解析</el-tag>
</template>
<el-button size="small" @click="refresh" :loading="loading">刷新</el-button>
<el-button size="small" @click="doBatchParse" :loading="parseLoading" v-if="selectedConfigId">全部重新解析</el-button>
<!-- 审批/拒绝(待审核配置) -->
<template v-if="selectedConfig?.status==='pending'">
<el-divider direction="vertical" />
<el-button size="small" type="success" @click="approveConfig" :loading="approving">审批通过</el-button>
<el-button size="small" type="danger" @click="rejectConfig" :loading="rejecting">拒绝</el-button>
</template>
<el-button size="small" @click="configVisible=true" v-if="selectedConfigId">编辑脚本</el-button>
</div>
</el-card>
<!-- 脚本编辑弹窗 -->
<el-dialog v-model="configVisible" title="Groovy 脚本 · v{{ selectedConfig?.version }}" width="880px" :close-on-click-modal="false">
<div v-if="scriptLoading" style="text-align:center;padding:20px;color:#909399">加载中...</div>
<template v-else>
<div style="margin-bottom:6px;font-size:11px;color:#909399">
内置变量: <code>content</code>(页面内容) <code>url</code>(访问URL) <code>jsoup</code> <code>json</code> <code>utils</code>
· 返回 Map{pageName,extracted,message} 或 List
</div>
<el-input v-model="scriptContent" type="textarea" :rows="22" style="font-family:monospace;font-size:12px"
placeholder="def doc = jsoup.parse(content)&#10;def rows = []&#10;doc.select('table tr').each { row ->&#10; rows &lt;&lt; [&#10; title: utils.text(utils.selectFirst(row,'td.name a')),&#10; size: utils.text(utils.selectFirst(row,'td.size'))&#10; ]&#10;}&#10;return [pageName:'search', extracted:rows]" />
</template>
<template #footer>
<el-button @click="configVisible=false">关闭</el-button>
<el-button type="primary" @click="saveScript" :loading="saving">保存脚本</el-button>
</template>
</el-dialog>
<!-- 解析中 -->
<div v-if="parseLoading" style="text-align:center;padding:12px;color:#909399">
<el-icon class="is-loading"><Loading /></el-icon> 正在解析各页面适配结果...
</div>
<!-- URL 卡片列表 -->
<div 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">
<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>
<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>
<!-- 解析信息 -->
<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>
</div>
<div style="display:flex;gap:6px">
<el-button size="small" @click="toggleView(page)">
{{ expandedSet.has(page.url) ? '收起' : '查看' }}
</el-button>
<el-button size="small" @click="reparseUrl(page)" :loading="reparsingUrls.has(page.url)">重新解析</el-button>
<el-popconfirm title="确认删除此访问结果?" @confirm="deleteResult(page)">
<template #reference>
<el-button size="small" type="danger" text>删除</el-button>
</template>
</el-popconfirm>
</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:#909399;font-size:12px;margin-bottom:8px">
{{ page.parsed?.message || '暂无提取数据' }}
</div>
</template>
</el-card>
<div v-if="pages.length===0 && !loading" style="text-align:center;padding:60px;color:#909399">
暂无访问结果
</div>
</div>
</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 parseLoading = ref(false)
const selectedConfigId = ref(null)
const selectedConfig = ref(null)
const configVisible = ref(false)
watch(configVisible, (v) => { if (v) loadScript() })
const scriptContent = ref('')
const scriptLoading = ref(false)
const saving = ref(false)
const approving = ref(false)
const rejecting = ref(false)
const allConfigVersions = ref([])
const allResults = ref([])
const pageParsed = ref({})
const expandedSet = reactive(new Set())
const reparsingUrls = reactive(new Set())
const totalResults = computed(() => allResults.value.length)
// 始终从 allResults 按 URL 分组
const pages = computed(() => {
const map = new Map()
for (const r of allResults.value) {
const key = r.url || '(无URL)'
if (!map.has(key)) {
map.set(key, { url: key, accessResultId: r.id, statusCode: r.statusCode, responseTimeMs: r.responseTimeMs,
createdAt: r.createdAt, totalCount: 0, parsed: null })
}
map.get(key).totalCount++
}
for (const [url, parsed] of Object.entries(pageParsed.value)) {
if (map.has(url)) map.get(url).parsed = parsed
}
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 () => {
try {
const [ar, pr] = await Promise.allSettled([
api.configs.bySite(siteId), api.adminConfigs.pendingBySite(siteId)
])
const approved = ar.status==='fulfilled' ? (ar.value.data||[]) : []
const pending = pr.status==='fulfilled' ? (pr.value.data||[]) : []
allConfigVersions.value = [...pending, ...approved]
if (allConfigVersions.value.length) selectedConfigId.value = allConfigVersions.value[0].id
} catch {}
}
const onConfigChange = async () => {
const cfg = allConfigVersions.value.find(v => v.id===selectedConfigId.value)
if (!cfg) return
selectedConfig.value = cfg
}
const doBatchParse = async () => {
if (!selectedConfigId.value) return
parseLoading.value = true
try {
const r = await api.adapter.parseBatch(selectedConfigId.value, siteId)
const map = {}
for (const p of (r.data?.pages || [])) {
if (p.parsed) map[p.url] = p.parsed
}
pageParsed.value = map
} catch { /* 非关键 */ }
finally { parseLoading.value = false }
}
const refresh = async () => {
loading.value = true
try {
const r = await api.accessResults.bySite(siteId)
allResults.value = r.data||[]
if (selectedConfigId.value) await doBatchParse()
} catch { ElMessage.error('获取访问结果失败') }
finally { loading.value = false }
}
// 编辑
const loadScript = async () => {
if (!selectedConfig.value) return
scriptLoading.value = true
try { const r = await api.adminConfigs.getScript(selectedConfig.value.id); scriptContent.value = r.data?.script || '' }
catch { scriptContent.value = '' }
finally { scriptLoading.value = false }
}
const saveScript = async () => {
saving.value = true
try {
await api.adminConfigs.updateScript(selectedConfig.value.id, scriptContent.value)
ElMessage.success('脚本已保存'); doBatchParse()
} catch(e) { ElMessage.error(e.response?.data?.message||'保存失败') }
finally { saving.value = false }
}
const approveConfig = async () => {
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||'审批失败') }
finally { approving.value = false }
}
const rejectConfig = async () => {
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||'拒绝失败') }
finally { rejecting.value = false }
}
const toggleView = (page) => {
if (expandedSet.has(page.url)) expandedSet.delete(page.url)
else expandedSet.add(page.url)
}
const reparseUrl = async (page) => {
if (!selectedConfigId.value || !page.accessResultId) return
reparsingUrls.add(page.url)
try {
const r = await api.adapter.parse(selectedConfigId.value, page.accessResultId)
const parsed = r.data
if (parsed) {
const newParsed = { ...pageParsed.value }
newParsed[page.url] = parsed
pageParsed.value = newParsed
}
ElMessage.success('已重新解析')
} catch { ElMessage.error('解析失败') }
finally { reparsingUrls.delete(page.url) }
}
const deleteResult = async (page) => {
try {
await api.accessResults.delete(page.accessResultId)
ElMessage.success('已删除')
refresh()
} catch { ElMessage.error('删除失败') }
}
const statusTag = (code) => {
if (code>=200&&code<300) return 'success'
if (code>=300&&code<400) return 'warning'
return 'danger'
}
onMounted(async () => {
await refresh()
await loadConfigs()
if (selectedConfigId.value) {
const cfg = allConfigVersions.value.find(v => v.id===selectedConfigId.value)
if (cfg) selectedConfig.value = cfg
}
})
</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:500px; overflow:auto;
}
.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>