/** * Copyright (c) 2025 TDCAT.CN */ import { router } from '@kit.ArkUI'; import promptAction from '@ohos.promptAction'; import settingsService, { SettingsModel, TextResources } from '../common/SettingsService'; import { ClassRoomService, ClassSessionModel, MessageModel, QuestionModel, QuestionOption, QuestionAnswer, WebSocketEventType, SenderRole, QuestionStatus, EventData } from '../common/ClassRoomService'; import logManager, { LogCategory, LogEventType } from '../common/logtext'; import http from '@ohos.net.http'; import hilog from '@ohos.hilog'; import WebSocketMessage from '../util/WebsocketMessage'; import { BusinessError } from '@kit.BasicServicesKit'; import { DatabaseService } from '../common/DatabaseService'; // 添加hilog和tianChannel常量 const TIAN_CHANNEL_DOMAIN_ID = 0x00201; // 自定义域ID const TIAN_CHANNEL_TAG = 'tianChannel'; // 自定义TAG用于筛选日志 // 定义选项对象接口 interface ScrollOptions { index: number; } // 定义ListElement接口来替代使用UIMockCore的导入 interface ListElement { scrollTo(options: ScrollOptions): void; } // 路由URL接口 interface RouterUrlOptions { url: string; params?: object; } // 路由参数接口 interface RouteParams { mode?: string; } // 选项统计接口 interface OptionStats { count: number; percentage: number; } // 正确率统计接口 interface CorrectRateStats { correctCount: number; totalCount: number; percentage: number; } // 统计数据接口 interface StatisticsData { correctRate: CorrectRateStats; getOptionStats: (index: number) => OptionStats; } // 对话框按钮定义 interface DialogButton { value: string; action: () => void; } // 对话框配置 interface AlertDialogConfig { title: string; message: string; primaryButton: DialogButton; secondaryButton?: DialogButton; } // 课堂界面模式 enum ClassLiveMode { TEACHER = 'teacher', STUDENT = 'student' } @Entry @Component struct ClassLivePage { // 从路由参数获取模式 @State mode: ClassLiveMode = ClassLiveMode.STUDENT; // 课堂会话 @State classSession: ClassSessionModel | null = null; // 消息列表 @State messages: MessageModel[] = []; // 当前消息 @State messageText: string = ''; // 当前题目 @State currentQuestion: QuestionModel | null = null; // 学生已选答案 @State selectedOption: number = -1; // 是否显示题目统计 @State showQuestionStats: boolean = false; // 是否显示题目编辑器 @State showQuestionEditor: boolean = false; // 是否显示答题窗口 @State showAnswerDialog: boolean = false; // 题目编辑相关状态 @State questionTitle: string = ''; @State questionOptions: QuestionOption[] = []; @State correctOption: number = 0; @State questionDuration: number = 30; // 错误信息 @State errorMessage: string = ''; // 是否加载中 @State isLoading: boolean = false; // 倒计时 @State countdownSeconds: number = 0; private countdownTimer: number = -1; // 从设置服务获取设置 @State settings: SettingsModel = settingsService.getSettings(); // 获取文本资源 @State texts: TextResources = settingsService.getTextResources(); // 事件回调函数引用 private messageCallback: (data: EventData) => void = () => { }; private questionCallback: (data: EventData) => void = () => { }; private endQuestionCallback: (data: EventData) => void = () => { }; private endClassCallback: (data: EventData) => void = () => { }; // 用于服务实例 private classRoomService: ClassRoomService = ClassRoomService.getInstance(); wsClient: WebSocketMessage = new WebSocketMessage(DatabaseService.getApiConfig().websocketDomain, "111111") aboutToAppear() { this.wsClient.connect(); // 注册语言变化的回调 settingsService.registerLanguageChangeCallback(() => { this.texts = settingsService.getTextResources(); }); // 注册主题颜色变化的回调 settingsService.registerColorChangeCallback(() => { this.settings = settingsService.getSettings(); }); // 获取路由参数中的模式 const params = router.getParams() as RouteParams; if (params && params.mode) { this.mode = params.mode as ClassLiveMode; } // 获取当前课堂会话 this.classSession = this.classRoomService.getCurrentSession(); if (!this.classSession) { // 如果没有课堂会话,返回上课页面 this.showErrorAndReturn('未找到课堂会话,请重新加入或创建课堂'); return; } // 获取当前消息列表 this.messages = this.classSession.messages || []; // 获取当前活动题目 this.currentQuestion = this.classRoomService.getCurrentQuestion(); // 注册事件监听 this.registerEventListeners(); logManager.info(LogCategory.CLASS, LogEventType.PAGE_APPEAR, 'ClassLivePage'); this.wsClient.setOnMessage((data) => { hilog.debug(TIAN_CHANNEL_DOMAIN_ID, TIAN_CHANNEL_TAG, "收到消息: " + data.toString()); try { let j: MessageModel = MessageModel.fromJSON(data.toString()) //JSON.parse(data.toString()) as MessageModel this.messages.push(j) hilog.debug(TIAN_CHANNEL_DOMAIN_ID, TIAN_CHANNEL_TAG, "渲染: " + j); // this.messages.push(data.toString()) } catch (e) { hilog.debug(TIAN_CHANNEL_DOMAIN_ID, TIAN_CHANNEL_TAG, "消息解析失败: " + e); } }); } // 注册事件监听 private registerEventListeners() { // 接收新消息 this.messageCallback = (data: EventData) => { const message = data as MessageModel; message.useTime() if (message && message.id) { // 检查消息是否已经存在(防止重复) const existingMessage = this.messages.find(m => m.id === message.id); if (!existingMessage) { // 输出调试信息 console.info(`收到新消息事件: ID=${message.id}, 发送者=${message.senderName}, 内容=${message.content}`); // 使用新数组方式更新消息列表,确保 UI 刷新 this.messages = [...this.messages, message]; // 新消息到达时自动滚动到底部 this.scrollToLatestMessage(); } else { // 记录重复消息(不常见,但可能发生) console.info(`收到重复消息,已忽略: ID=${message.id}`); } } }; this.classRoomService.addEventListener(WebSocketEventType.SEND_MESSAGE, this.messageCallback); // 题目发布 this.questionCallback = (data: EventData) => { const question = data as QuestionModel; if (question && question.questionId) { this.currentQuestion = question; if (this.mode === ClassLiveMode.STUDENT) { // 学生模式,显示答题窗口 this.selectedOption = -1; this.showAnswerDialog = true; } // 设置倒计时 this.startCountdown(question.duration); } }; this.classRoomService.addEventListener(WebSocketEventType.PUBLISH_QUESTION, this.questionCallback); // 题目结束 this.endQuestionCallback = (data: EventData) => { const question = data as QuestionModel; if (question && question.questionId) { // 更新题目状态 this.currentQuestion = question; // 停止倒计时 this.stopCountdown(); // 显示题目统计 this.showAnswerDialog = false; this.showQuestionStats = true; } }; this.classRoomService.addEventListener(WebSocketEventType.END_QUESTION, this.endQuestionCallback); // 课堂结束 this.endClassCallback = (data: EventData) => { const session = data as ClassSessionModel; if (session && session.sessionId) { // 显示课堂结束提示 const primaryButton: DialogButton = { value: '确定', action: () => { // 返回上课页面 - 使用明确的导航而不是back() router.pushUrl({ url: 'pages/ClassPage' } as RouterUrlOptions); } }; const alertConfig: AlertDialogConfig = { title: '课堂已结束', message: '教师已结束当前课堂', primaryButton: primaryButton }; AlertDialog.show(alertConfig); } }; this.classRoomService.addEventListener(WebSocketEventType.END_CLASS, this.endClassCallback); } // 创建问题编辑器内的选项 private addOption() { if (!this.questionOptions) { this.questionOptions = []; } const newIndex = this.questionOptions.length; const newOption: QuestionOption = { index: newIndex, content: '' }; this.questionOptions.push(newOption); } // 更新选项内容 private updateOptionContent(index: number, content: string) { if (index >= 0 && index < this.questionOptions.length) { const options = [...this.questionOptions]; options[index].content = content; this.questionOptions = options; } } // 移除选项 private removeOption(index: number) { if (this.questionOptions.length <= 2) { this.errorMessage = '至少需要两个选项'; return; } // 移除选项 this.questionOptions = this.questionOptions.filter((_, i) => i !== index); // 重新编号 for (let i = 0; i < this.questionOptions.length; i++) { this.questionOptions[i].index = i; } // 如果正确选项是被删除的选项或之后的选项,需要调整 if (this.correctOption >= index) { // 如果是最后一个选项,则正确选项为前一个 if (this.correctOption >= this.questionOptions.length) { this.correctOption = this.questionOptions.length - 1; } } } // 计算选项计数和百分比 private getOptionStats(index: number): OptionStats { if (!this.currentQuestion || this.currentQuestion.answers.length === 0) { const emptyStats: OptionStats = { count: 0, percentage: 0 }; return emptyStats; } // 使用普通for循环来避免索引访问的问题 let optionCount = 0; for (let i = 0; i < this.currentQuestion.answers.length; i++) { const answer = this.currentQuestion.answers[i]; if (answer.selectedOption === index) { optionCount++; } } const totalCount = this.currentQuestion.answers.length; const percentage = totalCount > 0 ? Math.round((optionCount / totalCount) * 100) : 0; const stats: OptionStats = { count: optionCount, percentage: percentage }; return stats; } // 计算正确率 private getCorrectRate(): CorrectRateStats { if (!this.currentQuestion || this.currentQuestion.answers.length === 0) { const emptyStats: CorrectRateStats = { correctCount: 0, totalCount: 0, percentage: 0 }; return emptyStats; } // 使用普通for循环来避免索引访问的问题 let correctCount = 0; for (let i = 0; i < this.currentQuestion.answers.length; i++) { const answer = this.currentQuestion.answers[i]; if (answer.isCorrect) { correctCount++; } } const totalCount = this.currentQuestion.answers.length; const percentage = totalCount > 0 ? Math.round((correctCount / totalCount) * 100) : 0; const stats: CorrectRateStats = { correctCount: correctCount, totalCount: totalCount, percentage: percentage }; return stats; } // 获取当前用户的答案 private getMyAnswer(): QuestionAnswer | null { if (!this.currentQuestion) { return null; } const currentAccount = settingsService.getCurrentAccount(); // 使用for循环查找 for (let i = 0; i < this.currentQuestion.answers.length; i++) { const answer = this.currentQuestion.answers[i]; if (answer.studentAccount === currentAccount) { return answer; } } return null; } // 重置题目编辑器 private resetQuestionEditor() { this.questionTitle = ''; this.questionOptions = []; this.correctOption = 0; this.questionDuration = 30; this.errorMessage = ''; } // 发送消息 private async sendMessage() { hilog.info(TIAN_CHANNEL_DOMAIN_ID, TIAN_CHANNEL_TAG, `[UI] ======== 开始UI层消息发送 ========`); if (!this.messageText || this.messageText.trim() === '') { hilog.info(TIAN_CHANNEL_DOMAIN_ID, TIAN_CHANNEL_TAG, `[UI] 消息内容为空,不发送`); return; } try { // 防止重复点击、重复发送 if (this.isLoading) { hilog.info(TIAN_CHANNEL_DOMAIN_ID, TIAN_CHANNEL_TAG, `[UI] 正在加载中,防止重复发送`); return; } hilog.info(TIAN_CHANNEL_DOMAIN_ID, TIAN_CHANNEL_TAG, `[UI] 设置加载状态为true`); this.isLoading = true; // 保存消息内容,因为后面会清空输入框 const messageContent = this.messageText.trim(); hilog.info(TIAN_CHANNEL_DOMAIN_ID, TIAN_CHANNEL_TAG, `[UI] 消息内容: ${messageContent}`); // 创建一个本地消息对象用于立即显示 hilog.info(TIAN_CHANNEL_DOMAIN_ID, TIAN_CHANNEL_TAG, `[UI] 创建本地消息对象`); const localMessage = new MessageModel( settingsService.getCurrentAccount(), settingsService.getUserNickname() || settingsService.getCurrentAccount(), this.mode === ClassLiveMode.TEACHER ? SenderRole.TEACHER : SenderRole.STUDENT, messageContent ); // 保存消息ID,以便后续可以在发送失败时移除 const localMessageId = localMessage.id; hilog.info(TIAN_CHANNEL_DOMAIN_ID, TIAN_CHANNEL_TAG, `[UI] 本地消息ID: ${localMessageId}`); // 清空消息输入框 - 提前清空避免用户重复点击 hilog.info(TIAN_CHANNEL_DOMAIN_ID, TIAN_CHANNEL_TAG, `[UI] 清空输入框`); this.messageText = ''; // 先在本地添加消息,让用户立即看到 hilog.info(TIAN_CHANNEL_DOMAIN_ID, TIAN_CHANNEL_TAG, `[UI] 添加消息到本地列表`); console.info(`添加本地消息: ID=${localMessageId}, 内容=${messageContent}`); this.messages = [...this.messages, localMessage]; // 自动滚动到最新消息 hilog.info(TIAN_CHANNEL_DOMAIN_ID, TIAN_CHANNEL_TAG, `[UI] 滚动到最新消息`); this.scrollToLatestMessage(); // 发送消息到服务器 hilog.info(TIAN_CHANNEL_DOMAIN_ID, TIAN_CHANNEL_TAG, `[UI] 调用服务层发送消息`); try { const result = await this.classRoomService.sendMessage(messageContent); hilog.info(TIAN_CHANNEL_DOMAIN_ID, TIAN_CHANNEL_TAG, `[UI] 服务层返回结果: ${result}`); if (!result) { // 如果发送失败,从消息列表中移除本地消息 hilog.error(TIAN_CHANNEL_DOMAIN_ID, TIAN_CHANNEL_TAG, `[UI] 消息发送失败,移除本地消息: ${localMessageId}`); console.error(`消息发送失败,移除本地消息: ${localMessageId}`); this.messages = this.messages.filter(msg => msg.id !== localMessageId); // 显示错误提示 hilog.info(TIAN_CHANNEL_DOMAIN_ID, TIAN_CHANNEL_TAG, `[UI] 显示发送失败提示`); promptAction.showToast({ message: '消息发送失败,请重试', duration: 2000 }); } else { hilog.info(TIAN_CHANNEL_DOMAIN_ID, TIAN_CHANNEL_TAG, `[UI] 消息发送成功: ${localMessageId}`); console.info(`消息发送成功: ${localMessageId}`); } } catch (serviceError) { // 处理服务层异常 hilog.error(TIAN_CHANNEL_DOMAIN_ID, TIAN_CHANNEL_TAG, `[UI] 服务层异常: ${serviceError}`); // 如果发送失败,从消息列表中移除本地消息 hilog.error(TIAN_CHANNEL_DOMAIN_ID, TIAN_CHANNEL_TAG, `[UI] 由于服务层异常,移除本地消息: ${localMessageId}`); this.messages = this.messages.filter(msg => msg.id !== localMessageId); // 显示错误提示 promptAction.showToast({ message: '消息发送失败,请重试', duration: 2000 }); } } catch (error) { // 处理错误 const errorMessage = error instanceof Error ? error.message : String(error); const errorStack = error instanceof Error ? error.stack : 'No stack trace'; hilog.error(TIAN_CHANNEL_DOMAIN_ID, TIAN_CHANNEL_TAG, `[UI] 顶层异常: ${errorMessage}`); hilog.error(TIAN_CHANNEL_DOMAIN_ID, TIAN_CHANNEL_TAG, `[UI] 异常堆栈: ${errorStack}`); logManager.error(LogCategory.CLASS, `Send message error: ${errorMessage}`); promptAction.showToast({ message: '消息发送失败,请重试', duration: 2000 }); } finally { hilog.info(TIAN_CHANNEL_DOMAIN_ID, TIAN_CHANNEL_TAG, `[UI] 设置加载状态为false`); this.isLoading = false; hilog.info(TIAN_CHANNEL_DOMAIN_ID, TIAN_CHANNEL_TAG, `[UI] ======== 结束UI层消息发送 ========`); } } // 自动滚动到最新消息 private scrollToLatestMessage() { console.info(`准备滚动到最新消息,当前消息数量: ${this.messages.length}`); setTimeout(() => { if (this.messages.length > 0) { try { // 直接访问DOM上的messageList元素 // 注意:当前版本不直接支持选择器,将使用替代方式滚动 console.info(`尝试滚动到索引: ${this.messages.length - 1}`); // 由于直接DOM选择器的限制,实际实现可能需根据实际框架支持调整 console.info('滚动到最新消息完成'); } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); console.error(`滚动到最新消息失败: ${errorMsg}`); } } else { console.info('没有消息,不需要滚动'); } }, 200); // 增加延迟确保DOM已更新 } // 开始倒计时 private startCountdown(seconds: number) { // 停止之前的计时器 this.stopCountdown(); // 设置初始倒计时 this.countdownSeconds = seconds; // 创建新计时器 - 使用明确的类型 this.countdownTimer = setInterval(() => { if (this.countdownSeconds > 0) { this.countdownSeconds--; } else { // 倒计时结束 this.stopCountdown(); } }, 1000) as number; } // 停止倒计时 private stopCountdown() { if (this.countdownTimer !== -1) { clearInterval(this.countdownTimer); this.countdownTimer = -1; } } // 发布题目 private async publishQuestion() { if (!this.questionTitle || this.questionTitle.trim() === '') { this.errorMessage = '请输入题目内容'; return; } if (this.questionOptions.length < 2) { this.errorMessage = '请至少添加两个选项'; return; } this.isLoading = true; try { const question = await this.classRoomService.publishQuestion( this.questionTitle, this.questionOptions, this.correctOption, this.questionDuration ); if (question) { // 清空题目编辑器 this.resetQuestionEditor(); // 关闭题目编辑器 this.showQuestionEditor = false; } else { this.errorMessage = '发布题目失败,请稍后再试'; } } catch (error) { this.errorMessage = '发布题目过程中发生错误'; const errorMessage = error instanceof Error ? error.message : String(error); logManager.error(LogCategory.CLASS, `Publish question error: ${errorMessage}`); } finally { this.isLoading = false; } } // 提交答案 private async submitAnswer() { if (this.selectedOption < 0 || !this.currentQuestion) { this.errorMessage = '请选择一个选项'; return; } this.isLoading = true; try { const result = await this.classRoomService.submitAnswer( this.currentQuestion.questionId, this.selectedOption ); if (result) { // 关闭答题窗口 this.showAnswerDialog = false; // 清空错误消息 this.errorMessage = ''; } else { this.errorMessage = '提交答案失败,请稍后再试'; } } catch (error) { this.errorMessage = '提交答案过程中发生错误'; const errorMessage = error instanceof Error ? error.message : String(error); logManager.error(LogCategory.CLASS, `Submit answer error: ${errorMessage}`); } finally { this.isLoading = false; } } // 结束课堂 private async endClass() { this.isLoading = true; try { const result = await this.classRoomService.endClassSession(); if (!result) { // 只在失败时显示错误提示,成功时不跳转,等待服务器通知 const primaryButton: DialogButton = { value: '确定', action: () => { } }; const alertConfig: AlertDialogConfig = { title: '错误', message: '结束课堂失败,请稍后再试', primaryButton: primaryButton }; AlertDialog.show(alertConfig); } // 成功时不做任何操作,等待endClassCallback处理跳转 } catch (error) { // 显示错误提示 const primaryButton: DialogButton = { value: '确定', action: () => { } }; const alertConfig: AlertDialogConfig = { title: '错误', message: '结束课堂失败,请稍后再试', primaryButton: primaryButton }; AlertDialog.show(alertConfig); const errorMessage = error instanceof Error ? error.message : String(error); logManager.error(LogCategory.CLASS, `End class error: ${errorMessage}`); } finally { this.isLoading = false; } } // 显示错误并返回 private showErrorAndReturn(message: string) { const primaryButton: DialogButton = { value: '确定', action: () => { // 使用明确的导航而不是back() router.pushUrl({ url: 'pages/ClassPage' } as RouterUrlOptions); } }; const alertConfig: AlertDialogConfig = { title: '错误', message: message, primaryButton: primaryButton }; AlertDialog.show(alertConfig); } // 格式化时间 private formatTime(timestamp: number): string { const date = new Date(timestamp); const hours = date.getHours().toString().padStart(2, '0'); const minutes = date.getMinutes().toString().padStart(2, '0'); return `${hours}:${minutes}`; } // 查找组件树中指定ID的元素 - 简化实现 private findChildById(id: string): ListElement | null { // 由于框架限制,返回null以避免编译错误 // 在实际支持选择器的环境中实现此方法 console.info(`尝试查找元素: ${id}`); return null; } aboutToDisappear() { // 移除事件监听 if (this.messageCallback) { this.classRoomService.removeEventListener(WebSocketEventType.SEND_MESSAGE, this.messageCallback); } if (this.questionCallback) { this.classRoomService.removeEventListener(WebSocketEventType.PUBLISH_QUESTION, this.questionCallback); } if (this.endQuestionCallback) { this.classRoomService.removeEventListener(WebSocketEventType.END_QUESTION, this.endQuestionCallback); } if (this.endClassCallback) { this.classRoomService.removeEventListener(WebSocketEventType.END_CLASS, this.endClassCallback); } // 停止倒计时 this.stopCountdown(); logManager.info(LogCategory.CLASS, LogEventType.PAGE_DISAPPEAR, 'ClassLivePage'); } onBackPress() { logManager.info(LogCategory.CLASS, LogEventType.PAGE_BACK, 'ClassLivePage'); // 拦截返回键,显示确认对话框 if (this.mode === ClassLiveMode.TEACHER) { const primaryButton: DialogButton = { value: '确定', action: () => { this.endClass(); } }; const secondaryButton: DialogButton = { value: '取消', action: () => { } }; const alertConfig: AlertDialogConfig = { title: '离开课堂', message: '确定要离开当前课堂吗?这将结束整个课堂。', primaryButton: primaryButton, secondaryButton: secondaryButton }; AlertDialog.show(alertConfig); } else { const primaryButton: DialogButton = { value: '确定', action: () => { // 使用明确的导航而不是back() router.pushUrl({ url: 'pages/ClassPage' } as RouterUrlOptions); } }; const secondaryButton: DialogButton = { value: '取消', action: () => { } }; const alertConfig: AlertDialogConfig = { title: '离开课堂', message: '确定要离开当前课堂吗?', primaryButton: primaryButton, secondaryButton: secondaryButton }; AlertDialog.show(alertConfig); } // 返回true表示已处理返回事件 return true; } build() { Stack({ alignContent: Alignment.TopStart }) { Column() { // 顶部导航栏 Row() { Row() { Image($r('app.media.back')) .width(24) .height(24) .fillColor(Color.White) .margin({ right: 16 }) .onClick(() => { this.onBackPress(); }) Text(this.classSession?.className || '在线课堂') .fontSize(20) .fontWeight(FontWeight.Bold) .fontColor(Color.White) } Row() { // 添加连接检查按钮 Button("检查") .fontSize(14) .fontWeight(FontWeight.Medium) .backgroundColor('rgba(255,255,255,0.2)') .borderRadius(16) .fontColor(Color.White) .padding({ left: 12, right: 12, top: 6, bottom: 6 }) .margin({ right: 8 }) .onClick(() => { this.checkConnectionStatus(); }) // 教师端显示结束课堂按钮 if (this.mode === ClassLiveMode.TEACHER) { Button("结束课堂") .fontSize(14) .fontWeight(FontWeight.Medium) .backgroundColor('rgba(255,255,255,0.2)') .borderRadius(16) .fontColor(Color.White) .padding({ left: 12, right: 12, top: 6, bottom: 6 }) .onClick(() => { this.endClass(); }) } } } .width('100%') .backgroundColor(this.settings.themeColor) .height(60) .justifyContent(FlexAlign.SpaceBetween) .padding({ left: 16, right: 16 }) // 使用Flex布局来确保页面元素适应不同屏幕尺寸 Flex({ direction: FlexDirection.Column }) { // 顶部固定区域(课堂信息和通知) Column() { // 课堂信息区域 Column() { Text(`课堂名称: ${this.classSession?.className || '-'}`) .fontSize(16) .margin({ bottom: 5 }) Text(`授课教师: ${this.classSession?.teacherName || '-'}`) .fontSize(16) .margin({ bottom: 5 }) Text(`角色: ${this.mode === ClassLiveMode.TEACHER ? '教师' : '学生'}`) .fontSize(16) } .width('100%') .padding(10) .backgroundColor('#f5f5f5') .borderRadius(8) .margin({ bottom: 10 }) // 通知消息 if (this.currentQuestion) { Column() { Text("当前有一个正在进行的题目") .fontSize(16) .fontWeight(FontWeight.Bold) .margin({ bottom: 10 }) Text(`题目: ${this.currentQuestion.title}`) .fontSize(16) .margin({ bottom: 10 }) if (this.countdownSeconds > 0) { Text(`剩余时间: ${this.countdownSeconds}秒`) .fontSize(16) .fontColor(this.countdownSeconds > 10 ? '#333' : '#F56C6C') .margin({ bottom: 10 }) } } .width('100%') .padding(16) .backgroundColor('#f0f0f0') .borderRadius(8) .margin({ bottom: 10 }) } } .width('100%') // 聊天历史记录区域(可伸缩) Column() { // Text("聊天记录") // .fontSize(18) // .fontWeight(FontWeight.Bold) // .margin({ bottom: 10 }) List({ space: 8 }) { ForEach(this.messages, (message: MessageModel, index) => { ListItem() { Row() { // 消息气泡 Column() { // 发送者信息 Row() { Text(message.senderName) .fontSize(14) .fontWeight(FontWeight.Bold) Text(message.senderRole === SenderRole.TEACHER ? ' (教师)' : ' (学生)') .fontSize(12) .fontColor('#666') } .width('100%') .margin({ bottom: 4 }) // 消息内容 Text(message.content) .fontSize(16) .margin({ top: 4 }) // 时间 Text(this.formatTime(message.timestamp.getTime())) .fontSize(12) .fontColor('#999') .margin({ top: 4 }) .alignSelf(message.senderId === settingsService.getCurrentAccount() ? ItemAlign.End : ItemAlign.Start) } .padding(12) .backgroundColor(message.senderRole === SenderRole.TEACHER ? '#e1f3d8' : '#edf2fc') .borderRadius(8) .alignSelf(ItemAlign.Start) .width('85%') } .width('100%') .justifyContent(message.senderId === settingsService.getCurrentAccount() ? FlexAlign.End : FlexAlign.Start) .margin({ top: 4, bottom: 4 }) } }, (message: MessageModel) => message.id) // 使用消息ID作为唯一键,避免重复渲染 } .width('100%') // .layoutWeight(1) .padding(10) .backgroundColor('#f9f9f9') .borderRadius(8) .scrollBar(BarState.Auto) .id('messageList') .onAppear(() => { // 首次显示时滚动到底部 this.scrollToLatestMessage(); }) } .flexGrow(1) // 使聊天记录区域占用所有可用空间 .width('100%') // 底部固定区域 (错误消息、加载状态和输入框) Column() { // 错误消息和加载状态 if (this.errorMessage) { Text(this.errorMessage) .fontSize(14) .fontColor('#F56C6C') .width('100%') .textAlign(TextAlign.Center) .margin({ top: 8, bottom: 8 }) } if (this.isLoading) { Row() { LoadingProgress() .width(24) .height(24) .margin({ right: 10 }) Text("处理中...") .fontSize(14) .fontColor('#666') } .justifyContent(FlexAlign.Center) .width('100%') .margin({ top: 8, bottom: 8 }) } // 消息输入区域 Row() { TextInput({ placeholder: '输入消息...', text: this.messageText }) .layoutWeight(1) .height(44) .borderRadius(22) .backgroundColor('#f5f5f5') .padding({ left: 20, right: 20 }) .onChange((value: string) => { this.messageText = value; }) Button("发送") .height(44) .width(80) .margin({ left: 10 }) .borderRadius(22) .backgroundColor(this.settings.themeColor) .onClick(() => { this.sendMessage(); }) } .width('100%') .padding({ top: 8, bottom: 8 }) } .width('100%') } .width('100%') .layoutWeight(1) // 确保Flex容器占据全部可用空间 .padding(16) } .width('100%') .height('100%') // 题目编辑窗口 if (this.showQuestionEditor) { this.QuestionEditorDialog() } // 题目答题窗口 if (this.showAnswerDialog && this.currentQuestion) { this.QuestionAnswerDialog() } // 题目统计窗口 if (this.showQuestionStats && this.currentQuestion) { this.QuestionStatsDialog() } // 浮动操作按钮区域 Column() { if (this.mode === ClassLiveMode.TEACHER && !this.showQuestionEditor) { Button({ type: ButtonType.Circle }) { Image($r('app.media.add')) .width(24) .height(24) .fillColor(Color.White) } .width(56) .height(56) .backgroundColor(this.settings.themeColor) .shadow({ radius: 8, color: 'rgba(0, 0, 0, 0.2)' }) .onClick(() => { this.showQuestionEditor = true; // 初始化题目选项 if (this.questionOptions.length === 0) { this.addOption(); this.addOption(); } }) } if (this.mode === ClassLiveMode.STUDENT && this.currentQuestion && !this.showAnswerDialog && !this.showQuestionStats) { Button({ type: ButtonType.Circle }) { Image($r('app.media.question')) .width(24) .height(24) .fillColor(Color.White) } .width(56) .height(56) .backgroundColor(this.settings.themeColor) .shadow({ radius: 8, color: 'rgba(0, 0, 0, 0.2)' }) .onClick(() => { this.showAnswerDialog = true; }) } } .position({ x: 24, y: '85%' }) } .width('100%') .height('100%') } // 题目编辑窗口 @Builder QuestionEditorDialog() { Column() { // 对话框内容 Column() { // 标题 Row() { Text("发布题目") .fontSize(20) .fontWeight(FontWeight.Bold) Blank() Button({ type: ButtonType.Circle }) { Image($r('app.media.close')) .width(16) .height(16) } .width(32) .height(32) .backgroundColor('#f0f0f0') .onClick(() => { this.showQuestionEditor = false; }) } .width('100%') .margin({ bottom: 20 }) // 题目内容 Column({ space: 10 }) { Text("题目内容") .fontSize(16) .fontColor('#666666') .alignSelf(ItemAlign.Start) TextArea({ placeholder: '请输入题目内容', text: this.questionTitle }) .width('100%') .height(100) .fontSize(16) .backgroundColor('#F5F5F5') .borderRadius(8) .padding(10) .onChange((value: string) => { this.questionTitle = value; }) } .width('100%') .margin({ bottom: 16 }) // 选项部分 Column({ space: 10 }) { Row() { Text("选项") .fontSize(16) .fontColor('#666666') Blank() Button({ type: ButtonType.Circle }) { Image($r('app.media.add')) .width(16) .height(16) } .width(32) .height(32) .backgroundColor(this.settings.themeColor) .onClick(() => { this.addOption(); }) } .width('100%') // 选项列表 Column({ space: 10 }) { ForEach(this.questionOptions, (option: QuestionOption, index: number) => { Row() { Radio({ value: index.toString(), group: 'correctOption' }) .checked(index === this.correctOption) .onChange((isChecked: boolean) => { if (isChecked) { this.correctOption = index; } }) Text(`选项 ${String.fromCharCode(65 + index)}`) .fontSize(16) .fontWeight(FontWeight.Medium) .margin({ left: 8, right: 8 }) TextInput({ placeholder: '请输入选项内容', text: option.content }) .layoutWeight(1) .height(40) .fontSize(16) .backgroundColor('#F5F5F5') .borderRadius(8) .padding({ left: 10, right: 10 }) .onChange((value: string) => { this.updateOptionContent(index, value); }) // 删除选项按钮 if (this.questionOptions.length > 2) { Button({ type: ButtonType.Circle }) { Image($r('app.media.delete')) .width(16) .height(16) } .width(32) .height(32) .backgroundColor('#f5f5f5') .margin({ left: 8 }) .onClick(() => { this.removeOption(index); }) } } .width('100%') .alignItems(VerticalAlign.Center) }) } .width('100%') } .width('100%') .margin({ bottom: 16 }) // 答题时间 Column({ space: 10 }) { Text("答题时间(秒)") .fontSize(16) .fontColor('#666666') .alignSelf(ItemAlign.Start) Row() { Slider({ value: this.questionDuration, min: 10, max: 120, step: 5, style: SliderStyle.OutSet }) .layoutWeight(1) .blockColor(this.settings.themeColor) .trackColor('#E1E1E1') .selectedColor(this.settings.themeColor) .showSteps(true) .showTips(true) .onChange((value: number) => { this.questionDuration = value; }) Text(this.questionDuration.toString()) .fontSize(16) .fontColor('#333') .margin({ left: 16 }) .width(40) .textAlign(TextAlign.End) } .width('100%') } .width('100%') .margin({ bottom: 20 }) // 错误信息 if (this.errorMessage) { Text(this.errorMessage) .fontSize(14) .fontColor('#F56C6C') .width('100%') .textAlign(TextAlign.Center) .margin({ bottom: 16 }) } // 发布按钮 Button("发布题目") .width('100%') .height(45) .fontSize(16) .fontWeight(FontWeight.Medium) .backgroundColor(this.settings.themeColor) .borderRadius(8) .fontColor(Color.White) .enabled(!this.isLoading) .opacity(this.isLoading ? 0.6 : 1) .onClick(() => { this.publishQuestion(); }) } .width('90%') .backgroundColor(Color.White) .borderRadius(16) .padding(24) } .width('100%') .height('100%') .backgroundColor('rgba(0,0,0,0.5)') .justifyContent(FlexAlign.Center) .onClick(() => { // 点击遮罩不关闭对话框 }) } // 答题窗口 @Builder QuestionAnswerDialog() { Column() { // 对话框内容 Column() { // 标题 Row() { Text("答题") .fontSize(20) .fontWeight(FontWeight.Bold) Blank() Button({ type: ButtonType.Circle }) { Image($r('app.media.close')) .width(16) .height(16) } .width(32) .height(32) .backgroundColor('#f0f0f0') .onClick(() => { this.showAnswerDialog = false; }) } .width('100%') .margin({ bottom: 20 }) // 题目内容 Column() { Text(this.currentQuestion?.title || '') .fontSize(18) .fontWeight(FontWeight.Medium) .margin({ bottom: 10 }) if (this.countdownSeconds > 0) { Text(`剩余时间: ${this.countdownSeconds}秒`) .fontSize(16) .fontColor(this.countdownSeconds > 10 ? '#333' : '#F56C6C') .margin({ bottom: 20 }) } } .width('100%') .alignItems(HorizontalAlign.Start) .margin({ bottom: 16 }) // 选项列表 Column({ space: 10 }) { if (this.currentQuestion) { ForEach(this.currentQuestion.options, (option: QuestionOption, index: number) => { Row() { Radio({ value: index.toString(), group: 'answerOption' }) .checked(this.selectedOption === index) .onChange((isChecked: boolean) => { if (isChecked) { this.selectedOption = index; } }) Text(`${String.fromCharCode(65 + index)}. ${option.content}`) .fontSize(16) .margin({ left: 10 }) .layoutWeight(1) } .width('100%') .padding(10) .borderRadius(8) .backgroundColor(this.selectedOption === index ? '#f0f7ff' : '#f5f5f5') .alignItems(VerticalAlign.Center) }) } } .width('100%') .margin({ bottom: 20 }) // 错误信息 if (this.errorMessage) { Text(this.errorMessage) .fontSize(14) .fontColor('#F56C6C') .width('100%') .textAlign(TextAlign.Center) .margin({ bottom: 16 }) } // 提交按钮 Button("提交答案") .width('100%') .height(45) .fontSize(16) .fontWeight(FontWeight.Medium) .backgroundColor(this.settings.themeColor) .borderRadius(8) .fontColor(Color.White) .enabled(!this.isLoading) .opacity(this.isLoading ? 0.6 : 1) .onClick(() => { this.submitAnswer(); }) } .width('90%') .backgroundColor(Color.White) .borderRadius(16) .padding(24) } .width('100%') .height('100%') .backgroundColor('rgba(0,0,0,0.5)') .justifyContent(FlexAlign.Center) .onClick(() => { // 点击遮罩不关闭对话框 }) } // 获取题目统计数据 - 辅助方法 private getStatisticsData(): StatisticsData { return { correctRate: this.getCorrectRate(), getOptionStats: (index: number) => this.getOptionStats(index) }; } // 题目统计内容 @Builder QuestionStatsContent() { Column() { // 总体正确率 Row() { Text("参与人数: ") .fontSize(16) .fontWeight(FontWeight.Medium) Text(`${this.getCorrectRate().totalCount}人`) .fontSize(16) .fontWeight(FontWeight.Medium) Blank() Text("正确率: ") .fontSize(16) .fontWeight(FontWeight.Medium) Text(`${this.getCorrectRate().percentage}%`) .fontSize(16) .fontColor(this.getCorrectRate().percentage > 50 ? '#67C23A' : '#F56C6C') .fontWeight(FontWeight.Medium) } .width('100%') .padding(10) .borderRadius(8) .backgroundColor('#f5f5f5') .margin({ bottom: 16 }) // 选项统计 if (this.currentQuestion) { ForEach(this.currentQuestion.options, (option: QuestionOption, index: number) => { Column() { Row() { Text(`选项 ${String.fromCharCode(65 + index)}`) .fontSize(16) .fontWeight(FontWeight.Medium) Text(`${this.getOptionStats(index).count}票`) .fontSize(16) .fontWeight(FontWeight.Medium) Text(`${this.getOptionStats(index).percentage}%`) .fontSize(16) .fontColor(this.getOptionStats(index).percentage > 50 ? '#67C23A' : '#F56C6C') .fontWeight(FontWeight.Medium) } .width('100%') .padding(10) .borderRadius(8) .backgroundColor('#f5f5f5') .margin({ bottom: 8 }) } }) } } } // 增加检查连接状态方法 private checkConnectionStatus() { try { const connectionStatus = this.classRoomService.isConnected() ? '已连接' : '未连接'; const messageCount = this.messages.length; const lastMessageId = this.messages.length > 0 ? this.messages[this.messages.length - 1].id : '无消息'; // 显示状态对话框 const primaryButton: DialogButton = { value: '确定', action: () => { } }; const secondaryButton: DialogButton = { value: '重新连接', action: () => { // 尝试重新连接 this.classRoomService.reconnect(); } }; const alertConfig: AlertDialogConfig = { title: '连接状态检查', message: `连接状态: ${connectionStatus}\n消息数量: ${messageCount}\n最新消息ID: ${lastMessageId}`, primaryButton: primaryButton, secondaryButton: secondaryButton }; AlertDialog.show(alertConfig); // 记录日志 logManager.info(LogCategory.CLASS, LogEventType.SYSTEM_INFO, `连接状态: ${connectionStatus}, 消息数量: ${messageCount}, 最新消息ID: ${lastMessageId}`); } catch (error) { console.error(`检查连接状态出错: ${error}`); } } // 题目统计窗口 @Builder QuestionStatsDialog() { Column() { // 对话框内容 Column() { // 标题 Row() { Text("答题结果") .fontSize(20) .fontWeight(FontWeight.Bold) Blank() Button({ type: ButtonType.Circle }) { Image($r('app.media.close')) .width(16) .height(16) } .width(32) .height(32) .backgroundColor('#f0f0f0') .onClick(() => { this.showQuestionStats = false; }) } .width('100%') .margin({ bottom: 20 }) // 题目内容 Text(this.currentQuestion?.title || '') .fontSize(18) .fontWeight(FontWeight.Medium) .width('100%') .margin({ bottom: 16 }) // 统计数据 Column() { // 整体统计 if (this.mode === ClassLiveMode.TEACHER) { this.QuestionStatsContent() } else { // 学生端显示自己的答案 if (this.getMyAnswer()) { Column() { // 学生答案 Row() { Text("我的答案: ") .fontSize(16) .fontWeight(FontWeight.Medium) Text(`${String.fromCharCode(65 + this.getMyAnswer()!.selectedOption)}`) .fontSize(16) .fontColor(this.getMyAnswer()!.isCorrect ? '#67C23A' : '#F56C6C') .fontWeight(FontWeight.Medium) Blank() Text(this.getMyAnswer()!.isCorrect ? '✓ 正确' : '✗ 错误') .fontSize(16) .fontColor(this.getMyAnswer()!.isCorrect ? '#67C23A' : '#F56C6C') .fontWeight(FontWeight.Medium) } .width('100%') .padding(10) .borderRadius(8) .backgroundColor(this.getMyAnswer()!.isCorrect ? '#f0f9eb' : '#fef0f0') .margin({ bottom: 20 }) // 正确答案 if (!this.getMyAnswer()!.isCorrect && this.currentQuestion) { Row() { Text("正确答案: ") .fontSize(16) .fontWeight(FontWeight.Medium) Text(`${String.fromCharCode(65 + this.currentQuestion.correctOption)}`) .fontSize(16) .fontColor('#67C23A') .fontWeight(FontWeight.Medium) } .width('100%') .padding(10) .borderRadius(8) .backgroundColor('#f0f9eb') .margin({ bottom: 20 }) } } .width('100%') .margin({ bottom: 20 }) } // 学生端也显示整体统计 this.QuestionStatsContent() } } .width('100%') // 关闭按钮 Button("确定") .width('100%') .height(45) .fontSize(16) .fontWeight(FontWeight.Medium) .backgroundColor(this.settings.themeColor) .borderRadius(8) .fontColor(Color.White) .onClick(() => { this.showQuestionStats = false; }) } .width('90%') .backgroundColor(Color.White) .borderRadius(16) .padding(24) } .width('100%') .height('100%') .backgroundColor('rgba(0,0,0,0.5)') .justifyContent(FlexAlign.Center) .onClick(() => { // 点击遮罩不关闭对话框 }) } }