refactor: 适配校验改为站点汇总+适配器页面批量校验模式
This commit is contained in:
@@ -67,10 +67,12 @@ export default {
|
||||
pathStats: () => api.get('/admin/logs/stats/path'),
|
||||
},
|
||||
accessResults: {
|
||||
summary: () => api.get('/admin/access-results/summary'),
|
||||
list: () => api.get('/admin/access-results'),
|
||||
bySite: (siteId) => api.get(`/admin/access-results/by-site/${siteId}`),
|
||||
get: (id) => api.get(`/admin/access-results/${id}`),
|
||||
validate: (id, isValid, note) => api.post(`/admin/access-results/${id}/validate`, { isValid, note: note || '' }),
|
||||
batchValidate: (siteId, url, isValid) => api.post('/admin/access-results/batch-validate', { siteId, url, isValid }),
|
||||
},
|
||||
health: () => api.get('/health', { baseURL: '' })
|
||||
}
|
||||
|
||||
@@ -59,6 +59,12 @@ const routes = [
|
||||
name: 'AccessResults',
|
||||
component: () => import('../views/AccessResults.vue'),
|
||||
meta: { title: '适配验证' }
|
||||
},
|
||||
{
|
||||
path: 'access-results/check/:siteId',
|
||||
name: 'AccessResultCheck',
|
||||
component: () => import('../views/AccessResultCheck.vue'),
|
||||
meta: { title: '适配校验详情' }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
299
web/src/views/AccessResultCheck.vue
Normal file
299
web/src/views/AccessResultCheck.vue
Normal file
@@ -0,0 +1,299 @@
|
||||
<template>
|
||||
<div>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
|
||||
<h2 style="margin:0">适配校验:{{ siteId }}</h2>
|
||||
<el-button @click="$router.push('/access-results')">← 返回列表</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 统计概览 -->
|
||||
<el-card style="margin-bottom: 16px" shadow="never">
|
||||
<div style="display:flex;gap:20px;align-items:center;flex-wrap:wrap">
|
||||
<div><strong>配置版本:</strong>{{ configVersion || '-' }}</div>
|
||||
<div><strong>页面数:</strong>{{ groupedPages.length }}</div>
|
||||
<div><strong>总次数:</strong>{{ allResults.length }}</div>
|
||||
<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" type="primary" @click="refresh" :loading="loading">刷新</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<div style="display:flex;gap:16px;height:calc(100vh - 200px)">
|
||||
<!-- 左侧:适配配置 -->
|
||||
<el-card style="flex:0 0 360px;overflow:auto" shadow="never">
|
||||
<template #header>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center">
|
||||
<span><strong>适配配置</strong></span>
|
||||
<el-button size="small" text @click="toggleConfigCollapse">{{ configCollapsed ? '展开' : '折叠' }}</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="configLoading" style="text-align:center;padding:20px;color:#909399">加载配置中...</div>
|
||||
<div v-else-if="configError" style="padding:10px;color:#f56c6c">{{ configError }}</div>
|
||||
<div v-else-if="configCollapsed" style="text-align:center;padding:20px;color:#909399">配置已折叠</div>
|
||||
<pre v-else class="config-json" v-html="highlightedConfig"></pre>
|
||||
</el-card>
|
||||
|
||||
<!-- 右侧:页面列表 -->
|
||||
<div style="flex:1;overflow:auto;display:flex;flex-direction:column;gap:8px">
|
||||
<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">
|
||||
<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">×{{ page.results.length }}</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.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 in page.results" :key="r.id"
|
||||
style="padding:6px 0;border-bottom:1px solid #ebeef5;display:flex;justify-content:space-between;align-items:center">
|
||||
<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)" style="flex-shrink:0">
|
||||
{{ r.statusCode }}
|
||||
</el-tag>
|
||||
<span v-if="r.responseTimeMs != null" class="text-xs" style="color:#909399;flex-shrink:0">{{ r.responseTimeMs }}ms</span>
|
||||
<span class="text-xs" style="color:#909399;flex-shrink:0">{{ formatTime(r.createdAt) }}</span>
|
||||
<span v-if="r.clientId" class="text-xs" style="color:#909399;flex-shrink:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{{ r.clientId }}</span>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:6px;flex-shrink:0">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<div v-if="groupedPages.length === 0 && !loading" style="text-align:center;padding:40px;color:#909399">
|
||||
暂无访问结果
|
||||
</div>
|
||||
</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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useRoute } from 'vue-router'
|
||||
import api from '../api'
|
||||
|
||||
const route = useRoute()
|
||||
const siteId = route.params.siteId
|
||||
|
||||
const loading = ref(false)
|
||||
const allResults = ref([])
|
||||
const configVersion = ref('')
|
||||
const configLoading = ref(true)
|
||||
const configError = ref('')
|
||||
const configContent = ref('')
|
||||
const configCollapsed = ref(false)
|
||||
|
||||
const contentVisible = ref(false)
|
||||
const contentLoading = ref(false)
|
||||
const viewingResult = ref({})
|
||||
const viewingContentType = ref('html')
|
||||
const viewingContent = ref('')
|
||||
|
||||
// 按 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)
|
||||
return { url, results, 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 loadConfig = async () => {
|
||||
configLoading.value = true
|
||||
try {
|
||||
const r = await api.configs.bySite(siteId)
|
||||
const configs = r.data || []
|
||||
if (configs.length) {
|
||||
configVersion.value = configs[0].version
|
||||
const resp = await api.configs.content(siteId, configs[0].version)
|
||||
configContent.value = typeof resp.data === 'string' ? resp.data : JSON.stringify(resp.data, null, 2)
|
||||
} else {
|
||||
configError.value = '该站点暂无适配配置'
|
||||
}
|
||||
} catch (e) {
|
||||
configError.value = e.response?.data?.message || '获取配置失败'
|
||||
} finally { configLoading.value = false }
|
||||
}
|
||||
|
||||
const toggleConfigCollapse = () => { configCollapsed.value = !configCollapsed.value }
|
||||
|
||||
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 singleValidate = async (id, isValid) => {
|
||||
try {
|
||||
await api.accessResults.validate(id, isValid)
|
||||
ElMessage.success(`已标记为${isValid ? '有效' : '无效'}`)
|
||||
contentVisible.value = false
|
||||
refresh()
|
||||
} catch { ElMessage.error('校验失败') }
|
||||
}
|
||||
|
||||
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) => {
|
||||
if (!t) return '-'
|
||||
return t.substring(5, 16) // MM-DD HH:mm
|
||||
}
|
||||
|
||||
const statusTag = (code) => {
|
||||
if (code >= 200 && code < 300) return 'success'
|
||||
if (code >= 300 && code < 400) return 'warning'
|
||||
return 'danger'
|
||||
}
|
||||
|
||||
// 配置 JSON 语法高亮
|
||||
const highlightedConfig = computed(() => {
|
||||
try {
|
||||
const json = typeof configContent.value === 'string' ? JSON.parse(configContent.value) : configContent.value
|
||||
return syntaxHighlight(JSON.stringify(json, null, 2))
|
||||
} catch { return escapeHtml(configContent.value || '') }
|
||||
})
|
||||
|
||||
const highlightedViewJson = computed(() => {
|
||||
try {
|
||||
const json = typeof viewingContent.value === 'string' ? JSON.parse(viewingContent.value) : viewingContent.value
|
||||
return syntaxHighlight(JSON.stringify(json, 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 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>`
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
refresh()
|
||||
loadConfig()
|
||||
})
|
||||
</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 - 280px);
|
||||
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; }
|
||||
</style>
|
||||
@@ -2,101 +2,45 @@
|
||||
<div>
|
||||
<h2>适配验证</h2>
|
||||
<el-card style="margin-top: 20px">
|
||||
<div style="margin-bottom: 12px; display: flex; justify-content: space-between; align-items: center">
|
||||
<div>
|
||||
<el-select v-model="filterSiteId" placeholder="按站点筛选" clearable @change="fetchAll" style="width: 200px">
|
||||
<el-option v-for="s in siteOptions" :key="s.siteId" :label="s.siteId" :value="s.siteId" />
|
||||
</el-select>
|
||||
<el-select v-model="filterValid" placeholder="验证状态" clearable @change="fetchAll" style="width: 140px; margin-left: 8px">
|
||||
<el-option label="已验证-有效" value="1" />
|
||||
<el-option label="已验证-无效" value="0" />
|
||||
<el-option label="未验证" value="null" />
|
||||
</el-select>
|
||||
</div>
|
||||
<div style="margin-bottom: 12px">
|
||||
<el-button @click="fetchAll" :icon="Refresh">刷新</el-button>
|
||||
</div>
|
||||
|
||||
<el-table :data="filteredList" v-loading="loading" stripe size="small" empty-text="暂无访问记录">
|
||||
<el-table-column type="index" label="#" width="50" />
|
||||
<el-table :data="sites" v-loading="loading" stripe size="small" empty-text="暂无访问记录">
|
||||
<el-table-column prop="siteId" label="站点ID" width="130" />
|
||||
<el-table-column prop="configVersion" label="配置版本" width="100">
|
||||
<el-table-column label="页面数" width="100">
|
||||
<template #default="scope">
|
||||
<span v-if="scope.row.configVersion" class="text-xs">{{ scope.row.configVersion }}</span>
|
||||
<span v-else class="text-xs" style="color:#909399">-</span>
|
||||
<el-tag size="small" type="info">{{ scope.row.pages?.length || 0 }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="clientId" label="来源" width="120">
|
||||
<el-table-column label="总次数" width="80">
|
||||
<template #default="scope">
|
||||
<span class="text-xs">{{ scope.row.clientId || '-' }}</span>
|
||||
<span class="text-xs">{{ scope.row.totalResults }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="url" label="访问URL" min-width="200">
|
||||
<el-table-column label="验证进度" min-width="220">
|
||||
<template #default="scope">
|
||||
<span class="text-xs" style="word-break: break-all">{{ scope.row.url || '-' }}</span>
|
||||
<div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap">
|
||||
<el-tag size="small" type="success">{{ scope.row.validCount }} 有效</el-tag>
|
||||
<el-tag size="small" type="danger">{{ scope.row.invalidCount }} 无效</el-tag>
|
||||
<el-tag size="small" type="warning">{{ scope.row.uncheckedCount }} 待验</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="statusCode" label="HTTP" width="70">
|
||||
<el-table-column label="最近提交" width="160">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.statusCode" size="small" :type="statusTag(scope.row.statusCode)">
|
||||
{{ scope.row.statusCode }}
|
||||
</el-tag>
|
||||
<span v-else class="text-xs" style="color:#909399">-</span>
|
||||
<span class="text-xs">{{ scope.row.lastSubmitted || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="responseTimeMs" label="耗时" width="80">
|
||||
<el-table-column label="操作" width="180">
|
||||
<template #default="scope">
|
||||
<span class="text-xs">{{ scope.row.responseTimeMs != null ? scope.row.responseTimeMs + 'ms' : '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="验证" width="110">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.isValid === true" size="small" type="success">有效</el-tag>
|
||||
<el-tag v-else-if="scope.row.isValid === false" size="small" type="danger">无效</el-tag>
|
||||
<el-tag v-else size="small" type="info">未验证</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createdAt" label="时间" width="160" />
|
||||
<el-table-column label="操作" width="180" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button size="small" @click="handleView(scope.row)">查看内容</el-button>
|
||||
<el-button v-if="scope.row.isValid == null" size="small" type="success" @click="handleValidate(scope.row, true)">有效</el-button>
|
||||
<el-button v-if="scope.row.isValid == null" size="small" type="danger" @click="handleValidate(scope.row, false)">无效</el-button>
|
||||
<el-button size="small" type="primary" @click="$router.push(`/access-results/check/${scope.row.siteId}`)">
|
||||
进入校验
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<!-- 查看内容对话框 -->
|
||||
<el-dialog v-model="detailVisible" title="访问结果详情" width="900px" :close-on-click-modal="false">
|
||||
<el-descriptions :column="2" border size="small" style="margin-bottom: 16px">
|
||||
<el-descriptions-item label="站点">{{ detail.siteId }}</el-descriptions-item>
|
||||
<el-descriptions-item label="配置版本">{{ detail.configVersion || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="来源">{{ detail.clientId || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="数据类型">{{ detail.contentType }}</el-descriptions-item>
|
||||
<el-descriptions-item label="访问URL" :span="2">{{ detail.url || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="HTTP状态码">
|
||||
<el-tag v-if="detail.statusCode" size="small" :type="statusTag(detail.statusCode)">{{ detail.statusCode }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="响应耗时">{{ detail.responseTimeMs != null ? detail.responseTimeMs + 'ms' : '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="验证结果">
|
||||
<el-tag v-if="detail.isValid === true" size="small" type="success">有效</el-tag>
|
||||
<el-tag v-else-if="detail.isValid === false" size="small" type="danger">无效</el-tag>
|
||||
<el-tag v-else size="small" type="info">未验证</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="验证备注">{{ detail.validationNote || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="提交时间" :span="2">{{ detail.createdAt }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<div style="margin-bottom: 4px; font-size: 13px; color: #606266; font-weight: 500">返回内容:</div>
|
||||
<pre v-if="detail.contentType === 'json'" class="content-json" v-html="highlightedJson"></pre>
|
||||
<pre v-else class="content-html" v-html="highlightedHtml"></pre>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="detailVisible = false">关闭</el-button>
|
||||
<el-button v-if="detail.isValid == null" type="success" @click="handleValidate(detail, true); detailVisible = false">标记有效</el-button>
|
||||
<el-button v-if="detail.isValid == null" type="danger" @click="handleValidate(detail, false); detailVisible = false">标记无效</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -105,138 +49,16 @@ import api from '../api'
|
||||
import { Refresh } from '@element-plus/icons-vue'
|
||||
|
||||
const loading = ref(false)
|
||||
const list = ref([])
|
||||
const filterSiteId = ref('')
|
||||
const filterValid = ref('')
|
||||
const siteOptions = ref([])
|
||||
|
||||
const detailVisible = ref(false)
|
||||
const detail = ref({})
|
||||
|
||||
const filteredList = computed(() => {
|
||||
let items = list.value
|
||||
if (filterSiteId.value) {
|
||||
items = items.filter(r => r.siteId === filterSiteId.value)
|
||||
}
|
||||
if (filterValid.value === '1') {
|
||||
items = items.filter(r => r.isValid === true)
|
||||
} else if (filterValid.value === '0') {
|
||||
items = items.filter(r => r.isValid === false)
|
||||
} else if (filterValid.value === 'null') {
|
||||
items = items.filter(r => r.isValid == null)
|
||||
}
|
||||
return items
|
||||
})
|
||||
|
||||
const highlightedJson = computed(() => {
|
||||
try {
|
||||
const json = typeof detail.value.content === 'string' ? JSON.parse(detail.value.content) : detail.value.content
|
||||
return syntaxHighlight(JSON.stringify(json, null, 2))
|
||||
} catch {
|
||||
return escapeHtml(detail.value.content || '')
|
||||
}
|
||||
})
|
||||
|
||||
const highlightedHtml = computed(() => {
|
||||
return escapeHtml(detail.value.content || '')
|
||||
.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 sites = ref([])
|
||||
|
||||
const fetchAll = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const r = await api.accessResults.list()
|
||||
list.value = r.data || []
|
||||
// 提取去重的站点列表用于筛选
|
||||
const siteIds = [...new Set(list.value.map(r => r.siteId))]
|
||||
siteOptions.value = siteIds.map(id => ({ siteId: id }))
|
||||
} catch { ElMessage.error('获取访问结果失败') }
|
||||
const r = await api.accessResults.summary()
|
||||
sites.value = r.data || []
|
||||
} catch { ElMessage.error('获取汇总失败') }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
|
||||
const handleView = async (row) => {
|
||||
// 详情需从服务端获取完整内容
|
||||
detailVisible.value = true
|
||||
try {
|
||||
const r = await api.accessResults.get(row.id)
|
||||
detail.value = r.data || {}
|
||||
} catch {
|
||||
ElMessage.error('获取详情失败')
|
||||
detail.value = { ...row }
|
||||
}
|
||||
}
|
||||
|
||||
const handleValidate = async (row, isValid) => {
|
||||
try {
|
||||
await api.accessResults.validate(row.id, isValid)
|
||||
ElMessage.success(`已标记为${isValid ? '有效' : '无效'}`)
|
||||
fetchAll()
|
||||
} catch { ElMessage.error('操作失败') }
|
||||
}
|
||||
|
||||
const statusTag = (code) => {
|
||||
if (code >= 200 && code < 300) return 'success'
|
||||
if (code >= 300 && code < 400) return 'warning'
|
||||
return 'danger'
|
||||
}
|
||||
|
||||
const escapeHtml = (str) => {
|
||||
if (!str) return ''
|
||||
return str.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>`
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(fetchAll)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.content-json {
|
||||
background: #1e1e1e;
|
||||
color: #d4d4d4;
|
||||
padding: 12px;
|
||||
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;
|
||||
}
|
||||
.content-html {
|
||||
background: #1e1e1e;
|
||||
color: #d4d4d4;
|
||||
padding: 12px;
|
||||
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;
|
||||
}
|
||||
.content-json :deep(.key) { color: #9cdcfe; }
|
||||
.content-json :deep(.string) { color: #ce9178; }
|
||||
.content-json :deep(.number) { color: #b5cea8; }
|
||||
.content-json :deep(.boolean) { color: #569cd6; }
|
||||
.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; }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user