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

417 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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,
saveHotWord,
updateHotWord,
type HotWordVO,
} from "../../api/business/hotword";
const { Option } = Select;
const { Text } = Typography;
type HotWordFormValues = {
word: string;
pinyin?: string;
category?: string;
weight: number;
status: number;
isPublic: number;
remark?: string;
};
const HotWords: React.FC = () => {
const { t } = useTranslation();
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);
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);
const userProfile = useMemo(() => {
const profileStr = sessionStorage.getItem("userProfile");
return profileStr ? JSON.parse(profileStr) : {};
}, []);
const isAdmin = useMemo(() => {
return userProfile.isPlatformAdmin === true || userProfile.isTenantAdmin === true;
}, [userProfile]);
useEffect(() => {
void fetchData();
}, [current, searchCategory, searchType, searchWord, size]);
const fetchData = async () => {
setLoading(true);
try {
const res = await getHotWordPage({
current,
size,
word: searchWord,
category: searchCategory,
isPublic: searchType,
});
if (res.data?.data) {
setData(res.data.data.records);
setTotal(res.data.data.total);
}
} finally {
setLoading(false);
}
};
const handleOpenModal = (record?: HotWordVO) => {
if (record?.isPublic === 1 && !isAdmin) {
message.error("公开热词仅限管理员修改");
return;
}
if (record) {
setEditingId(record.id);
form.setFieldsValue({
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 });
}
setModalVisible(true);
};
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);
const payload = {
...values,
matchStrategy: 1,
pinyinList: values.pinyin ? [values.pinyin.trim()] : [],
};
if (editingId) {
await updateHotWord({ ...payload, id: editingId });
message.success("更新成功");
} else {
await saveHotWord(payload);
message.success("新增成功");
}
setModalVisible(false);
await fetchData();
} finally {
setSubmitLoading(false);
}
};
const handleWordBlur = async (e: React.FocusEvent<HTMLInputElement>) => {
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);
}
} catch {
// handled by interceptor
}
};
const columns = [
{
title: "热词原文",
dataIndex: "word",
key: "word",
render: (text: string, record: HotWordVO) => (
<Space>
<Text strong>{text}</Text>
{record.isPublic === 1 ? (
<Tooltip title="租户公开">
<GlobalOutlined style={{ color: "#52c41a" }} />
</Tooltip>
) : (
<Tooltip title="个人私有">
<UserOutlined style={{ color: "#1890ff" }} />
</Tooltip>
)}
</Space>
),
},
{
title: "拼音",
dataIndex: "pinyinList",
key: "pinyinList",
render: (list: string[]) =>
list?.[0] ? <Tag style={{ borderRadius: 4 }}>{list[0]}</Tag> : <Text type="secondary">-</Text>,
},
{
title: "类别",
dataIndex: "category",
key: "category",
render: (value: string) => categories.find((item) => item.itemValue === value)?.itemLabel || value || "-",
},
{
title: "范围",
dataIndex: "isPublic",
key: "isPublic",
render: (value: number) => (value === 1 ? <Tag color="green"></Tag> : <Tag color="blue"></Tag>),
},
{
title: "权重",
dataIndex: "weight",
key: "weight",
render: (value: number) => <Tag color="orange">{value}</Tag>,
},
{
title: "状态",
dataIndex: "status",
key: "status",
render: (value: number) =>
value === 1 ? <Badge status="success" text="启用" /> : <Badge status="default" text="禁用" />,
},
{
title: "操作",
key: "action",
render: (_: unknown, record: HotWordVO) => {
const isMine = record.creatorId === userProfile.userId;
const canEdit = record.isPublic === 1 ? isAdmin : isMine || isAdmin;
if (!canEdit) {
return <Text type="secondary"></Text>;
}
return (
<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>
);
},
},
];
return (
<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>
<Select
placeholder="热词类型"
style={{ width: 110 }}
allowClear
onChange={(value) => {
setSearchType(value);
setCurrent(1);
}}
>
<Option value={1}></Option>
<Option value={0}></Option>
</Select>
<Select
placeholder="按类别筛选"
style={{ width: 150 }}
allowClear
onChange={(value) => {
setSearchCategory(value);
setCurrent(1);
}}
>
{categories.map((item) => (
<Option key={item.itemValue} value={item.itemValue}>
{item.itemLabel}
</Option>
))}
</Select>
<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>
}
>
<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>
<Modal
title={editingId ? "编辑热词" : "新增热词"}
open={modalVisible}
onOk={handleSubmit}
onCancel={() => setModalVisible(false)}
confirmLoading={submitLoading}
width={560}
destroyOnHidden
>
<Form form={form} layout="vertical" style={{ marginTop: 16 }}>
<Form.Item
name="word"
label="热词原文"
rules={[{ required: true, message: "请输入热词原文" }]}
>
<Input placeholder="输入识别关键词" onBlur={handleWordBlur} />
</Form.Item>
{/*<Form.Item*/}
{/* name="pinyin"*/}
{/* label="拼音"*/}
{/* tooltip="仅保留一个拼音值,失焦后会自动带出推荐结果"*/}
{/*>*/}
{/* <Input placeholder="例如hui yi" />*/}
{/*</Form.Item>*/}
<Row gutter={16}>
<Col span={12}>
<Form.Item name="category" label="热词分类">
<Select placeholder="请选择分类" allowClear>
{categories.map((item) => (
<Option key={item.itemValue} value={item.itemValue}>
{item.itemLabel}
</Option>
))}
</Select>
</Form.Item>
</Col>
<Col span={12}>
<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>
{isAdmin ? (
<Col span={12}>
<Form.Item name="isPublic" label="租户公开" tooltip="开启后,租户内成员都可以共享这条热词">
<Radio.Group>
<Radio value={1}></Radio>
<Radio value={0}></Radio>
</Radio.Group>
</Form.Item>
</Col>
) : null}
</Row>
<Form.Item name="remark" label="备注">
<Input.TextArea rows={2} placeholder="记录热词来源或适用场景" />
</Form.Item>
</Form>
</Modal>
</div>
);
};
export default HotWords;