import React, { useState, useEffect, useRef } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { Card, Row, Col, Typography, Tag, Space, Divider, Button, Skeleton, Empty, List, Avatar, Breadcrumb, Popover, Input, Select, message, Drawer, Form, Modal } from 'antd';
import { LeftOutlined, UserOutlined, ClockCircleOutlined, AudioOutlined, RobotOutlined, LoadingOutlined, EditOutlined, SyncOutlined, SettingOutlined } from '@ant-design/icons';
import ReactMarkdown from 'react-markdown';
import dayjs from 'dayjs';
import { getMeetingDetail, getTranscripts, updateSpeakerInfo, reSummary, updateMeeting, MeetingVO, MeetingTranscriptVO } from '../../api/business/meeting';
import { getAiModelPage, getAiModelDefault, AiModelVO } from '../../api/business/aimodel';
import { getPromptPage, PromptTemplateVO } from '../../api/business/prompt';
import { useDict } from '../../hooks/useDict';
import { listUsers } from '../../api';
import { SysUser } from '../../types';
const { Title, Text } = Typography;
const { Option } = Select;
const SpeakerEditor: React.FC<{
meetingId: number;
speakerId: string;
initialName: string;
initialLabel: string;
onSuccess: () => void;
}> = ({ meetingId, speakerId, initialName, initialLabel, onSuccess }) => {
const [name, setName] = useState(initialName || speakerId);
const [label, setLabel] = useState(initialLabel);
const [loading, setLoading] = useState(false);
const { items: speakerLabels } = useDict('biz_speaker_label');
const handleSave = async (e: React.MouseEvent) => {
e.stopPropagation();
setLoading(true);
try {
await updateSpeakerInfo({ meetingId, speakerId, newName: name, label });
message.success('发言人信息已全局更新');
onSuccess();
} catch (err) {
console.error(err);
} finally {
setLoading(false);
}
};
return (
e.stopPropagation()}>
发言人姓名
setName(e.target.value)} placeholder="输入姓名" size="small" style={{ marginTop: 4 }} />
角色标签
);
};
const MeetingDetail: React.FC = () => {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const [form] = Form.useForm();
const [summaryForm] = Form.useForm();
const [meeting, setMeeting] = useState(null);
const [transcripts, setTranscripts] = useState([]);
const [loading, setLoading] = useState(true);
const [editVisible, setEditVisible] = useState(false);
const [summaryVisible, setSummaryVisible] = useState(false);
const [actionLoading, setActionLoading] = useState(false);
const [llmModels, setLlmModels] = useState([]);
const [prompts, setPrompts] = useState([]);
const [userList, setUserList] = useState([]);
const { items: speakerLabels } = useDict('biz_speaker_label');
const audioRef = useRef(null);
// 核心权限判断
const isOwner = React.useMemo(() => {
if (!meeting) return false;
const profileStr = sessionStorage.getItem("userProfile");
if (profileStr) {
const profile = JSON.parse(profileStr);
return profile.isPlatformAdmin === true || profile.userId === meeting.creatorId;
}
return false;
}, [meeting]);
useEffect(() => {
if (id) {
fetchData(Number(id));
loadAiConfigs();
loadUsers();
}
}, [id]);
const fetchData = async (meetingId: number) => {
try {
const [detailRes, transcriptRes] = await Promise.all([
getMeetingDetail(meetingId),
getTranscripts(meetingId)
]);
setMeeting(detailRes.data.data);
setTranscripts(transcriptRes.data.data || []);
} catch (err) {
console.error(err);
} finally {
setLoading(false);
}
};
const loadAiConfigs = async () => {
try {
const [mRes, pRes, dRes] = await Promise.all([
getAiModelPage({ current: 1, size: 100, type: 'LLM' }),
getPromptPage({ current: 1, size: 100 }),
getAiModelDefault('LLM')
]);
setLlmModels(mRes.data.data.records.filter(m => m.status === 1));
setPrompts(pRes.data.data.records.filter(p => p.status === 1));
summaryForm.setFieldsValue({ summaryModelId: dRes.data.data?.id });
} catch (e) {}
};
const loadUsers = async () => {
try {
const users = await listUsers();
setUserList(users || []);
} catch (err) {}
};
const handleEditMeeting = () => {
if (!meeting || !isOwner) return;
// 由于后端存储的是姓名字符串,而我们现在需要 ID 匹配,
// 这里简单处理:让发起人依然可以修改基础元数据。
// 如果需要修改参会人 ID,需要前端存储 ID 列表快照。
form.setFieldsValue({
...meeting,
tags: meeting.tags?.split(',').filter(Boolean)
});
setEditVisible(true);
};
const handleUpdateBasic = async () => {
const vals = await form.validateFields();
setActionLoading(true);
try {
await updateMeeting({
...vals,
id: meeting?.id,
tags: vals.tags?.join(',')
});
message.success('会议信息已更新');
setEditVisible(false);
fetchData(Number(id));
} catch (err) {
console.error(err);
} finally {
setActionLoading(false);
}
};
const handleReSummary = async () => {
const vals = await summaryForm.validateFields();
setActionLoading(true);
try {
await reSummary({
meetingId: Number(id),
summaryModelId: vals.summaryModelId,
promptId: vals.promptId
});
message.success('已重新发起总结任务');
setSummaryVisible(false);
fetchData(Number(id));
} catch (err) {
console.error(err);
} finally {
setActionLoading(false);
}
};
const formatTime = (ms: number) => {
const seconds = Math.floor(ms / 1000);
const m = Math.floor(seconds / 60);
const s = seconds % 60;
return `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
};
const seekTo = (timeMs: number) => {
if (audioRef.current) {
audioRef.current.currentTime = timeMs / 1000;
audioRef.current.play();
}
};
if (loading) return
;
if (!meeting) return
;
return (
navigate('/meetings')}>会议中心
会议详情
{meeting.title} {isOwner && }
}>
{dayjs(meeting.meetingTime).format('YYYY-MM-DD HH:mm')}
{meeting.tags?.split(',').filter(Boolean).map(t => {t})}
{meeting.participants || '未指定'}
{isOwner && } type="primary" ghost onClick={() => setSummaryVisible(true)}>重新总结}
} onClick={() => navigate('/meetings')}>返回列表
语音转录} style={{ height: '100%', display: 'flex', flexDirection: 'column' }} bodyStyle={{ flex: 1, overflowY: 'auto', padding: '16px', minHeight: 0 }}
extra={meeting.audioUrl && }>
(
seekTo(item.startTime)}>
} style={{ backgroundColor: '#87d068' }} />}
title={
{isOwner ? (
fetchData(meeting.id)} />} title="编辑发言人" trigger="click">
e.stopPropagation()}>{item.speakerName || item.speakerId || '发言人'}
) : (
{item.speakerName || item.speakerId || '发言人'}
)}
{item.speakerLabel && {speakerLabels.find(l => l.itemValue === item.speakerLabel)?.itemLabel || item.speakerLabel}}
{formatTime(item.startTime)}
} description={{item.content}} />
)} locale={{ emptyText: meeting.status < 3 ? '识别任务进行中...' : '暂无数据' }} />
AI 总结} style={{ height: '100%', display: 'flex', flexDirection: 'column' }} bodyStyle={{ flex: 1, overflowY: 'auto', padding: '24px', minHeight: 0 }}>
{meeting.summaryContent ? {meeting.summaryContent}
:
{meeting.status === 2 ? 正在重新总结... : }
}
{/* 修改基础信息弹窗 - 仅限 Owner */}
{isOwner && (
setEditVisible(false)} confirmLoading={actionLoading} width={600}>
注:参会人员 ID 绑定后暂不支持在此编辑,如需调整请联系系统管理员。
)}
{/* 重新总结抽屉 - 仅限 Owner */}
{isOwner && (
setSummaryVisible(false)} open={summaryVisible} extra={}>
提示:重新总结将基于当前的语音转录全文重新生成纪要,原有的总结内容将被覆盖。
)}
);
};
export default MeetingDetail;