feat: 完成鸽达达后台安全与审计改造
This commit is contained in:
@@ -0,0 +1,330 @@
|
||||
<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,
|
||||
Status,
|
||||
} from '@/api/imSensitiveWord';
|
||||
import { columns, groupOptions, loadOptions, schemas } from './model';
|
||||
import { loadPolicyTenantOptions, parseJSON } 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 = computed(() => dict.getOptions('ImSensitiveMatchTypeOptions')),
|
||||
actionOptions = computed(() => dict.getOptions('ImSensitiveActionOptions')),
|
||||
sceneOptions = computed(() => dict.getOptions('ImSensitiveSceneOptions')),
|
||||
riskOptions = computed(() => dict.getOptions('ImRiskLevelOptions')),
|
||||
statusOptions = computed(() => dict.getOptions('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 = () => userStore.isCompanyDept;
|
||||
const [register] = useForm({
|
||||
gridProps: { cols: '1 s:1 m:2 l:3 xl:4' },
|
||||
labelWidth: 90,
|
||||
schemas,
|
||||
});
|
||||
const loadTable = (res) => List({ ...searchFormRef.value?.formModel, ...res });
|
||||
const reload = () => tableRef.value?.reload();
|
||||
const actionColumn = reactive({
|
||||
width: 230,
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
fixed: 'right',
|
||||
render: (row) =>
|
||||
h(TableAction as any, {
|
||||
style: 'button',
|
||||
actions: [
|
||||
{ label: '编辑', auth: ['/imSensitiveWord/edit'], onClick: () => openEdit(row) },
|
||||
{
|
||||
label: row.status === 1 ? '停用' : '启用',
|
||||
auth: ['/imSensitiveWord/status'],
|
||||
onClick: () => changeStatus(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 changeStatus(row) {
|
||||
Status({ id: row.id, status: row.status === 1 ? 2 : 1 }).then(() => {
|
||||
message.success('操作成功');
|
||||
reload();
|
||||
});
|
||||
}
|
||||
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();
|
||||
if (userStore.isCompanyDept) await loadPolicyTenantOptions(true);
|
||||
await loadGroups();
|
||||
reload();
|
||||
});
|
||||
</script>
|
||||
Reference in New Issue
Block a user