XuqmGroup-Web/ops-platform/src/views/risk/RiskControlView.vue

284 行
9.2 KiB
Vue

<template>
<div>
<h2 style="margin-bottom:12px">风控配置</h2>
<p style="margin:0 0 24px;color:#606266">
管理敏感词库与全局风控规则保障平台安全运行
</p>
<!-- 全局风控规则 -->
<el-card style="margin-bottom: 16px">
<template #header>
<span>全局风控规则</span>
</template>
<el-form :model="ruleForm" label-width="160px" :label-position="isMobile ? 'top' : 'right'">
<el-form-item label="IP限流阈值次/分)">
<el-input-number v-model="ruleForm.ipRateLimit" :min="10" :max="10000" />
</el-form-item>
<el-form-item label="登录失败次数阈值">
<el-input-number v-model="ruleForm.loginFailThreshold" :min="3" :max="20" />
</el-form-item>
<el-form-item label="登录失败锁定时长(分)">
<el-input-number v-model="ruleForm.loginLockMinutes" :min="5" :max="1440" />
</el-form-item>
<el-form-item label="账号异常检测">
<el-switch v-model="ruleForm.abnormalDetection" active-text="开启" inactive-text="关闭" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="saveRules" :loading="ruleLoading">保存配置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 敏感词库 -->
<el-card>
<template #header>
<div class="card-header">
<span>敏感词库</span>
<el-button type="primary" @click="openDialog()">新增敏感词</el-button>
</div>
</template>
<el-table :data="wordList" v-loading="tableLoading" border stripe>
<el-table-column prop="word" label="敏感词" min-width="160" />
<el-table-column prop="level" label="风险等级" width="120">
<template #default="{ row }">
<el-tag :type="levelTagType(row.level)">{{ row.level }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="category" label="分类" width="140" />
<el-table-column prop="enabled" label="状态" width="100">
<template #default="{ row }">
<el-switch v-model="row.enabled" @change="toggleWord(row)" />
</template>
</el-table-column>
<el-table-column prop="updatedAt" label="更新时间" width="180">
<template #default="{ row }">{{ fmt(row.updatedAt) }}</template>
</el-table-column>
<el-table-column label="操作" width="140" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="openDialog(row)">编辑</el-button>
<el-button link type="danger" @click="deleteWord(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-wrap">
<el-pagination
v-model:current-page="page"
v-model:page-size="size"
:page-sizes="[10, 20, 50]"
:total="total"
layout="total, sizes, prev, pager, next"
@size-change="loadWords"
@current-change="loadWords"
/>
</div>
</el-card>
<!-- 新增/编辑弹窗 -->
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="520px" destroy-on-close>
<el-form ref="formRef" :model="form" :rules="formRules" label-width="100px">
<el-form-item label="敏感词" prop="word">
<el-input v-model="form.word" placeholder="请输入敏感词" />
</el-form-item>
<el-form-item label="风险等级" prop="level">
<el-select v-model="form.level" placeholder="请选择风险等级" style="width:100%">
<el-option label="低" value="低" />
<el-option label="中" value="中" />
<el-option label="高" value="高" />
</el-select>
</el-form-item>
<el-form-item label="分类" prop="category">
<el-input v-model="form.category" placeholder="如:政治、色情、广告" />
</el-form-item>
<el-form-item label="启用状态">
<el-switch v-model="form.enabled" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm" :loading="submitLoading">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import { opsApi, type RiskRuleForm, type SensitiveWord } from '@/api/ops'
const isMobile = ref(window.innerWidth < 768)
const updateViewport = () => { isMobile.value = window.innerWidth < 768 }
onMounted(() => { window.addEventListener('resize', updateViewport) })
/* ---------- 全局规则 ---------- */
const ruleForm = reactive<RiskRuleForm>({
ipRateLimit: 300,
loginFailThreshold: 5,
loginLockMinutes: 30,
abnormalDetection: true,
})
const ruleLoading = ref(false)
async function saveRules() {
ruleLoading.value = true
try {
// TODO: 接入后端接口
// await opsApi.saveRiskRules({ ...ruleForm })
await new Promise(r => setTimeout(r, 500))
ElMessage.success('配置已保存')
} catch {
ElMessage.error('保存失败')
} finally {
ruleLoading.value = false
}
}
/* ---------- 敏感词 ---------- */
const wordList = ref<SensitiveWord[]>([])
const tableLoading = ref(false)
const page = ref(1)
const size = ref(20)
const total = ref(0)
const dialogVisible = ref(false)
const dialogTitle = ref('新增敏感词')
const formRef = ref<FormInstance>()
const submitLoading = ref(false)
const isEdit = ref(false)
const form = reactive<Partial<SensitiveWord>>({
word: '',
level: '中',
category: '',
enabled: true,
})
const formRules: FormRules = {
word: [{ required: true, message: '请输入敏感词', trigger: 'blur' }],
level: [{ required: true, message: '请选择风险等级', trigger: 'change' }],
category: [{ required: true, message: '请输入分类', trigger: 'blur' }],
}
function levelTagType(level: string) {
if (level === '高') return 'danger'
if (level === '中') return 'warning'
return 'info'
}
async function loadWords() {
tableLoading.value = true
try {
// TODO: 接入后端接口
// const res = await opsApi.listSensitiveWords(page.value - 1, size.value)
await new Promise(r => setTimeout(r, 300))
wordList.value = generateMockWords(page.value, size.value)
total.value = 63
} catch {
ElMessage.error('加载失败')
} finally {
tableLoading.value = false
}
}
function generateMockWords(pageNum: number, pageSize: number): SensitiveWord[] {
const words = ['违规', '垃圾广告', '诈骗', '恶意攻击', '敏感信息', '暴力', '赌博', '毒品']
const levels: Array<'高' | '中' | '低'> = ['高', '中', '低']
const categories = ['政治', '色情', '广告', '欺诈', '暴力']
const result: SensitiveWord[] = []
const start = (pageNum - 1) * pageSize
for (let i = 0; i < pageSize; i++) {
const idx = start + i
if (idx >= 63) break
result.push({
id: `word_${idx}`,
word: `${words[idx % words.length]}_${idx + 1}`,
level: levels[idx % levels.length],
category: categories[idx % categories.length],
enabled: idx % 3 !== 0,
updatedAt: new Date(Date.now() - idx * 86400000).toISOString(),
})
}
return result
}
function openDialog(row?: SensitiveWord) {
isEdit.value = !!row
dialogTitle.value = row ? '编辑敏感词' : '新增敏感词'
if (row) {
Object.assign(form, { ...row })
} else {
Object.assign(form, { word: '', level: '中', category: '', enabled: true })
}
dialogVisible.value = true
}
async function submitForm() {
const valid = await formRef.value?.validate().catch(() => false)
if (!valid) return
submitLoading.value = true
try {
// TODO: 接入后端接口
// if (isEdit.value && form.id) {
// await opsApi.updateSensitiveWord(form.id, form as SensitiveWord)
// } else {
// await opsApi.createSensitiveWord(form as SensitiveWord)
// }
await new Promise(r => setTimeout(r, 400))
ElMessage.success(isEdit.value ? '编辑成功' : '新增成功')
dialogVisible.value = false
loadWords()
} catch {
ElMessage.error('操作失败')
} finally {
submitLoading.value = false
}
}
async function toggleWord(row: SensitiveWord) {
try {
// TODO: 接入后端接口
// await opsApi.toggleSensitiveWord(row.id, row.enabled)
ElMessage.success('状态已更新')
} catch {
row.enabled = !row.enabled
ElMessage.error('更新失败')
}
}
async function deleteWord(row: SensitiveWord) {
try {
await ElMessageBox.confirm(`确定删除敏感词「${row.word}」吗?`, '提示', { type: 'warning' })
// TODO: 接入后端接口
// await opsApi.deleteSensitiveWord(row.id)
ElMessage.success('删除成功')
loadWords()
} catch {
// cancel
}
}
function fmt(value: string) {
return value ? new Date(value).toLocaleString('zh-CN') : '-'
}
onMounted(() => {
loadWords()
})
</script>
<style scoped>
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.pagination-wrap {
margin-top: 16px;
display: flex;
justify-content: flex-end;
}
</style>