2026-03-10 09:43:33 +00:00
|
|
|
import React, { useEffect, useMemo, useState } from "react";
|
2026-04-22 07:28:06 +00:00
|
|
|
import {
|
|
|
|
|
App,
|
|
|
|
|
Badge,
|
|
|
|
|
Button,
|
|
|
|
|
Card,
|
|
|
|
|
Col,
|
|
|
|
|
Form,
|
|
|
|
|
Input,
|
|
|
|
|
InputNumber,
|
2026-04-23 07:47:29 +00:00
|
|
|
List,
|
2026-04-22 07:28:06 +00:00
|
|
|
Modal,
|
|
|
|
|
Popconfirm,
|
|
|
|
|
Row,
|
|
|
|
|
Select,
|
|
|
|
|
Space,
|
|
|
|
|
Table,
|
|
|
|
|
Tag,
|
|
|
|
|
Typography,
|
|
|
|
|
} from "antd";
|
2026-03-10 09:43:33 +00:00
|
|
|
import {
|
|
|
|
|
DeleteOutlined,
|
|
|
|
|
EditOutlined,
|
|
|
|
|
PlusOutlined,
|
|
|
|
|
} from "@ant-design/icons";
|
|
|
|
|
import { useTranslation } from "react-i18next";
|
|
|
|
|
import { useDict } from "../../hooks/useDict";
|
|
|
|
|
import {
|
|
|
|
|
deleteHotWord,
|
|
|
|
|
getHotWordPage,
|
2026-03-02 11:59:47 +00:00
|
|
|
getPinyinSuggestion,
|
2026-03-10 09:43:33 +00:00
|
|
|
saveHotWord,
|
|
|
|
|
updateHotWord,
|
|
|
|
|
type HotWordVO,
|
|
|
|
|
} from "../../api/business/hotword";
|
2026-04-22 07:28:06 +00:00
|
|
|
import {
|
|
|
|
|
deleteHotWordGroup,
|
|
|
|
|
getHotWordGroupOptions,
|
|
|
|
|
getHotWordGroupPage,
|
|
|
|
|
saveHotWordGroup,
|
|
|
|
|
updateHotWordGroup,
|
|
|
|
|
type HotWordGroupVO,
|
|
|
|
|
} from "../../api/business/hotwordGroup";
|
2026-04-15 09:47:31 +00:00
|
|
|
import AppPagination from "../../components/shared/AppPagination";
|
2026-04-23 07:47:29 +00:00
|
|
|
import ListActionBar from "../../components/shared/ListActionBar/ListActionBar";
|
2026-03-02 11:59:47 +00:00
|
|
|
|
|
|
|
|
const { Option } = Select;
|
|
|
|
|
const { Text } = Typography;
|
|
|
|
|
|
2026-03-10 09:43:33 +00:00
|
|
|
type HotWordFormValues = {
|
|
|
|
|
word: string;
|
|
|
|
|
pinyin?: string;
|
|
|
|
|
category?: string;
|
2026-04-22 07:28:06 +00:00
|
|
|
hotWordGroupId?: number;
|
2026-03-10 09:43:33 +00:00
|
|
|
weight: number;
|
|
|
|
|
status: number;
|
2026-04-22 07:28:06 +00:00
|
|
|
remark?: string;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type HotWordGroupFormValues = {
|
|
|
|
|
groupName: string;
|
|
|
|
|
status: number;
|
2026-03-10 09:43:33 +00:00
|
|
|
remark?: string;
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-02 11:59:47 +00:00
|
|
|
const HotWords: React.FC = () => {
|
2026-04-08 06:34:59 +00:00
|
|
|
const { message } = App.useApp();
|
2026-03-05 01:36:41 +00:00
|
|
|
const { t } = useTranslation();
|
2026-03-10 09:43:33 +00:00
|
|
|
const [form] = Form.useForm<HotWordFormValues>();
|
2026-04-22 07:28:06 +00:00
|
|
|
const [groupForm] = Form.useForm<HotWordGroupFormValues>();
|
2026-03-10 09:43:33 +00:00
|
|
|
const { items: categories } = useDict("biz_hotword_category");
|
2026-04-22 07:28:06 +00:00
|
|
|
const userProfile = useMemo(() => {
|
|
|
|
|
const profileStr = sessionStorage.getItem("userProfile");
|
|
|
|
|
return profileStr ? JSON.parse(profileStr) : {};
|
|
|
|
|
}, []);
|
|
|
|
|
const activeTenantId = useMemo(() => Number(localStorage.getItem("activeTenantId") || 0), []);
|
|
|
|
|
const isPlatformAdmin = userProfile.isPlatformAdmin === true;
|
|
|
|
|
|
2026-03-02 11:59:47 +00:00
|
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
|
const [data, setData] = useState<HotWordVO[]>([]);
|
|
|
|
|
const [total, setTotal] = useState(0);
|
|
|
|
|
const [current, setCurrent] = useState(1);
|
|
|
|
|
const [size, setSize] = useState(10);
|
2026-03-10 09:43:33 +00:00
|
|
|
const [searchWord, setSearchWord] = useState("");
|
2026-03-02 11:59:47 +00:00
|
|
|
const [searchCategory, setSearchCategory] = useState<string | undefined>(undefined);
|
2026-04-22 08:06:50 +00:00
|
|
|
const [searchGroupId, setSearchGroupId] = useState<number | undefined>(undefined);
|
2026-04-22 07:28:06 +00:00
|
|
|
|
2026-03-02 11:59:47 +00:00
|
|
|
const [modalVisible, setModalVisible] = useState(false);
|
|
|
|
|
const [editingId, setEditingId] = useState<number | null>(null);
|
|
|
|
|
const [submitLoading, setSubmitLoading] = useState(false);
|
|
|
|
|
|
2026-04-22 07:28:06 +00:00
|
|
|
const [groupOptions, setGroupOptions] = useState<HotWordGroupVO[]>([]);
|
|
|
|
|
const [groupEditorVisible, setGroupEditorVisible] = useState(false);
|
|
|
|
|
const [groupLoading, setGroupLoading] = useState(false);
|
|
|
|
|
const [groupSubmitLoading, setGroupSubmitLoading] = useState(false);
|
|
|
|
|
const [groupData, setGroupData] = useState<HotWordGroupVO[]>([]);
|
|
|
|
|
const [editingGroupId, setEditingGroupId] = useState<number | null>(null);
|
2026-03-02 11:59:47 +00:00
|
|
|
|
2026-04-23 07:47:29 +00:00
|
|
|
const [filterVisible, setFilterVisible] = useState(false);
|
|
|
|
|
|
2026-04-22 07:28:06 +00:00
|
|
|
const groupNameMap = useMemo(
|
|
|
|
|
() => Object.fromEntries(groupOptions.map((item) => [item.id, item.groupName])) as Record<number, string>,
|
|
|
|
|
[groupOptions]
|
|
|
|
|
);
|
2026-03-02 11:59:47 +00:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
2026-03-10 09:43:33 +00:00
|
|
|
void fetchData();
|
2026-04-23 07:47:29 +00:00
|
|
|
}, [current, searchCategory, searchGroupId, size]);
|
2026-04-22 07:28:06 +00:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
void loadGroupOptions();
|
2026-04-23 07:47:29 +00:00
|
|
|
void loadGroupPage();
|
2026-04-22 07:28:06 +00:00
|
|
|
}, []);
|
2026-03-02 11:59:47 +00:00
|
|
|
|
|
|
|
|
const fetchData = async () => {
|
|
|
|
|
setLoading(true);
|
|
|
|
|
try {
|
2026-03-10 09:43:33 +00:00
|
|
|
const res = await getHotWordPage({
|
|
|
|
|
current,
|
|
|
|
|
size,
|
|
|
|
|
word: searchWord,
|
2026-03-03 02:08:07 +00:00
|
|
|
category: searchCategory,
|
2026-04-22 08:06:50 +00:00
|
|
|
hotWordGroupId: searchGroupId,
|
2026-04-22 07:28:06 +00:00
|
|
|
tenantId: isPlatformAdmin ? activeTenantId : undefined,
|
2026-03-02 11:59:47 +00:00
|
|
|
});
|
2026-03-10 09:43:33 +00:00
|
|
|
if (res.data?.data) {
|
2026-03-02 11:59:47 +00:00
|
|
|
setData(res.data.data.records);
|
|
|
|
|
setTotal(res.data.data.total);
|
|
|
|
|
}
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-22 07:28:06 +00:00
|
|
|
const loadGroupOptions = async () => {
|
|
|
|
|
const res = await getHotWordGroupOptions(isPlatformAdmin ? activeTenantId : undefined);
|
|
|
|
|
setGroupOptions(res.data?.data || []);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const loadGroupPage = async () => {
|
|
|
|
|
setGroupLoading(true);
|
|
|
|
|
try {
|
|
|
|
|
const res = await getHotWordGroupPage({ current: 1, size: 200 });
|
|
|
|
|
if (isPlatformAdmin) {
|
|
|
|
|
const scoped = await getHotWordGroupPage({ current: 1, size: 200, tenantId: activeTenantId });
|
|
|
|
|
setGroupData(scoped.data?.data?.records || []);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
setGroupData(res.data?.data?.records || []);
|
|
|
|
|
} finally {
|
|
|
|
|
setGroupLoading(false);
|
2026-03-10 09:43:33 +00:00
|
|
|
}
|
2026-04-22 07:28:06 +00:00
|
|
|
};
|
2026-03-10 09:43:33 +00:00
|
|
|
|
2026-04-22 07:28:06 +00:00
|
|
|
const handleOpenModal = (record?: HotWordVO) => {
|
2026-03-02 11:59:47 +00:00
|
|
|
if (record) {
|
|
|
|
|
setEditingId(record.id);
|
|
|
|
|
form.setFieldsValue({
|
2026-03-10 09:43:33 +00:00
|
|
|
word: record.word,
|
|
|
|
|
pinyin: record.pinyinList?.[0] || "",
|
|
|
|
|
category: record.category,
|
2026-04-22 07:28:06 +00:00
|
|
|
hotWordGroupId: record.hotWordGroupId,
|
2026-03-10 09:43:33 +00:00
|
|
|
weight: record.weight,
|
|
|
|
|
status: record.status,
|
|
|
|
|
remark: record.remark,
|
2026-03-02 11:59:47 +00:00
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
setEditingId(null);
|
|
|
|
|
form.resetFields();
|
2026-04-23 07:47:29 +00:00
|
|
|
form.setFieldsValue({ weight: 2, status: 1, hotWordGroupId: searchGroupId });
|
2026-03-02 11:59:47 +00:00
|
|
|
}
|
|
|
|
|
setModalVisible(true);
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-10 09:43:33 +00:00
|
|
|
const handleDelete = async (id: number) => {
|
2026-04-22 07:28:06 +00:00
|
|
|
await deleteHotWord(id);
|
|
|
|
|
message.success("删除成功");
|
|
|
|
|
await fetchData();
|
2026-04-23 07:47:29 +00:00
|
|
|
await loadGroupPage();
|
2026-03-10 09:43:33 +00:00
|
|
|
};
|
|
|
|
|
|
2026-03-02 11:59:47 +00:00
|
|
|
const handleSubmit = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const values = await form.validateFields();
|
|
|
|
|
setSubmitLoading(true);
|
2026-03-10 09:43:33 +00:00
|
|
|
const payload = {
|
2026-03-02 11:59:47 +00:00
|
|
|
...values,
|
2026-04-22 07:28:06 +00:00
|
|
|
tenantId: isPlatformAdmin ? activeTenantId : undefined,
|
2026-03-10 09:43:33 +00:00
|
|
|
matchStrategy: 1,
|
|
|
|
|
pinyinList: values.pinyin ? [values.pinyin.trim()] : [],
|
2026-03-02 11:59:47 +00:00
|
|
|
};
|
|
|
|
|
if (editingId) {
|
|
|
|
|
await updateHotWord({ ...payload, id: editingId });
|
2026-03-10 09:43:33 +00:00
|
|
|
message.success("更新成功");
|
2026-03-02 11:59:47 +00:00
|
|
|
} else {
|
|
|
|
|
await saveHotWord(payload);
|
2026-03-10 09:43:33 +00:00
|
|
|
message.success("新增成功");
|
2026-03-02 11:59:47 +00:00
|
|
|
}
|
|
|
|
|
setModalVisible(false);
|
2026-04-23 07:47:29 +00:00
|
|
|
await Promise.all([fetchData(), loadGroupOptions(), loadGroupPage()]);
|
2026-03-02 11:59:47 +00:00
|
|
|
} finally {
|
|
|
|
|
setSubmitLoading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleWordBlur = async (e: React.FocusEvent<HTMLInputElement>) => {
|
2026-03-10 09:43:33 +00:00
|
|
|
const word = e.target.value?.trim();
|
|
|
|
|
if (!word || form.getFieldValue("pinyin")) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
const res = await getPinyinSuggestion(word);
|
|
|
|
|
const firstPinyin = res.data?.data?.[0];
|
|
|
|
|
if (firstPinyin) {
|
|
|
|
|
form.setFieldValue("pinyin", firstPinyin);
|
2026-03-02 11:59:47 +00:00
|
|
|
}
|
2026-03-10 09:43:33 +00:00
|
|
|
} catch {
|
|
|
|
|
// handled by interceptor
|
2026-03-02 11:59:47 +00:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-23 07:47:29 +00:00
|
|
|
const openGroupEditor = (record?: HotWordGroupVO, e?: React.MouseEvent) => {
|
|
|
|
|
e?.stopPropagation();
|
2026-04-22 07:28:06 +00:00
|
|
|
if (record) {
|
|
|
|
|
setEditingGroupId(record.id);
|
|
|
|
|
groupForm.setFieldsValue({
|
|
|
|
|
groupName: record.groupName,
|
|
|
|
|
status: record.status,
|
|
|
|
|
remark: record.remark,
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
setEditingGroupId(null);
|
|
|
|
|
groupForm.resetFields();
|
|
|
|
|
groupForm.setFieldsValue({ status: 1 });
|
|
|
|
|
}
|
|
|
|
|
setGroupEditorVisible(true);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleGroupSubmit = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const values = await groupForm.validateFields();
|
|
|
|
|
setGroupSubmitLoading(true);
|
|
|
|
|
if (editingGroupId) {
|
|
|
|
|
await updateHotWordGroup({ ...values, id: editingGroupId, tenantId: isPlatformAdmin ? activeTenantId : undefined });
|
|
|
|
|
message.success("热词组更新成功");
|
|
|
|
|
} else {
|
|
|
|
|
await saveHotWordGroup({ ...values, tenantId: isPlatformAdmin ? activeTenantId : undefined });
|
|
|
|
|
message.success("热词组创建成功");
|
|
|
|
|
}
|
|
|
|
|
setGroupEditorVisible(false);
|
|
|
|
|
await Promise.all([loadGroupOptions(), loadGroupPage()]);
|
|
|
|
|
} finally {
|
|
|
|
|
setGroupSubmitLoading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-23 07:47:29 +00:00
|
|
|
const handleDeleteGroup = async (id: number, e?: React.MouseEvent) => {
|
|
|
|
|
e?.stopPropagation();
|
2026-04-22 07:28:06 +00:00
|
|
|
await deleteHotWordGroup(id, isPlatformAdmin ? activeTenantId : undefined);
|
|
|
|
|
message.success("热词组删除成功");
|
2026-04-23 07:47:29 +00:00
|
|
|
if (searchGroupId === id) {
|
|
|
|
|
setSearchGroupId(undefined);
|
|
|
|
|
}
|
2026-04-22 07:28:06 +00:00
|
|
|
await Promise.all([loadGroupOptions(), loadGroupPage(), fetchData()]);
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-02 11:59:47 +00:00
|
|
|
const columns = [
|
|
|
|
|
{
|
2026-03-10 09:43:33 +00:00
|
|
|
title: "热词原文",
|
|
|
|
|
dataIndex: "word",
|
|
|
|
|
key: "word",
|
2026-04-08 06:34:59 +00:00
|
|
|
ellipsis: true,
|
2026-04-22 07:28:06 +00:00
|
|
|
render: (text: string) => <Text strong ellipsis={{ tooltip: text }}>{text}</Text>,
|
2026-03-02 11:59:47 +00:00
|
|
|
},
|
|
|
|
|
{
|
2026-03-10 09:43:33 +00:00
|
|
|
title: "拼音",
|
|
|
|
|
dataIndex: "pinyinList",
|
|
|
|
|
key: "pinyinList",
|
2026-04-22 07:28:06 +00:00
|
|
|
render: (list: string[]) => list?.[0] ? <Tag style={{ borderRadius: 4 }}>{list[0]}</Tag> : <Text type="secondary">-</Text>,
|
2026-03-02 11:59:47 +00:00
|
|
|
},
|
|
|
|
|
{
|
2026-04-22 07:28:06 +00:00
|
|
|
title: "分类",
|
2026-03-10 09:43:33 +00:00
|
|
|
dataIndex: "category",
|
|
|
|
|
key: "category",
|
|
|
|
|
render: (value: string) => categories.find((item) => item.itemValue === value)?.itemLabel || value || "-",
|
2026-03-02 11:59:47 +00:00
|
|
|
},
|
|
|
|
|
{
|
2026-04-22 07:28:06 +00:00
|
|
|
title: "热词组",
|
|
|
|
|
dataIndex: "hotWordGroupId",
|
|
|
|
|
key: "hotWordGroupId",
|
|
|
|
|
render: (value?: number, record?: HotWordVO) => {
|
|
|
|
|
const name = record?.hotWordGroupName || (value ? groupNameMap[value] : undefined);
|
|
|
|
|
return name ? <Tag color="blue">{name}</Tag> : <Text type="secondary">未分组</Text>;
|
|
|
|
|
},
|
2026-03-02 11:59:47 +00:00
|
|
|
},
|
2026-03-04 07:19:40 +00:00
|
|
|
{
|
2026-03-10 09:43:33 +00:00
|
|
|
title: "权重",
|
|
|
|
|
dataIndex: "weight",
|
|
|
|
|
key: "weight",
|
|
|
|
|
render: (value: number) => <Tag color="orange">{value}</Tag>,
|
2026-03-04 07:19:40 +00:00
|
|
|
},
|
2026-03-02 11:59:47 +00:00
|
|
|
{
|
2026-03-10 09:43:33 +00:00
|
|
|
title: "状态",
|
|
|
|
|
dataIndex: "status",
|
|
|
|
|
key: "status",
|
2026-04-22 07:28:06 +00:00
|
|
|
render: (value: number) => value === 1 ? <Badge status="success" text="启用" /> : <Badge status="default" text="禁用" />,
|
2026-03-02 11:59:47 +00:00
|
|
|
},
|
|
|
|
|
{
|
2026-03-10 09:43:33 +00:00
|
|
|
title: "操作",
|
|
|
|
|
key: "action",
|
2026-04-22 07:28:06 +00:00
|
|
|
render: (_: unknown, record: HotWordVO) => (
|
|
|
|
|
<Space size="middle">
|
|
|
|
|
<Button type="link" size="small" icon={<EditOutlined />} onClick={() => handleOpenModal(record)}>
|
|
|
|
|
编辑
|
|
|
|
|
</Button>
|
|
|
|
|
<Popconfirm
|
|
|
|
|
title="确定删除这条热词吗?"
|
|
|
|
|
onConfirm={() => handleDelete(record.id)}
|
|
|
|
|
okText={t("common.confirm")}
|
|
|
|
|
cancelText={t("common.cancel")}
|
|
|
|
|
>
|
|
|
|
|
<Button type="link" size="small" danger icon={<DeleteOutlined />}>
|
|
|
|
|
删除
|
|
|
|
|
</Button>
|
|
|
|
|
</Popconfirm>
|
|
|
|
|
</Space>
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
];
|
2026-03-10 09:43:33 +00:00
|
|
|
|
2026-04-23 07:47:29 +00:00
|
|
|
const filterContent = (
|
|
|
|
|
<div style={{ width: 200 }}>
|
|
|
|
|
<div style={{ marginBottom: 8 }}>分类筛选</div>
|
|
|
|
|
<Select
|
|
|
|
|
placeholder="按分类筛选"
|
|
|
|
|
style={{ width: '100%' }}
|
|
|
|
|
allowClear
|
|
|
|
|
value={searchCategory}
|
|
|
|
|
onChange={(value) => {
|
|
|
|
|
setSearchCategory(value);
|
|
|
|
|
setCurrent(1);
|
|
|
|
|
setFilterVisible(false);
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{categories.map((item) => (
|
|
|
|
|
<Option key={item.itemValue} value={item.itemValue}>
|
|
|
|
|
{item.itemLabel}
|
|
|
|
|
</Option>
|
|
|
|
|
))}
|
|
|
|
|
</Select>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
2026-03-02 11:59:47 +00:00
|
|
|
|
|
|
|
|
return (
|
2026-04-23 07:47:29 +00:00
|
|
|
<div className="app-page" style={{ padding: '16px', display: 'flex', flexDirection: 'column', height: '100%' }}>
|
|
|
|
|
<div style={{ display: 'flex', gap: '16px', flex: 1, minHeight: 0 }}>
|
|
|
|
|
{/* Left Panel: Hotword Groups */}
|
|
|
|
|
<Card
|
|
|
|
|
className="shadow-sm"
|
|
|
|
|
title="热词组"
|
|
|
|
|
style={{ width: '25%', display: 'flex', flexDirection: 'column', minWidth: 280 }}
|
|
|
|
|
styles={{ body: { padding: 0, flex: 1, overflow: 'auto' } }}
|
|
|
|
|
extra={
|
|
|
|
|
<Button
|
|
|
|
|
type="primary"
|
|
|
|
|
icon={<PlusOutlined />}
|
|
|
|
|
onClick={() => openGroupEditor()}
|
|
|
|
|
size="small"
|
|
|
|
|
/>
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
<List
|
|
|
|
|
loading={groupLoading}
|
|
|
|
|
dataSource={[{ id: undefined, groupName: '全部热词' } as any, ...groupData]}
|
|
|
|
|
renderItem={(item) => {
|
|
|
|
|
const isSelected = searchGroupId === item.id;
|
|
|
|
|
|
|
|
|
|
const actions = [];
|
|
|
|
|
if (item.id) {
|
|
|
|
|
actions.push(
|
|
|
|
|
<Button
|
|
|
|
|
type="text"
|
|
|
|
|
icon={<EditOutlined />}
|
|
|
|
|
onClick={(e) => openGroupEditor(item, e)}
|
|
|
|
|
size="small"
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
actions.push(
|
|
|
|
|
<Popconfirm
|
|
|
|
|
title="确定删除这个热词组吗?"
|
|
|
|
|
description="删除前必须先解除模板引用并清空组内热词。"
|
|
|
|
|
onConfirm={(e) => handleDeleteGroup(item.id, e)}
|
|
|
|
|
onCancel={e => e?.stopPropagation()}
|
|
|
|
|
>
|
|
|
|
|
<Button
|
|
|
|
|
type="text"
|
|
|
|
|
danger
|
|
|
|
|
icon={<DeleteOutlined />}
|
|
|
|
|
onClick={e => e.stopPropagation()}
|
|
|
|
|
size="small"
|
|
|
|
|
/>
|
|
|
|
|
</Popconfirm>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<List.Item
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setSearchGroupId(item.id);
|
|
|
|
|
setCurrent(1);
|
|
|
|
|
}}
|
|
|
|
|
style={{
|
|
|
|
|
cursor: 'pointer',
|
|
|
|
|
padding: '12px 24px',
|
|
|
|
|
background: isSelected ? 'var(--ant-color-primary-bg, #e6f4ff)' : 'transparent',
|
|
|
|
|
borderBottom: '1px solid var(--ant-color-border-secondary, #f0f0f0)',
|
|
|
|
|
transition: 'background 0.3s'
|
|
|
|
|
}}
|
|
|
|
|
actions={actions}
|
|
|
|
|
>
|
|
|
|
|
<List.Item.Meta
|
|
|
|
|
title={
|
|
|
|
|
<Text strong style={{ color: isSelected ? 'var(--ant-color-primary, #1677ff)' : 'inherit' }}>
|
|
|
|
|
{item.groupName}
|
|
|
|
|
</Text>
|
|
|
|
|
}
|
|
|
|
|
description={
|
|
|
|
|
item.id
|
|
|
|
|
? <><Tag color={item.hotWordCount >= 200 ? "red" : "processing"}>{item.hotWordCount}/200</Tag> {item.remark}</>
|
|
|
|
|
: '查看所有热词'
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
</List.Item>
|
|
|
|
|
);
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
{/* Right Panel: Hotwords */}
|
|
|
|
|
<Card
|
|
|
|
|
className="shadow-sm"
|
|
|
|
|
title={searchGroupId ? groupData.find(g => g.id === searchGroupId)?.groupName || '热词列表' : '全部热词'}
|
|
|
|
|
style={{ flex: 1, display: 'flex', flexDirection: 'column', minWidth: 0 }}
|
|
|
|
|
styles={{ body: { padding: 0, flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" } }}
|
|
|
|
|
>
|
|
|
|
|
<div style={{ padding: '16px 24px 0' }}>
|
|
|
|
|
<ListActionBar
|
|
|
|
|
actions={[
|
|
|
|
|
{
|
|
|
|
|
key: 'add',
|
|
|
|
|
label: '新增热词',
|
|
|
|
|
type: 'primary',
|
|
|
|
|
icon: <PlusOutlined />,
|
|
|
|
|
onClick: () => handleOpenModal()
|
|
|
|
|
}
|
|
|
|
|
]}
|
|
|
|
|
search={{
|
|
|
|
|
placeholder: '搜索热词原文',
|
|
|
|
|
value: searchWord,
|
|
|
|
|
onChange: (val) => setSearchWord(val),
|
|
|
|
|
onSearch: () => {
|
|
|
|
|
setCurrent(1);
|
|
|
|
|
void fetchData();
|
|
|
|
|
}
|
2026-03-10 09:43:33 +00:00
|
|
|
}}
|
2026-04-23 07:47:29 +00:00
|
|
|
filter={{
|
|
|
|
|
content: filterContent,
|
|
|
|
|
title: '高级筛选',
|
|
|
|
|
visible: filterVisible,
|
|
|
|
|
onVisibleChange: setFilterVisible,
|
|
|
|
|
isActive: !!searchCategory,
|
|
|
|
|
selectedLabel: searchCategory ? categories.find(c => c.itemValue === searchCategory)?.itemLabel : '筛选'
|
2026-04-22 08:06:50 +00:00
|
|
|
}}
|
2026-04-23 07:47:29 +00:00
|
|
|
showRefresh
|
|
|
|
|
onRefresh={() => {
|
2026-03-10 09:43:33 +00:00
|
|
|
setCurrent(1);
|
2026-04-23 07:47:29 +00:00
|
|
|
void fetchData();
|
|
|
|
|
void loadGroupPage();
|
2026-03-10 09:43:33 +00:00
|
|
|
}}
|
2026-03-02 11:59:47 +00:00
|
|
|
/>
|
2026-04-23 07:47:29 +00:00
|
|
|
</div>
|
|
|
|
|
<div className="app-page__table-wrap" style={{ flex: 1, minHeight: 0, overflow: "auto", padding: "16px 24px 0" }}>
|
|
|
|
|
<Table
|
|
|
|
|
columns={columns}
|
|
|
|
|
dataSource={data}
|
|
|
|
|
rowKey="id"
|
|
|
|
|
loading={loading}
|
|
|
|
|
scroll={{ x: "max-content" }}
|
|
|
|
|
pagination={false}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<AppPagination
|
|
|
|
|
current={current}
|
|
|
|
|
pageSize={size}
|
|
|
|
|
total={total}
|
|
|
|
|
onChange={(page, pageSize) => {
|
|
|
|
|
setCurrent(page);
|
|
|
|
|
setSize(pageSize);
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
</Card>
|
|
|
|
|
</div>
|
2026-03-02 11:59:47 +00:00
|
|
|
|
2026-03-10 09:43:33 +00:00
|
|
|
<Modal
|
|
|
|
|
title={editingId ? "编辑热词" : "新增热词"}
|
|
|
|
|
open={modalVisible}
|
2026-04-22 07:28:06 +00:00
|
|
|
onOk={() => void handleSubmit()}
|
2026-03-10 09:43:33 +00:00
|
|
|
onCancel={() => setModalVisible(false)}
|
|
|
|
|
confirmLoading={submitLoading}
|
|
|
|
|
width={560}
|
|
|
|
|
destroyOnHidden
|
2026-03-02 11:59:47 +00:00
|
|
|
>
|
2026-03-10 09:43:33 +00:00
|
|
|
<Form form={form} layout="vertical" style={{ marginTop: 16 }}>
|
2026-04-22 07:28:06 +00:00
|
|
|
<Form.Item name="word" label="热词原文" rules={[{ required: true, message: "请输入热词原文" }]}>
|
2026-03-03 02:08:07 +00:00
|
|
|
<Input placeholder="输入识别关键词" onBlur={handleWordBlur} />
|
2026-03-02 11:59:47 +00:00
|
|
|
</Form.Item>
|
2026-03-10 09:43:33 +00:00
|
|
|
|
2026-03-02 11:59:47 +00:00
|
|
|
<Row gutter={16}>
|
|
|
|
|
<Col span={12}>
|
|
|
|
|
<Form.Item name="category" label="热词分类">
|
|
|
|
|
<Select placeholder="请选择分类" allowClear>
|
2026-03-10 09:43:33 +00:00
|
|
|
{categories.map((item) => (
|
|
|
|
|
<Option key={item.itemValue} value={item.itemValue}>
|
|
|
|
|
{item.itemLabel}
|
|
|
|
|
</Option>
|
|
|
|
|
))}
|
2026-03-02 11:59:47 +00:00
|
|
|
</Select>
|
|
|
|
|
</Form.Item>
|
|
|
|
|
</Col>
|
2026-04-22 07:28:06 +00:00
|
|
|
<Col span={12}>
|
|
|
|
|
<Form.Item name="hotWordGroupId" label="所属热词组">
|
|
|
|
|
<Select placeholder="请选择热词组" allowClear options={groupOptions.map((item) => ({ label: `${item.groupName} (${item.hotWordCount}/200)`, value: item.id }))} />
|
|
|
|
|
</Form.Item>
|
|
|
|
|
</Col>
|
|
|
|
|
</Row>
|
|
|
|
|
|
|
|
|
|
<Row gutter={16}>
|
2026-03-02 11:59:47 +00:00
|
|
|
<Col span={12}>
|
2026-03-10 09:43:33 +00:00
|
|
|
<Form.Item
|
|
|
|
|
name="weight"
|
|
|
|
|
label="识别权重 (1-5)"
|
|
|
|
|
tooltip="权重越高,识别引擎越倾向于将其识别为该热词"
|
|
|
|
|
>
|
|
|
|
|
<InputNumber min={1} max={5} precision={1} step={0.1} style={{ width: "100%" }} />
|
2026-03-02 11:59:47 +00:00
|
|
|
</Form.Item>
|
|
|
|
|
</Col>
|
|
|
|
|
<Col span={12}>
|
|
|
|
|
<Form.Item name="status" label="使用状态">
|
|
|
|
|
<Select>
|
|
|
|
|
<Option value={1}>启用</Option>
|
|
|
|
|
<Option value={0}>禁用</Option>
|
|
|
|
|
</Select>
|
|
|
|
|
</Form.Item>
|
|
|
|
|
</Col>
|
|
|
|
|
</Row>
|
|
|
|
|
|
|
|
|
|
<Form.Item name="remark" label="备注">
|
2026-03-10 09:43:33 +00:00
|
|
|
<Input.TextArea rows={2} placeholder="记录热词来源或适用场景" />
|
2026-03-02 11:59:47 +00:00
|
|
|
</Form.Item>
|
|
|
|
|
</Form>
|
|
|
|
|
</Modal>
|
2026-04-22 07:28:06 +00:00
|
|
|
|
|
|
|
|
<Modal
|
|
|
|
|
title={editingGroupId ? "编辑热词组" : "新增热词组"}
|
|
|
|
|
open={groupEditorVisible}
|
|
|
|
|
onCancel={() => setGroupEditorVisible(false)}
|
|
|
|
|
onOk={() => void handleGroupSubmit()}
|
|
|
|
|
confirmLoading={groupSubmitLoading}
|
|
|
|
|
destroyOnHidden
|
|
|
|
|
>
|
|
|
|
|
<Form form={groupForm} layout="vertical" style={{ marginTop: 16 }}>
|
|
|
|
|
<Form.Item name="groupName" label="热词组名称" rules={[{ required: true, message: "请输入热词组名称" }]}>
|
|
|
|
|
<Input placeholder="例如:项目术语、客户名单" maxLength={100} />
|
|
|
|
|
</Form.Item>
|
|
|
|
|
<Form.Item name="status" label="状态">
|
|
|
|
|
<Select>
|
|
|
|
|
<Option value={1}>启用</Option>
|
|
|
|
|
<Option value={0}>禁用</Option>
|
|
|
|
|
</Select>
|
|
|
|
|
</Form.Item>
|
|
|
|
|
<Form.Item name="remark" label="备注">
|
|
|
|
|
<Input.TextArea rows={3} placeholder="说明这个热词组的适用范围" />
|
|
|
|
|
</Form.Item>
|
|
|
|
|
</Form>
|
|
|
|
|
</Modal>
|
2026-03-02 11:59:47 +00:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export default HotWords;
|