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

268 lines
12 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 style="display:flex;flex-direction:column;height:calc(100vh - 80px)">
<!-- 顶栏 -->
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;flex-shrink:0">
<h2 style="margin:0">适配校验:{{ siteId }}</h2>
<el-button @click="$router.push('/access-results')">← 返回列表</el-button>
</div>
<!-- 控制栏 -->
<div style="display:flex;gap:12px;align-items:center;flex-wrap:wrap;margin-bottom:8px;flex-shrink:0">
<el-select v-model="selectedConfigId" placeholder="选择版本" @change="onConfigChange" style="width:200px">
<el-option v-for="v in allConfigVersions" :key="v.id"
:label="'v'+v.version + (v.status==='pending'?' [待审]':'') + (v.isLatest?' [当前]':'')" :value="v.id" />
</el-select>
<span class="text-xs">{{ pages.length }}页 {{ totalResults }}次</span>
<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>
</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-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" type="success" @click="approveConfig" :loading="approving" v-if="selectedConfigId && selectedConfig?.status!=='pending'">发布脚本</el-button>
</div>
<!-- 主体:脚本 + 结果 -->
<div style="display:flex;gap:12px;flex:1;min-height:0">
<!-- 左侧:脚本编辑器 -->
<div style="flex:0 0 440px;display:flex;flex-direction:column;min-height:0">
<div style="font-size:13px;font-weight:500;margin-bottom:4px;display:flex;justify-content:space-between;align-items:center">
<span>Groovy 脚本</span>
<el-button size="small" type="primary" @click="saveScript" :loading="saving" v-if="selectedConfigId">保存</el-button>
</div>
<div style="flex:1;min-height:0;border-radius:4px;overflow:hidden">
<CodeEditor v-model="scriptContent" />
</div>
<div style="font-size:10px;color:#909399;margin-top:2px">
内置: <code>content</code> <code>url</code> <code>jsoup</code> <code>json</code> <code>utils</code>
· 返回 Map{pageName,extracted} 或 List
</div>
</div>
<!-- 右侧:URL卡片 -->
<div style="flex:1;overflow:auto;display:flex;flex-direction:column;gap:8px;min-height:0">
<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">✓</span>
<span v-else-if="page.parsed" style="color:#e6a23c">⚠</span>
<span v-else style="color:#c0c4cc">○</span>
<strong style="font-size:13px;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.parsed?.matched" size="small" type="success">{{ page.parsed.pageName }}</el-tag>
<!-- 选择历史结果 -->
<el-select v-if="page.historyIds?.length>1" v-model="page.activeId" size="small" style="width:140px" @change="v=>parseHistory(page,v)">
<el-option v-for="h in page.historyIds" :key="h.id" :label="'#'+h.id+' '+h.time" :value="h.id" />
</el-select>
<el-tag size="small" type="info">×{{ page.totalCount }}</el-tag>
</div>
<div style="display:flex;gap:4px">
<el-button size="small" @click="viewRaw(page)">原始</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>
<!-- 解析结果 -->
<div v-if="page.parsed?.matched && page.parsed.extracted?.length">
<el-table :data="page.parsed.extracted" border stripe size="small" max-height="280">
<el-table-column type="index" label="#" width="40" />
<el-table-column v-for="col in Object.keys(page.parsed.extracted[0])" :key="col"
:prop="col" :label="col" min-width="90" 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-if="page.parsed" style="color:#e6a23c;font-size:12px">
{{ page.parsed.message || '解析失败' }}
</div>
<div v-else style="color:#909399;font-size:12px">未解析</div>
</el-card>
<div v-if="pages.length===0 && !loading" style="text-align:center;padding:60px;color:#909399">暂无访问结果</div>
</div>
</div>
<!-- 原始内容弹窗 -->
<el-dialog v-model="rawVisible" title="原始内容" width="900px" :close-on-click-modal="false">
<div v-if="rawLoading" style="text-align:center;padding:40px">加载中...</div>
<pre v-else class="raw-content">{{ rawContent }}</pre>
<template #footer><el-button @click="rawVisible=false">关闭</el-button></template>
</el-dialog>
</div>
</template>
<script setup>
import { useRoute } from 'vue-router'
import api from '../api'
import CodeEditor from '../components/CodeEditor.vue'
import { Loading } from '@element-plus/icons-vue'
const route = useRoute()
const siteId = route.params.siteId
const loading = ref(false), parseLoading = ref(false)
const selectedConfigId = ref(null), selectedConfig = ref(null)
const scriptContent = ref(''), saving = ref(false), approving = ref(false), rejecting = ref(false)
const allConfigVersions = ref([]), allResults = ref([])
const pageParsed = ref({}), reparsingUrls = reactive(new Set())
const rawVisible = ref(false), rawLoading = ref(false), rawContent = ref('')
const totalResults = computed(() => allResults.value.length)
const pages = computed(() => {
const map = new LinkedHashMap()
for (const r of allResults.value) {
const key = r.url || '(无URL)'
if (!map.has(key)) {
map.set(key, { url: key, activeId: r.id, accessResultId: r.id,
statusCode: r.statusCode, responseTimeMs: r.responseTimeMs,
createdAt: r.createdAt, totalCount: 0, historyIds: [], parsed: null })
}
const p = map.get(key)
p.totalCount++
p.historyIds.push({ id: r.id, time: (r.createdAt||'').substring(5,16) })
}
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)])
allConfigVersions.value = [...(pr.status==='fulfilled'?pr.value.data||[]:[]), ...(ar.status==='fulfilled'?ar.value.data||[]:[])]
if (allConfigVersions.value.length) selectedConfigId.value = allConfigVersions.value[0].id
} catch {}
}
const onConfigChange = () => {
const cfg = allConfigVersions.value.find(v => v.id===selectedConfigId.value)
if (cfg) selectedConfig.value = cfg
}
const loadScript = async () => {
if (!selectedConfig.value) return
try { const r = await api.adminConfigs.getScript(selectedConfig.value.id); scriptContent.value = r.data?.script||'' }
catch { scriptContent.value = '' }
}
watch(selectedConfig, (v) => { if (v) loadScript() })
const saveScript = async () => {
if (!selectedConfig.value) return
saving.value = true
try { await api.adminConfigs.updateScript(selectedConfig.value.id, scriptContent.value); ElMessage.success('已保存') }
catch(e) { ElMessage.error(e.response?.data?.message||'保存失败') }
finally { saving.value = false }
}
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 reparseUrl = async (page) => {
if (!selectedConfigId.value) return
reparsingUrls.add(page.url)
try {
const r = await api.adapter.parse(selectedConfigId.value, page.activeId)
if (r.data) {
const np = { ...pageParsed.value }; np[page.url] = r.data; pageParsed.value = np
}
} catch {}
finally { reparsingUrls.delete(page.url) }
}
const parseHistory = async (page, newId) => {
page.accessResultId = newId
await reparseUrl(page)
}
const refresh = async () => {
loading.value = true
try { allResults.value = (await api.accessResults.bySite(siteId)).data||[] }
catch { ElMessage.error('获取访问结果失败') }
finally { loading.value = false }
}
const viewRaw = async (page) => {
rawVisible.value = true; rawLoading.value = true; rawContent.value = ''
try { rawContent.value = (await api.accessResults.get(page.activeId)).data?.content || '(空)' }
catch { rawContent.value = '加载失败' }
finally { rawLoading.value = false }
}
const deleteResult = async (page) => {
if (!page.accessResultId) return
try { await api.accessResults.delete(page.accessResultId); ElMessage.success('已删除'); refresh() }
catch { ElMessage.error('删除失败') }
}
const approveConfig = async () => {
if (!selectedConfig.value) return
const label = selectedConfig.value.status==='pending' ? '确认审批?版本号将自动+1' : '确认发布脚本?版本号将+1'
try { await ElMessageBox.confirm(label,'确认',{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 statusTag = (c) => c>=200&&c<300?'success':c>=300&&c<400?'warning':'danger'
onMounted(async () => {
await refresh()
await loadConfigs()
if (selectedConfigId.value) {
selectedConfig.value = allConfigVersions.value.find(v=>v.id===selectedConfigId.value)
}
})
</script>
<style scoped>
.raw-content { background:#1e1e1e; color:#d4d4d4; padding:12px; border-radius:4px;
font-size:11px; line-height:1.4; margin:0; white-space:pre-wrap; word-break:break-all; max-height:550px; overflow:auto }
</style>