|
@@ -7,11 +7,15 @@ import {
|
|
|
MicrophoneIcon,
|
|
MicrophoneIcon,
|
|
|
ShareIcon,
|
|
ShareIcon,
|
|
|
ClipboardDocumentIcon,
|
|
ClipboardDocumentIcon,
|
|
|
- PaperAirplaneIcon
|
|
|
|
|
|
|
+ PaperAirplaneIcon,
|
|
|
|
|
+ PhotoIcon
|
|
|
} from '@heroicons/react/24/outline';
|
|
} from '@heroicons/react/24/outline';
|
|
|
import { Button } from '@/client/components/ui/button';
|
|
import { Button } from '@/client/components/ui/button';
|
|
|
import { Textarea } from '@/client/components/ui/textarea';
|
|
import { Textarea } from '@/client/components/ui/textarea';
|
|
|
import { Card, CardContent } from '@/client/components/ui/card';
|
|
import { Card, CardContent } from '@/client/components/ui/card';
|
|
|
|
|
+import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/client/components/ui/dialog';
|
|
|
|
|
+import { XMarkIcon } from '@heroicons/react/24/outline';
|
|
|
|
|
+import { toast } from 'sonner';
|
|
|
import { TeacherClassControlButton } from './TeacherClassControlButton';
|
|
import { TeacherClassControlButton } from './TeacherClassControlButton';
|
|
|
import { TeacherStudentManagementButton } from './TeacherStudentManagementButton';
|
|
import { TeacherStudentManagementButton } from './TeacherStudentManagementButton';
|
|
|
import { TeacherHandUpManagementButton } from './TeacherHandUpManagementButton';
|
|
import { TeacherHandUpManagementButton } from './TeacherHandUpManagementButton';
|
|
@@ -19,6 +23,7 @@ import { TeacherQuestionManagementButton } from './TeacherQuestionManagementButt
|
|
|
import { AllMuteToggleButton } from './AllMuteToggleButton';
|
|
import { AllMuteToggleButton } from './AllMuteToggleButton';
|
|
|
import { StudentHandUpButton } from './StudentHandUpButton';
|
|
import { StudentHandUpButton } from './StudentHandUpButton';
|
|
|
import { StudentQuestionButton } from './StudentQuestionButton';
|
|
import { StudentQuestionButton } from './StudentQuestionButton';
|
|
|
|
|
+import { FileSelector } from '@/client/mobile/components/FileSelector';
|
|
|
|
|
|
|
|
interface ClassroomLayoutProps {
|
|
interface ClassroomLayoutProps {
|
|
|
children: ReactNode;
|
|
children: ReactNode;
|
|
@@ -28,6 +33,7 @@ interface ClassroomLayoutProps {
|
|
|
export const ClassroomLayout = ({ children, role }: ClassroomLayoutProps) => {
|
|
export const ClassroomLayout = ({ children, role }: ClassroomLayoutProps) => {
|
|
|
const [showVideo, setShowVideo] = React.useState(role !== Role.Teacher);
|
|
const [showVideo, setShowVideo] = React.useState(role !== Role.Teacher);
|
|
|
const [showShareLink, setShowShareLink] = React.useState(false);
|
|
const [showShareLink, setShowShareLink] = React.useState(false);
|
|
|
|
|
+ const [showImageSelector, setShowImageSelector] = React.useState(false);
|
|
|
const {
|
|
const {
|
|
|
remoteScreenContainer,
|
|
remoteScreenContainer,
|
|
|
remoteCameraContainer,
|
|
remoteCameraContainer,
|
|
@@ -46,7 +52,9 @@ export const ClassroomLayout = ({ children, role }: ClassroomLayoutProps) => {
|
|
|
classStatus,
|
|
classStatus,
|
|
|
shareLink,
|
|
shareLink,
|
|
|
showCameraOverlay,
|
|
showCameraOverlay,
|
|
|
- setShowCameraOverlay
|
|
|
|
|
|
|
+ setShowCameraOverlay,
|
|
|
|
|
+ showToast,
|
|
|
|
|
+ sendImageMessageById
|
|
|
} = useClassroomContext();
|
|
} = useClassroomContext();
|
|
|
|
|
|
|
|
return (
|
|
return (
|
|
@@ -93,9 +101,34 @@ export const ClassroomLayout = ({ children, role }: ClassroomLayoutProps) => {
|
|
|
<div className="flex flex-col h-full">
|
|
<div className="flex flex-col h-full">
|
|
|
{/* 消息列表 - 填充剩余空间 */}
|
|
{/* 消息列表 - 填充剩余空间 */}
|
|
|
<div className="flex-1 overflow-y-auto bg-white shadow-lg p-4">
|
|
<div className="flex-1 overflow-y-auto bg-white shadow-lg p-4">
|
|
|
- {messageList.map((msg, i) => (
|
|
|
|
|
- <div key={i} className="text-sm mb-1">{msg}</div>
|
|
|
|
|
- ))}
|
|
|
|
|
|
|
+ {messageList.map((msg, i) => {
|
|
|
|
|
+ // 检查是否是图片消息
|
|
|
|
|
+ if (msg.startsWith('[图片]')) {
|
|
|
|
|
+ const parts = msg.split(': ');
|
|
|
|
|
+ const senderInfo = parts[0].replace('[图片] ', '');
|
|
|
|
|
+ const imageUrl = parts.slice(1).join(': ');
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div key={i} className="mb-3 p-2 bg-gray-50 rounded-lg">
|
|
|
|
|
+ <div className="text-xs text-gray-500 mb-1">{senderInfo}</div>
|
|
|
|
|
+ <img
|
|
|
|
|
+ src={imageUrl}
|
|
|
|
|
+ alt="图片消息"
|
|
|
|
|
+ className="max-w-full max-h-48 rounded object-contain"
|
|
|
|
|
+ onError={(e) => {
|
|
|
|
|
+ (e.target as HTMLImageElement).style.display = 'none';
|
|
|
|
|
+ }}
|
|
|
|
|
+ />
|
|
|
|
|
+ <div className="text-xs text-gray-400 mt-1">[图片]</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 普通文本消息
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div key={i} className="text-sm mb-1">{msg}</div>
|
|
|
|
|
+ );
|
|
|
|
|
+ })}
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
@@ -161,6 +194,18 @@ export const ClassroomLayout = ({ children, role }: ClassroomLayoutProps) => {
|
|
|
</Button>
|
|
</Button>
|
|
|
)}
|
|
)}
|
|
|
|
|
|
|
|
|
|
+ {/* 图片选择按钮 */}
|
|
|
|
|
+ <Button
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ onClick={() => setShowImageSelector(true)}
|
|
|
|
|
+ className="p-2 rounded-full bg-blue-500 text-white"
|
|
|
|
|
+ title="选择图片"
|
|
|
|
|
+ size="icon"
|
|
|
|
|
+ variant="ghost"
|
|
|
|
|
+ >
|
|
|
|
|
+ <PhotoIcon className="w-4 h-4" />
|
|
|
|
|
+ </Button>
|
|
|
|
|
+
|
|
|
{/* 角色特定按钮 */}
|
|
{/* 角色特定按钮 */}
|
|
|
{role === Role.Teacher && (
|
|
{role === Role.Teacher && (
|
|
|
<>
|
|
<>
|
|
@@ -171,7 +216,7 @@ export const ClassroomLayout = ({ children, role }: ClassroomLayoutProps) => {
|
|
|
<AllMuteToggleButton />
|
|
<AllMuteToggleButton />
|
|
|
</>
|
|
</>
|
|
|
)}
|
|
)}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
{role === Role.Student && (
|
|
{role === Role.Student && (
|
|
|
<>
|
|
<>
|
|
|
<StudentHandUpButton />
|
|
<StudentHandUpButton />
|
|
@@ -236,6 +281,45 @@ export const ClassroomLayout = ({ children, role }: ClassroomLayoutProps) => {
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* 图片选择器对话框 */}
|
|
|
|
|
+ <Dialog open={showImageSelector} onOpenChange={setShowImageSelector}>
|
|
|
|
|
+ <DialogContent className="max-w-md">
|
|
|
|
|
+ <DialogHeader>
|
|
|
|
|
+ <DialogTitle>选择图片</DialogTitle>
|
|
|
|
|
+ <DialogDescription>
|
|
|
|
|
+ 选择要发送的图片文件
|
|
|
|
|
+ </DialogDescription>
|
|
|
|
|
+ </DialogHeader>
|
|
|
|
|
+
|
|
|
|
|
+ <FileSelector
|
|
|
|
|
+ filterType="image"
|
|
|
|
|
+ allowMultiple={false}
|
|
|
|
|
+ showPreview={true}
|
|
|
|
|
+ placeholder="选择图片"
|
|
|
|
|
+ title="选择图片"
|
|
|
|
|
+ description="选择要发送的图片文件"
|
|
|
|
|
+ onChange={(fileId) => {
|
|
|
|
|
+ if (fileId) {
|
|
|
|
|
+ // 使用文件ID发送图片消息
|
|
|
|
|
+ sendImageMessageById(fileId as number);
|
|
|
|
|
+ setShowImageSelector(false);
|
|
|
|
|
+ }
|
|
|
|
|
+ }}
|
|
|
|
|
+ />
|
|
|
|
|
+
|
|
|
|
|
+ <DialogFooter>
|
|
|
|
|
+ <Button
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ variant="outline"
|
|
|
|
|
+ onClick={() => setShowImageSelector(false)}
|
|
|
|
|
+ >
|
|
|
|
|
+ 取消
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </DialogFooter>
|
|
|
|
|
+ </DialogContent>
|
|
|
|
|
+ </Dialog>
|
|
|
|
|
+
|
|
|
</div>
|
|
</div>
|
|
|
);
|
|
);
|
|
|
};
|
|
};
|