<template>
|
<div class="page">
|
<div class="header">
|
<h3 class="section-title">敏感词库</h3>
|
<div class="header-actions">
|
<el-button v-permission="'config:sensitive-words:import'" type="primary" plain @click="downloadTemplate">
|
下载模板
|
</el-button>
|
<el-upload
|
v-permission="'config:sensitive-words:import'"
|
:show-file-list="false"
|
accept=".xls,.xlsx"
|
:http-request="importSensitiveWords"
|
>
|
<el-button type="primary" :loading="importing">导入</el-button>
|
</el-upload>
|
<el-button v-permission="'config:sensitive-words:create'" type="primary" @click="openCreate">新增</el-button>
|
</div>
|
</div>
|
|
<el-form class="filters" :inline="true" :model="filters" @submit.prevent>
|
<el-form-item label="敏感词">
|
<el-input
|
v-model="filters.keyword"
|
clearable
|
placeholder="请输入敏感词"
|
style="width: 220px"
|
@keyup.enter="handleSearch"
|
/>
|
</el-form-item>
|
<el-form-item>
|
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
<el-button @click="handleReset">重置</el-button>
|
</el-form-item>
|
</el-form>
|
|
<div class="table-wrap">
|
<el-table :data="rows" v-loading="loading" stripe border height="100%">
|
<el-table-column label="序号" width="72" type="index" :index="indexMethod" />
|
<el-table-column prop="word" label="敏感词" min-width="220" show-overflow-tooltip />
|
<el-table-column label="状态" width="120">
|
<template #default="{ row }">
|
<el-tag :type="row.enabled === 1 ? 'success' : 'info'">
|
{{ row.enabled === 1 ? "启用" : "禁用" }}
|
</el-tag>
|
</template>
|
</el-table-column>
|
<el-table-column label="创建时间" width="180">
|
<template #default="{ row }">{{ formatTime(row.createTime) }}</template>
|
</el-table-column>
|
<el-table-column label="操作" width="220" fixed="right">
|
<template #default="{ row }">
|
<div class="table-ops">
|
<el-button v-permission="'config:sensitive-words:update'" type="primary" link @click="openEdit(row)">
|
编辑
|
</el-button>
|
<el-button v-permission="'config:sensitive-words:status'" type="primary" link @click="toggleEnabled(row)">
|
{{ row.enabled === 1 ? "禁用" : "启用" }}
|
</el-button>
|
<el-button v-permission="'config:sensitive-words:delete'" type="primary" link @click="confirmDelete(row)">
|
删除
|
</el-button>
|
</div>
|
</template>
|
</el-table-column>
|
</el-table>
|
</div>
|
|
<el-pagination
|
class="pager"
|
background
|
layout="sizes, prev, pager, next"
|
:total="total"
|
:page-size="size"
|
:page-sizes="[10, 20, 50, 100]"
|
v-model:current-page="page"
|
@current-change="load"
|
@size-change="onSizeChange"
|
/>
|
|
<el-dialog v-model="dlg" :title="dlgTitle" width="460px" destroy-on-close>
|
<el-form :model="form" label-width="86px">
|
<el-form-item label="敏感词" required>
|
<el-input v-model="form.word" maxlength="50" placeholder="请输入敏感词" />
|
</el-form-item>
|
</el-form>
|
<template #footer>
|
<el-button @click="dlg = false">取消</el-button>
|
<el-button
|
v-permission="editingId == null ? 'config:sensitive-words:create' : 'config:sensitive-words:update'"
|
type="primary"
|
:loading="saving"
|
@click="submitForm"
|
>
|
保存
|
</el-button>
|
</template>
|
</el-dialog>
|
</div>
|
</template>
|
|
<script setup lang="ts">
|
import { onMounted, reactive, ref } from "vue";
|
import { ElMessage, ElMessageBox } from "element-plus";
|
import type { UploadRequestOptions } from "element-plus";
|
import http from "../api/http";
|
import { normalizeFormData } from "../utils/formHelper";
|
|
type Row = {
|
id: number;
|
uuid: string;
|
word: string;
|
enabled: number;
|
createTime: string;
|
};
|
|
const loading = ref(false);
|
const saving = ref(false);
|
const importing = ref(false);
|
const rows = ref<Row[]>([]);
|
const page = ref(1);
|
const size = ref(10);
|
const total = ref(0);
|
|
const filters = reactive({
|
keyword: "",
|
});
|
|
const dlg = ref(false);
|
const dlgTitle = ref("新增敏感词");
|
const editingId = ref<number | null>(null);
|
|
const form = reactive({
|
uuid: "",
|
word: "",
|
});
|
|
function indexMethod(i: number) {
|
return (page.value - 1) * size.value + i + 1;
|
}
|
|
function formatTime(s: string) {
|
if (!s) return "";
|
return s.replace("T", " ").slice(0, 19);
|
}
|
|
async function load() {
|
loading.value = true;
|
try {
|
const { data } = await http.get("/config/sensitive-words/manage", {
|
params: {
|
page: page.value,
|
size: size.value,
|
keyword: filters.keyword.trim() || undefined,
|
},
|
});
|
rows.value = (data.result || []) as Row[];
|
total.value = Number(data.total) || 0;
|
} catch (e: unknown) {
|
ElMessage.error(e instanceof Error ? e.message : "加载失败");
|
} finally {
|
loading.value = false;
|
}
|
}
|
|
function handleSearch() {
|
page.value = 1;
|
load();
|
}
|
|
function handleReset() {
|
filters.keyword = "";
|
page.value = 1;
|
load();
|
}
|
|
function onSizeChange(nextSize: number) {
|
size.value = nextSize;
|
page.value = 1;
|
load();
|
}
|
|
function openCreate() {
|
dlgTitle.value = "新增敏感词";
|
editingId.value = null;
|
form.uuid = "";
|
form.word = "";
|
dlg.value = true;
|
}
|
|
function openEdit(row: Row) {
|
dlgTitle.value = "编辑敏感词";
|
editingId.value = row.id;
|
form.uuid = row.uuid;
|
form.word = row.word || "";
|
dlg.value = true;
|
}
|
|
async function submitForm() {
|
const word = form.word.trim();
|
if (!word) {
|
ElMessage.warning("请填写敏感词");
|
return;
|
}
|
if (word.length > 50) {
|
ElMessage.warning("敏感词不能超过50个字");
|
return;
|
}
|
|
saving.value = true;
|
try {
|
const payload = normalizeFormData({ word });
|
if (editingId.value == null) {
|
await http.post("/config/sensitive-words/manage", payload);
|
ElMessage.success("已新增");
|
page.value = 1;
|
} else {
|
await http.put(`/config/sensitive-words/manage/${editingId.value}`, {
|
uuid: form.uuid,
|
...payload,
|
});
|
ElMessage.success("已保存");
|
}
|
dlg.value = false;
|
load();
|
} catch (e: unknown) {
|
ElMessage.error(e instanceof Error ? e.message : "保存失败");
|
} finally {
|
saving.value = false;
|
}
|
}
|
|
async function importSensitiveWords(opt: UploadRequestOptions) {
|
const file = opt.file as File;
|
if (!file) return;
|
if (!/\.(xls|xlsx)$/i.test(file.name)) {
|
ElMessage.warning("请上传 xls 或 xlsx 文件");
|
return;
|
}
|
const fd = new FormData();
|
fd.append("file", file);
|
importing.value = true;
|
try {
|
const { data } = await http.post("/config/sensitive-words/manage/import", fd, {
|
headers: { "Content-Type": "multipart/form-data" },
|
});
|
const result = data.result || {};
|
const success = Number(result.successCount) || 0;
|
const duplicate = Number(result.duplicateCount) || 0;
|
const invalid = Number(result.invalidCount) || 0;
|
const errors = Array.isArray(result.errorMessages) ? result.errorMessages : [];
|
const message = `导入完成:成功${success}条,重复跳过${duplicate}条,无效${invalid}条`;
|
if (errors.length) {
|
ElMessageBox.alert(`${message}\n${errors.join("\n")}`, "导入结果", {
|
confirmButtonText: "知道了",
|
});
|
} else {
|
ElMessage.success(message);
|
}
|
page.value = 1;
|
load();
|
} catch (e: unknown) {
|
ElMessage.error(e instanceof Error ? e.message : "导入失败");
|
} finally {
|
importing.value = false;
|
}
|
}
|
|
async function downloadTemplate() {
|
try {
|
const res = await http.get("/config/sensitive-words/manage/import-template", { responseType: "blob" });
|
const blob = new Blob([res.data], {
|
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
});
|
const url = window.URL.createObjectURL(blob);
|
const a = document.createElement("a");
|
a.href = url;
|
a.download = "sensitive-word-import-template.xlsx";
|
a.click();
|
window.URL.revokeObjectURL(url);
|
} catch (e: unknown) {
|
ElMessage.error(e instanceof Error ? e.message : "下载模板失败");
|
}
|
}
|
|
function toggleEnabled(row: Row) {
|
const nextEnabled = row.enabled === 1 ? 0 : 1;
|
const actionText = nextEnabled === 1 ? "启用" : "禁用";
|
ElMessageBox.confirm(`确认${actionText}敏感词「${row.word}」?`, `${actionText}确认`, { type: "warning" })
|
.then(async () => {
|
try {
|
await http.patch(`/config/sensitive-words/manage/${row.id}/enabled`, {
|
uuid: row.uuid,
|
enabled: nextEnabled,
|
});
|
ElMessage.success(`已${actionText}`);
|
load();
|
} catch (e: unknown) {
|
ElMessage.error(e instanceof Error ? e.message : `${actionText}失败`);
|
}
|
})
|
.catch(() => {});
|
}
|
|
function confirmDelete(row: Row) {
|
ElMessageBox.confirm(`确认删除敏感词「${row.word}」?`, "删除确认", { type: "warning" })
|
.then(async () => {
|
try {
|
await http.delete(`/config/sensitive-words/manage/${row.id}`, {
|
params: { uuid: row.uuid },
|
});
|
ElMessage.success("已删除");
|
if (rows.value.length === 1 && page.value > 1) {
|
page.value -= 1;
|
}
|
load();
|
} catch (e: unknown) {
|
ElMessage.error(e instanceof Error ? e.message : "删除失败");
|
}
|
})
|
.catch(() => {});
|
}
|
|
onMounted(load);
|
</script>
|
|
<style scoped>
|
.page {
|
padding: 0;
|
display: flex;
|
flex-direction: column;
|
height: calc(100vh - 190px);
|
min-height: 420px;
|
}
|
.header {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
margin-bottom: 12px;
|
flex: none;
|
}
|
.header-actions {
|
display: flex;
|
align-items: center;
|
gap: 10px;
|
}
|
.section-title {
|
font-size: 16px;
|
font-weight: 600;
|
margin: 0;
|
padding-left: 10px;
|
border-left: 3px solid #409eff;
|
color: #303133;
|
}
|
.filters {
|
flex: none;
|
margin-bottom: 12px;
|
}
|
.table-wrap {
|
flex: 1;
|
min-height: 0;
|
}
|
.pager {
|
flex: none;
|
margin-top: 12px;
|
display: flex;
|
justify-content: flex-end;
|
}
|
.table-ops {
|
display: flex;
|
align-items: center;
|
gap: 8px;
|
}
|
</style>
|