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-text" style={{ fontSize: 14 }}>点击或拖拽录音文件</p>
|
||||
{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>
|
||||
</Card>
|
||||
|
||||
|
|
|
|||
|
|
@ -227,6 +227,9 @@ const MeetingDetail: React.FC = () => {
|
|||
const [actionLoading, setActionLoading] = useState(false);
|
||||
const [downloadLoading, setDownloadLoading] = useState<'pdf' | 'word' | null>(null);
|
||||
|
||||
const [isEditingSummary, setIsEditingSummary] = useState(false);
|
||||
const [summaryDraft, setSummaryDraft] = useState('');
|
||||
|
||||
const [llmModels, setLlmModels] = useState<AiModelVO[]>([]);
|
||||
const [prompts, setPrompts] = useState<PromptTemplateVO[]>([]);
|
||||
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 vals = await summaryForm.validateFields();
|
||||
setActionLoading(true);
|
||||
|
|
@ -527,12 +547,37 @@ const MeetingDetail: React.FC = () => {
|
|||
<Col span={12} style={{ height: '100%' }}>
|
||||
<Card
|
||||
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' }}
|
||||
bodyStyle={{ flex: 1, overflowY: 'auto', padding: '24px', minHeight: 0 }}
|
||||
>
|
||||
<div ref={summaryPdfRef}>
|
||||
<div ref={summaryPdfRef} style={{ height: '100%' }}>
|
||||
{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' }}>
|
||||
{meeting.status === 2 ? (
|
||||
|
|
|
|||
|
|
@ -190,12 +190,12 @@ const MeetingCreateForm: React.FC<{
|
|||
marginTop: 20,
|
||||
padding: '4px 12px',
|
||||
fontSize: 13,
|
||||
maxWidth: '90%',
|
||||
maxWidth: '500px',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap'
|
||||
}}
|
||||
size="large"
|
||||
// size="large"
|
||||
>
|
||||
就绪: {audioUrl.split('/').pop()}
|
||||
</Tag>
|
||||
|
|
@ -299,7 +299,8 @@ const MeetingCardItem: React.FC<{ item: MeetingVO, config: any, fetchData: () =>
|
|||
width: '100%', // 占满 Space 容器
|
||||
overflow: 'hidden',
|
||||
boxSizing: 'border-box',
|
||||
minWidth: 0 // 关键:允许 flex 子项收缩
|
||||
minWidth: 0 ,// 关键:允许 flex 子项收缩
|
||||
maxWidth: 250
|
||||
}}>
|
||||
<InfoCircleOutlined style={{ marginRight: 6, flexShrink: 0 }} />
|
||||
<Text
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
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 {
|
||||
AudioOutlined, StopOutlined, CloudUploadOutlined,
|
||||
DeleteOutlined, CheckCircleOutlined, InfoCircleOutlined,
|
||||
ClockCircleOutlined, FormOutlined
|
||||
FormOutlined, UploadOutlined
|
||||
} from '@ant-design/icons';
|
||||
import type { UploadProps } from 'antd';
|
||||
import dayjs from 'dayjs';
|
||||
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 () => {
|
||||
if (!audioBlob) {
|
||||
message.warning('请先录制声纹文件');
|
||||
message.warning('请先录制或上传声纹文件');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -149,67 +164,79 @@ const SpeakerReg: React.FC = () => {
|
|||
{/* 左侧:采集与录音 */}
|
||||
<Col span={15}>
|
||||
<Card bordered={false} style={{ borderRadius: 16, boxShadow: '0 4px 12px rgba(0,0,0,0.03)' }}>
|
||||
<Divider orientation="left" style={{ marginTop: 0 }}><FormOutlined /> 录音指导文本</Divider>
|
||||
|
||||
<div style={{
|
||||
padding: '24px',
|
||||
backgroundColor: '#fafafa',
|
||||
borderRadius: 12,
|
||||
border: '1px solid #f0f0f0',
|
||||
marginBottom: 24,
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
<Paragraph style={{ fontSize: 18, color: recording ? '#1890ff' : '#262626', fontWeight: 500, lineHeight: 1.8 }}>
|
||||
“{REG_CONTENT}”
|
||||
</Paragraph>
|
||||
<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" />
|
||||
<Tabs defaultActiveKey="record">
|
||||
<Tabs.TabPane tab="在线录制" key="record">
|
||||
<div style={{
|
||||
padding: '24px',
|
||||
backgroundColor: '#fafafa',
|
||||
borderRadius: 12,
|
||||
border: '1px solid #f0f0f0',
|
||||
marginBottom: 24,
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
<Paragraph style={{ fontSize: 18, color: recording ? '#1890ff' : '#262626', fontWeight: 500, lineHeight: 1.8 }}>
|
||||
“{REG_CONTENT}”
|
||||
</Paragraph>
|
||||
<Text type="secondary" size="small">请在点击录音后,自然、清晰地朗读以上内容</Text>
|
||||
</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 && (
|
||||
<div style={{ backgroundColor: '#f0f5ff', padding: '16px', borderRadius: 12, marginBottom: 24, border: '1px solid #adc6ff' }}>
|
||||
<div style={{ marginBottom: 8, display: 'flex', justifyContent: 'space-between' }}>
|
||||
<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>
|
||||
<audio src={audioUrl} controls style={{ width: '100%', height: 32 }} />
|
||||
</div>
|
||||
|
|
@ -259,9 +286,9 @@ const SpeakerReg: React.FC = () => {
|
|||
<InfoCircleOutlined style={{ color: '#faad14', marginTop: 4 }} />
|
||||
<div style={{ fontSize: 12, color: '#856404' }}>
|
||||
<b>注册技巧:</b><br/>
|
||||
1. 尽可能在安静的环境下录音。<br/>
|
||||
2. 使用平时的语速和语调朗读文字。<br/>
|
||||
3. 倒计时结束后系统将自动停止。
|
||||
1. 尽可能在安静的环境下录音或上传清晰的音频。<br/>
|
||||
2. 使用平时的语速和语调。<br/>
|
||||
3. 在线录音倒计时结束后系统将自动停止。
|
||||
</div>
|
||||
</Space>
|
||||
</Card>
|
||||
|
|
|
|||
Loading…
Reference in New Issue