Răsfoiți Sursa

✨ feat(chat): 实现老师消息特殊样式展示

- 添加senderType字段标识消息发送者类型
- 老师消息使用红色背景、更大字号和"👨‍🏫"图标标识
- 老师名称后添加"(老师)"标签
- 优化消息气泡样式逻辑,区分老师、自己和其他学生消息
- 完善消息发送和存储逻辑,传递senderType信息到前端
- 数据库添加sender_type字段存储用户类型信息
- API schema添加senderType字段定义

💄 style(chat): 优化消息气泡样式

- 调整消息气泡圆角样式统一使用rounded-2xl基础样式
- 优化老师消息排版,添加适当间距和行高
- 调整发送者名称显示样式,增强视觉层次感
- 优化时间戳颜色,提高不同类型消息的辨识度
yourname 6 luni în urmă
părinte
comite
fa6fddfb9d

+ 33 - 11
src/client/mobile/components/Classroom/MessageBubble.tsx

@@ -1,6 +1,7 @@
 import React from 'react';
 import { Message } from './useClassroom';
 import { useClassroomContext } from './ClassroomProvider';
+import { UserType } from '@/server/modules/users/user.enum';
 
 interface MessageBubbleProps {
   message: Message;
@@ -16,24 +17,36 @@ export const MessageBubble: React.FC<MessageBubbleProps> = ({ message, isOwnMess
     });
   };
 
+  // 判断是否为老师消息(且不是自己发送的)
+  const isTeacherMessage = message.senderType === UserType.TEACHER && !isOwnMessage;
+
   switch (message.type) {
     case 'text':
       return (
         <div className={`flex ${isOwnMessage ? 'justify-end' : 'justify-start'} mb-3`}>
-          <div className={`max-w-xs lg:max-w-md px-4 py-2 rounded-2xl ${
-            isOwnMessage 
-              ? 'bg-blue-500 text-white rounded-br-none' 
-              : 'bg-gray-200 text-gray-800 rounded-bl-none'
+          <div className={`max-w-xs lg:max-w-md px-4 py-2 ${
+            isTeacherMessage 
+              ? 'bg-red-500 text-white font-bold text-base rounded-2xl' 
+              : isOwnMessage 
+                ? 'bg-blue-500 text-white rounded-2xl rounded-br-none' 
+                : 'bg-gray-200 text-gray-800 rounded-2xl rounded-bl-none'
           }`}>
-            {!isOwnMessage && message.sender && (
+            {isTeacherMessage && message.sender && (
+              <div className="text-xs font-medium mb-1 opacity-80 flex items-center">
+                <span className="mr-1">👨‍🏫</span>
+                {message.sender} (老师)
+              </div>
+            )}
+            {!isOwnMessage && !isTeacherMessage && message.sender && (
               <div className="text-xs font-medium mb-1 opacity-80">
                 {message.sender}
               </div>
             )}
-            <div className="text-sm break-words">
+            <div className={`break-words ${isTeacherMessage ? 'text-base' : 'text-sm'}`}>
               {message.content}
             </div>
             <div className={`text-xs mt-1 opacity-70 ${
+              isTeacherMessage ? 'text-red-100' : 
               isOwnMessage ? 'text-blue-100' : 'text-gray-500'
             }`}>
               {formatTime(message.timestamp)}
@@ -45,12 +58,20 @@ export const MessageBubble: React.FC<MessageBubbleProps> = ({ message, isOwnMess
     case 'image':
       return (
         <div className={`flex ${isOwnMessage ? 'justify-end' : 'justify-start'} mb-3`}>
-          <div className={`max-w-xs px-3 py-2 rounded-2xl ${
-            isOwnMessage 
-              ? 'bg-blue-500 text-white rounded-br-none' 
-              : 'bg-gray-200 text-gray-800 rounded-bl-none'
+          <div className={`max-w-xs px-3 py-2 ${
+            isTeacherMessage 
+              ? 'bg-red-500 text-white font-bold rounded-2xl' 
+              : isOwnMessage 
+                ? 'bg-blue-500 text-white rounded-2xl rounded-br-none' 
+                : 'bg-gray-200 text-gray-800 rounded-2xl rounded-bl-none'
           }`}>
-            {!isOwnMessage && message.sender && (
+            {isTeacherMessage && message.sender && (
+              <div className="text-xs font-medium mb-1 opacity-80 flex items-center">
+                <span className="mr-1">👨‍🏫</span>
+                {message.sender} (老师)
+              </div>
+            )}
+            {!isOwnMessage && !isTeacherMessage && message.sender && (
               <div className="text-xs font-medium mb-1 opacity-80">
                 {message.sender}
               </div>
@@ -64,6 +85,7 @@ export const MessageBubble: React.FC<MessageBubbleProps> = ({ message, isOwnMess
               }}
             />
             <div className={`text-xs mt-1 opacity-70 ${
+              isTeacherMessage ? 'text-red-100' : 
               isOwnMessage ? 'text-blue-100' : 'text-gray-500'
             }`}>
               {formatTime(message.timestamp)}

+ 18 - 10
src/client/mobile/components/Classroom/useClassroom.ts

@@ -121,23 +121,25 @@ export const useClassroom = ({ user }:{ user : User }) => {
   const remoteCameraContainer = useRef<HTMLDivElement>(null); // 摄像头小窗容器
 
   // 辅助函数
-  const showMessage = (text: string, sender?: string, senderId?: string): void => {
+  const showMessage = (text: string, sender?: string, senderId?: string, senderType?: UserType): void => {
     const message: Message = {
       type: 'text',
       content: text,
       sender,
       senderId,
+      senderType,
       timestamp: Date.now()
     };
     setMessageList((prevMessageList) => [...prevMessageList, message]);
   };
 
-  const showImageMessage = (imageUrl: string, senderName: string, senderId?: string): void => {
+  const showImageMessage = (imageUrl: string, senderName: string, senderId?: string, senderType?: UserType): void => {
     const message: Message = {
       type: 'image',
       content: imageUrl,
       sender: senderName,
       senderId,
+      senderType,
       timestamp: Date.now()
     };
     setMessageList((prevMessageList) => [...prevMessageList, message]);
@@ -164,6 +166,7 @@ export const useClassroom = ({ user }:{ user : User }) => {
           content: message.content,
           senderId: message.senderId || null,
           senderName: message.sender || null,
+          senderType: message.senderType || null, // 添加senderType字段
           timestamp: message.timestamp,
           fileId: message.fileId || null // 添加fileId字段
         }
@@ -430,27 +433,27 @@ export const useClassroom = ({ user }:{ user : User }) => {
           console.error('解析踢人消息失败', err);
         }
       } else if (msg.type === 88888) { // 普通文本消息
-        const sender = msg.sender;
-        // 检查消息来源,避免重复处理自己发送的消息
+        const sender = msg.sender;  console.log('sender', sender)
         // 使用正确的 ImUser 类型处理发送者信息
-        const userExtension = sender?.userExtension ? JSON.parse(sender.userExtension) : {};
+        const userExtension = (sender?.userExtension ? JSON.parse(sender.userExtension) : {}) as User;
         const senderName = userExtension.nickname || userExtension.username || sender?.userId || '未知用户';
-        showMessage(msg.data, senderName, sender?.userId);
+        const senderType = userExtension.userType // 从用户扩展信息中获取类型
+        showMessage(msg.data, senderName, sender?.userId, senderType);
       } else if (msg.type === 88895) { // 图片消息
         try {
           const data = JSON.parse(msg.data);
           if (data.type === 'image' && data.fileId) {
             const sender = msg.sender;
-            // 检查消息来源,避免重复处理自己发送的消息
             // 使用正确的 ImUser 类型处理发送者信息
-            const userExtension = sender?.userExtension ? JSON.parse(sender.userExtension) : {};
+            const userExtension = (sender?.userExtension ? JSON.parse(sender.userExtension) : {}) as User;
             const senderName = userExtension.nickname || userExtension.username || sender?.userId || '未知用户';
+            const senderType = userExtension.userType // 从用户扩展信息中获取类型
             
             // 收到文件ID后获取文件详情
             const response = await fileClient[':id']['$get']({ param: { id: data.fileId.toString() } });
             if (response.status === 200) {
               const fileInfo = await response.json();
-              showImageMessage(fileInfo.fullUrl, senderName, sender?.userId);
+              showImageMessage(fileInfo.fullUrl, senderName, sender?.userId, senderType);
             } else {
               showSystemMessage(`${senderName}: 图片获取失败`);
             }
@@ -649,7 +652,10 @@ export const useClassroom = ({ user }:{ user : User }) => {
       await imEngine.current.login({
         user: {
           userId,
-          userExtension: JSON.stringify({ nickname: user?.nickname || user?.username || '' })
+          userExtension: JSON.stringify({ 
+            nickname: user?.nickname || user?.username || '',
+            userType: user.userType
+          })
         },
         userAuth: {
           nonce,
@@ -751,6 +757,7 @@ export const useClassroom = ({ user }:{ user : User }) => {
               content: msg.type === 'image' && msg.file ? msg.file.fullUrl : msg.content,
               sender: msg.senderName || '',
               senderId: msg.senderId?.toString() || '',
+              senderType: msg.senderType as UserType, // 添加senderType字段
               timestamp: msg.timestamp
             }));
             
@@ -860,6 +867,7 @@ export const useClassroom = ({ user }:{ user : User }) => {
         content: msgText,
         sender: user?.nickname || user?.username || '',
         senderId: userId,
+        senderType: user?.userType, // 添加用户类型
         timestamp: Date.now()
       };
       await saveMessageToDatabase(message);

+ 10 - 0
src/server/modules/chat/chat-message.entity.ts

@@ -1,6 +1,7 @@
 import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn } from 'typeorm';
 import { File } from '@/server/modules/files/file.entity';
 import { UserEntity } from '@/server/modules/users/user.entity';
+import { UserType } from '@/server/modules/users/user.enum';
 
 @Entity('chat_messages')
 export class ChatMessage {
@@ -62,6 +63,15 @@ export class ChatMessage {
   })
   senderName!: string | null;
 
+  @Column({ 
+    name: 'sender_type', 
+    type: 'enum', 
+    enum: UserType, 
+    nullable: true, 
+    comment: '发送者类型(teacher/student/trainee)' 
+  })
+  senderType!: UserType | null;
+
   @Column({ name: 'timestamp', type: 'bigint', comment: '消息时间戳' })
   timestamp!: number;
 

+ 13 - 0
src/server/modules/chat/chat-message.schema.ts

@@ -1,5 +1,6 @@
 import { z } from '@hono/zod-openapi';
 import { UserSchema } from '@/server/modules/users/user.schema';
+import { UserType } from '@/server/modules/users/user.enum';
 
 // 基础消息Schema
 export const ChatMessageSchema = z.object({
@@ -30,6 +31,10 @@ export const ChatMessageSchema = z.object({
     example: '张三',
     description: '发送者名称'
   }),
+  senderType: z.enum([UserType.TEACHER, UserType.STUDENT, UserType.TRAINEE]).nullable().openapi({
+    example: UserType.TEACHER,
+    description: '发送者用户类型(teacher/student/trainee)'
+  }),
   timestamp: z.coerce.number<number>().int().positive().openapi({
     example: 1704067200000,
     description: '消息时间戳'
@@ -87,6 +92,10 @@ export const CreateChatMessageDto = z.object({
     example: '张三',
     description: '发送者名称'
   }),
+  senderType: z.enum([UserType.TEACHER, UserType.STUDENT, UserType.TRAINEE]).nullable().optional().openapi({
+    example: UserType.TEACHER,
+    description: '发送者用户类型(teacher/student/trainee)'
+  }),
   timestamp: z.coerce.number().int().positive().openapi({
     example: 1704067200000,
     description: '消息时间戳(毫秒)'
@@ -119,6 +128,10 @@ export const UpdateChatMessageDto = z.object({
     example: '张三',
     description: '发送者名称'
   }),
+  senderType: z.enum([UserType.TEACHER, UserType.STUDENT, UserType.TRAINEE]).nullable().optional().openapi({
+    example: UserType.TEACHER,
+    description: '发送者用户类型(teacher/student/trainee)'
+  }),
   timestamp: z.coerce.number().int().positive().optional().openapi({
     example: 1704067200000,
     description: '消息时间戳'