1447 lines
42 KiB
Plaintext
1447 lines
42 KiB
Plaintext
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';
|
||
|
||
// 路由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();
|
||
|
||
aboutToAppear() {
|
||
// 注册语言变化的回调
|
||
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');
|
||
}
|
||
|
||
// 注册事件监听
|
||
private registerEventListeners() {
|
||
// 接收新消息
|
||
this.messageCallback = (data: EventData) => {
|
||
const message = data as MessageModel;
|
||
if (message && message.id) {
|
||
// 检查消息是否已经存在(防止重复)
|
||
const existingMessage = this.messages.find(m => m.id === message.id);
|
||
if (!existingMessage) {
|
||
this.messages.push(message);
|
||
}
|
||
}
|
||
};
|
||
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() {
|
||
if (!this.messageText || this.messageText.trim() === '') {
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const result = await this.classRoomService.sendMessage(this.messageText);
|
||
|
||
if (result) {
|
||
// 清空消息
|
||
this.messageText = '';
|
||
}
|
||
} catch (error) {
|
||
// 处理错误
|
||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||
logManager.error(LogCategory.CLASS, `Send message error: ${errorMessage}`);
|
||
}
|
||
}
|
||
|
||
// 开始倒计时
|
||
private startCountdown(seconds: number) {
|
||
// 停止之前的计时器
|
||
this.stopCountdown();
|
||
|
||
// 设置初始倒计时
|
||
this.countdownSeconds = seconds;
|
||
|
||
// 创建新计时器 - 使用明确的类型
|
||
this.countdownTimer = setInterval(() => {
|
||
if (this.countdownSeconds > 0) {
|
||
this.countdownSeconds--;
|
||
} else {
|
||
// 倒计时结束
|
||
this.stopCountdown();
|
||
}
|
||
}, 1000) as number; // 直接转换为number,不用中间unknown
|
||
}
|
||
|
||
// 停止倒计时
|
||
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) {
|
||
// 返回上课页面 - 使用明确的导航而不是back()
|
||
router.pushUrl({
|
||
url: 'pages/ClassPage'
|
||
} as RouterUrlOptions);
|
||
} else {
|
||
// 显示错误提示
|
||
const primaryButton: DialogButton = {
|
||
value: '确定',
|
||
action: () => {}
|
||
};
|
||
|
||
const alertConfig: AlertDialogConfig = {
|
||
title: '错误',
|
||
message: '结束课堂失败,请稍后再试',
|
||
primaryButton: primaryButton
|
||
};
|
||
AlertDialog.show(alertConfig);
|
||
}
|
||
} 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}`;
|
||
}
|
||
|
||
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)
|
||
}
|
||
|
||
// 教师端显示结束课堂按钮
|
||
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 })
|
||
|
||
// 课堂内容区域,包含聊天功能
|
||
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({ top: 10, 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 })
|
||
}
|
||
|
||
// 聊天历史记录区域
|
||
Column() {
|
||
Text("聊天记录")
|
||
.fontSize(18)
|
||
.fontWeight(FontWeight.Bold)
|
||
.margin({ bottom: 10 })
|
||
|
||
List({ space: 8 }) {
|
||
ForEach(this.messages, (message: MessageModel) => {
|
||
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 })
|
||
}
|
||
})
|
||
}
|
||
.width('100%')
|
||
.layoutWeight(1)
|
||
.padding(10)
|
||
.backgroundColor('#f9f9f9')
|
||
.borderRadius(8)
|
||
.scrollBar(BarState.Auto)
|
||
}
|
||
.width('100%')
|
||
.layoutWeight(1)
|
||
.margin({ bottom: 10 })
|
||
|
||
// 消息输入区域
|
||
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%')
|
||
.margin({ top: 10 })
|
||
|
||
// 错误消息和加载状态
|
||
if (this.errorMessage) {
|
||
Text(this.errorMessage)
|
||
.fontSize(14)
|
||
.fontColor('#F56C6C')
|
||
.width('100%')
|
||
.textAlign(TextAlign.Center)
|
||
.margin({ top: 10 })
|
||
}
|
||
|
||
if (this.isLoading) {
|
||
Row() {
|
||
LoadingProgress()
|
||
.width(24)
|
||
.height(24)
|
||
.margin({ right: 10 })
|
||
|
||
Text("处理中...")
|
||
.fontSize(14)
|
||
.fontColor('#666')
|
||
}
|
||
.justifyContent(FlexAlign.Center)
|
||
.width('100%')
|
||
.margin({ top: 10 })
|
||
}
|
||
}
|
||
.width('100%')
|
||
.height('100%')
|
||
.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)}: ${option.content}`)
|
||
.fontSize(16)
|
||
.fontWeight(index === this.currentQuestion?.correctOption ? FontWeight.Bold : FontWeight.Normal)
|
||
.fontColor(index === this.currentQuestion?.correctOption ? '#67C23A' : '#333')
|
||
}
|
||
.width('100%')
|
||
.margin({ bottom: 4 })
|
||
|
||
Row() {
|
||
// 进度条
|
||
Column() {
|
||
Stack({ alignContent: Alignment.Start }) {
|
||
Row()
|
||
.width('100%')
|
||
.height(20)
|
||
.backgroundColor('#f0f0f0')
|
||
.borderRadius(10)
|
||
|
||
Row()
|
||
.width(`${this.getOptionStats(index).percentage}%`)
|
||
.height(20)
|
||
.backgroundColor(index === this.currentQuestion?.correctOption ? '#67C23A' : '#409EFF')
|
||
.borderRadius(10)
|
||
}
|
||
}
|
||
.layoutWeight(1)
|
||
|
||
// 百分比和人数
|
||
Text(`${this.getOptionStats(index).percentage}% (${this.getOptionStats(index).count}人)`)
|
||
.fontSize(14)
|
||
.fontColor('#666')
|
||
.margin({ left: 8 })
|
||
.width(100)
|
||
.textAlign(TextAlign.End)
|
||
}
|
||
.width('100%')
|
||
}
|
||
.width('100%')
|
||
.margin({ bottom: 12 })
|
||
.padding(10)
|
||
.borderRadius(8)
|
||
.backgroundColor(index === this.currentQuestion?.correctOption ? '#f0f9eb' : '#f9f9f9')
|
||
})
|
||
}
|
||
}
|
||
.width('100%')
|
||
}
|
||
|
||
// 题目统计窗口
|
||
@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(() => {
|
||
// 点击遮罩不关闭对话框
|
||
})
|
||
}
|
||
} |