124 lines
7.0 KiB
Vue
124 lines
7.0 KiB
Vue
<template>
|
||
<!-- 列表模式 -->
|
||
<div v-if="!editType">
|
||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
|
||
<h2 style="margin:0">风险规则</h2>
|
||
<el-button size="small" @click="fetchAll" :icon="Refresh">刷新</el-button>
|
||
</div>
|
||
<el-table :data="merged" stripe v-loading="loading" border empty-text="暂无站点" style="font-size:13px" :row-style="{height:'44px'}">
|
||
<el-table-column prop="siteId" label="站点" width="140" />
|
||
<el-table-column label="名称" min-width="130">
|
||
<template #default="s">{{ (s.row.names||[]).join('/') || '-' }}</template>
|
||
</el-table-column>
|
||
<el-table-column prop="siteType" label="类型" width="100" />
|
||
<el-table-column label="规则数" width="80" align="center">
|
||
<template #default="s"><el-tag size="small">{{ s.row.ruleCount || 0 }}</el-tag></template>
|
||
</el-table-column>
|
||
<el-table-column label="版本" width="80" 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">
|
||
<template #default="s"><el-button size="small" @click="editRule(s.row)">编辑</el-button></template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</div>
|
||
|
||
<!-- 编辑模式:全屏双栏 -->
|
||
<div v-else style="display:flex;flex-direction:column;height:calc(100vh - 80px)">
|
||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
|
||
<h2 style="margin:0">编辑: {{ editType }}</h2>
|
||
<div style="display:flex;gap:8px">
|
||
<el-button size="small" type="primary" @click="saveRules" :loading="saving">保存</el-button>
|
||
<el-button size="small" @click="editType=''">返回列表</el-button>
|
||
</div>
|
||
</div>
|
||
<div style="display:flex;gap:12px;flex:1;min-height:0">
|
||
<div style="flex:0 0 400px;display:flex;flex-direction:column;min-height:0">
|
||
<div style="font-size:13px;font-weight:500;margin-bottom:4px">JSON 配置</div>
|
||
<div style="flex:1;min-height:0;border-radius:4px;overflow:hidden">
|
||
<CodeEditor v-model="editorJson" language="json" />
|
||
</div>
|
||
</div>
|
||
<div style="flex:1;overflow:auto;min-height:0">
|
||
<div style="font-size:13px;font-weight:500;margin-bottom:4px">双向可视化</div>
|
||
<el-table :data="parsed" stripe border style="font-size:13px" :row-style="{height:'42px'}" v-if="parsed.length">
|
||
<el-table-column label="级别" width="80">
|
||
<template #default="s"><el-select v-model="s.row.level" size="small" @change="sync"><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="90">
|
||
<template #default="s"><el-select v-model="s.row.type" size="small" @change="sync"><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" @input="sync"/></template>
|
||
</el-table-column>
|
||
<el-table-column prop="metric" label="指标" width="120">
|
||
<template #default="s"><el-input v-model="s.row.metric" size="small" @input="sync"/></template>
|
||
</el-table-column>
|
||
<el-table-column prop="threshold" label="阈值" width="70">
|
||
<template #default="s"><el-input v-model.number="s.row.threshold" size="small" type="number" @input="sync"/></template>
|
||
</el-table-column>
|
||
<el-table-column prop="unit" label="单位" width="60">
|
||
<template #default="s"><el-input v-model="s.row.unit" size="small" @input="sync"/></template>
|
||
</el-table-column>
|
||
<el-table-column label="条件" width="70">
|
||
<template #default="s"><el-select v-model="s.row.condition" size="small" @change="sync"><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 width="50">
|
||
<template #default="s"><el-button size="small" type="danger" text @click="remove(s.$index)">×</el-button></template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<div v-else style="text-align:center;padding:40px;color:#909399">该类型暂无规则</div>
|
||
<div style="margin-top:8px"><el-button size="small" @click="addRule">+ 添加</el-button></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import api from '../api'; import { Refresh } from '@element-plus/icons-vue'
|
||
|
||
const loading = ref(false), saving = ref(false), sites = ref([]), rulesMap = ref({})
|
||
const editType = ref(''), editorJson = ref('[]')
|
||
const fmt = (t) => t ? (t.substring(0,19)||'').replace('T',' ') : '-'
|
||
const parsed = computed(() => { try { return JSON.parse(editorJson.value) } catch { return [] } })
|
||
const merged = computed(() => (Array.isArray(sites.value)?sites.value:[]).map(s => ({
|
||
siteId: s.siteId, names: s.names, siteType: s.siteType,
|
||
ruleCount: rulesMap.value[s.siteType]?.ruleCount || 0, version: rulesMap.value[s.siteType]?.version || null,
|
||
updatedAt: rulesMap.value[s.siteType]?.updatedAt || null, rulesJson: rulesMap.value[s.siteType]?.rulesJson || null,
|
||
})))
|
||
|
||
const fetchAll = async () => {
|
||
loading.value = true
|
||
try {
|
||
const [sr, rr] = await Promise.allSettled([api.raw.get('/admin/sites'), api.raw.get('/admin/risk-rules')])
|
||
sites.value = sr.status==='fulfilled' ? (sr.value?.data||sr.value||[]) : []
|
||
const rulesData = rr.status==='fulfilled' ? (rr.value?.data||rr.value||[]) : []
|
||
const map = {}
|
||
for (const r of rulesData) {
|
||
const cnt = (() => { try { return JSON.parse(r.rulesJson||'[]').length } catch { return 0 } })()
|
||
if (!map[r.siteType] || r.version > map[r.siteType].version)
|
||
map[r.siteType] = { ruleCount: cnt, version: r.version, updatedAt: r.updatedAt, rulesJson: r.rulesJson }
|
||
}
|
||
rulesMap.value = map
|
||
} finally { loading.value = false }
|
||
}
|
||
const editRule = async (row) => {
|
||
editType.value = row.siteType
|
||
try { const r = await api.raw.get('/admin/risk-rules/'+row.siteType); editorJson.value = (r?.data||r)?.rules ? JSON.stringify((r.data||r).rules, null, 2) : '[]' }
|
||
catch { editorJson.value = '[]' }
|
||
}
|
||
const sync = () => { editorJson.value = JSON.stringify(parsed.value, null, 2) }
|
||
const remove = (idx) => { parsed.value.splice(idx,1); sync() }
|
||
const addRule = () => { parsed.value.push({type:'TRAFFIC',name:'',metric:'',threshold:0,unit:'DAY',condition:'GT',level:'WARN'}); sync() }
|
||
const saveRules = async () => {
|
||
saving.value = true
|
||
try { await api.raw.post('/risk-rules/'+editType.value, { rules: parsed.value }); ElMessage.success('已保存'); editType.value=''; fetchAll() }
|
||
catch { ElMessage.error('保存失败') }
|
||
finally { saving.value = false }
|
||
}
|
||
onMounted(fetchAll)
|
||
</script>
|