269 lines
8.7 KiB
TypeScript
269 lines
8.7 KiB
TypeScript
|
|
import React, { useState, useEffect, useMemo } from 'react';
|
|||
|
|
import { Card, Button, Modal, Form, Input, InputNumber, message, Tag, Popconfirm, Tooltip, Empty } from 'antd';
|
|||
|
|
import { PlusOutlined, DeleteOutlined, FireOutlined, QuestionCircleOutlined, ReloadOutlined, EditOutlined } from '@ant-design/icons';
|
|||
|
|
import { api } from '../api';
|
|||
|
|
// @ts-ignore
|
|||
|
|
import PageTitleBar from '../components/PageTitleBar/PageTitleBar';
|
|||
|
|
// @ts-ignore
|
|||
|
|
import ListActionBar from '../components/ListActionBar/ListActionBar';
|
|||
|
|
import ListTable from '../components/ListTable/ListTable';
|
|||
|
|
|
|||
|
|
interface HotwordItem {
|
|||
|
|
id: number;
|
|||
|
|
word: string;
|
|||
|
|
pinyin: string;
|
|||
|
|
weight: number;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const Hotwords: React.FC = () => {
|
|||
|
|
const [hotwords, setHotwords] = useState<HotwordItem[]>([]);
|
|||
|
|
const [loading, setLoading] = useState(false);
|
|||
|
|
const [modalVisible, setModalVisible] = useState(false);
|
|||
|
|
const [searchText, setSearchText] = useState('');
|
|||
|
|
const [currentHotword, setCurrentHotword] = useState<HotwordItem | null>(null);
|
|||
|
|
const [form] = Form.useForm();
|
|||
|
|
|
|||
|
|
const fetchHotwords = async () => {
|
|||
|
|
setLoading(true);
|
|||
|
|
try {
|
|||
|
|
const data = await api.listHotwords();
|
|||
|
|
setHotwords(data);
|
|||
|
|
} catch (error) {
|
|||
|
|
message.error('获取热词列表失败');
|
|||
|
|
} finally {
|
|||
|
|
setLoading(false);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
fetchHotwords();
|
|||
|
|
}, []);
|
|||
|
|
|
|||
|
|
const handleSubmit = async () => {
|
|||
|
|
try {
|
|||
|
|
const values = await form.validateFields();
|
|||
|
|
if (currentHotword) {
|
|||
|
|
await api.updateHotword(currentHotword.id, values);
|
|||
|
|
message.success('修改成功');
|
|||
|
|
} else {
|
|||
|
|
await api.createHotword(values);
|
|||
|
|
message.success('添加成功');
|
|||
|
|
}
|
|||
|
|
handleModalClose();
|
|||
|
|
fetchHotwords();
|
|||
|
|
} catch (error) {
|
|||
|
|
// message.error('操作失败');
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleDelete = async (id: number) => {
|
|||
|
|
try {
|
|||
|
|
await api.deleteHotword(id);
|
|||
|
|
message.success('删除成功');
|
|||
|
|
fetchHotwords();
|
|||
|
|
} catch (error) {
|
|||
|
|
message.error('删除失败');
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleEdit = (record: HotwordItem) => {
|
|||
|
|
setCurrentHotword(record);
|
|||
|
|
form.setFieldsValue(record);
|
|||
|
|
setModalVisible(true);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleModalClose = () => {
|
|||
|
|
setModalVisible(false);
|
|||
|
|
setCurrentHotword(null);
|
|||
|
|
form.resetFields();
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const filteredHotwords = useMemo(() => {
|
|||
|
|
if (!searchText) return hotwords;
|
|||
|
|
return hotwords.filter(item =>
|
|||
|
|
item.word.includes(searchText) ||
|
|||
|
|
item.pinyin.toLowerCase().includes(searchText.toLowerCase())
|
|||
|
|
);
|
|||
|
|
}, [hotwords, searchText]);
|
|||
|
|
|
|||
|
|
const columns = [
|
|||
|
|
{
|
|||
|
|
title: '热词',
|
|||
|
|
dataIndex: 'word',
|
|||
|
|
key: 'word',
|
|||
|
|
width: '30%',
|
|||
|
|
render: (t: string) => (
|
|||
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|||
|
|
<FireOutlined style={{ color: '#ff4d4f' }} />
|
|||
|
|
<span style={{ fontWeight: 600, fontSize: '15px' }}>{t}</span>
|
|||
|
|
</div>
|
|||
|
|
)
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
title: '拼音',
|
|||
|
|
dataIndex: 'pinyin',
|
|||
|
|
key: 'pinyin',
|
|||
|
|
width: '30%',
|
|||
|
|
render: (t: string) => <Tag color="blue" style={{ fontFamily: 'monospace' }}>{t}</Tag>
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
title: '权重',
|
|||
|
|
dataIndex: 'weight',
|
|||
|
|
key: 'weight',
|
|||
|
|
width: '20%',
|
|||
|
|
render: (w: number) => {
|
|||
|
|
let color = 'default';
|
|||
|
|
if (w >= 5) color = 'red';
|
|||
|
|
else if (w >= 2) color = 'orange';
|
|||
|
|
else if (w >= 1) color = 'green';
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<Tooltip title={`权重: ${w}`}>
|
|||
|
|
<Tag color={color} style={{ minWidth: 40, textAlign: 'center', fontWeight: 'bold' }}>{w}x</Tag>
|
|||
|
|
</Tooltip>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
title: '操作',
|
|||
|
|
key: 'action',
|
|||
|
|
width: 180,
|
|||
|
|
render: (_: any, record: HotwordItem) => (
|
|||
|
|
<div style={{ display: 'flex', gap: 8 }}>
|
|||
|
|
<Button
|
|||
|
|
type="text"
|
|||
|
|
icon={<EditOutlined />}
|
|||
|
|
onClick={() => handleEdit(record)}
|
|||
|
|
style={{ color: '#1890ff' }}
|
|||
|
|
>
|
|||
|
|
编辑
|
|||
|
|
</Button>
|
|||
|
|
<Popconfirm
|
|||
|
|
title="确定删除该热词吗?"
|
|||
|
|
description="删除后将不再对该词进行强化识别"
|
|||
|
|
onConfirm={() => handleDelete(record.id)}
|
|||
|
|
okText="删除"
|
|||
|
|
cancelText="取消"
|
|||
|
|
okButtonProps={{ danger: true }}
|
|||
|
|
>
|
|||
|
|
<Button type="text" danger icon={<DeleteOutlined />}>
|
|||
|
|
删除
|
|||
|
|
</Button>
|
|||
|
|
</Popconfirm>
|
|||
|
|
</div>
|
|||
|
|
),
|
|||
|
|
},
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="page-wrapper" style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
|
|||
|
|
<PageTitleBar
|
|||
|
|
title="热词管理"
|
|||
|
|
description="配置语音识别热词,提升特定词汇(如人名、地名、专业术语)的识别准确率"
|
|||
|
|
badge={hotwords.length}
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
<div style={{ flex: 1, padding: '0 24px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
|
|||
|
|
<Card bodyStyle={{ padding: 0 }} bordered={false} style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
|
|||
|
|
<div style={{ padding: '16px 24px', borderBottom: '1px solid #f0f0f0' }}>
|
|||
|
|
<ListActionBar
|
|||
|
|
actions={[
|
|||
|
|
{
|
|||
|
|
key: 'add',
|
|||
|
|
label: '添加热词',
|
|||
|
|
type: 'primary',
|
|||
|
|
icon: <PlusOutlined />,
|
|||
|
|
onClick: () => setModalVisible(true),
|
|||
|
|
}
|
|||
|
|
]}
|
|||
|
|
search={{
|
|||
|
|
placeholder: "搜索热词或拼音...",
|
|||
|
|
value: searchText,
|
|||
|
|
onChange: (val: string) => setSearchText(val),
|
|||
|
|
width: 320
|
|||
|
|
}}
|
|||
|
|
showRefresh
|
|||
|
|
onRefresh={fetchHotwords}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div style={{ flex: 1, overflow: 'hidden' }}>
|
|||
|
|
<ListTable
|
|||
|
|
columns={columns}
|
|||
|
|
dataSource={filteredHotwords}
|
|||
|
|
rowKey="id"
|
|||
|
|
loading={loading}
|
|||
|
|
pagination={{ pageSize: 10, showSizeChanger: true, showQuickJumper: true }}
|
|||
|
|
scroll={{ x: 800, y: 'calc(100vh - 340px)' }}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
</Card>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<Modal
|
|||
|
|
title={
|
|||
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
|||
|
|
<div style={{ background: '#e6f7ff', padding: 8, borderRadius: '50%', color: '#1890ff', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
|||
|
|
<FireOutlined style={{ fontSize: 18 }} />
|
|||
|
|
</div>
|
|||
|
|
<span style={{ fontSize: 16 }}>{currentHotword ? '编辑热词' : '添加新热词'}</span>
|
|||
|
|
</div>
|
|||
|
|
}
|
|||
|
|
open={modalVisible}
|
|||
|
|
onCancel={handleModalClose}
|
|||
|
|
onOk={handleSubmit}
|
|||
|
|
destroyOnClose
|
|||
|
|
width={520}
|
|||
|
|
okText={currentHotword ? '保存' : '添加'}
|
|||
|
|
cancelText="取消"
|
|||
|
|
>
|
|||
|
|
<Form form={form} layout="vertical" style={{ marginTop: 24 }}>
|
|||
|
|
<Form.Item
|
|||
|
|
name="word"
|
|||
|
|
label={
|
|||
|
|
<span>
|
|||
|
|
热词文本 <Tooltip title="需要提高识别准确率的专有名词、术语等"><QuestionCircleOutlined style={{ color: '#999' }} /></Tooltip>
|
|||
|
|
</span>
|
|||
|
|
}
|
|||
|
|
rules={[{ required: true, message: '请输入热词' }]}
|
|||
|
|
>
|
|||
|
|
<Input placeholder="请输入需要强化的词汇,如:人工智能" size="large" />
|
|||
|
|
</Form.Item>
|
|||
|
|
|
|||
|
|
<Form.Item
|
|||
|
|
name="weight"
|
|||
|
|
label={
|
|||
|
|
<span>
|
|||
|
|
权重倍数 <Tooltip title="权重越高,该词被识别出的概率越大。推荐范围 2.0 - 5.0"><QuestionCircleOutlined style={{ color: '#999' }} /></Tooltip>
|
|||
|
|
</span>
|
|||
|
|
}
|
|||
|
|
initialValue={2.0}
|
|||
|
|
>
|
|||
|
|
<InputNumber
|
|||
|
|
min={1.0}
|
|||
|
|
max={10.0}
|
|||
|
|
step={0.5}
|
|||
|
|
style={{ width: '100%' }}
|
|||
|
|
size="large"
|
|||
|
|
addonAfter="倍"
|
|||
|
|
/>
|
|||
|
|
</Form.Item>
|
|||
|
|
|
|||
|
|
<div style={{ background: '#f9f9f9', padding: 16, borderRadius: 8, border: '1px solid #f0f0f0' }}>
|
|||
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8, fontWeight: 500, color: '#333' }}>
|
|||
|
|
<QuestionCircleOutlined /> 使用说明
|
|||
|
|
</div>
|
|||
|
|
<ul style={{ paddingLeft: 20, margin: 0, color: '#666', fontSize: 13, lineHeight: 1.8 }}>
|
|||
|
|
<li>热词生效需要一定时间,通常在下一次识别会话开始时生效。</li>
|
|||
|
|
<li>系统会自动生成拼音,无需手动输入。</li>
|
|||
|
|
<li>建议权重设置在 <b>2.0 - 5.0</b> 之间,过高可能导致发音相近的词被误识别。</li>
|
|||
|
|
</ul>
|
|||
|
|
</div>
|
|||
|
|
</Form>
|
|||
|
|
</Modal>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
export default Hotwords;
|