@@ -5,36 +5,80 @@
< 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 36 0px;overflow:auto" shadow = "never" >
<!-- 左侧 : 适配配置面板 -- >
< el-card style = "flex:0 0 38 0px;overflow:auto" shadow = "never" >
< template # header >
< div style = "display:flex;justify-content:space-between;align-items:center" >
< div style = "display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:8px " >
< span > < strong > 适配配置 < / strong > < / span >
< el-button size = "small" text @click ="toggleConfigCollapse" > {{ configCollapsed ? ' 展开 ' : ' 折叠 ' }} < / el -button >
< 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' ? '已批准' : '待审核' } }
< / 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 = "configError" style=" padding:1 0px;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 >
< div v-else-if = "!selectedConfig" style="text-align:center; padding:2 0px;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" >
@@ -50,38 +94,28 @@
< 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 >
< 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 >
<!-- 该页面的结果 ( 默认最近1条 , 展开后最多5条 ) -- >
< 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)" 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;max-width:80px" > {{ r.clientId }} < / span >
< 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;flex-shrink:0 " >
< 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 >
< / div >
< / div >
<!-- 收起时提示还有更多 -- >
< div v-if = "!expandedUrls.has(page.url) && page.totalCount > 5"
style = "padding:2px 0;color:#909399;font-size:12px;text-align:center" >
< 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 >
@@ -94,7 +128,7 @@
< ! - - 查看内容弹窗 - - >
< 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 :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 >
@@ -123,17 +157,24 @@ const siteId = route.params.siteId
const loading = ref ( false )
const allResults = ref ( [ ] )
const configVersion = ref ( '' )
const allConfigVersions = ref ( [ ] )
const selectedConfig = ref ( null )
const selectedConfigJson = ref ( '' )
const configTab = ref ( '' )
const configLoading = ref ( true )
const configError = ref ( '' )
const config Content = ref ( '' )
const configCollapsed = ref ( false )
const editing = ref ( false )
const edit Content = 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 expandedUrls = reactive ( new Set ( ) )
const MAX _VISIBLE = 5
@@ -142,7 +183,6 @@ const toggleExpand = (url) => {
else expandedUrls . add ( url )
}
// 按 URL 分组( 最新在前) , 默认显示最近5条
const groupedPages = computed ( ( ) => {
const map = new Map ( )
for ( const r of allResults . value ) {
@@ -156,8 +196,7 @@ const groupedPages = computed(() => {
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 )
const visibleCount = visibleResults . length
return { url , results , totalCount , visibleResults , visibleCount , allValid , allInvalid , hasUnchecked }
return { url , results , totalCount , visibleResults , visibleCount : visibleResults . length , allValid , allInvalid , hasUnchecked }
} )
} )
@@ -167,31 +206,104 @@ const uncheckedCount = computed(() => allResults.value.filter(r => r.isValid ==
const refresh = async ( ) => {
loading . value = true
try {
const r = await api . accessResults . bySite ( siteId )
allResults . value = r . data || [ ]
} catch { ElMessage . error ( '获取访问结果失败' ) }
try { const r = await api . accessResults . bySite ( siteId ) ; allResults . value = r . data || [ ] }
catch { ElMessage . error ( '获取访问结果失败' ) }
finally { loading . value = false }
}
const loadConfig = async ( ) => {
// 加载所有配置版本(已批准 + 待审核)
const loadConfigs = async ( ) => {
configLoading . value = true
try {
const r = await api . configs . bySite ( siteId )
const configs = r . data || [ ]
if ( c onfigs. l ength ) {
configVersion . value = configs [ 0 ] . version
const resp = await api . configs . content ( siteId , configs [ 0 ] . version )
configContent . value = typeof r esp . data === 'string' ? resp . data : JSON . stringify ( resp . data , null , 2 )
} else {
c onfigError . value = '该站点暂无适配配置'
const [ approvedRes , pendingRes ] = await Promise . allSettled ( [
api . configs . bySite ( siteId ) ,
api . adminC onfigs . p endingBySite ( siteId )
] )
const approved = approvedRes . status === 'fulfilled' ? ( approvedRes . value . data || [ ] ) : [ ]
const pending = pendingR es. status === 'fulfilled' ? ( pendingRes . value . data || [ ] ) : [ ]
// 合并:待审核在前,已批准在后
allC onfigVersions . 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 )
}
} catch ( e ) {
configError . value = e . response ? . data ? . message || '获取配置失败'
} finally { configLoading . value = false }
}
}
const toggleConfigCollaps e = ( ) => { configCollapsed . value = ! configCollapsed . value }
const onConfigTabChang e = ( tabName ) => {
const cfg = allConfigVersions . value . find ( v => String ( v . id ) === tabName )
if ( cfg ) selectConfig ( cfg )
}
const selectConfig = async ( cfg ) => {
selectedConfig . value = cfg
editing . value = false
// 加载配置内容
try {
if ( cfg . status === 'pending' ) {
const r = await api . adminConfigs . getContent ( cfg . id )
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 )
}
} catch {
selectedConfigJson . value = ''
}
}
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 || '保存失败' ) }
finally { saving . value = false }
}
const approveConfig = async ( ) => {
if ( ! selectedConfig . value ) 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 || '审批失败' ) }
finally { approving . value = false }
}
const rejectConfig = async ( ) => {
if ( ! selectedConfig . value ) 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 || '拒绝失败' ) }
finally { rejecting . value = false }
}
const batchValidate = async ( url , isValid ) => {
try {
@@ -220,51 +332,36 @@ const viewContent = async (row) => {
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
} catch { ElMessage . error ( '获取内容失败' ) ; viewingContent . value = '加载失败' }
finally { contentLoading . value = false }
}
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'
return 'danger'
}
// 配置 JSON 语法高亮
// 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 || '' ) }
try { return syntaxHighlight ( JSON . stringify ( JSON . parse ( selectedConfigJson . value ) , null , 2 ) ) }
catch { return escapeHtml ( selectedConfigJson . 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 || '' ) }
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>' )
. 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 ) => {
@@ -278,7 +375,7 @@ const syntaxHighlight = (json) => escapeHtml(json).replace(
onMounted ( ( ) => {
refresh ( )
loadConfig ( )
loadConfigs ( )
} )
< / script >
@@ -293,7 +390,7 @@ onMounted(() => {
margin : 0 ;
white - space : pre - wrap ;
word - break : break - all ;
max - height : calc ( 100 vh - 28 0px ) ;
max - height : calc ( 100 vh - 44 0px ) ;
overflow : auto ;
}
. content - json , . content - html {