From e85dfe5f77ac1f832370b2e1da7c7bcac3a65ddb Mon Sep 17 00:00:00 2001 From: tanlianwang Date: Fri, 27 Feb 2026 16:57:20 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=87=8D=E5=BB=BA=E4=BC=9A?= =?UTF-8?q?=E8=AE=AE=E5=BC=B9=E7=AA=97=E5=86=85=E5=AE=B9=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/MeetingLive.tsx | 578 +++++++++++++++++++++++++--- frontend/src/pages/PromptManage.tsx | 29 +- 2 files changed, 532 insertions(+), 75 deletions(-) diff --git a/frontend/src/pages/MeetingLive.tsx b/frontend/src/pages/MeetingLive.tsx index 5e9ee55..954b146 100644 --- a/frontend/src/pages/MeetingLive.tsx +++ b/frontend/src/pages/MeetingLive.tsx @@ -1,23 +1,164 @@ -import React, { useState } from 'react'; -import { Card, Form, Input, Button, Upload, message, Progress, Select, Row, Col } from 'antd'; -import { InboxOutlined, AudioOutlined, UserAddOutlined } from '@ant-design/icons'; +import React, { useState, useEffect } from 'react'; +import { + Card, Form, Input, Button, Upload, message, Progress, Select, Modal, + Typography, Space, DatePicker, Row, Col, Avatar, Radio +} from 'antd'; +import { + AudioOutlined, + CloudUploadOutlined, + VideoCameraOutlined, + CalendarOutlined, + TagOutlined, + FileTextOutlined, + CheckOutlined, + UserOutlined, + RightOutlined +} from '@ant-design/icons'; import { useNavigate } from 'react-router-dom'; +import dayjs from 'dayjs'; import { api } from '../api'; import PageHeader from '../components/PageHeader/PageHeader'; const { Dragger } = Upload; const { Option } = Select; +const { Text, Title } = Typography; + +const PromptSelector = ({ value, onChange, prompts }: { value?: number, onChange?: (val: number) => void, prompts: any[] }) => { + if (!prompts || prompts.length === 0) { + return ( +
+ 暂无可用模版 +
+ ); + } + + return ( +
+ {prompts.map(prompt => { + const isSelected = value === prompt.id; + return ( +
onChange?.(prompt.id)} + style={{ + padding: '8px 12px', + borderRadius: 6, + border: isSelected ? '1px solid #2563eb' : '1px solid #e2e8f0', + backgroundColor: isSelected ? '#eff6ff' : '#fff', + cursor: 'pointer', + transition: 'all 0.2s', + position: 'relative', + display: 'flex', + flexDirection: 'column', + height: 64, + justifyContent: 'center' + }} + > +
+ +
+ {prompt.name} +
+
+ {prompt.is_system ? '系统' : '个人'} +
+
+ +
+ {prompt.description || '暂无描述'} +
+ + {isSelected && ( +
+ +
+ )} +
+ ); + })} +
+ ); +}; const MeetingLive: React.FC = () => { - const [form] = Form.useForm(); + const [liveForm] = Form.useForm(); + const [uploadForm] = Form.useForm(); + + const [users, setUsers] = useState([]); + const [prompts, setPrompts] = useState([]); + + const [isLiveModalVisible, setIsLiveModalVisible] = useState(false); + const [isUploadModalVisible, setIsUploadModalVisible] = useState(false); + const [uploading, setUploading] = useState(false); const [progress, setProgress] = useState(0); + const [selectedFile, setSelectedFile] = useState(null); + const navigate = useNavigate(); - const handleCreate = async (values: any) => { + useEffect(() => { + fetchData(); + }, []); + + const fetchData = async () => { try { - const res = await api.createMeeting(values); + const [usersData, promptsData] = await Promise.all([ + api.listUsers(), + api.listPrompts({ scope: 'personal' }) + ]); + setUsers(usersData); + setPrompts(promptsData.filter((p: any) => p.is_active === true || p.is_active === 1)); + } catch (error) { + console.error('Failed to fetch data:', error); + message.error('加载基础数据失败'); + } + }; + + const handleCreateLive = async (values: any) => { + try { + const payload = { + title: values.title, + participants: values.participants, + prompt_id: values.promptTemplateId, + tags: values.tags, + meeting_time: values.time ? values.time.valueOf() : dayjs().valueOf(), + type: 'live' + }; + + const res = await api.createMeeting(payload); message.success('会议创建成功'); + setIsLiveModalVisible(false); navigate(`/meeting/history/${res.meeting_id}`); } catch (error) { console.error(error); @@ -25,13 +166,40 @@ const MeetingLive: React.FC = () => { } }; - const handleUpload = async (file: File) => { + const handleCreateUpload = async (values: any) => { + if (!selectedFile) { + message.error('请先上传录音文件'); + return; + } + setUploading(true); setProgress(0); + try { - // Mock progress since fetch doesn't support it easily without XHR + const formData = new FormData(); + formData.append('file', selectedFile); + formData.append('title', values.title); + if (values.participants) { + // Backend might expect list or comma separated string depending on implementation + // Assuming api.ts handles FormData correctly, we append each participant or join them + // If backend uses FastAPI List[str] = Form(...), we append multiple times + // If backend uses str = Form(...), we join them. + // Safer to try JSON string if complex, but standard form data usually repeats keys + // For now, let's append as JSON string or multiple keys? + // Let's assume the backend parses JSON for complex fields if passed as string + // OR we append each one. + // Given existing code elsewhere, usually standard form submission. + // Let's iterate. + values.participants.forEach((p: any) => formData.append('participants', String(p))); + } + if (values.promptTemplateId) formData.append('prompt_id', String(values.promptTemplateId)); + if (values.tags) values.tags.forEach((t: string) => formData.append('tags', t)); + if (values.time) formData.append('meeting_time', String(values.time.valueOf())); + formData.append('type', 'upload'); + + // Simulated progress for better UX since fetch doesn't support upload progress natively easily const timer = setInterval(() => { - setProgress((prev) => { + setProgress(prev => { if (prev >= 90) { clearInterval(timer); return 90; @@ -40,80 +208,372 @@ const MeetingLive: React.FC = () => { }); }, 500); - const res = await api.uploadMeetingAudio(file); + const res = await api.createMeeting(formData); + clearInterval(timer); setProgress(100); - message.success('上传成功,开始转译'); - // Assuming upload returns meeting_id or we navigate to list - if (res && res.meeting_id) { - navigate(`/meeting/history/${res.meeting_id}`); - } else { - navigate('/meeting/history'); - } + message.success('上传并创建成功'); + setIsUploadModalVisible(false); + navigate(`/meeting/history/${res.meeting_id}`); } catch (error) { console.error(error); - message.error('上传失败'); + message.error('上传失败: ' + (error as any).message); + } finally { setUploading(false); } - return false; // Prevent default upload behavior }; - return ( -
- - - - - -
- - } /> - - - - - - - -
-
- + const CardButton = ({ + icon, + title, + description, + color, + onClick + }: { + icon: React.ReactNode, + title: string, + description: string, + color: string, + onClick: () => void + }) => ( + +
+ {icon} +
+ {title} + {description} +
+ 立即开始 +
+
+ ); + + const renderFormFields = (isUpload: boolean) => ( + + +
+ + 基本信息 + +
- - + + } + style={{ borderRadius: 12, padding: '10px 16px' }} + /> + + + + } + /> + + + + + {users.map(user => ( + + ))} + + + +
+ + 会议模版 + +
+ + + + +
+ ); + + return ( +
+ +
+ + + + } + title="实时会议" + description="开始即时会议录音,实时转写并生成会议纪要" + color="#1890ff" + onClick={() => setIsLiveModalVisible(true)} + /> + + + } + title="上传录音" + description="上传已有的音频/视频文件,AI 自动整理会议内容" + color="#fa8c16" + onClick={() => setIsUploadModalVisible(true)} + /> + + {/* Live Meeting Modal */} + +
+ +
+
+
创建实时会议
+
配置会议信息并开始录制
+
+
+ } + open={isLiveModalVisible} + onCancel={() => setIsLiveModalVisible(false)} + footer={ +
+ + +
+ } + width={800} + destroyOnClose + centered + styles={{ body: { padding: '24px 0 0 0' } }} + > +
+ {renderFormFields(false)} +
+ + + {/* Upload Modal */} + +
+ +
+
+
上传会议录音
+
支持 MP3, WAV, M4A 等常见音频格式
+
+
+ } + open={isUploadModalVisible} + onCancel={() => { + if (!uploading) setIsUploadModalVisible(false); + }} + footer={ +
+ + +
+ } + width={800} + destroyOnClose + centered + maskClosable={!uploading} + styles={{ body: { padding: '24px 0 0 0' } }} + > +
+ {renderFormFields(true)} +
+
); }; diff --git a/frontend/src/pages/PromptManage.tsx b/frontend/src/pages/PromptManage.tsx index 8127a59..bc777a9 100644 --- a/frontend/src/pages/PromptManage.tsx +++ b/frontend/src/pages/PromptManage.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from 'react'; -import { Button, Card, Tag, Space, Form, Input, InputNumber, Switch, message, Select, Modal, Tooltip, Divider } from 'antd'; +import { App, Button, Card, Tag, Space, Form, Input, InputNumber, Switch, Select, Modal, Tooltip, Divider } from 'antd'; import { PlusOutlined, EditOutlined, @@ -18,6 +18,7 @@ import ListTable from '../components/ListTable/ListTable'; const { Option } = Select; const PromptManage: React.FC = () => { + const { message, modal } = App.useApp(); const [loading, setLoading] = useState(false); const [data, setData] = useState([]); const [categories, setCategories] = useState([]); @@ -63,7 +64,7 @@ const PromptManage: React.FC = () => { }; const handleDelete = (id: number) => { - Modal.confirm({ + modal.confirm({ title: '确认删除', content: '确定要删除此个人模板吗?', onOk: async () => { @@ -74,10 +75,9 @@ const PromptManage: React.FC = () => { }); }; - const handleTogglePersonalActive = async (e: React.MouseEvent, record: any) => { - e.stopPropagation(); - await api.updatePromptConfig(record.id, { is_active: !record.is_active }); - message.success(record.is_active ? '已隐藏' : '已显示'); + const handleTogglePersonalActive = async (checked: boolean, record: any) => { + await api.updatePromptConfig(record.id, { is_active: checked }); + message.success(checked ? '已启用' : '已停用'); fetchPrompts(searchKeyword); }; @@ -128,20 +128,17 @@ const PromptManage: React.FC = () => { } }, { - title: '总结显示', + title: '是否启用', dataIndex: 'is_active', key: 'is_active', width: 100, render: (active: boolean, record: any) => ( - + handleTogglePersonalActive(checked, record)} + onClick={(_, e) => e.stopPropagation()} + /> ), }, {