refactor: 拆分两个独立菜单(页面配置+风险规则)+CRUD完整+删除接口
This commit is contained in:
@@ -65,6 +65,14 @@ public class RiskRuleController {
|
|||||||
return ApiResponse.success(data);
|
return ApiResponse.success(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 管理员删除风险规则 */
|
||||||
|
@DeleteMapping("/api/v1/admin/risk-rules/{siteType}")
|
||||||
|
public ApiResponse<Void> adminDelete(@PathVariable String siteType, HttpServletRequest request) {
|
||||||
|
checkAdmin(request);
|
||||||
|
riskRuleService.deleteRules(siteType);
|
||||||
|
return ApiResponse.success();
|
||||||
|
}
|
||||||
|
|
||||||
private void checkAdmin(HttpServletRequest request) {
|
private void checkAdmin(HttpServletRequest request) {
|
||||||
var level = request.getAttribute("trustLevel");
|
var level = request.getAttribute("trustLevel");
|
||||||
if (level == null || !"ADMIN".equals(level.toString())) {
|
if (level == null || !"ADMIN".equals(level.toString())) {
|
||||||
|
|||||||
@@ -67,6 +67,14 @@ public class SitePagesController {
|
|||||||
return ApiResponse.success(data);
|
return ApiResponse.success(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 管理员删除站点页面配置 */
|
||||||
|
@DeleteMapping("/api/v1/admin/site-pages/{siteId}")
|
||||||
|
public ApiResponse<Void> adminDelete(@PathVariable String siteId, HttpServletRequest request) {
|
||||||
|
checkAdmin(request);
|
||||||
|
sitePagesService.deletePages(siteId);
|
||||||
|
return ApiResponse.success();
|
||||||
|
}
|
||||||
|
|
||||||
private void checkAdmin(HttpServletRequest request) {
|
private void checkAdmin(HttpServletRequest request) {
|
||||||
var level = request.getAttribute("trustLevel");
|
var level = request.getAttribute("trustLevel");
|
||||||
if (level == null || !"ADMIN".equals(level.toString())) {
|
if (level == null || !"ADMIN".equals(level.toString())) {
|
||||||
|
|||||||
@@ -12,4 +12,6 @@ public interface RiskRuleService {
|
|||||||
void saveRules(String siteType, List<Map<String, Object>> rules);
|
void saveRules(String siteType, List<Map<String, Object>> rules);
|
||||||
/** 获取所有规则列表(管理员) */
|
/** 获取所有规则列表(管理员) */
|
||||||
List<Map<String, Object>> listAll();
|
List<Map<String, Object>> listAll();
|
||||||
|
/** 删除风险规则 */
|
||||||
|
void deleteRules(String siteType);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,4 +12,6 @@ public interface SitePagesService {
|
|||||||
void savePages(String siteId, List<Map<String, Object>> entries);
|
void savePages(String siteId, List<Map<String, Object>> entries);
|
||||||
/** 获取所有站点页面配置列表(管理员) */
|
/** 获取所有站点页面配置列表(管理员) */
|
||||||
List<Map<String, Object>> listAll();
|
List<Map<String, Object>> listAll();
|
||||||
|
/** 删除站点页面配置 */
|
||||||
|
void deletePages(String siteId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,6 +64,12 @@ public class RiskRuleServiceImpl implements RiskRuleService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deleteRules(String siteType) {
|
||||||
|
mapper.delete(new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<RiskRule>()
|
||||||
|
.eq(RiskRule::getSiteType, siteType).eq(RiskRule::getDeleted, 0));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<Map<String, Object>> listAll() {
|
public List<Map<String, Object>> listAll() {
|
||||||
List<RiskRule> all = mapper.selectList(
|
List<RiskRule> all = mapper.selectList(
|
||||||
|
|||||||
@@ -70,6 +70,12 @@ public class SitePagesServiceImpl implements SitePagesService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deletePages(String siteId) {
|
||||||
|
mapper.delete(new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<SitePages>()
|
||||||
|
.eq(SitePages::getSiteId, siteId).eq(SitePages::getDeleted, 0));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<Map<String, Object>> listAll() {
|
public List<Map<String, Object>> listAll() {
|
||||||
List<SitePages> all = mapper.selectList(
|
List<SitePages> all = mapper.selectList(
|
||||||
|
|||||||
@@ -71,10 +71,16 @@ const routes = [
|
|||||||
meta: { title: '适配校验详情' }
|
meta: { title: '适配校验详情' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'config-mgr',
|
path: 'site-pages',
|
||||||
name: 'ConfigMgr',
|
name: 'SitePages',
|
||||||
component: () => import('../views/ConfigMgr.vue'),
|
component: () => import('../views/SitePages.vue'),
|
||||||
meta: { title: '配置管理' }
|
meta: { title: '页面配置' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'risk-rules',
|
||||||
|
name: 'RiskRules',
|
||||||
|
component: () => import('../views/RiskRules.vue'),
|
||||||
|
meta: { title: '风险规则' }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,184 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<h2>配置管理</h2>
|
|
||||||
<el-card style="margin-top:20px">
|
|
||||||
<el-tabs v-model="activeTab">
|
|
||||||
<el-tab-pane label="站点页面配置" name="pages">
|
|
||||||
<div style="margin-bottom:12px;display:flex;gap:8px">
|
|
||||||
<el-select v-model="editSiteId" placeholder="选择站点" style="width:200px" clearable filterable>
|
|
||||||
<el-option v-for="s in sites" :key="s.siteId" :label="(s.names||[]).join('/')||s.siteId" :value="s.siteId" />
|
|
||||||
</el-select>
|
|
||||||
<el-button @click="loadPages" :loading="loading">加载</el-button>
|
|
||||||
<el-button @click="newPages">新建</el-button>
|
|
||||||
</div>
|
|
||||||
<div v-if="currentPages">
|
|
||||||
<div style="margin-bottom:8px;display:flex;gap:8px;align-items:center">
|
|
||||||
<span style="color:#909399">版本: {{ currentPages.version }} | 更新: {{ fmt(currentPages.updatedAt) }}</span>
|
|
||||||
</div>
|
|
||||||
<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>
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else style="padding:40px;text-align:center;color:#909399" v-if="editSiteId">选择站点后加载</div>
|
|
||||||
|
|
||||||
<el-divider />
|
|
||||||
<div style="font-size:13px;font-weight:500;margin-bottom:8px">已配置的站点</div>
|
|
||||||
<el-table :data="allPages" stripe size="small" v-loading="loadingAll">
|
|
||||||
<el-table-column prop="siteId" label="站点" width="120" />
|
|
||||||
<el-table-column prop="version" label="版本" width="80" />
|
|
||||||
<el-table-column prop="updatedAt" label="更新时间" width="160">
|
|
||||||
<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="editSiteId=s.row.siteId;loadPages()">编辑</el-button></template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
</el-tab-pane>
|
|
||||||
|
|
||||||
<el-tab-pane label="风险规则" name="rules">
|
|
||||||
<div style="margin-bottom:12px;display:flex;gap:8px">
|
|
||||||
<el-select v-model="editSiteType" placeholder="选择站点类型" style="width:200px" clearable>
|
|
||||||
<el-option v-for="t in siteTypes" :key="t" :label="t" :value="t" />
|
|
||||||
</el-select>
|
|
||||||
<el-button @click="loadRules" :loading="loading2">加载</el-button>
|
|
||||||
<el-button @click="newRules">新建</el-button>
|
|
||||||
</div>
|
|
||||||
<div v-if="currentRules">
|
|
||||||
<div style="margin-bottom:8px;color:#909399">版本: {{ currentRules.version }} | 更新: {{ fmt(currentRules.updatedAt) }}</div>
|
|
||||||
<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']" :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="70">
|
|
||||||
<template #default="s"><el-input v-model.number="s.row.threshold" size="small" type="number" /></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']" :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']" :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>
|
|
||||||
<div style="display:flex;gap:8px">
|
|
||||||
<el-button size="small" @click="editingRules.push({type:'TRAFFIC',name:'',metric:'',threshold:0,condition:'GT',level:'WARN'})">+ 添加</el-button>
|
|
||||||
<el-button size="small" type="primary" @click="saveRules" :loading="saving2">保存</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else style="padding:40px;text-align:center;color:#909399" v-if="editSiteType">选择类型后加载</div>
|
|
||||||
|
|
||||||
<el-divider />
|
|
||||||
<div style="font-size:13px;font-weight:500;margin-bottom:8px">已配置的规则</div>
|
|
||||||
<el-table :data="allRules" stripe size="small" v-loading="loadingAll2">
|
|
||||||
<el-table-column prop="siteType" label="站点类型" width="120" />
|
|
||||||
<el-table-column prop="version" label="版本" width="80" />
|
|
||||||
<el-table-column prop="updatedAt" label="更新时间" width="160">
|
|
||||||
<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="editSiteType=s.row.siteType;loadRules()">编辑</el-button></template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
</el-tab-pane>
|
|
||||||
</el-tabs>
|
|
||||||
</el-card>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import api from '../api'
|
|
||||||
|
|
||||||
const activeTab = ref('pages')
|
|
||||||
const editSiteId = ref(''), editSiteType = ref('')
|
|
||||||
const currentPages = ref(null), currentRules = ref(null)
|
|
||||||
const editingEntries = ref([]), editingRules = ref([])
|
|
||||||
const allPages = ref([]), allRules = ref([])
|
|
||||||
const sites = ref([])
|
|
||||||
const siteTypes = ['nexusphp','mtorrent','gazelle','unit3d']
|
|
||||||
const loading = ref(false), loading2 = ref(false), loadingAll = ref(false), loadingAll2 = ref(false)
|
|
||||||
const saving = ref(false), saving2 = ref(false)
|
|
||||||
|
|
||||||
const fmt = (t) => t ? t.substring(0,19).replace('T',' ') : '-'
|
|
||||||
|
|
||||||
const loadSites = async () => { try { sites.value = (await api.sites.adminList()).data || [] } catch {} }
|
|
||||||
|
|
||||||
const loadPages = async () => {
|
|
||||||
if (!editSiteId.value) return
|
|
||||||
loading.value = true
|
|
||||||
try {
|
|
||||||
const r = await api.get('/admin/site-pages/' + editSiteId.value)
|
|
||||||
currentPages.value = r.data || r
|
|
||||||
editingEntries.value = JSON.parse(JSON.stringify((currentPages.value?.entries||[])))
|
|
||||||
} catch { currentPages.value = null; ElMessage.error('加载失败') }
|
|
||||||
finally { loading.value = false }
|
|
||||||
}
|
|
||||||
|
|
||||||
const newPages = () => { currentPages.value = { version:'新版本' }; editingEntries.value = [{key:'home',label:'首页',url:'{base}',method:'GET'}] }
|
|
||||||
|
|
||||||
const savePages = async () => {
|
|
||||||
saving.value = true
|
|
||||||
try {
|
|
||||||
await api.put('/sites/' + editSiteId.value + '/pages', { entries: editingEntries.value })
|
|
||||||
ElMessage.success('已保存'); loadPages(); loadAllPages()
|
|
||||||
} catch { ElMessage.error('保存失败') }
|
|
||||||
finally { saving.value = false }
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadAllPages = async () => { loadingAll.value = true; try { allPages.value = (await api.get('/admin/site-pages')).data || [] } catch {} finally { loadingAll.value = false } }
|
|
||||||
|
|
||||||
const loadRules = async () => {
|
|
||||||
if (!editSiteType.value) return
|
|
||||||
loading2.value = true
|
|
||||||
try {
|
|
||||||
const r = await api.get('/admin/risk-rules/' + editSiteType.value)
|
|
||||||
currentRules.value = r.data || r
|
|
||||||
editingRules.value = JSON.parse(JSON.stringify((currentRules.value?.rules||[])))
|
|
||||||
} catch { currentRules.value = null; ElMessage.error('加载失败') }
|
|
||||||
finally { loading2.value = false }
|
|
||||||
}
|
|
||||||
|
|
||||||
const newRules = () => { currentRules.value = { version:1 }; editingRules.value = [{type:'KILL',name:'',metric:'',threshold:0,condition:'GT',level:'CRITICAL'}] }
|
|
||||||
|
|
||||||
const saveRules = async () => {
|
|
||||||
saving2.value = true
|
|
||||||
try {
|
|
||||||
await api.post('/risk-rules/' + editSiteType.value, { rules: editingRules.value })
|
|
||||||
ElMessage.success('已保存'); loadRules(); loadAllRules()
|
|
||||||
} catch { ElMessage.error('保存失败') }
|
|
||||||
finally { saving2.value = false }
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadAllRules = async () => { loadingAll2.value = true; try { allRules.value = (await api.get('/admin/risk-rules')).data || [] } catch {} finally { loadingAll2.value = false } }
|
|
||||||
|
|
||||||
onMounted(() => { loadSites(); loadAllPages(); loadAllRules() })
|
|
||||||
</script>
|
|
||||||
@@ -26,9 +26,13 @@
|
|||||||
<el-icon><Connection /></el-icon>
|
<el-icon><Connection /></el-icon>
|
||||||
<span>适配验证</span>
|
<span>适配验证</span>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
<el-menu-item index="/config-mgr">
|
<el-menu-item index="/site-pages">
|
||||||
<el-icon><Setting /></el-icon>
|
<el-icon><Tickets /></el-icon>
|
||||||
<span>配置管理</span>
|
<span>页面配置</span>
|
||||||
|
</el-menu-item>
|
||||||
|
<el-menu-item index="/risk-rules">
|
||||||
|
<el-icon><Warning /></el-icon>
|
||||||
|
<span>风险规则</span>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
<el-menu-item index="/users">
|
<el-menu-item index="/users">
|
||||||
<el-icon><UserFilled /></el-icon>
|
<el-icon><UserFilled /></el-icon>
|
||||||
|
|||||||
119
web/src/views/RiskRules.vue
Normal file
119
web/src/views/RiskRules.vue
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h2>风险规则</h2>
|
||||||
|
<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-column prop="siteType" label="站点类型" width="120" />
|
||||||
|
<el-table-column prop="version" label="版本" width="80" />
|
||||||
|
<el-table-column label="规则数" width="80">
|
||||||
|
<template #default="s">
|
||||||
|
<el-tag size="small">{{ getRuleCount(s.row) }}</el-tag>
|
||||||
|
</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">
|
||||||
|
<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>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-dialog v-model="editorVisible" :title="editorTitle" 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>
|
||||||
|
</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="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>
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import api from '../api'
|
||||||
|
|
||||||
|
const loading = ref(false), saving = ref(false), rules = ref([])
|
||||||
|
const editorVisible = ref(false), editorType = ref(''), editorTitle = 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 fetchAll = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try { rules.value = (await api.get('/admin/risk-rules')).data || [] }
|
||||||
|
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'}]
|
||||||
|
}
|
||||||
|
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 })
|
||||||
|
ElMessage.success('已保存')
|
||||||
|
editorVisible.value = false
|
||||||
|
fetchAll()
|
||||||
|
} catch { ElMessage.error('保存失败') }
|
||||||
|
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>
|
||||||
118
web/src/views/SitePages.vue
Normal file
118
web/src/views/SitePages.vue
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h2>页面配置</h2>
|
||||||
|
<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-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>
|
||||||
|
<el-table-column label="更新时间" width="160">
|
||||||
|
<template #default="s">{{ fmt(s.row.updatedAt) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="140">
|
||||||
|
<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>
|
||||||
|
</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-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>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<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 fmt = (t) => t ? t.substring(0,19).replace('T',' ') : '-'
|
||||||
|
const getEntries = (row) => { try { return JSON.parse(row.entriesJson || '[]') } catch { return [] } }
|
||||||
|
|
||||||
|
const fetchAll = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try { sites.value = (await api.get('/admin/site-pages')).data || [] }
|
||||||
|
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)))
|
||||||
|
}
|
||||||
|
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 })
|
||||||
|
ElMessage.success('已保存')
|
||||||
|
editorVisible.value = false
|
||||||
|
fetchAll()
|
||||||
|
} catch { ElMessage.error('保存失败') }
|
||||||
|
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>
|
||||||
Reference in New Issue
Block a user