417 lines
12 KiB
TypeScript
417 lines
12 KiB
TypeScript
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;
|