Files
par/web/src/views/RiskRules.vue

124 lines
7.0 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>