feat: 添加会议转录文件初始化和下载功能
- 在 `MeetingCommandServiceImpl` 和 `AiTaskServiceImpl` 中添加 `initializeTranscriptFileIfAbsent` 方法调用 - 在 `MeetingController` 中添加 `exportTranscripts` 接口,支持下载会议转录 Markdown 文件 - 更新前端 `meeting.ts` 和 `MeetingDetail.tsx` 以支持会议转录文件的下载 - 在相关测试类中添加对 `MeetingTranscriptFileService` 的 mockdev_na
parent
aaa2624fe2
commit
35698287de
|
|
@ -7,6 +7,7 @@ import com.imeeting.dto.biz.CreateRealtimeMeetingCommand;
|
|||
import com.imeeting.dto.biz.MeetingResummaryDTO;
|
||||
import com.imeeting.dto.biz.MeetingSpeakerUpdateDTO;
|
||||
import com.imeeting.dto.biz.MeetingSummaryExportResult;
|
||||
import com.imeeting.dto.biz.MeetingTranscriptExportResult;
|
||||
import com.imeeting.dto.biz.MeetingTranscriptVO;
|
||||
import com.imeeting.dto.biz.MeetingVO;
|
||||
import com.imeeting.dto.biz.OpenRealtimeSocketSessionCommand;
|
||||
|
|
@ -23,6 +24,7 @@ import com.imeeting.service.biz.MeetingAccessService;
|
|||
import com.imeeting.service.biz.MeetingCommandService;
|
||||
import com.imeeting.service.biz.MeetingExportService;
|
||||
import com.imeeting.service.biz.MeetingQueryService;
|
||||
import com.imeeting.service.biz.MeetingTranscriptFileService;
|
||||
import com.imeeting.service.biz.PromptTemplateService;
|
||||
import com.imeeting.service.biz.RealtimeMeetingSessionStateService;
|
||||
import com.imeeting.service.biz.RealtimeMeetingSocketSessionService;
|
||||
|
|
@ -67,6 +69,7 @@ public class MeetingController {
|
|||
private final MeetingCommandService meetingCommandService;
|
||||
private final MeetingAccessService meetingAccessService;
|
||||
private final MeetingExportService meetingExportService;
|
||||
private final MeetingTranscriptFileService meetingTranscriptFileService;
|
||||
private final PromptTemplateService promptTemplateService;
|
||||
private final RealtimeMeetingSocketSessionService realtimeMeetingSocketSessionService;
|
||||
private final RealtimeMeetingSessionStateService realtimeMeetingSessionStateService;
|
||||
|
|
@ -77,6 +80,7 @@ public class MeetingController {
|
|||
MeetingCommandService meetingCommandService,
|
||||
MeetingAccessService meetingAccessService,
|
||||
MeetingExportService meetingExportService,
|
||||
MeetingTranscriptFileService meetingTranscriptFileService,
|
||||
PromptTemplateService promptTemplateService,
|
||||
RealtimeMeetingSocketSessionService realtimeMeetingSocketSessionService,
|
||||
RealtimeMeetingSessionStateService realtimeMeetingSessionStateService,
|
||||
|
|
@ -86,6 +90,7 @@ public class MeetingController {
|
|||
this.meetingCommandService = meetingCommandService;
|
||||
this.meetingAccessService = meetingAccessService;
|
||||
this.meetingExportService = meetingExportService;
|
||||
this.meetingTranscriptFileService = meetingTranscriptFileService;
|
||||
this.promptTemplateService = promptTemplateService;
|
||||
this.realtimeMeetingSocketSessionService = realtimeMeetingSocketSessionService;
|
||||
this.realtimeMeetingSessionStateService = realtimeMeetingSessionStateService;
|
||||
|
|
@ -227,6 +232,27 @@ public class MeetingController {
|
|||
return ApiResponse.ok(meetingQueryService.getTranscripts(id));
|
||||
}
|
||||
|
||||
@Operation(summary = "下载会议转录 Markdown")
|
||||
@GetMapping("/{id}/transcripts/export")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ResponseEntity<byte[]> exportTranscripts(@PathVariable Long id) {
|
||||
LoginUser loginUser = currentLoginUser();
|
||||
Meeting meeting = meetingAccessService.requireMeeting(id);
|
||||
meetingAccessService.assertCanExportMeeting(meeting, loginUser);
|
||||
|
||||
MeetingVO meetingDetail = meetingQueryService.getDetail(id);
|
||||
if (meetingDetail == null) {
|
||||
throw new RuntimeException("会议不存在");
|
||||
}
|
||||
|
||||
MeetingTranscriptExportResult exportResult = meetingTranscriptFileService.exportTranscript(meeting, meetingDetail);
|
||||
String encodedFilename = URLEncoder.encode(exportResult.getFileName(), StandardCharsets.UTF_8).replace("+", "%20");
|
||||
return ResponseEntity.ok()
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + encodedFilename)
|
||||
.contentType(MediaType.parseMediaType(exportResult.getContentType()))
|
||||
.body(exportResult.getContent());
|
||||
}
|
||||
|
||||
@Operation(summary = "查询实时会议状态")
|
||||
@GetMapping("/{id}/realtime/session-status")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import com.imeeting.service.biz.AiModelService;
|
|||
import com.imeeting.service.biz.AiTaskService;
|
||||
import com.imeeting.service.biz.HotWordService;
|
||||
import com.imeeting.service.biz.MeetingSummaryFileService;
|
||||
import com.imeeting.service.biz.MeetingTranscriptFileService;
|
||||
|
||||
import com.unisbase.entity.SysUser;
|
||||
import com.unisbase.mapper.SysUserMapper;
|
||||
|
|
@ -58,6 +59,7 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
|||
private final HotWordService hotWordService;
|
||||
private final StringRedisTemplate redisTemplate;
|
||||
private final MeetingSummaryFileService meetingSummaryFileService;
|
||||
private final MeetingTranscriptFileService meetingTranscriptFileService;
|
||||
private final MeetingSummaryPromptAssembler meetingSummaryPromptAssembler;
|
||||
private final TaskSecurityContextRunner taskSecurityContextRunner;
|
||||
|
||||
|
|
@ -310,6 +312,7 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
|||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
JsonNode segments = resultNode.path("segments");
|
||||
int savedCount = 0;
|
||||
if (segments.isArray()) {
|
||||
int order = 0;
|
||||
for (JsonNode seg : segments) {
|
||||
|
|
@ -325,9 +328,13 @@ public class AiTaskServiceImpl extends ServiceImpl<AiTaskMapper, AiTask> impleme
|
|||
fillTranscriptTime(mt, seg);
|
||||
mt.setSortOrder(order++);
|
||||
transcriptMapper.insert(mt);
|
||||
savedCount++;
|
||||
sb.append(mt.getSpeakerName()).append(": ").append(mt.getContent()).append("\n");
|
||||
}
|
||||
}
|
||||
if (savedCount > 0) {
|
||||
meetingTranscriptFileService.initializeTranscriptFileIfAbsent(meeting.getId());
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import com.imeeting.service.biz.MeetingCommandService;
|
|||
import com.imeeting.service.biz.MeetingRuntimeProfileResolver;
|
||||
import com.imeeting.service.biz.MeetingService;
|
||||
import com.imeeting.service.biz.MeetingSummaryFileService;
|
||||
import com.imeeting.service.biz.MeetingTranscriptFileService;
|
||||
import com.imeeting.service.biz.RealtimeMeetingSessionStateService;
|
||||
import com.imeeting.service.realtime.RealtimeMeetingAudioStorageService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
|
@ -53,6 +54,7 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
|||
private final HotWordService hotWordService;
|
||||
private final com.imeeting.mapper.biz.MeetingTranscriptMapper transcriptMapper;
|
||||
private final MeetingSummaryFileService meetingSummaryFileService;
|
||||
private final MeetingTranscriptFileService meetingTranscriptFileService;
|
||||
private final MeetingDomainSupport meetingDomainSupport;
|
||||
private final MeetingRuntimeProfileResolver meetingRuntimeProfileResolver;
|
||||
private final RealtimeMeetingSessionStateService realtimeMeetingSessionStateService;
|
||||
|
|
@ -194,6 +196,7 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
|||
}
|
||||
|
||||
if (inserted) {
|
||||
meetingTranscriptFileService.initializeTranscriptFileIfAbsent(meetingId);
|
||||
realtimeMeetingSessionStateService.refreshAfterTranscript(meetingId);
|
||||
}
|
||||
}
|
||||
|
|
@ -222,6 +225,7 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
|||
.set(MeetingTranscript::getContent, content)
|
||||
.set(item.getStartTime() != null, MeetingTranscript::getStartTime, item.getStartTime())
|
||||
.set(item.getEndTime() != null, MeetingTranscript::getEndTime, item.getEndTime()));
|
||||
meetingTranscriptFileService.initializeTranscriptFileIfAbsent(meetingId);
|
||||
realtimeMeetingSessionStateService.refreshAfterTranscript(meetingId);
|
||||
return;
|
||||
}
|
||||
|
|
@ -244,6 +248,7 @@ public class MeetingCommandServiceImpl implements MeetingCommandService {
|
|||
transcript.setEndTime(item.getEndTime());
|
||||
transcript.setSortOrder(maxSortOrder == null ? 0 : maxSortOrder + 1);
|
||||
transcriptMapper.insert(transcript);
|
||||
meetingTranscriptFileService.initializeTranscriptFileIfAbsent(meetingId);
|
||||
realtimeMeetingSessionStateService.refreshAfterTranscript(meetingId);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import com.imeeting.mapper.biz.MeetingTranscriptMapper;
|
|||
import com.imeeting.service.biz.AiModelService;
|
||||
import com.imeeting.service.biz.HotWordService;
|
||||
import com.imeeting.service.biz.MeetingSummaryFileService;
|
||||
import com.imeeting.service.biz.MeetingTranscriptFileService;
|
||||
import com.imeeting.support.TaskSecurityContextRunner;
|
||||
import com.unisbase.mapper.SysUserMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
|
@ -21,9 +22,11 @@ import java.util.List;
|
|||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
|
|
@ -90,7 +93,8 @@ class AiTaskServiceImplTest {
|
|||
transcriptMapper,
|
||||
aiModelService,
|
||||
redisTemplate,
|
||||
new TaskSecurityContextRunner()
|
||||
new TaskSecurityContextRunner(),
|
||||
mock(MeetingTranscriptFileService.class)
|
||||
));
|
||||
doReturn(true).when(service).updateById(any());
|
||||
|
||||
|
|
@ -133,7 +137,8 @@ class AiTaskServiceImplTest {
|
|||
transcriptMapper,
|
||||
aiModelService,
|
||||
redisTemplate,
|
||||
new TaskSecurityContextRunner()
|
||||
new TaskSecurityContextRunner(),
|
||||
mock(MeetingTranscriptFileService.class)
|
||||
));
|
||||
doReturn(true).when(service).updateById(any());
|
||||
|
||||
|
|
@ -160,13 +165,53 @@ class AiTaskServiceImplTest {
|
|||
verify(aiModelService, never()).getModelById(anyLong(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void saveTranscriptsShouldInitializeTranscriptFileAfterFirstPersist() throws Exception {
|
||||
MeetingMapper meetingMapper = mock(MeetingMapper.class);
|
||||
MeetingTranscriptMapper transcriptMapper = mock(MeetingTranscriptMapper.class);
|
||||
MeetingTranscriptFileService transcriptFileService = mock(MeetingTranscriptFileService.class);
|
||||
AiTaskServiceImpl service = createService(
|
||||
meetingMapper,
|
||||
transcriptMapper,
|
||||
mock(AiModelService.class),
|
||||
mock(StringRedisTemplate.class),
|
||||
mock(TaskSecurityContextRunner.class),
|
||||
transcriptFileService
|
||||
);
|
||||
|
||||
Meeting meeting = new Meeting();
|
||||
meeting.setId(88L);
|
||||
|
||||
ReflectionTestUtils.invokeMethod(
|
||||
service,
|
||||
"saveTranscripts",
|
||||
meeting,
|
||||
new ObjectMapper().readTree("""
|
||||
{
|
||||
"segments": [
|
||||
{
|
||||
"speaker_id": "spk-1",
|
||||
"speaker_name": "Alice",
|
||||
"text": "第一段转录",
|
||||
"timestamp": [0, 1200]
|
||||
}
|
||||
]
|
||||
}
|
||||
""")
|
||||
);
|
||||
|
||||
verify(transcriptMapper, times(1)).insert(any(MeetingTranscript.class));
|
||||
verify(transcriptFileService, times(1)).initializeTranscriptFileIfAbsent(eq(88L));
|
||||
}
|
||||
|
||||
private AiTaskServiceImpl createService() {
|
||||
return createService(
|
||||
mock(MeetingMapper.class),
|
||||
mock(MeetingTranscriptMapper.class),
|
||||
mock(AiModelService.class),
|
||||
mock(StringRedisTemplate.class),
|
||||
mock(TaskSecurityContextRunner.class)
|
||||
mock(TaskSecurityContextRunner.class),
|
||||
mock(MeetingTranscriptFileService.class)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -174,7 +219,8 @@ class AiTaskServiceImplTest {
|
|||
MeetingTranscriptMapper transcriptMapper,
|
||||
AiModelService aiModelService,
|
||||
StringRedisTemplate redisTemplate,
|
||||
TaskSecurityContextRunner taskSecurityContextRunner) {
|
||||
TaskSecurityContextRunner taskSecurityContextRunner,
|
||||
MeetingTranscriptFileService meetingTranscriptFileService) {
|
||||
return new AiTaskServiceImpl(
|
||||
meetingMapper,
|
||||
transcriptMapper,
|
||||
|
|
@ -184,6 +230,7 @@ class AiTaskServiceImplTest {
|
|||
mock(HotWordService.class),
|
||||
redisTemplate,
|
||||
mock(MeetingSummaryFileService.class),
|
||||
meetingTranscriptFileService,
|
||||
mock(MeetingSummaryPromptAssembler.class),
|
||||
taskSecurityContextRunner
|
||||
);
|
||||
|
|
|
|||
|
|
@ -398,3 +398,11 @@ export const downloadMeetingSummary = (id: number, format: "pdf" | "word") => {
|
|||
headers: token ? { Authorization: `Bearer ${token}` } : {}
|
||||
});
|
||||
};
|
||||
|
||||
export const downloadMeetingTranscript = (id: number) => {
|
||||
const token = localStorage.getItem("accessToken");
|
||||
return axios.get(`/api/biz/meeting/${id}/transcripts/export`, {
|
||||
responseType: "blob",
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {}
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -60,6 +60,10 @@ type CachedUserProfile = { displayName?: string; username?: string; avatarUrl?:
|
|||
function getAvatarUrl(profile?: CachedUserProfile | null) {
|
||||
return profile?.avatarUrl?.trim() || "";
|
||||
}
|
||||
|
||||
function getDisplayName(profile: CachedUserProfile | null | undefined, fallbackLabel: string) {
|
||||
return profile?.displayName || profile?.username || localStorage.getItem("displayName") || localStorage.getItem("username") || fallbackLabel;
|
||||
}
|
||||
export default function AppLayout() {
|
||||
const { message } = App.useApp();
|
||||
const { t, i18n } = useTranslation();
|
||||
|
|
@ -79,19 +83,19 @@ export default function AppLayout() {
|
|||
const { load: loadPermissions, can } = usePermission();
|
||||
const { layoutMode } = useThemeStore();
|
||||
|
||||
const currentUserDisplayName = useMemo(() => {
|
||||
const [currentUserDisplayName, setCurrentUserDisplayName] = useState(() => {
|
||||
try {
|
||||
const profileStr = sessionStorage.getItem("userProfile");
|
||||
if (profileStr) {
|
||||
const profile = JSON.parse(profileStr) as CachedUserProfile;
|
||||
return profile.displayName || profile.username || localStorage.getItem("displayName") || localStorage.getItem("username") || t("layout.admin");
|
||||
return getDisplayName(profile, t("layout.admin"));
|
||||
}
|
||||
} catch {
|
||||
// Ignore invalid cached profile and continue with storage fallback.
|
||||
}
|
||||
|
||||
return localStorage.getItem("displayName") || localStorage.getItem("username") || t("layout.admin");
|
||||
}, [t]);
|
||||
return getDisplayName(null, t("layout.admin"));
|
||||
});
|
||||
const [currentUserAvatarUrl, setCurrentUserAvatarUrl] = useState<string>(() => {
|
||||
try {
|
||||
const profileStr = sessionStorage.getItem("userProfile");
|
||||
|
|
@ -131,16 +135,26 @@ export default function AppLayout() {
|
|||
const syncUserProfile = () => {
|
||||
try {
|
||||
const profileStr = sessionStorage.getItem("userProfile");
|
||||
if (!profileStr) return;
|
||||
if (!profileStr) {
|
||||
setCurrentUserDisplayName(getDisplayName(null, t("layout.admin")));
|
||||
setCurrentUserAvatarUrl("");
|
||||
return;
|
||||
}
|
||||
const profile = JSON.parse(profileStr) as CachedUserProfile;
|
||||
localStorage.setItem("displayName", profile.displayName || profile.username || "");
|
||||
localStorage.setItem("username", profile.username || localStorage.getItem("username") || "");
|
||||
setCurrentUserDisplayName(getDisplayName(profile, t("layout.admin")));
|
||||
setCurrentUserAvatarUrl(getAvatarUrl(profile));
|
||||
} catch {
|
||||
setCurrentUserDisplayName(getDisplayName(null, t("layout.admin")));
|
||||
setCurrentUserAvatarUrl("");
|
||||
}
|
||||
};
|
||||
|
||||
syncUserProfile();
|
||||
window.addEventListener("user-profile-updated", syncUserProfile);
|
||||
return () => window.removeEventListener("user-profile-updated", syncUserProfile);
|
||||
}, []);
|
||||
}, [t]);
|
||||
useEffect(() => {
|
||||
const syncPlatformConfig = () => {
|
||||
const configStr = sessionStorage.getItem("platformConfig");
|
||||
|
|
@ -165,6 +179,7 @@ export default function AppLayout() {
|
|||
sessionStorage.setItem("userProfile", JSON.stringify(profile));
|
||||
localStorage.setItem("displayName", profile.displayName || profile.username || "");
|
||||
localStorage.setItem("username", profile.username || localStorage.getItem("username") || "");
|
||||
window.dispatchEvent(new Event("user-profile-updated"));
|
||||
|
||||
message.success(t("common.success"));
|
||||
window.location.reload();
|
||||
|
|
|
|||
|
|
@ -86,8 +86,11 @@ export default function Login() {
|
|||
try {
|
||||
const profile = await getCurrentUser();
|
||||
sessionStorage.setItem("userProfile", JSON.stringify(profile));
|
||||
localStorage.setItem("displayName", profile.displayName || profile.username || "");
|
||||
localStorage.setItem("username", profile.username || values.username);
|
||||
} catch {
|
||||
sessionStorage.removeItem("userProfile");
|
||||
localStorage.removeItem("displayName");
|
||||
}
|
||||
|
||||
message.success(t("common.success"));
|
||||
|
|
|
|||
|
|
@ -596,6 +596,10 @@ const HotWords: React.FC = () => {
|
|||
<Input placeholder="输入识别关键词" onBlur={handleWordBlur} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item name="pinyin" hidden>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item name="category" label="热词分类">
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import {
|
|||
import dayjs from 'dayjs';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import {
|
||||
downloadMeetingTranscript,
|
||||
downloadMeetingSummary,
|
||||
getMeetingDetail,
|
||||
getMeetingProgress,
|
||||
|
|
@ -651,7 +652,7 @@ const MeetingDetail: React.FC = () => {
|
|||
const [editVisible, setEditVisible] = useState(false);
|
||||
const [summaryVisible, setSummaryVisible] = useState(false);
|
||||
const [actionLoading, setActionLoading] = useState(false);
|
||||
const [downloadLoading, setDownloadLoading] = useState<'pdf' | 'word' | null>(null);
|
||||
const [downloadLoading, setDownloadLoading] = useState<'pdf' | 'word' | 'transcript' | null>(null);
|
||||
const [isEditingSummary, setIsEditingSummary] = useState(false);
|
||||
const [summaryDraft, setSummaryDraft] = useState('');
|
||||
const [summaryTab, setSummaryTab] = useState<'chapters' | 'speakers' | 'actions' | 'todos'>('chapters');
|
||||
|
|
@ -1236,6 +1237,49 @@ const MeetingDetail: React.FC = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const handleDownloadTranscript = async () => {
|
||||
if (!meeting) return;
|
||||
if (transcripts.length === 0) {
|
||||
message.warning('当前暂无可下载的会议转录');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setDownloadLoading('transcript');
|
||||
const res = await downloadMeetingTranscript(meeting.id);
|
||||
const contentType = res.headers['content-type'] || 'text/markdown; charset=UTF-8';
|
||||
|
||||
if (contentType.includes('application/json')) {
|
||||
const text = await (res.data as Blob).text();
|
||||
try {
|
||||
const json = JSON.parse(text);
|
||||
message.error(json?.msg || '下载失败');
|
||||
} catch {
|
||||
message.error('下载失败');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const blob = new Blob([res.data], { type: contentType });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const anchor = document.createElement('a');
|
||||
anchor.href = url;
|
||||
anchor.download = getFileNameFromDisposition(
|
||||
res.headers['content-disposition'],
|
||||
`${sanitizeDownloadFileName(meeting.title, 'meeting-transcript')}-Transcript.md`,
|
||||
);
|
||||
document.body.appendChild(anchor);
|
||||
anchor.click();
|
||||
anchor.remove();
|
||||
window.URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
message.error('转录下载失败');
|
||||
} finally {
|
||||
setDownloadLoading(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownloadAudio = () => {
|
||||
if (!playbackAudioUrl) {
|
||||
message.warning('当前暂无可下载的会议录音');
|
||||
|
|
@ -1460,7 +1504,7 @@ const MeetingDetail: React.FC = () => {
|
|||
正在总结
|
||||
</Button>
|
||||
)}
|
||||
{(playbackAudioUrl || (meeting.status === 3 && !!meeting.summaryContent)) && (
|
||||
{(playbackAudioUrl || transcripts.length > 0 || (meeting.status === 3 && !!meeting.summaryContent)) && (
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
|
|
@ -1470,7 +1514,19 @@ const MeetingDetail: React.FC = () => {
|
|||
icon: <AudioOutlined />,
|
||||
onClick: handleDownloadAudio,
|
||||
}] : []),
|
||||
...(transcripts.length > 0 && !(meeting.status === 3 && !!meeting.summaryContent) ? [{
|
||||
key: 'transcript',
|
||||
label: '下载转录 MD',
|
||||
icon: <DownloadOutlined />,
|
||||
onClick: handleDownloadTranscript,
|
||||
disabled: downloadLoading === 'transcript'
|
||||
}] : []),
|
||||
...(meeting.status === 3 && !!meeting.summaryContent ? [
|
||||
{
|
||||
key: 'transcript',
|
||||
label: '下载转录 MD',
|
||||
icon: <DownloadOutlined />,
|
||||
},
|
||||
{
|
||||
key: 'pdf',
|
||||
label: '下载 PDF',
|
||||
|
|
|
|||
|
|
@ -34,6 +34,8 @@ export default function Profile() {
|
|||
const data = await getCurrentUser();
|
||||
setUser(data);
|
||||
sessionStorage.setItem("userProfile", JSON.stringify(data));
|
||||
localStorage.setItem("displayName", data.displayName || data.username || "");
|
||||
localStorage.setItem("username", data.username || localStorage.getItem("username") || "");
|
||||
window.dispatchEvent(new Event("user-profile-updated"));
|
||||
profileForm.setFieldsValue(data);
|
||||
} finally {
|
||||
|
|
|
|||
Loading…
Reference in New Issue