feat:导出pdf优化
parent
430459c331
commit
c60db64578
|
|
@ -150,7 +150,23 @@ const MeetingCreate: React.FC = () => {
|
||||||
<p className="ant-upload-drag-icon" style={{ marginBottom: 4 }}><CloudUploadOutlined style={{ fontSize: 32 }} /></p>
|
<p className="ant-upload-drag-icon" style={{ marginBottom: 4 }}><CloudUploadOutlined style={{ fontSize: 32 }} /></p>
|
||||||
<p className="ant-upload-text" style={{ fontSize: 14 }}>点击或拖拽录音文件</p>
|
<p className="ant-upload-text" style={{ fontSize: 14 }}>点击或拖拽录音文件</p>
|
||||||
{uploadProgress > 0 && uploadProgress < 100 && <Progress percent={uploadProgress} size="small" style={{ width: '60%', margin: '0 auto' }} />}
|
{uploadProgress > 0 && uploadProgress < 100 && <Progress percent={uploadProgress} size="small" style={{ width: '60%', margin: '0 auto' }} />}
|
||||||
{audioUrl && <Tag color="success" style={{ marginTop: 4 }} size="small">就绪: {audioUrl.split('/').pop()}</Tag>}
|
{audioUrl && (
|
||||||
|
<div style={{ marginTop: 8 }}>
|
||||||
|
<Tag
|
||||||
|
color="success"
|
||||||
|
style={{
|
||||||
|
maxWidth: '90%',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
display: 'inline-block',
|
||||||
|
verticalAlign: 'middle'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
就绪: {audioUrl.split('/').pop()}
|
||||||
|
</Tag>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</Dragger>
|
</Dragger>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -227,6 +227,9 @@ const MeetingDetail: React.FC = () => {
|
||||||
const [actionLoading, setActionLoading] = useState(false);
|
const [actionLoading, setActionLoading] = useState(false);
|
||||||
const [downloadLoading, setDownloadLoading] = useState<'pdf' | 'word' | null>(null);
|
const [downloadLoading, setDownloadLoading] = useState<'pdf' | 'word' | null>(null);
|
||||||
|
|
||||||
|
const [isEditingSummary, setIsEditingSummary] = useState(false);
|
||||||
|
const [summaryDraft, setSummaryDraft] = useState('');
|
||||||
|
|
||||||
const [llmModels, setLlmModels] = useState<AiModelVO[]>([]);
|
const [llmModels, setLlmModels] = useState<AiModelVO[]>([]);
|
||||||
const [prompts, setPrompts] = useState<PromptTemplateVO[]>([]);
|
const [prompts, setPrompts] = useState<PromptTemplateVO[]>([]);
|
||||||
const [, setUserList] = useState<SysUser[]>([]);
|
const [, setUserList] = useState<SysUser[]>([]);
|
||||||
|
|
@ -317,6 +320,23 @@ const MeetingDetail: React.FC = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSaveSummary = async () => {
|
||||||
|
setActionLoading(true);
|
||||||
|
try {
|
||||||
|
await updateMeeting({
|
||||||
|
id: meeting?.id,
|
||||||
|
summaryContent: summaryDraft,
|
||||||
|
});
|
||||||
|
message.success('总结内容已更新');
|
||||||
|
setIsEditingSummary(false);
|
||||||
|
fetchData(Number(id));
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
} finally {
|
||||||
|
setActionLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleReSummary = async () => {
|
const handleReSummary = async () => {
|
||||||
const vals = await summaryForm.validateFields();
|
const vals = await summaryForm.validateFields();
|
||||||
setActionLoading(true);
|
setActionLoading(true);
|
||||||
|
|
@ -527,12 +547,37 @@ const MeetingDetail: React.FC = () => {
|
||||||
<Col span={12} style={{ height: '100%' }}>
|
<Col span={12} style={{ height: '100%' }}>
|
||||||
<Card
|
<Card
|
||||||
title={<span><RobotOutlined /> AI 总结</span>}
|
title={<span><RobotOutlined /> AI 总结</span>}
|
||||||
|
extra={
|
||||||
|
meeting.summaryContent && isOwner && (
|
||||||
|
<Space>
|
||||||
|
{isEditingSummary ? (
|
||||||
|
<>
|
||||||
|
<Button size="small" onClick={() => setIsEditingSummary(false)}>取消</Button>
|
||||||
|
<Button size="small" type="primary" onClick={handleSaveSummary} loading={actionLoading}>保存</Button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Button size="small" type="link" icon={<EditOutlined />} onClick={() => {
|
||||||
|
setSummaryDraft(meeting.summaryContent || '');
|
||||||
|
setIsEditingSummary(true);
|
||||||
|
}}>编辑</Button>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
)
|
||||||
|
}
|
||||||
style={{ height: '100%', display: 'flex', flexDirection: 'column' }}
|
style={{ height: '100%', display: 'flex', flexDirection: 'column' }}
|
||||||
bodyStyle={{ flex: 1, overflowY: 'auto', padding: '24px', minHeight: 0 }}
|
bodyStyle={{ flex: 1, overflowY: 'auto', padding: '24px', minHeight: 0 }}
|
||||||
>
|
>
|
||||||
<div ref={summaryPdfRef}>
|
<div ref={summaryPdfRef} style={{ height: '100%' }}>
|
||||||
{meeting.summaryContent ? (
|
{meeting.summaryContent ? (
|
||||||
<div className="markdown-body"><ReactMarkdown>{meeting.summaryContent}</ReactMarkdown></div>
|
isEditingSummary ? (
|
||||||
|
<Input.TextArea
|
||||||
|
value={summaryDraft}
|
||||||
|
onChange={(e) => setSummaryDraft(e.target.value)}
|
||||||
|
style={{ height: '100%', resize: 'none' }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="markdown-body"><ReactMarkdown>{meeting.summaryContent}</ReactMarkdown></div>
|
||||||
|
)
|
||||||
) : (
|
) : (
|
||||||
<div style={{ textAlign: 'center', marginTop: '100px' }}>
|
<div style={{ textAlign: 'center', marginTop: '100px' }}>
|
||||||
{meeting.status === 2 ? (
|
{meeting.status === 2 ? (
|
||||||
|
|
|
||||||
|
|
@ -190,12 +190,12 @@ const MeetingCreateForm: React.FC<{
|
||||||
marginTop: 20,
|
marginTop: 20,
|
||||||
padding: '4px 12px',
|
padding: '4px 12px',
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
maxWidth: '90%',
|
maxWidth: '500px',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
textOverflow: 'ellipsis',
|
textOverflow: 'ellipsis',
|
||||||
whiteSpace: 'nowrap'
|
whiteSpace: 'nowrap'
|
||||||
}}
|
}}
|
||||||
size="large"
|
// size="large"
|
||||||
>
|
>
|
||||||
就绪: {audioUrl.split('/').pop()}
|
就绪: {audioUrl.split('/').pop()}
|
||||||
</Tag>
|
</Tag>
|
||||||
|
|
@ -299,7 +299,8 @@ const MeetingCardItem: React.FC<{ item: MeetingVO, config: any, fetchData: () =>
|
||||||
width: '100%', // 占满 Space 容器
|
width: '100%', // 占满 Space 容器
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
boxSizing: 'border-box',
|
boxSizing: 'border-box',
|
||||||
minWidth: 0 // 关键:允许 flex 子项收缩
|
minWidth: 0 ,// 关键:允许 flex 子项收缩
|
||||||
|
maxWidth: 250
|
||||||
}}>
|
}}>
|
||||||
<InfoCircleOutlined style={{ marginRight: 6, flexShrink: 0 }} />
|
<InfoCircleOutlined style={{ marginRight: 6, flexShrink: 0 }} />
|
||||||
<Text
|
<Text
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import React, { useState, useRef, useEffect, useMemo } from 'react';
|
import React, { useState, useRef, useEffect, useMemo } from 'react';
|
||||||
import { Card, Button, Space, message, Typography, Divider, Tag, Progress, Row, Col, Empty, Badge } from 'antd';
|
import { Card, Button, Space, message, Typography, Divider, Tag, Progress, Row, Col, Empty, Badge, Upload, Tabs } from 'antd';
|
||||||
import {
|
import {
|
||||||
AudioOutlined, StopOutlined, CloudUploadOutlined,
|
AudioOutlined, StopOutlined, CloudUploadOutlined,
|
||||||
DeleteOutlined, CheckCircleOutlined, InfoCircleOutlined,
|
DeleteOutlined, CheckCircleOutlined, InfoCircleOutlined,
|
||||||
ClockCircleOutlined, FormOutlined
|
FormOutlined, UploadOutlined
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
|
import type { UploadProps } from 'antd';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { registerSpeaker, getSpeakerList, SpeakerVO } from '../../api/business/speaker';
|
import { registerSpeaker, getSpeakerList, SpeakerVO } from '../../api/business/speaker';
|
||||||
|
|
||||||
|
|
@ -116,9 +117,23 @@ const SpeakerReg: React.FC = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const uploadProps: UploadProps = {
|
||||||
|
beforeUpload: (file) => {
|
||||||
|
const isAudio = file.type.startsWith('audio/');
|
||||||
|
if (!isAudio) {
|
||||||
|
message.error('只能上传音频文件!');
|
||||||
|
return Upload.LIST_IGNORE;
|
||||||
|
}
|
||||||
|
setAudioBlob(file);
|
||||||
|
setAudioUrl(URL.createObjectURL(file));
|
||||||
|
return false; // Prevent auto upload
|
||||||
|
},
|
||||||
|
showUploadList: false,
|
||||||
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!audioBlob) {
|
if (!audioBlob) {
|
||||||
message.warning('请先录制声纹文件');
|
message.warning('请先录制或上传声纹文件');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -149,67 +164,79 @@ const SpeakerReg: React.FC = () => {
|
||||||
{/* 左侧:采集与录音 */}
|
{/* 左侧:采集与录音 */}
|
||||||
<Col span={15}>
|
<Col span={15}>
|
||||||
<Card bordered={false} style={{ borderRadius: 16, boxShadow: '0 4px 12px rgba(0,0,0,0.03)' }}>
|
<Card bordered={false} style={{ borderRadius: 16, boxShadow: '0 4px 12px rgba(0,0,0,0.03)' }}>
|
||||||
<Divider orientation="left" style={{ marginTop: 0 }}><FormOutlined /> 录音指导文本</Divider>
|
<Tabs defaultActiveKey="record">
|
||||||
|
<Tabs.TabPane tab="在线录制" key="record">
|
||||||
<div style={{
|
<div style={{
|
||||||
padding: '24px',
|
padding: '24px',
|
||||||
backgroundColor: '#fafafa',
|
backgroundColor: '#fafafa',
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
border: '1px solid #f0f0f0',
|
border: '1px solid #f0f0f0',
|
||||||
marginBottom: 24,
|
marginBottom: 24,
|
||||||
textAlign: 'center'
|
textAlign: 'center'
|
||||||
}}>
|
}}>
|
||||||
<Paragraph style={{ fontSize: 18, color: recording ? '#1890ff' : '#262626', fontWeight: 500, lineHeight: 1.8 }}>
|
<Paragraph style={{ fontSize: 18, color: recording ? '#1890ff' : '#262626', fontWeight: 500, lineHeight: 1.8 }}>
|
||||||
“{REG_CONTENT}”
|
“{REG_CONTENT}”
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
<Text type="secondary" size="small">请在点击录音后,自然、清晰地朗读以上内容</Text>
|
<Text type="secondary" size="small">请在点击录音后,自然、清晰地朗读以上内容</Text>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ textAlign: 'center', margin: '32px 0' }}>
|
|
||||||
<div style={{ position: 'relative', display: 'inline-block', marginBottom: 16 }}>
|
|
||||||
{!recording ? (
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
danger
|
|
||||||
shape="circle"
|
|
||||||
style={{ width: 80, height: 80, boxShadow: '0 4px 14px rgba(255, 77, 79, 0.3)' }}
|
|
||||||
icon={<AudioOutlined style={{ fontSize: 32 }} />}
|
|
||||||
onClick={startRecording}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
shape="circle"
|
|
||||||
style={{ width: 80, height: 80, boxShadow: '0 4px 14px rgba(24, 144, 255, 0.3)' }}
|
|
||||||
icon={<StopOutlined style={{ fontSize: 32 }} />}
|
|
||||||
onClick={stopRecording}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{recording && (
|
|
||||||
<div style={{ position: 'absolute', top: -10, right: -10 }}>
|
|
||||||
<Badge count={`${DEFAULT_DURATION - seconds}s`} color="#ff4d4f" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Text strong type={recording ? "danger" : "secondary"}>
|
|
||||||
{recording ? `录制中... ${seconds}/${DEFAULT_DURATION}s` : "点击红色图标开始录音"}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
{recording && (
|
|
||||||
<div style={{ maxWidth: 300, margin: '16px auto 0' }}>
|
|
||||||
<Progress percent={(seconds / DEFAULT_DURATION) * 100} showInfo={false} strokeColor="#ff4d4f" />
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
<div style={{ textAlign: 'center', margin: '32px 0' }}>
|
||||||
|
<div style={{ position: 'relative', display: 'inline-block', marginBottom: 16 }}>
|
||||||
|
{!recording ? (
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
danger
|
||||||
|
shape="circle"
|
||||||
|
style={{ width: 80, height: 80, boxShadow: '0 4px 14px rgba(255, 77, 79, 0.3)' }}
|
||||||
|
icon={<AudioOutlined style={{ fontSize: 32 }} />}
|
||||||
|
onClick={startRecording}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
shape="circle"
|
||||||
|
style={{ width: 80, height: 80, boxShadow: '0 4px 14px rgba(24, 144, 255, 0.3)' }}
|
||||||
|
icon={<StopOutlined style={{ fontSize: 32 }} />}
|
||||||
|
onClick={stopRecording}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{recording && (
|
||||||
|
<div style={{ position: 'absolute', top: -10, right: -10 }}>
|
||||||
|
<Badge count={`${DEFAULT_DURATION - seconds}s`} color="#ff4d4f" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Text strong type={recording ? "danger" : "secondary"}>
|
||||||
|
{recording ? `录制中... ${seconds}/${DEFAULT_DURATION}s` : "点击红色图标开始录音"}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
{recording && (
|
||||||
|
<div style={{ maxWidth: 300, margin: '16px auto 0' }}>
|
||||||
|
<Progress percent={(seconds / DEFAULT_DURATION) * 100} showInfo={false} strokeColor="#ff4d4f" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Tabs.TabPane>
|
||||||
|
<Tabs.TabPane tab="本地上传" key="upload">
|
||||||
|
<div style={{ textAlign: 'center', padding: '40px 0', border: '1px dashed #d9d9d9', borderRadius: '8px', marginBottom: 24, backgroundColor: '#fafafa' }}>
|
||||||
|
<Upload {...uploadProps} accept="audio/*">
|
||||||
|
<Button icon={<UploadOutlined />} size="large">选择音频文件</Button>
|
||||||
|
</Upload>
|
||||||
|
<div style={{ marginTop: 16 }}>
|
||||||
|
<Text type="secondary">支持上传 mp3, wav, m4a 等常见音频格式</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tabs.TabPane>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
{audioUrl && (
|
{audioUrl && (
|
||||||
<div style={{ backgroundColor: '#f0f5ff', padding: '16px', borderRadius: 12, marginBottom: 24, border: '1px solid #adc6ff' }}>
|
<div style={{ backgroundColor: '#f0f5ff', padding: '16px', borderRadius: 12, marginBottom: 24, border: '1px solid #adc6ff' }}>
|
||||||
<div style={{ marginBottom: 8, display: 'flex', justifyContent: 'space-between' }}>
|
<div style={{ marginBottom: 8, display: 'flex', justifyContent: 'space-between' }}>
|
||||||
<Text strong>采集完成,请试听:</Text>
|
<Text strong>采集完成,请试听:</Text>
|
||||||
<Button type="link" danger size="small" icon={<DeleteOutlined />} onClick={() => { setAudioBlob(null); setAudioUrl(null); }}>重新录制</Button>
|
<Button type="link" danger size="small" icon={<DeleteOutlined />} onClick={() => { setAudioBlob(null); setAudioUrl(null); }}>清除文件</Button>
|
||||||
</div>
|
</div>
|
||||||
<audio src={audioUrl} controls style={{ width: '100%', height: 32 }} />
|
<audio src={audioUrl} controls style={{ width: '100%', height: 32 }} />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -259,9 +286,9 @@ const SpeakerReg: React.FC = () => {
|
||||||
<InfoCircleOutlined style={{ color: '#faad14', marginTop: 4 }} />
|
<InfoCircleOutlined style={{ color: '#faad14', marginTop: 4 }} />
|
||||||
<div style={{ fontSize: 12, color: '#856404' }}>
|
<div style={{ fontSize: 12, color: '#856404' }}>
|
||||||
<b>注册技巧:</b><br/>
|
<b>注册技巧:</b><br/>
|
||||||
1. 尽可能在安静的环境下录音。<br/>
|
1. 尽可能在安静的环境下录音或上传清晰的音频。<br/>
|
||||||
2. 使用平时的语速和语调朗读文字。<br/>
|
2. 使用平时的语速和语调。<br/>
|
||||||
3. 倒计时结束后系统将自动停止。
|
3. 在线录音倒计时结束后系统将自动停止。
|
||||||
</div>
|
</div>
|
||||||
</Space>
|
</Space>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue