1. 为imSensitiveWord、imUserPolicyGroup、imSecurityPolicy页面添加租户ID校验,避免表格挂载时因租户默认值未就绪发起无效请求 2. 更新imLoginFailureLog、imUserLoginHistory的搜索占位符与账号列宽 3. 修正后端登录审计相关结构体的字段注释描述
322 lines
11 KiB
Vue
322 lines
11 KiB
Vue
<template>
|
|
<div>
|
|
<div class="n-layout-page-header"><n-card :bordered="false" title="敏感词库" /></div>
|
|
<n-card :bordered="false" class="proCard">
|
|
<BasicForm
|
|
ref="searchFormRef"
|
|
@register="register"
|
|
@submit="reload"
|
|
@reset="reload"
|
|
@keyup.enter="reload"
|
|
/>
|
|
<BasicTable
|
|
ref="tableRef"
|
|
:columns="columns"
|
|
:request="loadTable"
|
|
:row-key="(r) => r.id"
|
|
:actionColumn="actionColumn"
|
|
:scroll-x="scrollX"
|
|
:resizeHeightOffset="-10000"
|
|
>
|
|
<template #tableTitle
|
|
><n-space
|
|
><n-button
|
|
type="primary"
|
|
:disabled="!hasPermission(['/imSensitiveWord/edit'])"
|
|
@click="openEdit()"
|
|
>新增敏感词</n-button
|
|
><n-button
|
|
:disabled="!hasPermission(['/imSensitiveWord/batchImport'])"
|
|
@click="openImport"
|
|
>批量导入</n-button
|
|
><n-button
|
|
:disabled="!hasPermission(['/imSensitiveWord/groupEdit'])"
|
|
@click="groupVisible = true"
|
|
>新建分组</n-button
|
|
></n-space
|
|
></template
|
|
>
|
|
</BasicTable>
|
|
</n-card>
|
|
|
|
<n-modal
|
|
v-model:show="editVisible"
|
|
preset="dialog"
|
|
:show-icon="false"
|
|
:title="wordForm.id ? '编辑敏感词' : '新增敏感词'"
|
|
:style="{ width: dialogWidth }"
|
|
>
|
|
<n-form
|
|
ref="wordFormRef"
|
|
:model="wordForm"
|
|
:rules="wordRules"
|
|
label-placement="left"
|
|
label-width="100"
|
|
>
|
|
<n-form-item label="敏感词" path="word"
|
|
><n-input v-model:value="wordForm.word"
|
|
/></n-form-item>
|
|
<n-form-item label="分组" path="groupId"
|
|
><n-select v-model:value="wordForm.groupId" :options="groupOptions"
|
|
/></n-form-item>
|
|
<n-grid :cols="2" :x-gap="16"
|
|
><n-form-item-gi label="匹配类型"
|
|
><n-select v-model:value="wordForm.matchType" :options="matchOptions" /></n-form-item-gi
|
|
><n-form-item-gi label="风险等级"
|
|
><n-select v-model:value="wordForm.riskLevel" :options="riskOptions" /></n-form-item-gi
|
|
></n-grid>
|
|
<n-form-item label="生效场景"
|
|
><n-select v-model:value="wordForm.scenes" multiple :options="sceneOptions"
|
|
/></n-form-item>
|
|
<n-grid :cols="2" :x-gap="16"
|
|
><n-form-item-gi label="命中动作"
|
|
><n-select v-model:value="wordForm.action" :options="actionOptions" /></n-form-item-gi
|
|
><n-form-item-gi label="状态"
|
|
><n-select v-model:value="wordForm.status" :options="statusOptions" /></n-form-item-gi
|
|
></n-grid>
|
|
<n-form-item v-if="wordForm.action === 'replace'" label="替换文本"
|
|
><n-input v-model:value="wordForm.replacement"
|
|
/></n-form-item>
|
|
<n-form-item label="优先级"
|
|
><n-input-number v-model:value="wordForm.priority" :min="0"
|
|
/></n-form-item>
|
|
<n-form-item label="备注"
|
|
><n-input v-model:value="wordForm.remark" type="textarea"
|
|
/></n-form-item>
|
|
</n-form>
|
|
<template #action
|
|
><n-space
|
|
><n-button @click="editVisible = false">取消</n-button
|
|
><n-button type="primary" :loading="submitting" @click="submitWord"
|
|
>保存</n-button
|
|
></n-space
|
|
></template
|
|
>
|
|
</n-modal>
|
|
|
|
<n-modal
|
|
v-model:show="importVisible"
|
|
preset="dialog"
|
|
:show-icon="false"
|
|
title="批量导入"
|
|
:style="{ width: dialogWidth }"
|
|
>
|
|
<n-form label-placement="left" label-width="100"
|
|
><n-form-item label="分组"
|
|
><n-select v-model:value="importForm.groupId" :options="groupOptions" /></n-form-item
|
|
><n-form-item label="敏感词"
|
|
><n-input
|
|
v-model:value="importForm.text"
|
|
type="textarea"
|
|
:autosize="{ minRows: 10, maxRows: 18 }"
|
|
placeholder="每行一个敏感词" /></n-form-item
|
|
></n-form>
|
|
<template #action
|
|
><n-space
|
|
><n-button @click="importVisible = false">取消</n-button
|
|
><n-button type="primary" :loading="submitting" @click="submitImport"
|
|
>导入</n-button
|
|
></n-space
|
|
></template
|
|
>
|
|
</n-modal>
|
|
|
|
<n-modal
|
|
v-model:show="groupVisible"
|
|
preset="dialog"
|
|
:show-icon="false"
|
|
title="新建敏感词分组"
|
|
:style="{ width: '520px' }"
|
|
><n-form label-placement="left" label-width="90"
|
|
><n-form-item label="分组编码"><n-input v-model:value="groupForm.groupCode" /></n-form-item
|
|
><n-form-item label="分组名称"><n-input v-model:value="groupForm.groupName" /></n-form-item
|
|
><n-form-item label="优先级"
|
|
><n-input-number v-model:value="groupForm.priority" :min="0" /></n-form-item></n-form
|
|
><template #action
|
|
><n-space
|
|
><n-button @click="groupVisible = false">取消</n-button
|
|
><n-button type="primary" @click="submitGroup">保存</n-button></n-space
|
|
></template
|
|
></n-modal
|
|
>
|
|
</div>
|
|
</template>
|
|
|
|
<script lang="ts" setup>
|
|
import { computed, h, onMounted, reactive, ref, watch } from 'vue';
|
|
import { useDialog, useMessage } from 'naive-ui';
|
|
import { BasicForm, useForm } from '@/components/Form';
|
|
import { BasicTable, TableAction } from '@/components/Table';
|
|
import { useDictStore } from '@/store/modules/dict';
|
|
import { usePermission } from '@/hooks/web/usePermission';
|
|
import { useUserStore } from '@/store/modules/user';
|
|
import { adaModalWidth, adaTableScrollX } from '@/utils/hotgo';
|
|
import { BatchImport, Delete, Edit, GroupEdit, GroupList, List } from '@/api/imSensitiveWord';
|
|
import { columns, groupOptions, loadOptions, schemas } from './model';
|
|
import {
|
|
getDefaultTenantValue,
|
|
loadPolicyTenantOptions,
|
|
parseJSON,
|
|
policyTenantOptions,
|
|
} from '@/views/imPolicyShared';
|
|
const dict = useDictStore(),
|
|
message = useMessage(),
|
|
dialog = useDialog(),
|
|
userStore = useUserStore();
|
|
const { hasPermission } = usePermission();
|
|
const tableRef = ref(),
|
|
searchFormRef = ref<any>({}),
|
|
wordFormRef = ref();
|
|
const editVisible = ref(false),
|
|
importVisible = ref(false),
|
|
groupVisible = ref(false),
|
|
submitting = ref(false);
|
|
const dialogWidth = computed(() => adaModalWidth(680));
|
|
const matchOptions = dict.getOption('ImSensitiveMatchTypeOptions'),
|
|
actionOptions = dict.getOption('ImSensitiveActionOptions'),
|
|
sceneOptions = dict.getOption('ImSensitiveSceneOptions'),
|
|
riskOptions = dict.getOption('ImRiskLevelOptions'),
|
|
statusOptions = dict.getOption('sys_normal_disable');
|
|
const tenantId = () => Number(searchFormRef.value?.formModel?.tenantId || 0);
|
|
const emptyWord = () => ({
|
|
id: 0,
|
|
tenantId: tenantId(),
|
|
groupId: groupOptions.value[0]?.value || null,
|
|
word: '',
|
|
matchType: 'contains',
|
|
scenes: ['private_message', 'group_message'],
|
|
action: 'block',
|
|
replacement: '',
|
|
riskLevel: 'medium',
|
|
priority: 0,
|
|
status: 1,
|
|
remark: '',
|
|
});
|
|
const wordForm = ref<any>(emptyWord());
|
|
const importForm = ref<any>({ groupId: null, text: '' });
|
|
const groupForm = ref<any>({ groupCode: '', groupName: '', priority: 0, status: 1 });
|
|
const wordRules = {
|
|
word: { required: true, message: '请输入敏感词' },
|
|
groupId: { required: true, type: 'number', message: '请选择分组' },
|
|
};
|
|
const tenantSchema = schemas.value.find((v) => v.field === 'tenantId');
|
|
if (tenantSchema) tenantSchema.ifShow = () => true;
|
|
const [register, { setFieldsValue }] = useForm({
|
|
gridProps: { cols: '1 s:1 m:2 l:3 xl:4' },
|
|
labelWidth: 90,
|
|
schemas,
|
|
});
|
|
// 表格挂载自动请求早于租户默认值就绪,tenantId 未就绪时跳过,由 onMounted 的 reload 加载首屏
|
|
const loadTable = (res) =>
|
|
Number(searchFormRef.value?.formModel?.tenantId || 0)
|
|
? List({ ...searchFormRef.value?.formModel, ...res })
|
|
: Promise.resolve({});
|
|
const reload = () => tableRef.value?.reload();
|
|
const actionColumn = reactive({
|
|
width: 150,
|
|
title: '操作',
|
|
key: 'action',
|
|
fixed: 'right',
|
|
render: (row) =>
|
|
h(TableAction as any, {
|
|
style: 'button',
|
|
actions: [
|
|
{ label: '编辑', auth: ['/imSensitiveWord/edit'], onClick: () => openEdit(row) },
|
|
{
|
|
label: '删除',
|
|
type: 'error',
|
|
auth: ['/imSensitiveWord/delete'],
|
|
onClick: () => removeWord(row),
|
|
},
|
|
],
|
|
}),
|
|
});
|
|
const scrollX = computed(() => adaTableScrollX(columns, actionColumn.width));
|
|
async function loadGroups() {
|
|
const res = await GroupList({ tenantId: tenantId() });
|
|
groupOptions.value = (res?.list || [])
|
|
.filter((v) => v.status === 1)
|
|
.map((v) => ({ label: v.groupName, value: v.id }));
|
|
}
|
|
function openEdit(row?: any) {
|
|
wordForm.value = row ? { ...row, scenes: parseJSON(row.sceneJson, []) } : emptyWord();
|
|
editVisible.value = true;
|
|
}
|
|
async function submitWord() {
|
|
submitting.value = true;
|
|
try {
|
|
await wordFormRef.value?.validate();
|
|
const f = wordForm.value;
|
|
await Edit({ ...f, sceneJson: f.scenes });
|
|
message.success('保存成功');
|
|
editVisible.value = false;
|
|
reload();
|
|
} finally {
|
|
submitting.value = false;
|
|
}
|
|
}
|
|
function removeWord(row) {
|
|
dialog.error({
|
|
title: '删除敏感词',
|
|
content: `确认删除“${row.word}”?`,
|
|
positiveText: '删除',
|
|
negativeText: '取消',
|
|
onPositiveClick: async () => {
|
|
await Delete({ id: row.id });
|
|
message.success('删除成功');
|
|
reload();
|
|
},
|
|
});
|
|
}
|
|
function openImport() {
|
|
importForm.value = { groupId: groupOptions.value[0]?.value || null, text: '' };
|
|
importVisible.value = true;
|
|
}
|
|
async function submitImport() {
|
|
const words = importForm.value.text
|
|
.split(/\r?\n/)
|
|
.map((v) => v.trim())
|
|
.filter(Boolean);
|
|
if (!importForm.value.groupId || !words.length)
|
|
return message.warning('请选择分组并输入敏感词');
|
|
submitting.value = true;
|
|
try {
|
|
const res = await BatchImport({
|
|
tenantId: tenantId(),
|
|
groupId: importForm.value.groupId,
|
|
words,
|
|
matchType: 'contains',
|
|
sceneJson: ['private_message', 'group_message'],
|
|
action: 'block',
|
|
replacement: '',
|
|
riskLevel: 'medium',
|
|
});
|
|
message.success(`导入${res.inserted}条,跳过${res.skipped}条`);
|
|
importVisible.value = false;
|
|
reload();
|
|
} finally {
|
|
submitting.value = false;
|
|
}
|
|
}
|
|
async function submitGroup() {
|
|
if (!groupForm.value.groupCode || !groupForm.value.groupName)
|
|
return message.warning('请填写分组编码和名称');
|
|
await GroupEdit({ ...groupForm.value, tenantId: tenantId() });
|
|
message.success('保存成功');
|
|
groupVisible.value = false;
|
|
groupForm.value = { groupCode: '', groupName: '', priority: 0, status: 1 };
|
|
await loadGroups();
|
|
}
|
|
watch(
|
|
() => searchFormRef.value?.formModel?.tenantId,
|
|
() => loadGroups()
|
|
);
|
|
onMounted(async () => {
|
|
loadOptions();
|
|
await loadPolicyTenantOptions(true);
|
|
await setFieldsValue({ tenantId: getDefaultTenantValue(policyTenantOptions.value) });
|
|
await loadGroups();
|
|
reload();
|
|
});
|
|
</script>
|