fix: 页面配置合并站点列表+风险规则默认全类型+修复api.raw调用

This commit is contained in:
mediabot-pt
2026-07-05 00:16:16 +08:00
parent d5c7a485d6
commit e8a7c38eab
2 changed files with 85 additions and 90 deletions

View File

@@ -4,15 +4,14 @@
<el-card style="margin-top:20px">
<div style="margin-bottom:12px;display:flex;gap:8px">
<el-button @click="fetchAll" :loading="loading">刷新</el-button>
<el-button type="primary" @click="showEditor('')">新建</el-button>
</div>
<el-table :data="rules" 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" />
<el-table-column label="规则数" width="80">
<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">{{ getRuleCount(s.row) }}</el-tag>
<el-tag size="small">{{ s.row.ruleCount || 0 }}</el-tag>
</template>
</el-table-column>
<el-table-column label="更新时间" width="160">
@@ -20,14 +19,13 @@
</el-table-column>
<el-table-column label="操作" width="140">
<template #default="s">
<el-button size="small" @click="showEditor(s.row.siteType, s.row)">编辑</el-button>
<el-button size="small" type="danger" text @click="deleteRules(s.row.siteType)">删除</el-button>
<el-button size="small" @click="showEditor(s.row)">编辑</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<el-dialog v-model="editorVisible" :title="editorTitle" width="900px" destroy-on-close>
<el-dialog v-model="editorVisible" :title="'风险规则: ' + editorType" width="900px" destroy-on-close>
<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>
@@ -44,8 +42,8 @@
<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="70">
<template #default="s"><el-select v-model="s.row.condition" size="small"><el-option v-for="x in ['GT','LT','GTE','LTE','EQ']" :key="x" :label="x" :value="x"/></el-select></template>
<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>
@@ -65,40 +63,48 @@
<script setup>
import api from '../api'
const loading = ref(false), saving = ref(false), rules = ref([])
const editorVisible = ref(false), editorType = ref(''), editorTitle = ref('')
const loading = ref(false), saving = ref(false)
const rulesData = ref([])
const siteTypes = ['nexusphp','mtorrent','gazelle','unit3d']
const editorVisible = ref(false), editorType = ref('')
const editingRules = ref([])
const fmt = (t) => t ? t.substring(0,19).replace('T',' ') : '-'
const getRuleCount = (row) => { try { return JSON.parse(row.rulesJson || '[]').length } catch { return 0 } }
const fmt = (t) => t ? (t.substring(0,19)||'').replace('T',' ') : '-'
const merged = computed(() => {
const map = {}
for (const r of rulesData.value) {
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) {
if (!map[t]) map[t] = { siteType: t, version: null, ruleCount: 0, updatedAt: null, rulesJson: null }
}
return Object.values(map)
})
const fetchAll = async () => {
loading.value = true
try { rules.value = (await api.get('/admin/risk-rules')).data || [] }
catch { ElMessage.error('加载失败') }
try {
const r = await api.raw.get('/admin/risk-rules')
rulesData.value = (r.data || r || [])
} catch { ElMessage.error('加载失败') }
finally { loading.value = false }
}
const showEditor = (siteType, row) => {
editorType.value = siteType
editorTitle.value = siteType ? '编辑: ' + siteType : '新建风险规则'
if (row) {
try { editingRules.value = JSON.parse(JSON.stringify(JSON.parse(row.rulesJson || '[]'))) }
catch { editingRules.value = [] }
} else {
editingRules.value = [{type:'KILL',name:'',metric:'',threshold:0,unit:'DAY',condition:'GT',level:'CRITICAL'}]
}
const showEditor = (row) => {
editorType.value = row.siteType
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
}
const editTypeInput = ref('')
const saveRules = async () => {
const t = editorType.value || editTypeInput.value
if (!t) { ElMessage.warning('请输入站点类型'); return }
saving.value = true
try {
await api.post('/risk-rules/' + t, { rules: editingRules.value })
await api.raw.post('/risk-rules/' + editorType.value, { rules: editingRules.value })
ElMessage.success('已保存')
editorVisible.value = false
fetchAll()
@@ -106,14 +112,5 @@ const saveRules = async () => {
finally { saving.value = false }
}
const deleteRules = async (siteType) => {
if (!await ElMessageBox.confirm('确认删除 ' + siteType + ' 的风险规则?', '删除', { type: 'warning', confirmButtonText: '确定' }).catch(() => null)) return
try {
await api.delete('/admin/risk-rules/' + siteType)
ElMessage.success('已删除')
fetchAll()
} catch { ElMessage.error('删除失败') }
}
onMounted(fetchAll)
</script>

View File

@@ -4,33 +4,31 @@
<el-card style="margin-top:20px">
<div style="margin-bottom:12px;display:flex;gap:8px">
<el-button @click="fetchAll" :loading="loading">刷新</el-button>
<el-button type="primary" @click="showEditor(null, null)">新建</el-button>
</div>
<el-table :data="sites" stripe size="small" v-loading="loading" empty-text="暂无配置">
<el-table :data="merged" stripe size="small" v-loading="loading" empty-text="暂无站点">
<el-table-column prop="siteId" label="站点" width="140" />
<el-table-column prop="version" label="版本" width="80" />
<el-table-column label="页面入口" min-width="200">
<template #default="s">
<el-tag v-for="e in getEntries(s.row)" :key="e.key" size="small" style="margin:1px">{{ e.label || e.key }}</el-tag>
</template>
<el-table-column label="名称" min-width="130">
<template #default="s">{{ (s.row.names||[]).join(' / ') || '-' }}</template>
</el-table-column>
<el-table-column label="更新时间" width="160">
<el-table-column label="版本" width="80">
<template #default="s">{{ s.row.version || '-' }}</template>
</el-table-column>
<el-table-column label="入口数" width="70" align="center">
<template #default="s">{{ s.row.entryCount || 0 }}</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="140">
<el-table-column label="操作" width="100">
<template #default="s">
<el-button size="small" @click="showEditor(s.row.siteId, s.row)">编辑</el-button>
<el-button size="small" type="danger" text @click="deletePages(s.row.siteId)">删除</el-button>
<el-button size="small" @click="showEditor(s.row)">编辑</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<el-dialog v-model="editorVisible" :title="editorTitle" width="800px" destroy-on-close>
<div style="margin-bottom:8px;color:#909399">
<span v-if="!isNewSite">版本: {{ editorData?.version }} | {{ editorData?.entries?.length || 0 }} 个入口</span>
</div>
<el-dialog v-model="editorVisible" :title="'页面配置: ' + editorSiteId" width="750px" destroy-on-close>
<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>
@@ -49,7 +47,7 @@
</el-table-column>
</el-table>
<div style="display:flex;gap:8px">
<el-button size="small" @click="editingEntries.push({key:'',label:'',url:'{base}',method:'GET'})">+ 添加入口</el-button>
<el-button size="small" @click="editingEntries.push({key:'',label:'',url:'{base}','method':'GET'})">+ 添加入口</el-button>
<el-button size="small" type="primary" @click="savePages" :loading="saving">保存</el-button>
</div>
</el-dialog>
@@ -59,45 +57,54 @@
<script setup>
import api from '../api'
const loading = ref(false), saving = ref(false), sites = ref([])
const editorVisible = ref(false), editorSiteId = ref(''), editorTitle = ref(''), isNewSite = ref(false)
const editorData = ref(null), editingEntries = ref([])
const loading = ref(false), saving = ref(false)
const sites = ref([]), pagesMap = ref({})
const editorVisible = ref(false), editorSiteId = ref(''), editingEntries = ref([])
const fmt = (t) => t ? t.substring(0,19).replace('T',' ') : '-'
const getEntries = (row) => { try { return JSON.parse(row.entriesJson || '[]') } catch { return [] } }
const fmt = (t) => t ? (t.substring(0,19)||'').replace('T',' ') : '-'
const merged = computed(() => {
return sites.value.map(s => ({
siteId: s.siteId,
names: s.names,
version: pagesMap.value[s.siteId]?.version || null,
entryCount: pagesMap.value[s.siteId]?.entryCount || 0,
updatedAt: pagesMap.value[s.siteId]?.updatedAt || null,
entriesJson: pagesMap.value[s.siteId]?.entriesJson || null,
}))
})
const fetchAll = async () => {
loading.value = true
try { sites.value = (await api.get('/admin/site-pages')).data || [] }
catch { ElMessage.error('加载失败') }
try {
const [sr, pr] = await Promise.all([
api.sites.adminList().catch(() => ({})),
api.raw.get('/admin/site-pages').catch(() => ({})),
])
sites.value = sr.data || sr || []
const pages = pr.data || pr || []
const map = {}
for (const p of pages) {
const entries = JSON.parse(p.entriesJson || '[]')
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 }
}
const showEditor = (siteId, row) => {
editorSiteId.value = siteId
isNewSite.value = !siteId
if (!siteId) {
// 新建:选站点
editorTitle.value = '新建页面配置'
editorData.value = null
editingEntries.value = [{key:'home',label:'首页',url:'{base}',method:'GET'}]
editSiteInput.value = ''
} else {
editorTitle.value = '编辑: ' + siteId
editorData.value = row
editingEntries.value = JSON.parse(JSON.stringify(getEntries(row)))
}
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 editSiteInput = ref('')
const savePages = async () => {
const sid = editorSiteId.value || editSiteInput.value
if (!sid) { ElMessage.warning('请输入站点ID'); return }
saving.value = true
try {
await api.put('/sites/' + sid + '/pages', { entries: editingEntries.value })
await api.raw.put('/sites/' + editorSiteId.value + '/pages', { entries: editingEntries.value })
ElMessage.success('已保存')
editorVisible.value = false
fetchAll()
@@ -105,14 +112,5 @@ const savePages = async () => {
finally { saving.value = false }
}
const deletePages = async (siteId) => {
if (!await ElMessageBox.confirm('确认删除 ' + siteId + ' 的页面配置?', '删除', { type: 'warning', confirmButtonText: '确定' }).catch(() => null)) return
try {
await api.delete('/admin/site-pages/' + siteId)
ElMessage.success('已删除')
fetchAll()
} catch { ElMessage.error('删除失败') }
}
onMounted(fetchAll)
</script>