feat:导出pdf优化

dev_na
chenhao 2026-03-06 13:45:56 +08:00
parent 430459c331
commit c60db64578
4 changed files with 156 additions and 67 deletions

View File

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

View File

@ -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 ? (
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 ? (

View File

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

View File

@ -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,8 +164,8 @@ 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>
<Tabs defaultActiveKey="record">
<Tabs.TabPane tab="在线录制" key="record">
<div style={{
padding: '24px',
backgroundColor: '#fafafa',
@ -204,12 +219,24 @@ const SpeakerReg: React.FC = () => {
</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>