ClassMG/entry/src/main/ets/pages/ClassLivePage.ets

1655 lines
50 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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(() => {
// 点击遮罩不关闭对话框
})
}
}