fix: 内联编辑替代弹窗+风险规则动态站点类型+页面配置修复加载

This commit is contained in:
mediabot-pt
2026-07-05 00:42:26 +08:00
parent 2999e848f8
commit 340a375dcf
2 changed files with 162 additions and 151 deletions

View File

@@ -6,70 +6,73 @@
<el-button @click="fetchAll" :loading="loading">刷新</el-button>
</div>
<el-table :data="merged" stripe size="small" v-loading="loading" empty-text="暂无规则">
<el-table :data="merged" stripe size="small" v-loading="loading" empty-text="暂无站点类型">
<el-table-column prop="siteType" label="站点类型" width="120" />
<el-table-column prop="version" label="版本" width="80" align="center" />
<el-table-column label="规则数" width="80" align="center">
<template #default="s">
<el-tag size="small">{{ s.row.ruleCount || 0 }}</el-tag>
</template>
<template #default="s">{{ s.row.ruleCount || 0 }}</template>
</el-table-column>
<el-table-column label="版本" width="70" align="center">
<template #default="s">{{ s.row.version || '-' }}</template>
</el-table-column>
<el-table-column label="更新时间" width="160">
<template #default="s">{{ fmt(s.row.updatedAt) }}</template>
</el-table-column>
<el-table-column label="操作" width="140">
<el-table-column label="操作" width="100" fixed="right">
<template #default="s">
<el-button size="small" @click="showEditor(s.row)">编辑</el-button>
<el-button size="small" @click="toggleEdit(s.row)" :type="editType===s.row.siteType?'warning':''">
{{ editType===s.row.siteType ? '收起' : '编辑' }}
</el-button>
</template>
</el-table-column>
<el-table-column type="expand" width="1" class-name="expand-hide">
<template #default="s" v-if="editType===s.row.siteType">
<div style="padding:12px 0">
<div style="margin-bottom:8px;display:flex;gap:8px">
<el-radio-group v-model="editorMode" size="small">
<el-radio-button label="table">表格</el-radio-button>
<el-radio-button label="json">JSON</el-radio-button>
</el-radio-group>
</div>
<template v-if="editorMode==='table'">
<el-table :data="editingRules" stripe size="small" style="margin-bottom:8px">
<el-table-column prop="type" label="类型" width="90">
<template #default="e"><el-select v-model="e.row.type" size="small"><el-option v-for="x in ['KILL','TRAFFIC','RATIO','LOGIN','HD']" :key="x" :label="x" :value="x"/></el-select></template>
</el-table-column>
<el-table-column prop="name" label="名称" min-width="160">
<template #default="e"><el-input v-model="e.row.name" size="small"/></template>
</el-table-column>
<el-table-column prop="metric" label="指标" width="130">
<template #default="e"><el-input v-model="e.row.metric" size="small"/></template>
</el-table-column>
<el-table-column prop="threshold" label="阈值" width="70">
<template #default="e"><el-input v-model.number="e.row.threshold" size="small" type="number"/></template>
</el-table-column>
<el-table-column prop="unit" label="单位" width="60">
<template #default="e"><el-input v-model="e.row.unit" size="small"/></template>
</el-table-column>
<el-table-column prop="condition" label="条件" width="70">
<template #default="e"><el-select v-model="e.row.condition" size="small"><el-option v-for="x in ['GT','LT','GTE','LTE']" :key="x" :label="x" :value="x"/></el-select></template>
</el-table-column>
<el-table-column prop="level" label="级别" width="80">
<template #default="e"><el-select v-model="e.row.level" size="small"><el-option v-for="x in ['CRITICAL','WARN','INFO']" :key="x" :label="x" :value="x"/></el-select></template>
</el-table-column>
<el-table-column label="" width="50"><template #default="e"><el-button size="small" type="danger" text @click="editingRules.splice(e.$index,1)">×</el-button></template></el-table-column>
</el-table>
<el-button size="small" @click="editingRules.push({type:'TRAFFIC',name:'',metric:'',threshold:0,unit:'DAY',condition:'GT',level:'WARN'})">+ 添加</el-button>
</template>
<template v-else>
<div style="height:300px;border-radius:4px;overflow:hidden;margin-bottom:8px">
<CodeEditor v-model="editorJson" language="json" />
</div>
</template>
<div style="margin-top:8px">
<el-button size="small" type="primary" @click="saveRules" :loading="saving">保存</el-button>
</div>
</div>
</template>
</el-table-column>
</el-table>
</el-card>
<el-dialog v-model="editorVisible" :title="'风险规则: ' + editorType" width="900px" destroy-on-close>
<div style="margin-bottom:8px;display:flex;gap:8px;align-items:center">
<el-radio-group v-model="editorMode" size="small">
<el-radio-button label="table">表格编辑</el-radio-button>
<el-radio-button label="json">JSON 编辑</el-radio-button>
</el-radio-group>
</div>
<template v-if="editorMode === 'table'">
<el-table :data="editingRules" stripe size="small" style="margin-bottom:12px">
<el-table-column prop="type" label="类型" width="90">
<template #default="s"><el-select v-model="s.row.type" size="small"><el-option v-for="x in ['KILL','TRAFFIC','RATIO','LOGIN','HD']" :key="x" :label="x" :value="x"/></el-select></template>
</el-table-column>
<el-table-column prop="name" label="名称" min-width="160">
<template #default="s"><el-input v-model="s.row.name" size="small" /></template>
</el-table-column>
<el-table-column prop="metric" label="指标" width="140">
<template #default="s"><el-input v-model="s.row.metric" size="small" /></template>
</el-table-column>
<el-table-column prop="threshold" label="阈值" width="80">
<template #default="s"><el-input v-model.number="s.row.threshold" size="small" type="number" /></template>
</el-table-column>
<el-table-column prop="unit" label="单位" width="70">
<template #default="s"><el-input v-model="s.row.unit" size="small" /></template>
</el-table-column>
<el-table-column prop="condition" label="条件" width="80">
<template #default="s"><el-select v-model="s.row.condition" size="small"><el-option v-for="x in ['GT','LT','GTE','LTE']" :key="x" :label="x" :value="x"/></el-select></template>
</el-table-column>
<el-table-column prop="level" label="级别" width="90">
<template #default="s"><el-select v-model="s.row.level" size="small"><el-option v-for="x in ['CRITICAL','WARN','INFO']" :key="x" :label="x" :value="x"/></el-select></template>
</el-table-column>
<el-table-column label="操作" width="60">
<template #default="s"><el-button size="small" type="danger" text @click="editingRules.splice(s.$index,1)">删除</el-button></template>
</el-table-column>
</el-table>
<el-button size="small" @click="editingRules.push({type:'TRAFFIC',name:'',metric:'',threshold:0,unit:'DAY',condition:'GT',level:'WARN'})" style="margin-bottom:12px">+ 添加规则</el-button>
</template>
<template v-else>
<div style="height:400px;border-radius:4px;overflow:hidden;margin-bottom:12px">
<CodeEditor v-model="editorJson" language="json" />
</div>
</template>
<div style="display:flex;gap:8px">
<el-button size="small" type="primary" @click="saveRules" :loading="saving">保存</el-button>
</div>
</el-dialog>
</div>
</template>
@@ -77,28 +80,25 @@
import api from '../api'
const loading = ref(false), saving = ref(false)
const rulesData = ref([])
const siteTypes = ['nexusphp','mtorrent','gazelle','unit3d']
const editorVisible = ref(false), editorType = ref(''), editorMode = ref('table'), editorJson = ref('')
const siteTypes = ref([]), rulesData = ref([])
const editType = ref(''), editorMode = ref('table'), editorJson = ref('')
const editingRules = ref([])
watch(editorMode, (m) => {
if (m === 'json') editorJson.value = JSON.stringify(editingRules.value, null, 2)
else {
try { editingRules.value = JSON.parse(editorJson.value) } catch { /* keep table */ }
}
})
const fmt = (t) => t ? (t.substring(0,19)||'').replace('T',' ') : '-'
watch(editorMode, (m) => {
if (m === 'json') editorJson.value = JSON.stringify(editingRules.value, null, 2)
else { try { editingRules.value = JSON.parse(editorJson.value) } catch {} }
})
const merged = computed(() => {
const map = {}
const list = Array.isArray(rulesData.value) ? rulesData.value : []
for (const r of list) {
const cnt = (() => { try { return JSON.parse(r.rulesJson || '[]').length } catch { return 0 } })()
const rules = Array.isArray(rulesData.value) ? rulesData.value : []
for (const r of rules) {
const cnt = (() => { try { return JSON.parse(r.rulesJson||'[]').length } catch { return 0 } })()
map[r.siteType] = { siteType: r.siteType, version: r.version, ruleCount: cnt, updatedAt: r.updatedAt, rulesJson: r.rulesJson }
}
for (const t of siteTypes) {
for (const t of siteTypes.value) {
if (!map[t]) map[t] = { siteType: t, version: null, ruleCount: 0, updatedAt: null, rulesJson: null }
}
return Object.values(map)
@@ -107,31 +107,40 @@ const merged = computed(() => {
const fetchAll = async () => {
loading.value = true
try {
const r = await api.raw.get('/admin/risk-rules')
rulesData.value = Array.isArray(r?.data) ? r.data : Array.isArray(r) ? r : []
// 获取站点类型(从实际站点数据中提取)
const sr = await api.raw.get('/admin/sites').catch(() => ({}))
const siteList = (sr?.data || sr || [])
siteTypes.value = [...new Set(siteList.map(s => s.siteType).filter(Boolean))]
if (siteTypes.value.length === 0) siteTypes.value = ['nexusphp','mtorrent','gazelle','unit3d']
const rr = await api.raw.get('/admin/risk-rules').catch(() => ({}))
rulesData.value = Array.isArray(rr?.data) ? rr.data : Array.isArray(rr) ? rr : []
} catch { ElMessage.error('加载失败') }
finally { loading.value = false }
}
const showEditor = (row) => {
editorType.value = row.siteType
try { editingRules.value = JSON.parse(JSON.stringify(JSON.parse(row.rulesJson || '[]'))) }
const toggleEdit = (row) => {
if (editType.value === row.siteType) { editType.value = ''; return }
editType.value = row.siteType
editorMode.value = 'table'
try { editingRules.value = JSON.parse(JSON.stringify(JSON.parse(row.rulesJson||'[]'))) }
catch { editingRules.value = [{type:'KILL',name:'',metric:'',threshold:0,unit:'DAY',condition:'GT',level:'CRITICAL'}] }
if (editingRules.value.length === 0) editingRules.value.push({type:'KILL',name:'',metric:'',threshold:0,unit:'DAY',condition:'GT',level:'CRITICAL'})
editorVisible.value = true
if (editingRules.value.length===0) editingRules.value.push({type:'KILL',name:'',metric:'',threshold:0,unit:'DAY',condition:'GT',level:'CRITICAL'})
}
const saveRules = async () => {
saving.value = true
try {
const rules = editorMode.value === 'json' ? JSON.parse(editorJson.value) : editingRules.value
await api.raw.post('/risk-rules/' + editorType.value, { rules })
ElMessage.success('已保存')
editorVisible.value = false
fetchAll()
const rules = editorMode.value==='json' ? JSON.parse(editorJson.value) : editingRules.value
await api.raw.post('/risk-rules/'+editType.value, { rules })
ElMessage.success('已保存'); editType.value = ''; fetchAll()
} catch { ElMessage.error('保存失败') }
finally { saving.value = false }
}
onMounted(fetchAll)
</script>
<style scoped>
.expand-hide :deep(.el-table__expand-column .cell) { display:none }
</style>

View File

@@ -11,59 +11,62 @@
<el-table-column label="名称" min-width="130">
<template #default="s">{{ (s.row.names||[]).join(' / ') || '-' }}</template>
</el-table-column>
<el-table-column label="版本" width="80">
<template #default="s">{{ s.row.version || '-' }}</template>
</el-table-column>
<el-table-column label="入口数" width="70" align="center">
<el-table-column label="入口数" width="80" align="center">
<template #default="s">{{ s.row.entryCount || 0 }}</template>
</el-table-column>
<el-table-column label="版本" width="70" align="center">
<template #default="s">{{ s.row.version || '-' }}</template>
</el-table-column>
<el-table-column label="更新时间" width="150">
<template #default="s">{{ fmt(s.row.updatedAt) }}</template>
</el-table-column>
<el-table-column label="操作" width="100">
<el-table-column label="操作" width="100" fixed="right">
<template #default="s">
<el-button size="small" @click="showEditor(s.row)">编辑</el-button>
<el-button size="small" @click="toggleEdit(s.row)" :type="editSiteId===s.row.siteId?'warning':''">
{{ editSiteId===s.row.siteId ? '收起' : '编辑' }}
</el-button>
</template>
</el-table-column>
<el-table-column type="expand" width="1" class-name="expand-hide">
<template #default="s" v-if="editSiteId===s.row.siteId">
<div style="padding:12px 0">
<div style="margin-bottom:8px;display:flex;gap:8px">
<el-radio-group v-model="editorMode" size="small">
<el-radio-button label="table">表格</el-radio-button>
<el-radio-button label="json">JSON</el-radio-button>
</el-radio-group>
</div>
<template v-if="editorMode==='table'">
<el-table :data="editingEntries" stripe size="small" style="margin-bottom:8px">
<el-table-column prop="key" label="key" width="100">
<template #default="e"><el-input v-model="e.row.key" size="small" /></template>
</el-table-column>
<el-table-column prop="label" label="名称" width="100">
<template #default="e"><el-input v-model="e.row.label" size="small" /></template>
</el-table-column>
<el-table-column prop="url" label="URL" min-width="200">
<template #default="e"><el-input v-model="e.row.url" size="small" /></template>
</el-table-column>
<el-table-column prop="method" label="方法" width="80">
<template #default="e"><el-select v-model="e.row.method" size="small"><el-option label="GET" value="GET"/><el-option label="POST" value="POST"/></el-select></template>
</el-table-column>
<el-table-column label="" width="50"><template #default="e"><el-button size="small" type="danger" text @click="editingEntries.splice(e.$index,1)">×</el-button></template></el-table-column>
</el-table>
<el-button size="small" @click="editingEntries.push({key:'',label:'',url:'{base}',method:'GET'})">+ 添加</el-button>
</template>
<template v-else>
<div style="height:300px;border-radius:4px;overflow:hidden;margin-bottom:8px">
<CodeEditor v-model="editorJson" language="json" />
</div>
</template>
<div style="margin-top:8px">
<el-button size="small" type="primary" @click="savePages" :loading="saving">保存</el-button>
</div>
</div>
</template>
</el-table-column>
</el-table>
</el-card>
<el-dialog v-model="editorVisible" :title="'页面配置: ' + editorSiteId" width="800px" destroy-on-close>
<div style="margin-bottom:8px;display:flex;gap:8px;align-items:center">
<el-radio-group v-model="editorMode" size="small">
<el-radio-button label="table">表格编辑</el-radio-button>
<el-radio-button label="json">JSON 编辑</el-radio-button>
</el-radio-group>
</div>
<template v-if="editorMode === 'table'">
<el-table :data="editingEntries" stripe size="small" style="margin-bottom:12px">
<el-table-column prop="key" label="标识" width="100">
<template #default="s"><el-input v-model="s.row.key" size="small" /></template>
</el-table-column>
<el-table-column prop="label" label="名称" width="100">
<template #default="s"><el-input v-model="s.row.label" size="small" /></template>
</el-table-column>
<el-table-column prop="url" label="URL" min-width="180">
<template #default="s"><el-input v-model="s.row.url" size="small" /></template>
</el-table-column>
<el-table-column prop="method" label="方法" width="80">
<template #default="s"><el-select v-model="s.row.method" size="small"><el-option label="GET" value="GET" /><el-option label="POST" value="POST" /></el-select></template>
</el-table-column>
<el-table-column label="操作" width="60">
<template #default="s"><el-button size="small" type="danger" text @click="editingEntries.splice(s.$index,1)">删除</el-button></template>
</el-table-column>
</el-table>
<el-button size="small" @click="editingEntries.push({key:'',label:'',url:'{base}','method':'GET'})" style="margin-bottom:12px">+ 添加入口</el-button>
</template>
<template v-else>
<div style="height:400px;border-radius:4px;overflow:hidden;margin-bottom:12px">
<CodeEditor v-model="editorJson" language="json" />
</div>
</template>
<div style="display:flex;gap:8px">
<el-button size="small" type="primary" @click="savePages" :loading="saving">保存</el-button>
</div>
</el-dialog>
</div>
</template>
@@ -72,21 +75,18 @@ import api from '../api'
const loading = ref(false), saving = ref(false)
const sites = ref([]), pagesMap = ref({})
const editorVisible = ref(false), editorSiteId = ref(''), editorMode = ref('table'), editorJson = ref('')
const editSiteId = ref(''), editorMode = ref('table'), editorJson = ref('')
const editingEntries = ref([])
watch(editorMode, (m) => {
if (m === 'json') editorJson.value = JSON.stringify(editingEntries.value, null, 2)
else {
try { editingEntries.value = JSON.parse(editorJson.value) } catch { /* keep table data */ }
}
})
const fmt = (t) => t ? (t.substring(0,19)||'').replace('T',' ') : '-'
watch(editorMode, (m) => {
if (m === 'json') editorJson.value = JSON.stringify(editingEntries.value, null, 2)
else { try { editingEntries.value = JSON.parse(editorJson.value) } catch {} }
})
const merged = computed(() => {
const list = Array.isArray(sites.value) ? sites.value : []
return list.map(s => ({
return (Array.isArray(sites.value)?sites.value:[]).map(s => ({
siteId: s.siteId,
names: s.names,
version: pagesMap.value[s.siteId]?.version || null,
@@ -99,41 +99,43 @@ const merged = computed(() => {
const fetchAll = async () => {
loading.value = true
try {
const [sr, pr] = await Promise.all([
api.sites.adminList().catch(() => ({})),
api.raw.get('/admin/site-pages').catch(() => ({})),
const [sr, pr] = await Promise.allSettled([
api.raw.get('/admin/sites'),
api.raw.get('/admin/site-pages'),
])
sites.value = sr.data || sr || []
const pages = pr.data || pr || []
sites.value = (sr.status==='fulfilled' ? (sr.value?.data || sr.value || []) : [])
const pages = pr.status==='fulfilled' ? (pr.value?.data || pr.value || []) : []
const map = {}
for (const p of pages) {
const entries = JSON.parse(p.entriesJson || '[]')
const entries = (() => { try { return JSON.parse(p.entriesJson||'[]') } catch {return []} })()
map[p.siteId] = { version: p.version, entryCount: entries.length, updatedAt: p.updatedAt, entriesJson: p.entriesJson }
}
pagesMap.value = map
} catch { ElMessage.error('加载失败') }
finally { loading.value = false }
} finally { loading.value = false }
}
const showEditor = (row) => {
editorSiteId.value = row.siteId
try { editingEntries.value = JSON.parse(JSON.stringify(JSON.parse(row.entriesJson || '[]'))) }
catch { editingEntries.value = [{key:'home',label:'首页',url:'{base}','method':'GET'}] }
if (editingEntries.value.length === 0) editingEntries.value.push({key:'home',label:'首页',url:'{base}','method':'GET'})
editorVisible.value = true
const toggleEdit = (row) => {
if (editSiteId.value === row.siteId) { editSiteId.value = ''; return }
editSiteId.value = row.siteId
editorMode.value = 'table'
try { editingEntries.value = JSON.parse(JSON.stringify(JSON.parse(row.entriesJson||'[]'))) }
catch { editingEntries.value = [{key:'home',label:'首页',url:'{base}',method:'GET'}] }
if (editingEntries.value.length===0) editingEntries.value.push({key:'home',label:'首页',url:'{base}',method:'GET'})
}
const savePages = async () => {
saving.value = true
try {
const entries = editorMode.value === 'json' ? JSON.parse(editorJson.value) : editingEntries.value
await api.raw.put('/sites/' + editorSiteId.value + '/pages', { entries })
ElMessage.success('已保存')
editorVisible.value = false
fetchAll()
const entries = editorMode.value==='json' ? JSON.parse(editorJson.value) : editingEntries.value
await api.raw.put('/sites/'+editSiteId.value+'/pages', { entries })
ElMessage.success('已保存'); editSiteId.value = ''; fetchAll()
} catch { ElMessage.error('保存失败') }
finally { saving.value = false }
}
onMounted(fetchAll)
</script>
<style scoped>
.expand-hide :deep(.el-table__expand-column .cell) { display:none }
</style>