- 实现了Android SDK的完整IM功能接口,包括消息、群组、好友、会话等核心功能 - 添加了消息收发、历史记录、撤回编辑等完整的消息操作能力 - 实现了群组管理功能,包括创建、成员管理、权限设置等操作 - 添加了好友关系链管理,支持添加、删除、分组等操作 - 实现了会话管理功能,包括置顶、免打扰、已读状态等 - 添加了黑名单、资料管理、搜索等辅助功能 - 补齐了批量操作接口,提升客户端操作效率 - 实现了WebSocket连接管理和事件监听机制 - 添加了离线消息同步和状态管理功能
272 行
8.5 KiB
Vue
272 行
8.5 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 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>
|