feat: 页面配置+风险规则编辑器支持表格/JSON双模式切换

This commit is contained in:
mediabot-pt
2026-07-05 00:22:28 +08:00
parent e8a7c38eab
commit 5ad6b043e4
2 changed files with 93 additions and 50 deletions

View File

@@ -26,34 +26,47 @@
</el-card> </el-card>
<el-dialog v-model="editorVisible" :title="'风险规则: ' + editorType" 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"> <div style="margin-bottom:8px;display:flex;gap:8px;align-items:center">
<el-table-column prop="type" label="类型" width="90"> <el-radio-group v-model="editorMode" size="small">
<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-radio-button label="table">表格编辑</el-radio-button>
</el-table-column> <el-radio-button label="json">JSON 编辑</el-radio-button>
<el-table-column prop="name" label="名称" min-width="160"> </el-radio-group>
<template #default="s"><el-input v-model="s.row.name" size="small" /></template> </div>
</el-table-column> <template v-if="editorMode === 'table'">
<el-table-column prop="metric" label="指标" width="140"> <el-table :data="editingRules" stripe size="small" style="margin-bottom:12px">
<template #default="s"><el-input v-model="s.row.metric" size="small" /></template> <el-table-column prop="type" label="类型" width="90">
</el-table-column> <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 prop="threshold" label="阈值" width="80"> </el-table-column>
<template #default="s"><el-input v-model.number="s.row.threshold" size="small" type="number" /></template> <el-table-column prop="name" label="名称" min-width="160">
</el-table-column> <template #default="s"><el-input v-model="s.row.name" size="small" /></template>
<el-table-column prop="unit" label="单位" width="70"> </el-table-column>
<template #default="s"><el-input v-model="s.row.unit" size="small" /></template> <el-table-column prop="metric" label="指标" width="140">
</el-table-column> <template #default="s"><el-input v-model="s.row.metric" size="small" /></template>
<el-table-column prop="condition" label="条件" width="80"> </el-table-column>
<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 prop="threshold" label="阈值" width="80">
</el-table-column> <template #default="s"><el-input v-model.number="s.row.threshold" size="small" type="number" /></template>
<el-table-column prop="level" label="级别" width="90"> </el-table-column>
<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 prop="unit" label="单位" width="70">
</el-table-column> <template #default="s"><el-input v-model="s.row.unit" size="small" /></template>
<el-table-column label="操作" width="60"> </el-table-column>
<template #default="s"><el-button size="small" type="danger" text @click="editingRules.splice(s.$index,1)">删除</el-button></template> <el-table-column prop="condition" label="条件" width="80">
</el-table-column> <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> </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"> <div style="display:flex;gap:8px">
<el-button size="small" @click="editingRules.push({type:'TRAFFIC',name:'',metric:'',threshold:0,unit:'DAY',condition:'GT',level:'WARN'})">+ 添加规则</el-button>
<el-button size="small" type="primary" @click="saveRules" :loading="saving">保存</el-button> <el-button size="small" type="primary" @click="saveRules" :loading="saving">保存</el-button>
</div> </div>
</el-dialog> </el-dialog>
@@ -66,9 +79,16 @@ import api from '../api'
const loading = ref(false), saving = ref(false) const loading = ref(false), saving = ref(false)
const rulesData = ref([]) const rulesData = ref([])
const siteTypes = ['nexusphp','mtorrent','gazelle','unit3d'] const siteTypes = ['nexusphp','mtorrent','gazelle','unit3d']
const editorVisible = ref(false), editorType = ref('') const editorVisible = ref(false), editorType = ref(''), editorMode = ref('table'), editorJson = ref('')
const editingRules = 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',' ') : '-' const fmt = (t) => t ? (t.substring(0,19)||'').replace('T',' ') : '-'
const merged = computed(() => { const merged = computed(() => {
@@ -104,7 +124,8 @@ const showEditor = (row) => {
const saveRules = async () => { const saveRules = async () => {
saving.value = true saving.value = true
try { try {
await api.raw.post('/risk-rules/' + editorType.value, { rules: editingRules.value }) const rules = editorMode.value === 'json' ? JSON.parse(editorJson.value) : editingRules.value
await api.raw.post('/risk-rules/' + editorType.value, { rules })
ElMessage.success('已保存') ElMessage.success('已保存')
editorVisible.value = false editorVisible.value = false
fetchAll() fetchAll()

View File

@@ -28,26 +28,39 @@
</el-table> </el-table>
</el-card> </el-card>
<el-dialog v-model="editorVisible" :title="'页面配置: ' + editorSiteId" width="750px" destroy-on-close> <el-dialog v-model="editorVisible" :title="'页面配置: ' + editorSiteId" width="800px" destroy-on-close>
<el-table :data="editingEntries" stripe size="small" style="margin-bottom:12px"> <div style="margin-bottom:8px;display:flex;gap:8px;align-items:center">
<el-table-column prop="key" label="标识" width="100"> <el-radio-group v-model="editorMode" size="small">
<template #default="s"><el-input v-model="s.row.key" size="small" /></template> <el-radio-button label="table">表格编辑</el-radio-button>
</el-table-column> <el-radio-button label="json">JSON 编辑</el-radio-button>
<el-table-column prop="label" label="名称" width="100"> </el-radio-group>
<template #default="s"><el-input v-model="s.row.label" size="small" /></template> </div>
</el-table-column> <template v-if="editorMode === 'table'">
<el-table-column prop="url" label="URL" min-width="180"> <el-table :data="editingEntries" stripe size="small" style="margin-bottom:12px">
<template #default="s"><el-input v-model="s.row.url" size="small" /></template> <el-table-column prop="key" label="标识" width="100">
</el-table-column> <template #default="s"><el-input v-model="s.row.key" size="small" /></template>
<el-table-column prop="method" label="方法" width="80"> </el-table-column>
<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 prop="label" label="名称" width="100">
</el-table-column> <template #default="s"><el-input v-model="s.row.label" size="small" /></template>
<el-table-column label="操作" width="60"> </el-table-column>
<template #default="s"><el-button size="small" type="danger" text @click="editingEntries.splice(s.$index,1)">删除</el-button></template> <el-table-column prop="url" label="URL" min-width="180">
</el-table-column> <template #default="s"><el-input v-model="s.row.url" size="small" /></template>
</el-table> </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"> <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" type="primary" @click="savePages" :loading="saving">保存</el-button> <el-button size="small" type="primary" @click="savePages" :loading="saving">保存</el-button>
</div> </div>
</el-dialog> </el-dialog>
@@ -59,7 +72,15 @@ import api from '../api'
const loading = ref(false), saving = ref(false) const loading = ref(false), saving = ref(false)
const sites = ref([]), pagesMap = ref({}) const sites = ref([]), pagesMap = ref({})
const editorVisible = ref(false), editorSiteId = ref(''), editingEntries = ref([]) const editorVisible = ref(false), editorSiteId = 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',' ') : '-' const fmt = (t) => t ? (t.substring(0,19)||'').replace('T',' ') : '-'
@@ -104,7 +125,8 @@ const showEditor = (row) => {
const savePages = async () => { const savePages = async () => {
saving.value = true saving.value = true
try { try {
await api.raw.put('/sites/' + editorSiteId.value + '/pages', { entries: editingEntries.value }) const entries = editorMode.value === 'json' ? JSON.parse(editorJson.value) : editingEntries.value
await api.raw.put('/sites/' + editorSiteId.value + '/pages', { entries })
ElMessage.success('已保存') ElMessage.success('已保存')
editorVisible.value = false editorVisible.value = false
fetchAll() fetchAll()