修复重建会议弹窗内容样式
parent
3738e14716
commit
e85dfe5f77
|
|
@ -1,23 +1,164 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Card, Form, Input, Button, Upload, message, Progress, Select, Row, Col } from 'antd';
|
import {
|
||||||
import { InboxOutlined, AudioOutlined, UserAddOutlined } from '@ant-design/icons';
|
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 { useNavigate } from 'react-router-dom';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
import { api } from '../api';
|
import { api } from '../api';
|
||||||
import PageHeader from '../components/PageHeader/PageHeader';
|
import PageHeader from '../components/PageHeader/PageHeader';
|
||||||
|
|
||||||
const { Dragger } = Upload;
|
const { Dragger } = Upload;
|
||||||
const { Option } = Select;
|
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 (
|
||||||
|
<div style={{ textAlign: 'center', padding: '12px', color: '#94a3b8', background: '#f8fafc', borderRadius: 8 }}>
|
||||||
|
暂无可用模版
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: 'repeat(2, 1fr)',
|
||||||
|
gap: 8,
|
||||||
|
maxHeight: 220,
|
||||||
|
overflowY: 'auto',
|
||||||
|
padding: 2
|
||||||
|
}}>
|
||||||
|
{prompts.map(prompt => {
|
||||||
|
const isSelected = value === prompt.id;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={prompt.id}
|
||||||
|
onClick={() => 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'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', marginBottom: 2 }}>
|
||||||
|
<FileTextOutlined style={{ color: isSelected ? '#2563eb' : '#64748b', fontSize: 14, marginRight: 6 }} />
|
||||||
|
<div style={{
|
||||||
|
fontWeight: 600,
|
||||||
|
color: isSelected ? '#1e293b' : '#334155',
|
||||||
|
fontSize: 13,
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
flex: 1
|
||||||
|
}}>
|
||||||
|
{prompt.name}
|
||||||
|
</div>
|
||||||
|
<div style={{
|
||||||
|
fontSize: 10,
|
||||||
|
padding: '1px 4px',
|
||||||
|
borderRadius: 3,
|
||||||
|
backgroundColor: prompt.is_system ? '#e0f2fe' : '#fff7ed',
|
||||||
|
color: prompt.is_system ? '#0284c7' : '#ea580c',
|
||||||
|
marginLeft: 6,
|
||||||
|
flexShrink: 0,
|
||||||
|
lineHeight: '1.2'
|
||||||
|
}}>
|
||||||
|
{prompt.is_system ? '系统' : '个人'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{
|
||||||
|
fontSize: 11,
|
||||||
|
color: isSelected ? '#60a5fa' : '#94a3b8',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
paddingLeft: 20
|
||||||
|
}}>
|
||||||
|
{prompt.description || '暂无描述'}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isSelected && (
|
||||||
|
<div style={{ position: 'absolute', bottom: 4, right: 4 }}>
|
||||||
|
<CheckOutlined style={{ color: '#2563eb', fontSize: 10 }} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const MeetingLive: React.FC = () => {
|
const MeetingLive: React.FC = () => {
|
||||||
const [form] = Form.useForm();
|
const [liveForm] = Form.useForm();
|
||||||
|
const [uploadForm] = Form.useForm();
|
||||||
|
|
||||||
|
const [users, setUsers] = useState<any[]>([]);
|
||||||
|
const [prompts, setPrompts] = useState<any[]>([]);
|
||||||
|
|
||||||
|
const [isLiveModalVisible, setIsLiveModalVisible] = useState(false);
|
||||||
|
const [isUploadModalVisible, setIsUploadModalVisible] = useState(false);
|
||||||
|
|
||||||
const [uploading, setUploading] = useState(false);
|
const [uploading, setUploading] = useState(false);
|
||||||
const [progress, setProgress] = useState(0);
|
const [progress, setProgress] = useState(0);
|
||||||
|
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const handleCreate = async (values: any) => {
|
useEffect(() => {
|
||||||
|
fetchData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchData = async () => {
|
||||||
try {
|
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('会议创建成功');
|
message.success('会议创建成功');
|
||||||
|
setIsLiveModalVisible(false);
|
||||||
navigate(`/meeting/history/${res.meeting_id}`);
|
navigate(`/meeting/history/${res.meeting_id}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(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);
|
setUploading(true);
|
||||||
setProgress(0);
|
setProgress(0);
|
||||||
|
|
||||||
try {
|
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(() => {
|
const timer = setInterval(() => {
|
||||||
setProgress((prev) => {
|
setProgress(prev => {
|
||||||
if (prev >= 90) {
|
if (prev >= 90) {
|
||||||
clearInterval(timer);
|
clearInterval(timer);
|
||||||
return 90;
|
return 90;
|
||||||
|
|
@ -40,80 +208,372 @@ const MeetingLive: React.FC = () => {
|
||||||
});
|
});
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
const res = await api.uploadMeetingAudio(file);
|
const res = await api.createMeeting(formData);
|
||||||
|
|
||||||
clearInterval(timer);
|
clearInterval(timer);
|
||||||
setProgress(100);
|
setProgress(100);
|
||||||
message.success('上传成功,开始转译');
|
message.success('上传并创建成功');
|
||||||
// Assuming upload returns meeting_id or we navigate to list
|
setIsUploadModalVisible(false);
|
||||||
if (res && res.meeting_id) {
|
navigate(`/meeting/history/${res.meeting_id}`);
|
||||||
navigate(`/meeting/history/${res.meeting_id}`);
|
|
||||||
} else {
|
|
||||||
navigate('/meeting/history');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
message.error('上传失败');
|
message.error('上传失败: ' + (error as any).message);
|
||||||
|
} finally {
|
||||||
setUploading(false);
|
setUploading(false);
|
||||||
}
|
}
|
||||||
return false; // Prevent default upload behavior
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
const CardButton = ({
|
||||||
<div style={{ padding: 24 }}>
|
icon,
|
||||||
<PageHeader
|
title,
|
||||||
title="新建会议"
|
description,
|
||||||
description="创建实时会议或上传音频文件进行转写"
|
color,
|
||||||
/>
|
onClick
|
||||||
|
}: {
|
||||||
<Row gutter={[24, 24]}>
|
icon: React.ReactNode,
|
||||||
<Col span={12}>
|
title: string,
|
||||||
<Card title="创建实时会议" bordered={false} hoverable>
|
description: string,
|
||||||
<Form form={form} layout="vertical" onFinish={handleCreate}>
|
color: string,
|
||||||
<Form.Item name="title" label="会议标题" rules={[{ required: true }]}>
|
onClick: () => void
|
||||||
<Input placeholder="请输入会议标题" prefix={<AudioOutlined />} />
|
}) => (
|
||||||
</Form.Item>
|
<Card
|
||||||
<Form.Item name="participants" label="参会人">
|
hoverable
|
||||||
<Select mode="tags" placeholder="请输入参会人姓名" open={false}>
|
style={{
|
||||||
</Select>
|
height: '100%',
|
||||||
</Form.Item>
|
borderRadius: 16,
|
||||||
<Form.Item>
|
border: '1px solid #f0f0f0',
|
||||||
<Button type="primary" htmlType="submit" icon={<UserAddOutlined />} block>
|
transition: 'all 0.3s ease',
|
||||||
开始会议
|
cursor: 'pointer'
|
||||||
</Button>
|
}}
|
||||||
</Form.Item>
|
bodyStyle={{ padding: 32, height: '100%', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', textAlign: 'center' }}
|
||||||
</Form>
|
onClick={onClick}
|
||||||
</Card>
|
>
|
||||||
</Col>
|
<div style={{
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
borderRadius: '50%',
|
||||||
|
background: `${color}15`, // 15% opacity
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginBottom: 24,
|
||||||
|
color: color,
|
||||||
|
fontSize: 36
|
||||||
|
}}>
|
||||||
|
{icon}
|
||||||
|
</div>
|
||||||
|
<Title level={4} style={{ marginBottom: 12 }}>{title}</Title>
|
||||||
|
<Text type="secondary" style={{ fontSize: 14 }}>{description}</Text>
|
||||||
|
<div style={{ marginTop: 24, color: color, display: 'flex', alignItems: 'center', fontWeight: 500 }}>
|
||||||
|
立即开始 <RightOutlined style={{ marginLeft: 8, fontSize: 12 }} />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderFormFields = (isUpload: boolean) => (
|
||||||
|
<Row gutter={32}>
|
||||||
|
<Col span={12}>
|
||||||
|
<div style={{ marginBottom: 24 }}>
|
||||||
|
<Text strong style={{ fontSize: 12, color: '#94a3b8', textTransform: 'uppercase', letterSpacing: '0.1em' }}>
|
||||||
|
基本信息
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Col span={12}>
|
<Form.Item
|
||||||
<Card title="上传录音文件" variant="borderless" hoverable>
|
name="title"
|
||||||
|
rules={[{ required: true, message: '请输入会议标题' }]}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
size="large"
|
||||||
|
placeholder="会议标题"
|
||||||
|
prefix={<AudioOutlined style={{ color: '#bfbfbf' }} />}
|
||||||
|
style={{ borderRadius: 12, padding: '10px 16px' }}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
name="time"
|
||||||
|
initialValue={dayjs()}
|
||||||
|
>
|
||||||
|
<DatePicker
|
||||||
|
showTime
|
||||||
|
size="large"
|
||||||
|
style={{ width: '100%', borderRadius: 12, padding: '10px 16px' }}
|
||||||
|
format="YYYY-MM-DD HH:mm"
|
||||||
|
placeholder="会议时间"
|
||||||
|
suffixIcon={<CalendarOutlined style={{ color: '#bfbfbf' }} />}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item name="tags">
|
||||||
|
<Select
|
||||||
|
mode="tags"
|
||||||
|
size="large"
|
||||||
|
placeholder="业务标签 (输入回车添加)"
|
||||||
|
style={{ width: '100%', borderRadius: 12 }}
|
||||||
|
tokenSeparators={[',']}
|
||||||
|
suffixIcon={<TagOutlined style={{ color: '#bfbfbf' }} />}
|
||||||
|
tagRender={(props) => (
|
||||||
|
<span style={{
|
||||||
|
backgroundColor: '#eff6ff',
|
||||||
|
color: '#2563eb',
|
||||||
|
border: '1px solid #dbeafe',
|
||||||
|
borderRadius: 6,
|
||||||
|
padding: '2px 8px',
|
||||||
|
marginRight: 4,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 600
|
||||||
|
}}>
|
||||||
|
{props.label}
|
||||||
|
<span onClick={props.onClose} style={{ marginLeft: 4, cursor: 'pointer' }}>×</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
{isUpload && (
|
||||||
|
<div style={{ marginTop: 24 }}>
|
||||||
|
<div style={{ marginBottom: 12 }}>
|
||||||
|
<Text strong style={{ fontSize: 12, color: '#94a3b8', textTransform: 'uppercase', letterSpacing: '0.1em' }}>
|
||||||
|
上传录音 (必选)
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
<Dragger
|
<Dragger
|
||||||
name="file"
|
name="file"
|
||||||
multiple={false}
|
multiple={false}
|
||||||
beforeUpload={handleUpload}
|
beforeUpload={(file) => {
|
||||||
|
setSelectedFile(file);
|
||||||
|
const currentTitle = uploadForm.getFieldValue('title');
|
||||||
|
if (!currentTitle) {
|
||||||
|
uploadForm.setFieldsValue({ title: file.name.replace(/\.[^/.]+$/, "") });
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}}
|
||||||
showUploadList={false}
|
showUploadList={false}
|
||||||
accept="audio/*,video/*"
|
accept="audio/*,video/*"
|
||||||
disabled={uploading}
|
disabled={uploading}
|
||||||
|
style={{
|
||||||
|
padding: 24,
|
||||||
|
background: selectedFile ? '#eff6ff' : '#f8fafc',
|
||||||
|
border: selectedFile ? '2px solid #2563eb' : '2px dashed #cbd5e1',
|
||||||
|
borderRadius: 16
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<p className="ant-upload-drag-icon">
|
{selectedFile ? (
|
||||||
<InboxOutlined />
|
<div style={{ padding: '12px 0' }}>
|
||||||
</p>
|
<FileTextOutlined style={{ fontSize: 32, color: '#2563eb', marginBottom: 12 }} />
|
||||||
<p className="ant-upload-text">点击或拖拽文件到此区域上传</p>
|
<div style={{ fontSize: 14, fontWeight: 600, color: '#1e293b' }}>{selectedFile.name}</div>
|
||||||
<p className="ant-upload-hint">
|
<div style={{ fontSize: 12, color: '#64748b', marginTop: 4 }}>
|
||||||
支持 MP3, WAV, M4A 等常见音频格式
|
{(selectedFile.size / 1024 / 1024).toFixed(2)} MB
|
||||||
</p>
|
</div>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
danger
|
||||||
|
size="small"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setSelectedFile(null);
|
||||||
|
}}
|
||||||
|
style={{ marginTop: 8 }}
|
||||||
|
>
|
||||||
|
移除文件
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<p className="ant-upload-drag-icon">
|
||||||
|
<CloudUploadOutlined style={{ color: '#94a3b8' }} />
|
||||||
|
</p>
|
||||||
|
<p className="ant-upload-text" style={{ fontSize: 14, color: '#64748b' }}>点击或拖拽上传音频</p>
|
||||||
|
<p className="ant-upload-hint" style={{ fontSize: 12, color: '#94a3b8' }}>
|
||||||
|
支持 MP3, WAV, M4A
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Dragger>
|
</Dragger>
|
||||||
{uploading && (
|
{uploading && (
|
||||||
<div style={{ marginTop: 16 }}>
|
<div style={{ marginTop: 16 }}>
|
||||||
<Progress percent={progress} status={progress === 100 ? 'success' : 'active'} />
|
<Progress percent={progress} status="active" strokeColor="#fa8c16" showInfo={false} size="small" />
|
||||||
<div style={{ textAlign: 'center', marginTop: 8 }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginTop: 4 }}>
|
||||||
{progress === 100 ? '上传完成,正在处理...' : '上传中...'}
|
<Text style={{ fontSize: 12, color: '#fa8c16' }}>正在上传...</Text>
|
||||||
|
<Text style={{ fontSize: 12, color: '#fa8c16' }}>{progress}%</Text>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</div>
|
||||||
|
)}
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
<Col span={12}>
|
||||||
|
<div style={{ marginBottom: 24 }}>
|
||||||
|
<Text strong style={{ fontSize: 12, color: '#94a3b8', textTransform: 'uppercase', letterSpacing: '0.1em' }}>
|
||||||
|
参会人员
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<Form.Item name="participants">
|
||||||
|
<Select
|
||||||
|
mode="multiple"
|
||||||
|
size="large"
|
||||||
|
placeholder="选择参会人员"
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
optionLabelProp="label"
|
||||||
|
dropdownStyle={{ borderRadius: 12, padding: 8 }}
|
||||||
|
>
|
||||||
|
{users.map(user => (
|
||||||
|
<Option key={user.user_id} value={user.user_id} label={user.display_name || user.username}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', padding: '4px 0' }}>
|
||||||
|
<Avatar size="small" style={{ backgroundColor: '#bfbfbf', marginRight: 8 }}>
|
||||||
|
{(user.display_name || user.username)[0]}
|
||||||
|
</Avatar>
|
||||||
|
<div style={{ fontWeight: 500 }}>{user.display_name || user.username}</div>
|
||||||
|
</div>
|
||||||
|
</Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<div style={{ marginTop: 32, marginBottom: 24 }}>
|
||||||
|
<Text strong style={{ fontSize: 12, color: '#94a3b8', textTransform: 'uppercase', letterSpacing: '0.1em' }}>
|
||||||
|
会议模版
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<Form.Item name="promptTemplateId">
|
||||||
|
<PromptSelector prompts={prompts} />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '24px', maxWidth: 1200, margin: '0 auto' }}>
|
||||||
|
<PageHeader
|
||||||
|
title="新建会议"
|
||||||
|
description="选择会议类型以开始记录,系统将自动为您生成智能会议纪要"
|
||||||
|
/>
|
||||||
|
<div style={{ marginBottom: 40 }} />
|
||||||
|
|
||||||
|
<Row gutter={40} justify="center">
|
||||||
|
<Col span={10}>
|
||||||
|
<CardButton
|
||||||
|
icon={<VideoCameraOutlined />}
|
||||||
|
title="实时会议"
|
||||||
|
description="开始即时会议录音,实时转写并生成会议纪要"
|
||||||
|
color="#1890ff"
|
||||||
|
onClick={() => setIsLiveModalVisible(true)}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col span={10}>
|
||||||
|
<CardButton
|
||||||
|
icon={<CloudUploadOutlined />}
|
||||||
|
title="上传录音"
|
||||||
|
description="上传已有的音频/视频文件,AI 自动整理会议内容"
|
||||||
|
color="#fa8c16"
|
||||||
|
onClick={() => setIsUploadModalVisible(true)}
|
||||||
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
|
{/* Live Meeting Modal */}
|
||||||
|
<Modal
|
||||||
|
title={
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12, paddingBottom: 12 }}>
|
||||||
|
<div style={{
|
||||||
|
background: '#e6f7ff',
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
borderRadius: '50%',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
color: '#1890ff',
|
||||||
|
fontSize: 20
|
||||||
|
}}>
|
||||||
|
<VideoCameraOutlined />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: 18, fontWeight: 700 }}>创建实时会议</div>
|
||||||
|
<div style={{ fontSize: 12, color: '#94a3b8', fontWeight: 400 }}>配置会议信息并开始录制</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
open={isLiveModalVisible}
|
||||||
|
onCancel={() => setIsLiveModalVisible(false)}
|
||||||
|
footer={
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 12, paddingTop: 12 }}>
|
||||||
|
<Button size="large" onClick={() => setIsLiveModalVisible(false)} style={{ borderRadius: 12 }}>取消</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
size="large"
|
||||||
|
onClick={() => liveForm.submit()}
|
||||||
|
icon={<VideoCameraOutlined />}
|
||||||
|
style={{ borderRadius: 12, backgroundColor: '#1890ff' }}
|
||||||
|
>
|
||||||
|
立即开始
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
width={800}
|
||||||
|
destroyOnClose
|
||||||
|
centered
|
||||||
|
styles={{ body: { padding: '24px 0 0 0' } }}
|
||||||
|
>
|
||||||
|
<Form form={liveForm} layout="vertical" onFinish={handleCreateLive}>
|
||||||
|
{renderFormFields(false)}
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
{/* Upload Modal */}
|
||||||
|
<Modal
|
||||||
|
title={
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12, paddingBottom: 12 }}>
|
||||||
|
<div style={{
|
||||||
|
background: '#fff7e6',
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
borderRadius: '50%',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
color: '#fa8c16',
|
||||||
|
fontSize: 20
|
||||||
|
}}>
|
||||||
|
<CloudUploadOutlined />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: 18, fontWeight: 700 }}>上传会议录音</div>
|
||||||
|
<div style={{ fontSize: 12, color: '#94a3b8', fontWeight: 400 }}>支持 MP3, WAV, M4A 等常见音频格式</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
open={isUploadModalVisible}
|
||||||
|
onCancel={() => {
|
||||||
|
if (!uploading) setIsUploadModalVisible(false);
|
||||||
|
}}
|
||||||
|
footer={
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 12, paddingTop: 12 }}>
|
||||||
|
<Button size="large" onClick={() => setIsUploadModalVisible(false)} disabled={uploading} style={{ borderRadius: 12 }}>取消</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
size="large"
|
||||||
|
onClick={() => uploadForm.submit()}
|
||||||
|
loading={uploading}
|
||||||
|
icon={<CloudUploadOutlined />}
|
||||||
|
style={{ borderRadius: 12, backgroundColor: '#fa8c16', borderColor: '#fa8c16' }}
|
||||||
|
>
|
||||||
|
{uploading ? '上传处理中...' : '开始上传'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
width={800}
|
||||||
|
destroyOnClose
|
||||||
|
centered
|
||||||
|
maskClosable={!uploading}
|
||||||
|
styles={{ body: { padding: '24px 0 0 0' } }}
|
||||||
|
>
|
||||||
|
<Form form={uploadForm} layout="vertical" onFinish={handleCreateUpload}>
|
||||||
|
{renderFormFields(true)}
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
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 {
|
import {
|
||||||
PlusOutlined,
|
PlusOutlined,
|
||||||
EditOutlined,
|
EditOutlined,
|
||||||
|
|
@ -18,6 +18,7 @@ import ListTable from '../components/ListTable/ListTable';
|
||||||
const { Option } = Select;
|
const { Option } = Select;
|
||||||
|
|
||||||
const PromptManage: React.FC = () => {
|
const PromptManage: React.FC = () => {
|
||||||
|
const { message, modal } = App.useApp();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [data, setData] = useState<any[]>([]);
|
const [data, setData] = useState<any[]>([]);
|
||||||
const [categories, setCategories] = useState<any[]>([]);
|
const [categories, setCategories] = useState<any[]>([]);
|
||||||
|
|
@ -63,7 +64,7 @@ const PromptManage: React.FC = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = (id: number) => {
|
const handleDelete = (id: number) => {
|
||||||
Modal.confirm({
|
modal.confirm({
|
||||||
title: '确认删除',
|
title: '确认删除',
|
||||||
content: '确定要删除此个人模板吗?',
|
content: '确定要删除此个人模板吗?',
|
||||||
onOk: async () => {
|
onOk: async () => {
|
||||||
|
|
@ -74,10 +75,9 @@ const PromptManage: React.FC = () => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTogglePersonalActive = async (e: React.MouseEvent, record: any) => {
|
const handleTogglePersonalActive = async (checked: boolean, record: any) => {
|
||||||
e.stopPropagation();
|
await api.updatePromptConfig(record.id, { is_active: checked });
|
||||||
await api.updatePromptConfig(record.id, { is_active: !record.is_active });
|
message.success(checked ? '已启用' : '已停用');
|
||||||
message.success(record.is_active ? '已隐藏' : '已显示');
|
|
||||||
fetchPrompts(searchKeyword);
|
fetchPrompts(searchKeyword);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -128,20 +128,17 @@ const PromptManage: React.FC = () => {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '总结显示',
|
title: '是否启用',
|
||||||
dataIndex: 'is_active',
|
dataIndex: 'is_active',
|
||||||
key: 'is_active',
|
key: 'is_active',
|
||||||
width: 100,
|
width: 100,
|
||||||
render: (active: boolean, record: any) => (
|
render: (active: boolean, record: any) => (
|
||||||
<Button
|
<Switch
|
||||||
type="text"
|
checked={!!active}
|
||||||
size="small"
|
size="small"
|
||||||
icon={active ? <EyeOutlined /> : <EyeInvisibleOutlined />}
|
onChange={(checked) => handleTogglePersonalActive(checked, record)}
|
||||||
onClick={(e) => handleTogglePersonalActive(e, record)}
|
onClick={(_, e) => e.stopPropagation()}
|
||||||
style={{ color: active ? '#1890ff' : '#ccc' }}
|
/>
|
||||||
>
|
|
||||||
{active ? '显示' : '隐藏'}
|
|
||||||
</Button>
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue