XuqmGroup-Web/ops-platform/src/views/risk/RiskControlView.vue
XuqmGroup 6cd938cfbc feat(android-sdk): 添加完整的IM客户端SDK实现
- 实现了Android SDK的完整IM功能接口,包括消息、群组、好友、会话等核心功能
- 添加了消息收发、历史记录、撤回编辑等完整的消息操作能力
- 实现了群组管理功能,包括创建、成员管理、权限设置等操作
- 添加了好友关系链管理,支持添加、删除、分组等操作
- 实现了会话管理功能,包括置顶、免打扰、已读状态等
- 添加了黑名单、资料管理、搜索等辅助功能
- 补齐了批量操作接口,提升客户端操作效率
- 实现了WebSocket连接管理和事件监听机制
- 添加了离线消息同步和状态管理功能
2026-05-02 22:57:55 +08:00

272 行
8.5 KiB
Vue

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

<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 loadRules() {
try {
const res = await opsApi.getRiskRules()
const cfg = res.data.data
if (cfg) {
ruleForm.ipRateLimit = cfg.ipRateLimit ?? 300
ruleForm.loginFailThreshold = cfg.loginFailThreshold ?? 5
ruleForm.loginLockMinutes = cfg.loginLockMinutes ?? 30
ruleForm.abnormalDetection = cfg.abnormalDetection ?? true
}
} catch {
// ignore
}
}
async function saveRules() {
ruleLoading.value = true
try {
await opsApi.saveRiskRules({ ...ruleForm })
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 {
const res = await opsApi.listSensitiveWords(page.value - 1, size.value)
const data = res.data.data
wordList.value = data.content ?? []
total.value = data.total ?? 0
} catch {
ElMessage.error('加载失败')
} finally {
tableLoading.value = false
}
}
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 {
if (isEdit.value && form.id) {
await opsApi.updateSensitiveWord(form.id, form as SensitiveWord)
} else {
await opsApi.createSensitiveWord(form as SensitiveWord)
}
ElMessage.success(isEdit.value ? '编辑成功' : '新增成功')
dialogVisible.value = false
loadWords()
} catch {
ElMessage.error('操作失败')
} finally {
submitLoading.value = false
}
}
async function toggleWord(row: SensitiveWord) {
try {
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' })
await opsApi.deleteSensitiveWord(row.id)
ElMessage.success('删除成功')
loadWords()
} catch {
// cancel
}
}
function fmt(value: string) {
return value ? new Date(value).toLocaleString('zh-CN') : '-'
}
onMounted(() => {
loadRules()
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>