imeeting/frontend/src/pages/business/HotWords.tsx

417 lines
12 KiB
TypeScript
Raw Normal View History

2026-03-10 09:43:33 +00:00
import React, { useEffect, useMemo, useState } from "react";
import {
Badge,
Button,
Card,
Col,
Form,
Input,
InputNumber,
message,
Modal,
Popconfirm,
Radio,
Row,
Select,
Space,
Table,
Tag,
Tooltip,
Typography,
} from "antd";
import {
DeleteOutlined,
EditOutlined,
GlobalOutlined,
PlusOutlined,
SearchOutlined,
UserOutlined,
} from "@ant-design/icons";
import { useTranslation } from "react-i18next";
import { useDict } from "../../hooks/useDict";
import {
deleteHotWord,
getHotWordPage,
getPinyinSuggestion,
2026-03-10 09:43:33 +00:00
saveHotWord,
updateHotWord,
type HotWordVO,
} from "../../api/business/hotword";
const { Option } = Select;
const { Text } = Typography;
2026-03-10 09:43:33 +00:00
type HotWordFormValues = {
word: string;
pinyin?: string;
category?: string;
weight: number;
status: number;
isPublic: number;
remark?: string;
};
const HotWords: React.FC = () => {
const { t } = useTranslation();
2026-03-10 09:43:33 +00:00
const [form] = Form.useForm<HotWordFormValues>();
const { items: categories } = useDict("biz_hotword_category");
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("");
const [searchCategory, setSearchCategory] = useState<string | undefined>(undefined);
const [searchType, setSearchType] = useState<number | undefined>(undefined);
const [modalVisible, setModalVisible] = useState(false);
const [editingId, setEditingId] = useState<number | null>(null);
const [submitLoading, setSubmitLoading] = useState(false);
2026-03-10 09:43:33 +00:00
const userProfile = useMemo(() => {
const profileStr = sessionStorage.getItem("userProfile");
return profileStr ? JSON.parse(profileStr) : {};
}, []);
2026-03-10 09:43:33 +00:00
const isAdmin = useMemo(() => {
return userProfile.isPlatformAdmin === true || userProfile.isTenantAdmin === true;
}, [userProfile]);
useEffect(() => {
2026-03-10 09:43:33 +00:00
void fetchData();
}, [current, searchCategory, searchType, searchWord, size]);
const fetchData = async () => {
setLoading(true);
try {
2026-03-10 09:43:33 +00:00
const res = await getHotWordPage({
current,
size,
word: searchWord,
category: searchCategory,
2026-03-10 09:43:33 +00:00
isPublic: searchType,
});
2026-03-10 09:43:33 +00:00
if (res.data?.data) {
setData(res.data.data.records);
setTotal(res.data.data.total);
}
} finally {
setLoading(false);
}
};
const handleOpenModal = (record?: HotWordVO) => {
2026-03-10 09:43:33 +00:00
if (record?.isPublic === 1 && !isAdmin) {
message.error("公开热词仅限管理员修改");
return;
}
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,
weight: record.weight,
status: record.status,
isPublic: record.isPublic,
remark: record.remark,
});
} else {
setEditingId(null);
form.resetFields();
form.setFieldsValue({ weight: 2, status: 1, isPublic: 0 });
}
2026-03-10 09:43:33 +00:00
setModalVisible(true);
};
2026-03-10 09:43:33 +00:00
const handleDelete = async (id: number) => {
try {
await deleteHotWord(id);
message.success("删除成功");
await fetchData();
} catch {
// handled by interceptor
}
};
const handleSubmit = async () => {
try {
const values = await form.validateFields();
setSubmitLoading(true);
2026-03-10 09:43:33 +00:00
const payload = {
...values,
2026-03-10 09:43:33 +00:00
matchStrategy: 1,
pinyinList: values.pinyin ? [values.pinyin.trim()] : [],
};
if (editingId) {
await updateHotWord({ ...payload, id: editingId });
2026-03-10 09:43:33 +00:00
message.success("更新成功");
} else {
await saveHotWord(payload);
2026-03-10 09:43:33 +00:00
message.success("新增成功");
}
2026-03-10 09:43:33 +00:00
setModalVisible(false);
2026-03-10 09:43:33 +00:00
await fetchData();
} 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-10 09:43:33 +00:00
} catch {
// handled by interceptor
}
};
const columns = [
{
2026-03-10 09:43:33 +00:00
title: "热词原文",
dataIndex: "word",
key: "word",
render: (text: string, record: HotWordVO) => (
<Space>
<Text strong>{text}</Text>
{record.isPublic === 1 ? (
2026-03-10 09:43:33 +00:00
<Tooltip title="租户公开">
<GlobalOutlined style={{ color: "#52c41a" }} />
</Tooltip>
) : (
2026-03-10 09:43:33 +00:00
<Tooltip title="个人私有">
<UserOutlined style={{ color: "#1890ff" }} />
</Tooltip>
)}
</Space>
2026-03-10 09:43:33 +00:00
),
},
{
2026-03-10 09:43:33 +00:00
title: "拼音",
dataIndex: "pinyinList",
key: "pinyinList",
render: (list: string[]) =>
list?.[0] ? <Tag style={{ borderRadius: 4 }}>{list[0]}</Tag> : <Text type="secondary">-</Text>,
},
{
2026-03-10 09:43:33 +00:00
title: "类别",
dataIndex: "category",
key: "category",
render: (value: string) => categories.find((item) => item.itemValue === value)?.itemLabel || value || "-",
},
{
2026-03-10 09:43:33 +00:00
title: "范围",
dataIndex: "isPublic",
key: "isPublic",
render: (value: number) => (value === 1 ? <Tag color="green"></Tag> : <Tag color="blue"></Tag>),
},
{
2026-03-10 09:43:33 +00:00
title: "权重",
dataIndex: "weight",
key: "weight",
render: (value: number) => <Tag color="orange">{value}</Tag>,
},
{
2026-03-10 09:43:33 +00:00
title: "状态",
dataIndex: "status",
key: "status",
render: (value: number) =>
value === 1 ? <Badge status="success" text="启用" /> : <Badge status="default" text="禁用" />,
},
{
2026-03-10 09:43:33 +00:00
title: "操作",
key: "action",
render: (_: unknown, record: HotWordVO) => {
const isMine = record.creatorId === userProfile.userId;
2026-03-10 09:43:33 +00:00
const canEdit = record.isPublic === 1 ? isAdmin : isMine || isAdmin;
if (!canEdit) {
return <Text type="secondary"></Text>;
}
return (
<Space size="middle">
2026-03-10 09:43:33 +00:00
<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
},
},
];
return (
2026-03-10 09:43:33 +00:00
<div className="flex h-full flex-col overflow-hidden">
<Card
className="flex-1 overflow-hidden shadow-sm"
styles={{ body: { padding: 24, height: "100%", display: "flex", flexDirection: "column", overflow: "hidden" } }}
title="热词管理"
extra={
<Space wrap>
2026-03-10 09:43:33 +00:00
<Select
placeholder="热词类型"
style={{ width: 110 }}
allowClear
onChange={(value) => {
setSearchType(value);
setCurrent(1);
}}
>
<Option value={1}></Option>
<Option value={0}></Option>
</Select>
2026-03-10 09:43:33 +00:00
<Select
placeholder="按类别筛选"
style={{ width: 150 }}
allowClear
onChange={(value) => {
setSearchCategory(value);
setCurrent(1);
}}
>
2026-03-10 09:43:33 +00:00
{categories.map((item) => (
<Option key={item.itemValue} value={item.itemValue}>
{item.itemLabel}
</Option>
))}
</Select>
2026-03-10 09:43:33 +00:00
<Input
placeholder="搜索热词原文"
prefix={<SearchOutlined />}
allowClear
onPressEnter={(e) => {
setSearchWord((e.target as HTMLInputElement).value);
setCurrent(1);
}}
style={{ width: 200 }}
/>
<Button type="primary" icon={<PlusOutlined />} onClick={() => handleOpenModal()}>
</Button>
</Space>
}
>
2026-03-10 09:43:33 +00:00
<div className="min-h-0 flex-1">
<Table
columns={columns}
dataSource={data}
rowKey="id"
loading={loading}
scroll={{ y: "calc(100vh - 340px)" }}
pagination={{
current,
pageSize: size,
total,
showTotal: (value) => `${value}`,
onChange: (page, pageSize) => {
setCurrent(page);
setSize(pageSize);
},
}}
/>
</div>
</Card>
2026-03-10 09:43:33 +00:00
<Modal
title={editingId ? "编辑热词" : "新增热词"}
open={modalVisible}
onOk={handleSubmit}
onCancel={() => setModalVisible(false)}
confirmLoading={submitLoading}
width={560}
destroyOnHidden
>
2026-03-10 09:43:33 +00:00
<Form form={form} layout="vertical" style={{ marginTop: 16 }}>
<Form.Item
name="word"
label="热词原文"
rules={[{ required: true, message: "请输入热词原文" }]}
>
<Input placeholder="输入识别关键词" onBlur={handleWordBlur} />
</Form.Item>
2026-03-10 09:43:33 +00:00
{/*<Form.Item*/}
{/* name="pinyin"*/}
{/* label="拼音"*/}
{/* tooltip="仅保留一个拼音值,失焦后会自动带出推荐结果"*/}
{/*>*/}
{/* <Input placeholder="例如hui yi" />*/}
{/*</Form.Item>*/}
2026-03-10 09:43:33 +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>
))}
</Select>
</Form.Item>
</Col>
<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%" }} />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item name="status" label="使用状态">
<Select>
<Option value={1}></Option>
<Option value={0}></Option>
</Select>
</Form.Item>
</Col>
2026-03-10 09:43:33 +00:00
{isAdmin ? (
<Col span={12}>
2026-03-10 09:43:33 +00:00
<Form.Item name="isPublic" label="租户公开" tooltip="开启后,租户内成员都可以共享这条热词">
<Radio.Group>
<Radio value={1}></Radio>
<Radio value={0}></Radio>
</Radio.Group>
</Form.Item>
</Col>
2026-03-10 09:43:33 +00:00
) : null}
</Row>
<Form.Item name="remark" label="备注">
2026-03-10 09:43:33 +00:00
<Input.TextArea rows={2} placeholder="记录热词来源或适用场景" />
</Form.Item>
</Form>
</Modal>
</div>
);
};
export default HotWords;