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;
|