Files
im_manager/web/src/views/imSensitiveWord/index.vue
T
Austin 197c6c86f4 fix: 修复多页面租户未就绪时表格提前请求问题,优化登录审计相关文案与列宽
1. 为imSensitiveWord、imUserPolicyGroup、imSecurityPolicy页面添加租户ID校验,避免表格挂载时因租户默认值未就绪发起无效请求
2. 更新imLoginFailureLog、imUserLoginHistory的搜索占位符与账号列宽
3. 修正后端登录审计相关结构体的字段注释描述
2026-09-21 16:25:12 +08:00

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>