2026-03-02 11:59:47 +00:00
|
|
|
import React, { useState, useEffect } from 'react';
|
|
|
|
|
import {
|
|
|
|
|
Table, Card, Button, Input, Space, Modal, Form, Select,
|
|
|
|
|
InputNumber, Tag, message, Popconfirm, Divider, Tooltip,
|
|
|
|
|
Radio, Row, Col, Typography, Badge
|
|
|
|
|
} from 'antd';
|
|
|
|
|
import {
|
|
|
|
|
PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined,
|
|
|
|
|
UserOutlined, GlobalOutlined
|
|
|
|
|
} from '@ant-design/icons';
|
2026-03-05 01:36:41 +00:00
|
|
|
import { useTranslation } from 'react-i18next';
|
2026-03-02 11:59:47 +00:00
|
|
|
import { useDict } from '../../hooks/useDict';
|
|
|
|
|
import {
|
|
|
|
|
getHotWordPage,
|
|
|
|
|
saveHotWord,
|
|
|
|
|
updateHotWord,
|
|
|
|
|
deleteHotWord,
|
|
|
|
|
getPinyinSuggestion,
|
|
|
|
|
HotWordVO,
|
|
|
|
|
HotWordDTO
|
|
|
|
|
} from '../../api/business/hotword';
|
|
|
|
|
|
|
|
|
|
const { Option } = Select;
|
|
|
|
|
const { Text } = Typography;
|
|
|
|
|
|
|
|
|
|
const HotWords: React.FC = () => {
|
2026-03-05 01:36:41 +00:00
|
|
|
const { t } = useTranslation();
|
2026-03-02 11:59:47 +00:00
|
|
|
const [form] = Form.useForm();
|
|
|
|
|
const { items: categories, loading: dictLoading } = 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);
|
2026-03-03 02:08:07 +00:00
|
|
|
const [searchType, setSearchType] = useState<number | undefined>(undefined);
|
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);
|
|
|
|
|
|
|
|
|
|
// 获取当前用户信息
|
|
|
|
|
const userProfile = React.useMemo(() => {
|
|
|
|
|
const profileStr = sessionStorage.getItem("userProfile");
|
|
|
|
|
return profileStr ? JSON.parse(profileStr) : {};
|
|
|
|
|
}, []);
|
|
|
|
|
|
2026-03-03 02:08:07 +00:00
|
|
|
// 判定是否具有管理员权限 (平台管理员或租户管理员)
|
|
|
|
|
const isAdmin = React.useMemo(() => {
|
|
|
|
|
return userProfile.isPlatformAdmin === true || userProfile.isTenantAdmin === true;
|
|
|
|
|
}, [userProfile]);
|
2026-03-02 11:59:47 +00:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
fetchData();
|
2026-03-03 02:08:07 +00:00
|
|
|
}, [current, size, searchWord, searchCategory, searchType]);
|
2026-03-02 11:59:47 +00:00
|
|
|
|
|
|
|
|
const fetchData = async () => {
|
|
|
|
|
setLoading(true);
|
|
|
|
|
try {
|
|
|
|
|
const res = await getHotWordPage({
|
|
|
|
|
current,
|
|
|
|
|
size,
|
|
|
|
|
word: searchWord,
|
2026-03-03 02:08:07 +00:00
|
|
|
category: searchCategory,
|
|
|
|
|
isPublic: searchType
|
2026-03-02 11:59:47 +00:00
|
|
|
});
|
|
|
|
|
if (res.data && res.data.data) {
|
|
|
|
|
setData(res.data.data.records);
|
|
|
|
|
setTotal(res.data.data.total);
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error(err);
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleOpenModal = (record?: HotWordVO) => {
|
|
|
|
|
if (record) {
|
2026-03-03 02:08:07 +00:00
|
|
|
if (record.isPublic === 1 && !isAdmin) {
|
2026-03-02 11:59:47 +00:00
|
|
|
message.error('公开热词仅限管理员修改');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
setEditingId(record.id);
|
|
|
|
|
form.setFieldsValue({
|
2026-03-04 12:49:32 +00:00
|
|
|
...record
|
2026-03-02 11:59:47 +00:00
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
setEditingId(null);
|
|
|
|
|
form.resetFields();
|
2026-03-04 12:49:32 +00:00
|
|
|
form.setFieldsValue({ weight: 2, status: 1, isPublic: 0 });
|
2026-03-02 11:59:47 +00:00
|
|
|
}
|
|
|
|
|
setModalVisible(true);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleSubmit = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const values = await form.validateFields();
|
|
|
|
|
setSubmitLoading(true);
|
|
|
|
|
|
|
|
|
|
const payload: any = {
|
|
|
|
|
...values,
|
|
|
|
|
pinyinList: values.pinyinList
|
|
|
|
|
? (Array.isArray(values.pinyinList) ? values.pinyinList : values.pinyinList.split(',').map((s: string) => s.trim()).filter(Boolean))
|
|
|
|
|
: []
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (editingId) {
|
|
|
|
|
await updateHotWord({ ...payload, id: editingId });
|
|
|
|
|
message.success('更新成功');
|
|
|
|
|
} else {
|
|
|
|
|
await saveHotWord(payload);
|
|
|
|
|
message.success('添加成功');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setModalVisible(false);
|
|
|
|
|
fetchData();
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error(err);
|
|
|
|
|
} finally {
|
|
|
|
|
setSubmitLoading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleWordBlur = async (e: React.FocusEvent<HTMLInputElement>) => {
|
|
|
|
|
const word = e.target.value;
|
|
|
|
|
if (word) {
|
|
|
|
|
try {
|
|
|
|
|
const res = await getPinyinSuggestion(word);
|
|
|
|
|
if (res.data && res.data.data) {
|
|
|
|
|
form.setFieldsValue({ pinyinList: res.data.data.join(', ') });
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error(err);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const columns = [
|
|
|
|
|
{
|
|
|
|
|
title: '热词原文',
|
|
|
|
|
dataIndex: 'word',
|
|
|
|
|
key: 'word',
|
|
|
|
|
render: (text: string, record: HotWordVO) => (
|
|
|
|
|
<Space>
|
|
|
|
|
<Text strong>{text}</Text>
|
|
|
|
|
{record.isPublic === 1 ? (
|
2026-03-03 02:08:07 +00:00
|
|
|
<Tooltip title="租户公开"><GlobalOutlined style={{ color: '#52c41a' }} /></Tooltip>
|
2026-03-02 11:59:47 +00:00
|
|
|
) : (
|
2026-03-03 02:08:07 +00:00
|
|
|
<Tooltip title="个人私有"><UserOutlined style={{ color: '#1890ff' }} /></Tooltip>
|
2026-03-02 11:59:47 +00:00
|
|
|
)}
|
|
|
|
|
</Space>
|
|
|
|
|
)
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
title: '拼音',
|
|
|
|
|
dataIndex: 'pinyinList',
|
|
|
|
|
key: 'pinyinList',
|
|
|
|
|
render: (list: string[]) => (
|
|
|
|
|
<Space size={[0, 4]} wrap>
|
|
|
|
|
{list?.map(p => <Tag key={p} style={{ fontSize: '11px', borderRadius: 4 }}>{p}</Tag>)}
|
|
|
|
|
</Space>
|
|
|
|
|
)
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
title: '类别',
|
|
|
|
|
dataIndex: 'category',
|
|
|
|
|
key: 'category',
|
|
|
|
|
render: (val: string) => categories.find(i => i.itemValue === val)?.itemLabel || val
|
|
|
|
|
},
|
|
|
|
|
{
|
2026-03-03 02:08:07 +00:00
|
|
|
title: '范围',
|
2026-03-02 11:59:47 +00:00
|
|
|
dataIndex: 'isPublic',
|
|
|
|
|
key: 'isPublic',
|
|
|
|
|
render: (val: number) => val === 1 ? <Tag color="green">公开</Tag> : <Tag color="blue">私有</Tag>
|
|
|
|
|
},
|
2026-03-04 07:19:40 +00:00
|
|
|
{
|
|
|
|
|
title: '权重',
|
|
|
|
|
dataIndex: 'weight',
|
|
|
|
|
key: 'weight',
|
|
|
|
|
render: (val: number) => <Tag color="orange">{val}</Tag>
|
|
|
|
|
},
|
2026-03-02 11:59:47 +00:00
|
|
|
{
|
|
|
|
|
title: '状态',
|
|
|
|
|
dataIndex: 'status',
|
|
|
|
|
key: 'status',
|
|
|
|
|
render: (status: number) => status === 1 ? <Badge status="success" text="启用" /> : <Badge status="default" text="禁用" />
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
title: '操作',
|
|
|
|
|
key: 'action',
|
|
|
|
|
render: (_: any, record: HotWordVO) => {
|
2026-03-03 02:08:07 +00:00
|
|
|
const isMine = record.creatorId === userProfile.userId;
|
|
|
|
|
const canEdit = record.isPublic === 1 ? isAdmin : (isMine || isAdmin);
|
2026-03-02 11:59:47 +00:00
|
|
|
return (
|
|
|
|
|
<Space size="middle">
|
|
|
|
|
{canEdit ? (
|
|
|
|
|
<>
|
|
|
|
|
<Button type="link" size="small" icon={<EditOutlined />} onClick={() => handleOpenModal(record)}>编辑</Button>
|
2026-03-04 12:49:32 +00:00
|
|
|
<Popconfirm
|
|
|
|
|
title="确定删除?"
|
|
|
|
|
onConfirm={() => handleDelete(record.id)}
|
|
|
|
|
okText={t('common.confirm')}
|
|
|
|
|
cancelText={t('common.cancel')}
|
|
|
|
|
>
|
2026-03-02 11:59:47 +00:00
|
|
|
<Button type="link" size="small" danger icon={<DeleteOutlined />}>删除</Button>
|
|
|
|
|
</Popconfirm>
|
|
|
|
|
</>
|
2026-03-04 12:49:32 +00:00
|
|
|
|
2026-03-02 11:59:47 +00:00
|
|
|
) : (
|
|
|
|
|
<Text type="secondary" size="small">无权操作</Text>
|
|
|
|
|
)}
|
|
|
|
|
</Space>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div style={{ padding: '24px' }}>
|
|
|
|
|
<Card
|
|
|
|
|
title="热词库管理"
|
|
|
|
|
extra={
|
2026-03-03 02:08:07 +00:00
|
|
|
<Space wrap>
|
|
|
|
|
<Select
|
|
|
|
|
placeholder="热词类型"
|
|
|
|
|
style={{ width: 110 }}
|
|
|
|
|
allowClear
|
|
|
|
|
onChange={v => {setSearchType(v); setCurrent(1);}}
|
|
|
|
|
>
|
|
|
|
|
<Option value={1}>租户公开</Option>
|
|
|
|
|
<Option value={0}>个人私有</Option>
|
|
|
|
|
</Select>
|
2026-03-02 11:59:47 +00:00
|
|
|
<Select
|
|
|
|
|
placeholder="按类别筛选"
|
2026-03-03 02:08:07 +00:00
|
|
|
style={{ width: 130 }}
|
2026-03-02 11:59:47 +00:00
|
|
|
allowClear
|
2026-03-03 02:08:07 +00:00
|
|
|
onChange={v => {setSearchCategory(v); setCurrent(1);}}
|
2026-03-02 11:59:47 +00:00
|
|
|
>
|
|
|
|
|
{categories.map(c => <Option key={c.itemValue} value={c.itemValue}>{c.itemLabel}</Option>)}
|
|
|
|
|
</Select>
|
|
|
|
|
<Input
|
|
|
|
|
placeholder="搜索热词原文..."
|
|
|
|
|
prefix={<SearchOutlined />}
|
|
|
|
|
allowClear
|
|
|
|
|
onPressEnter={(e) => {setSearchWord((e.target as any).value); setCurrent(1);}}
|
2026-03-03 02:08:07 +00:00
|
|
|
style={{ width: 180 }}
|
2026-03-02 11:59:47 +00:00
|
|
|
/>
|
|
|
|
|
<Button type="primary" icon={<PlusOutlined />} onClick={() => handleOpenModal()}>
|
|
|
|
|
新增热词
|
|
|
|
|
</Button>
|
|
|
|
|
</Space>
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
<Table
|
|
|
|
|
columns={columns}
|
|
|
|
|
dataSource={data}
|
|
|
|
|
rowKey="id"
|
|
|
|
|
loading={loading}
|
|
|
|
|
pagination={{
|
|
|
|
|
current,
|
|
|
|
|
pageSize: size,
|
|
|
|
|
total,
|
|
|
|
|
showTotal: (t) => `共 ${t} 条`,
|
|
|
|
|
onChange: (p, s) => { setCurrent(p); setSize(s); }
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
<Modal
|
|
|
|
|
title={editingId ? '编辑热词' : '新增热词'}
|
|
|
|
|
open={modalVisible}
|
|
|
|
|
onOk={handleSubmit}
|
|
|
|
|
onCancel={() => setModalVisible(false)}
|
|
|
|
|
confirmLoading={submitLoading}
|
|
|
|
|
width={550}
|
|
|
|
|
>
|
|
|
|
|
<Form form={form} layout="vertical" style={{ marginTop: '16px' }}>
|
|
|
|
|
<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>
|
|
|
|
|
|
|
|
|
|
<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}>
|
2026-03-04 12:49:32 +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>
|
|
|
|
|
</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-03 02:08:07 +00:00
|
|
|
{isAdmin && (
|
2026-03-02 11:59:47 +00:00
|
|
|
<Col span={12}>
|
|
|
|
|
<Form.Item name="isPublic" label="租户公开" tooltip="开启后,租户内所有成员均可共享此热词">
|
|
|
|
|
<Radio.Group>
|
|
|
|
|
<Radio value={1}>是</Radio>
|
|
|
|
|
<Radio value={0}>否</Radio>
|
|
|
|
|
</Radio.Group>
|
|
|
|
|
</Form.Item>
|
|
|
|
|
</Col>
|
|
|
|
|
)}
|
|
|
|
|
</Row>
|
|
|
|
|
|
|
|
|
|
<Form.Item name="remark" label="备注">
|
|
|
|
|
<Input.TextArea rows={2} placeholder="记录热词的来源或用途" />
|
|
|
|
|
</Form.Item>
|
|
|
|
|
</Form>
|
|
|
|
|
</Modal>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export default HotWords;
|