From 7faa9f1b35cd87d8da95d97fc1d3a8aa2cb4b46d Mon Sep 17 00:00:00 2001 From: cc <124141@qq.com> Date: Wed, 2 Apr 2025 15:15:20 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=B5=8B=E8=AF=95=E6=8C=89?= =?UTF-8?q?=E9=92=AE=EF=BC=8C=E4=BF=AE=E6=94=B9=E4=B8=8A=E8=AF=BE=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 103 +- .../src/main/ets/common/ClassRoomService.ets | 930 ++++++++++++++++++ entry/src/main/ets/common/SettingsService.ets | 5 + entry/src/main/ets/common/logtext.ets | 3 +- entry/src/main/ets/pages/ClassLivePage.ets | 885 +++++++++++++++++ entry/src/main/ets/pages/ClassPage.ets | 716 +++++++++++++- entry/src/main/resources/base/media/add.png | Bin 0 -> 3439 bytes entry/src/main/resources/base/media/back.png | Bin 0 -> 1362 bytes entry/src/main/resources/base/media/close.png | Bin 0 -> 1866 bytes .../src/main/resources/base/media/delete.png | Bin 0 -> 5197 bytes .../main/resources/base/media/question.png | Bin 0 -> 2459 bytes .../resources/base/profile/main_pages.json | 3 +- package.json | 14 + 13 files changed, 2548 insertions(+), 111 deletions(-) create mode 100644 entry/src/main/ets/common/ClassRoomService.ets create mode 100644 entry/src/main/ets/pages/ClassLivePage.ets create mode 100644 entry/src/main/resources/base/media/add.png create mode 100644 entry/src/main/resources/base/media/back.png create mode 100644 entry/src/main/resources/base/media/close.png create mode 100644 entry/src/main/resources/base/media/delete.png create mode 100644 entry/src/main/resources/base/media/question.png create mode 100644 package.json diff --git a/README.md b/README.md index 5d64cd8..e719d42 100644 --- a/README.md +++ b/README.md @@ -1,81 +1,64 @@ -# 智慧教室管理系统 +# ClassMG Communication Server -## 项目概述 +This server enables real-time communication between multiple instances of the ClassMG application running on different devices. It provides a simple HTTP API for message passing, classroom management, and question handling. -智慧教室管理系统是一款基于HarmonyOS/ArkTS开发的应用,旨在提供智能化的教室管理解决方案。系统包含多个功能模块,用于管理教室状态、课程信息和用户权限等。 +## Prerequisites -## 功能特点 +- Node.js (v12 or higher) +- NPM (Node Package Manager) -- **用户管理**: 支持教师和学生两种用户类型,提供差异化的功能体验 -- **教室监控**: 实时监控教室温度、湿度、人数等数据 -- **课程数据**: 展示课程信息、教师信息和学生出勤情况 -- **个人设置**: 用户可以修改个人信息和系统偏好设置 +## Installation -## 技术架构 +1. Install Node.js and NPM if you haven't already: + - Download from [nodejs.org](https://nodejs.org/) -- **前端**: HarmonyOS/ArkTS -- **后端**: MySQL数据库 -- **通信**: HTTP API +2. Clone or download this repository to your computer -## 系统要求 +3. Install dependencies: + ```bash + npm install + ``` -- HarmonyOS设备 -- 网络连接 -- 支持HarmonyOS 3.0及以上版本 +## Running the server -## 数据库配置 +1. Start the server: + ```bash + npm start + ``` + or + ```bash + node server.js + ``` -系统使用MySQL数据库存储用户信息和系统数据。数据库配置如下: +2. The server will start on port 5243. Make note of your computer's IP address, which will be displayed in the console output. -- **数据库地址**: 139.155.155.67:25342 -- **数据库名**: hongm -- **用户名**: hongm -- **密码**: JsKJeG7CX2WnyArt +## Connecting ClassMG app to the server -### 数据库初始化 +1. Make sure all devices (server and app clients) are on the same network -导入`database_setup.sql`文件以创建必要的表和初始数据。主要的数据表包括: +2. Open the ClassMG project in DevEco Studio -- **UserText**: 存储用户个人信息 -- **UserPassword**: 存储用户登录信息 +3. Update the `CHANNEL_API_CONFIG.baseUrl` in `entry/src/main/ets/common/ClassRoomService.ets` to: + ``` + http://:5243 + ``` -## 使用指南 +4. Compile and run the ClassMG app on your devices -### 登录系统 +## Testing -1. 在登录页面输入您的账号和密码(默认密码: 1) -2. 系统会根据账号自动识别用户类型: - - 账号包含"2"的识别为学生 - - 账号包含"0"的识别为教师 -3. 登录成功后将显示过渡页面,然后进入系统主页 +1. Run the server on one computer +2. Run the ClassMG app on two different devices (or emulators) +3. Log in as a teacher on one device and create a class +4. Log in as a student on the other device and join the class using the class code +5. Test messaging, question publishing, and answering functionality -### 系统导航 +## Troubleshooting -系统包含三个主要页面: +- **Connection Issues**: Make sure all devices are on the same network and can reach the server's IP address +- **Port Conflicts**: If port 5243 is already in use, change the PORT variable in server.js and update the app accordingly +- **CORS Issues**: If experiencing cross-origin problems, verify the CORS middleware is correctly configured in server.js -- **首页**: 显示教室监控和综合上课数据 -- **上课**: 提供课程相关功能 -- **设置**: 管理个人信息和系统设置 +## Note -### 个人信息设置 - -在设置页面,用户可以查看和修改个人信息: - -- **头像**: 显示用户头像 -- **账号**: 显示当前登录账号(不可修改) -- **昵称**: 显示用户昵称(不可修改) -- **邮箱**: 可修改,需符合邮箱格式 -- **电话**: 显示联系电话(不可修改) - -## 开发信息 - -- **开发团队**: 922213102班鸿蒙第一组 -- **版本**: 1.0.0 - -## 备注 - -- 用户密码默认为"1" -- 系统现有示例用户: - - 学生: 账号"2",昵称"张三" - - 学生: 账号"9222",昵称"李华" - - 教师: 账号"0",昵称"教师demo" \ No newline at end of file +This is a development server intended for local testing only. For production use, additional security measures would be required. \ No newline at end of file diff --git a/entry/src/main/ets/common/ClassRoomService.ets b/entry/src/main/ets/common/ClassRoomService.ets new file mode 100644 index 0000000..69e6392 --- /dev/null +++ b/entry/src/main/ets/common/ClassRoomService.ets @@ -0,0 +1,930 @@ +import { hilog } from '@kit.PerformanceAnalysisKit'; +import logManager, { LogCategory, LogEventType } from './logtext'; +import settingsService from './SettingsService'; +import { DatabaseService } from './DatabaseService'; +import http from '@ohos.net.http'; + +// 课堂会话状态 +export enum ClassSessionStatus { + ACTIVE = 'active', + ENDED = 'ended' +} + +// 学生状态 +export enum StudentStatus { + ONLINE = 'online', + OFFLINE = 'offline' +} + +// 题目状态 +export enum QuestionStatus { + ACTIVE = 'active', + ENDED = 'ended' +} + +// 发送者角色 +export enum SenderRole { + TEACHER = 'teacher', + STUDENT = 'student' +} + +// 课堂模型 +export class ClassSessionModel { + sessionId: string = ''; // 课堂ID(由系统生成) + className: string = ''; // 课堂名称 + classCode: string = ''; // 课堂暗号(由教师设置,学生使用此暗号加入) + teacherAccount: string = ''; // 教师账号ID + teacherName: string = ''; // 教师姓名 + startTime: Date = new Date(); // 课堂开始时间 + endTime?: Date; // 课堂结束时间(未结束为空) + status: ClassSessionStatus = ClassSessionStatus.ACTIVE; // 课堂状态 + students: StudentSession[] = []; // 参与学生列表 + questions: QuestionModel[] = []; // 课堂题目列表 + messages: MessageModel[] = []; // 课堂消息列表 + + constructor(sessionId: string = '', + className: string = '', + classCode: string = '', + teacherAccount: string = '', + teacherName: string = '') { + this.sessionId = sessionId || this.generateSessionId(); + this.className = className; + this.classCode = classCode; + this.teacherAccount = teacherAccount; + this.teacherName = teacherName; + this.startTime = new Date(); + this.status = ClassSessionStatus.ACTIVE; + } + + // 生成唯一会话ID + private generateSessionId(): string { + return Date.now().toString(36) + Math.random().toString(36).substring(2); + } +} + +// 学生会话信息 +export class StudentSession { + studentAccount: string = ''; // 学生账号 + studentName: string = ''; // 学生姓名 + joinTime: Date = new Date(); // 加入时间 + status: StudentStatus = StudentStatus.ONLINE; // 学生状态 + + constructor(studentAccount: string = '', studentName: string = '') { + this.studentAccount = studentAccount; + this.studentName = studentName; + this.joinTime = new Date(); + this.status = StudentStatus.ONLINE; + } +} + +// 课堂消息模型 +export class MessageModel { + id: string = ''; // 消息ID + senderId: string = ''; // 发送者账号 + senderName: string = ''; // 发送者姓名 + senderRole: SenderRole; // 发送者角色 + content: string = ''; // 消息内容 + timestamp: Date = new Date(); // 发送时间 + + constructor(senderId: string = '', + senderName: string = '', + senderRole: SenderRole, + content: string = '') { + this.id = Date.now().toString(36) + Math.random().toString(36).substring(2); + this.senderId = senderId; + this.senderName = senderName; + this.senderRole = senderRole; + this.content = content; + this.timestamp = new Date(); + } +} + +// 题目模型 +export class QuestionModel { + questionId: string = ''; // 题目ID + title: string = ''; // 题目标题 + options: QuestionOption[] = []; // 题目选项 + correctOption: number = 0; // 正确选项的索引 + startTime: Date = new Date(); // 题目开始时间 + endTime?: Date; // 题目结束时间(未结束为空) + duration: number = 30; // 答题时间(秒) + status: QuestionStatus = QuestionStatus.ACTIVE; // 题目状态 + answers: QuestionAnswer[] = []; // 学生答案列表 + classCode: string = ''; // 关联的课堂暗号 + + constructor(title: string = '', + options: QuestionOption[] = [], + correctOption: number = 0, + duration: number = 30, + classCode: string = '') { + this.questionId = Date.now().toString(36) + Math.random().toString(36).substring(2); + this.title = title; + this.options = options; + this.correctOption = correctOption; + this.duration = duration; + this.startTime = new Date(); + this.status = QuestionStatus.ACTIVE; + this.classCode = classCode; + } +} + +// 题目选项 +export class QuestionOption { + index: number = 0; // 选项索引 + content: string = ''; // 选项内容 + + constructor(index: number = 0, content: string = '') { + this.index = index; + this.content = content; + } +} + +// 学生答案 +export class QuestionAnswer { + studentAccount: string = ''; // 学生账号 + studentName: string = ''; // 学生姓名 + selectedOption: number = 0; // 所选选项索引 + timestamp: Date = new Date(); // 提交时间 + isCorrect: boolean = false; // 是否正确 + + constructor(studentAccount: string = '', + studentName: string = '', + selectedOption: number = 0, + correctOption: number = 0) { + this.studentAccount = studentAccount; + this.studentName = studentName; + this.selectedOption = selectedOption; + this.timestamp = new Date(); + this.isCorrect = selectedOption === correctOption; + } +} + +// 事件类型 +export enum WebSocketEventType { + JOIN_CLASS = 'joinClass', + LEAVE_CLASS = 'leaveClass', + SEND_MESSAGE = 'sendMessage', + PUBLISH_QUESTION = 'publishQuestion', + SUBMIT_ANSWER = 'submitAnswer', + END_QUESTION = 'endQuestion', + END_CLASS = 'endClass' +} + +// API响应接口定义 +export interface ChannelApiResponse { + success: boolean; + message?: string; + data?: ClassSessionModel | QuestionModel | MessageModel | QuestionAnswer | Array | Array | boolean | null; +} + +// HTTP请求选项接口 +export interface HttpRequestOptions { + method: number; // http.RequestMethod enum value + header?: object; + extraData?: string; + connectTimeout?: number; + readTimeout?: number; +} + +// HTTP响应接口 +export interface HttpResponseResult { + responseCode: number; + result?: string | object | ArrayBuffer; +} + +// 事件数据接口 +export interface EventConnection { + status: string; +} + +export interface QuestionAnswerEvent { + questionId: string; + answer: QuestionAnswer; +} + +// 请求数据类型接口 +export interface CreateSessionRequest { + className: string; + classCode: string; + teacherAccount: string; + teacherName: string; +} + +export interface JoinSessionRequest { + classCode: string; + studentAccount: string; + studentName: string; +} + +export interface SendMessageRequest { + classCode: string; + message: MessageModel; +} + +export interface SubmitAnswerRequest { + questionId: string; + classCode: string; + answer: QuestionAnswer; +} + +export interface EndQuestionRequest { + questionId: string; + classCode: string; +} + +export interface EndSessionRequest { + classCode: string; +} + +// API配置接口 +export interface ApiConfig { + baseUrl: string; + pollInterval: number; + timeout: number; +} + +// 事件数据联合类型 +export type EventData = ClassSessionModel | QuestionModel | MessageModel | QuestionAnswer | EventConnection | QuestionAnswerEvent | null; + +// 事件回调类型 +export type EventCallback = (data: EventData) => void; + +// API配置 +const CHANNEL_API_CONFIG: ApiConfig = { + baseUrl: 'http://139.155.155.67:5243', // Change this to your computer's IP address + pollInterval: 2000, // 轮询间隔(毫秒) + timeout: 10000 // 请求超时(毫秒) +}; + +// 课堂通信服务 +export class ClassRoomService { + private static instance: ClassRoomService; + private dbService: DatabaseService = DatabaseService.getInstance(); + + // 当前课堂会话 + private currentSession: ClassSessionModel | null = null; + private currentClassCode: string = ''; + + // 回调函数集合 + private eventCallbacks: Map> = new Map(); + + // 是否已连接 + private connected: boolean = false; + + // 是否为教师 + private isTeacher: boolean = false; + + // 消息轮询定时器ID + private pollTimerId: number = -1; + + // 最后接收的消息ID + private lastMessageId: string = ''; + + // 最后接收的题目ID + private lastQuestionId: string = ''; + + private constructor() { + // 初始化 + hilog.info(0, 'ClassMG', 'ClassRoom Service Initialized'); + } + + public static getInstance(): ClassRoomService { + if (!ClassRoomService.instance) { + ClassRoomService.instance = new ClassRoomService(); + } + return ClassRoomService.instance; + } + + // 创建HTTP请求客户端 + private createHttpClient(): http.HttpRequest { + let httpRequest = http.createHttp(); + return httpRequest; + } + + // 发起HTTP POST请求 + private async postRequest(endpoint: string, data: object): Promise { + try { + const httpRequest = this.createHttpClient(); + + const response = await httpRequest.request( + `${CHANNEL_API_CONFIG.baseUrl}/${endpoint}`, + { + method: http.RequestMethod.POST, + header: { + 'Content-Type': 'application/json' + }, + extraData: JSON.stringify(data), + connectTimeout: CHANNEL_API_CONFIG.timeout, + readTimeout: CHANNEL_API_CONFIG.timeout + } + ); + + httpRequest.destroy(); + + if (response.responseCode === 200) { + const jsonString: string = response.result ? response.result.toString() : '{}'; + try { + const responseData = JSON.parse(jsonString) as ChannelApiResponse; + return responseData; + } catch (parseError) { + logManager.error(LogCategory.NETWORK, `Error parsing response: ${parseError}`); + const errorResponse: ChannelApiResponse = { + success: false, + message: `Parse error: ${parseError}` + }; + return errorResponse; + } + } + + const errorResponse: ChannelApiResponse = { + success: false, + message: `HTTP error: ${response.responseCode}` + }; + return errorResponse; + } catch (error) { + logManager.error(LogCategory.NETWORK, `Network request error: ${error}`); + const errorResponse: ChannelApiResponse = { + success: false, + message: `Request error: ${error}` + }; + return errorResponse; + } + } + + // 发起HTTP GET请求 + private async getRequest(endpoint: string): Promise { + try { + const httpRequest = this.createHttpClient(); + + const response = await httpRequest.request( + `${CHANNEL_API_CONFIG.baseUrl}/${endpoint}`, + { + method: http.RequestMethod.GET, + connectTimeout: CHANNEL_API_CONFIG.timeout, + readTimeout: CHANNEL_API_CONFIG.timeout + } + ); + + httpRequest.destroy(); + + if (response.responseCode === 200) { + const jsonString: string = response.result ? response.result.toString() : '{}'; + try { + const responseData = JSON.parse(jsonString) as ChannelApiResponse; + return responseData; + } catch (parseError) { + logManager.error(LogCategory.NETWORK, `Error parsing response: ${parseError}`); + const errorResponse: ChannelApiResponse = { + success: false, + message: `Parse error: ${parseError}` + }; + return errorResponse; + } + } + + const errorResponse: ChannelApiResponse = { + success: false, + message: `HTTP error: ${response.responseCode}` + }; + return errorResponse; + } catch (error) { + logManager.error(LogCategory.NETWORK, `Network request error: ${error}`); + const errorResponse: ChannelApiResponse = { + success: false, + message: `Request error: ${error}` + }; + return errorResponse; + } + } + + // 检查当前用户是否为教师 + public checkIsTeacher(): boolean { + const currentAccount = settingsService.getCurrentAccount(); + const category = this.dbService.getUserCategory(currentAccount); + this.isTeacher = category === 'teacher'; + return this.isTeacher; + } + + // 启动消息轮询 + private startPolling(): void { + if (this.pollTimerId !== -1) { + clearInterval(this.pollTimerId); + } + + this.pollTimerId = setInterval(() => { + this.pollForUpdates(); + }, CHANNEL_API_CONFIG.pollInterval); + } + + // 停止消息轮询 + private stopPolling(): void { + if (this.pollTimerId !== -1) { + clearInterval(this.pollTimerId); + this.pollTimerId = -1; + } + } + + // 轮询更新 + private async pollForUpdates(): Promise { + if (!this.connected || !this.currentClassCode) { + return; + } + + try { + // 查询新消息 + const messagesResponse = await this.getRequest(`messages/${this.currentClassCode}?since=${this.lastMessageId}`); + + if (messagesResponse.success && messagesResponse.data && Array.isArray(messagesResponse.data)) { + const messages = messagesResponse.data as MessageModel[]; + + if (messages.length > 0) { + // 更新最后接收的消息ID + this.lastMessageId = messages[messages.length - 1].id; + + // 将消息添加到当前会话 + if (this.currentSession) { + messages.forEach(message => { + // 避免添加自己发送的消息(已在本地添加) + const isDuplicate = this.currentSession?.messages.some(m => m.id === message.id); + if (!isDuplicate) { + this.currentSession?.messages.push(message); + this.notifyEvent(WebSocketEventType.SEND_MESSAGE, message); + } + }); + } + } + } + + // 查询新题目 + const questionsResponse = await this.getRequest(`questions/${this.currentClassCode}?since=${this.lastQuestionId}`); + + if (questionsResponse.success && questionsResponse.data && Array.isArray(questionsResponse.data)) { + const questions = questionsResponse.data as QuestionModel[]; + + if (questions.length > 0) { + // 更新最后接收的题目ID + this.lastQuestionId = questions[questions.length - 1].questionId; + + // 处理题目 + questions.forEach(question => { + if (this.currentSession) { + // 检查题目是否已存在 + const existingQuestionIndex = this.currentSession.questions.findIndex(q => q.questionId === question.questionId); + + if (existingQuestionIndex === -1) { + // 新题目 + this.currentSession.questions.push(question); + this.notifyEvent(WebSocketEventType.PUBLISH_QUESTION, question); + + // 如果题目处于活动状态且有结束时间,设置倒计时 + if (question.status === QuestionStatus.ACTIVE && question.duration > 0) { + setTimeout(() => { + this.checkQuestionStatus(question.questionId); + }, question.duration * 1000); + } + } else { + // 更新已有题目 + const oldQuestion = this.currentSession.questions[existingQuestionIndex]; + const oldStatus = oldQuestion.status; + + // 更新题目 + this.currentSession.questions[existingQuestionIndex] = question; + + // 如果状态从活动变为结束,通知题目结束 + if (oldStatus === QuestionStatus.ACTIVE && question.status === QuestionStatus.ENDED) { + this.notifyEvent(WebSocketEventType.END_QUESTION, question); + } + } + } + }); + } + } + + // 查询课堂状态 + const sessionResponse = await this.getRequest(`session/${this.currentClassCode}`); + + if (sessionResponse.success && sessionResponse.data) { + const session = sessionResponse.data as ClassSessionModel; + + // 检查课堂是否已结束 + if (session.status === ClassSessionStatus.ENDED && this.currentSession?.status === ClassSessionStatus.ACTIVE) { + // 更新当前会话状态 + if (this.currentSession) { + this.currentSession.status = ClassSessionStatus.ENDED; + this.currentSession.endTime = session.endTime ? new Date(session.endTime) : new Date(); + } + + // 通知课堂结束 + this.notifyEvent(WebSocketEventType.END_CLASS, session); + + // 如果不是教师,停止轮询 + if (!this.isTeacher) { + this.disconnect(); + } + } + } + } catch (error) { + logManager.error(LogCategory.NETWORK, `Polling error: ${error}`); + } + } + + // 检查题目状态 + private async checkQuestionStatus(questionId: string): Promise { + try { + const response = await this.getRequest(`question/${questionId}`); + + if (response.success && response.data) { + const question = response.data as QuestionModel; + + // 如果题目仍处于活动状态,结束题目 + if (question.status === QuestionStatus.ACTIVE && this.currentSession) { + // 查找并更新本地题目 + const questionIndex = this.currentSession.questions.findIndex(q => q.questionId === questionId); + + if (questionIndex !== -1) { + // 更新题目状态 + this.currentSession.questions[questionIndex].status = QuestionStatus.ENDED; + this.currentSession.questions[questionIndex].endTime = new Date(); + + // 提交题目状态更新 + const request: EndQuestionRequest = { + questionId: questionId, + classCode: this.currentClassCode + }; + + await this.postRequest('endQuestion', request); + + // 通知题目结束 + this.notifyEvent(WebSocketEventType.END_QUESTION, this.currentSession.questions[questionIndex]); + } + } + } + } catch (error) { + logManager.error(LogCategory.CLASS, `Error checking question status: ${error}`); + } + } + + // 连接服务 + public async connect(): Promise { + if (this.connected) return true; + + try { + // 验证服务器连接 + const response = await this.getRequest('ping'); + + if (response.success) { + this.connected = true; + const connectionEvent: EventConnection = { status: 'connected' }; + this.notifyEvent('connection', connectionEvent); + logManager.info(LogCategory.NETWORK, LogEventType.SYSTEM_INFO, 'Channel service connected'); + return true; + } else { + logManager.error(LogCategory.NETWORK, `Connection failed: ${response.message}`); + return false; + } + } catch (error) { + logManager.error(LogCategory.NETWORK, `Connection error: ${error}`); + return false; + } + } + + // 断开连接 + public disconnect(): void { + if (!this.connected) return; + + // 停止轮询 + this.stopPolling(); + + this.connected = false; + this.notifyEvent('connection', { status: 'disconnected' }); + logManager.info(LogCategory.NETWORK, LogEventType.SYSTEM_INFO, 'Channel service disconnected'); + + // 清空当前会话 + this.currentSession = null; + this.currentClassCode = ''; + } + + // 创建新课堂(教师) + public async createClassSession(className: string, classCode: string): Promise { + try { + // 发送创建请求 + const reqData: CreateSessionRequest = { + className: className, + classCode: classCode, + teacherAccount: settingsService.getCurrentAccount(), + teacherName: settingsService.getUserNickname() + }; + + const response = await this.postRequest('createSession', reqData); + + if (response.success && response.data) { + // 设置当前会话 + this.currentSession = response.data as ClassSessionModel; + this.currentClassCode = classCode; + this.isTeacher = true; + this.connected = true; + + // 开始消息轮询 + this.startPolling(); + + // 通知连接状态 + const connectionEvent: EventConnection = { status: 'connected' }; + this.notifyEvent(WebSocketEventType.JOIN_CLASS, connectionEvent); + + return this.currentSession; + } else { + logManager.error(LogCategory.CLASS, `Create session failed: ${response.message}`); + return null; + } + } catch (error) { + logManager.error(LogCategory.CLASS, `Create session error: ${error}`); + return null; + } + } + + // 加入课堂(学生) + public async joinClassSession(classCode: string): Promise { + try { + // 发送加入请求 + const reqData: JoinSessionRequest = { + classCode: classCode, + studentAccount: settingsService.getCurrentAccount(), + studentName: settingsService.getUserNickname() + }; + + const response = await this.postRequest('joinSession', reqData); + + if (response.success && response.data) { + // 设置当前会话 + this.currentSession = response.data as ClassSessionModel; + this.currentClassCode = classCode; + this.isTeacher = false; + this.connected = true; + + // 开始消息轮询 + this.startPolling(); + + // 通知连接状态 + const connectionEvent: EventConnection = { status: 'connected' }; + this.notifyEvent(WebSocketEventType.JOIN_CLASS, connectionEvent); + + return true; + } else { + logManager.error(LogCategory.CLASS, `Join session failed: ${response.message}`); + return false; + } + } catch (error) { + logManager.error(LogCategory.CLASS, `Join session error: ${error}`); + return false; + } + } + + // 发送消息 + public async sendMessage(content: string): Promise { + if (!this.currentSession || !this.connected || !this.currentClassCode) { + logManager.error(LogCategory.CLASS, 'Cannot send message: No active session or not connected'); + return false; + } + + const currentAccount = settingsService.getCurrentAccount(); + const senderName = this.dbService.getUserNickname(currentAccount) || currentAccount; + const role = this.isTeacher ? SenderRole.TEACHER : SenderRole.STUDENT; + + // 创建消息 + const message = new MessageModel(currentAccount, senderName, role, content); + + try { + // 发送消息请求 + const request: SendMessageRequest = { + classCode: this.currentClassCode, + message: message + }; + + const response = await this.postRequest('sendMessage', request); + + if (response.success) { + // 添加到当前会话 + this.currentSession.messages.push(message); + + // 通知消息发送 + this.notifyEvent(WebSocketEventType.SEND_MESSAGE, message); + + return true; + } else { + logManager.error(LogCategory.CLASS, `Send message failed: ${response.message}`); + return false; + } + } catch (error) { + logManager.error(LogCategory.CLASS, `Send message error: ${error}`); + return false; + } + } + + // 发布题目(教师) + public async publishQuestion(title: string, options: QuestionOption[], correctOption: number, duration: number): Promise { + if (!this.isTeacher || !this.currentSession || !this.connected || !this.currentClassCode) { + logManager.error(LogCategory.CLASS, 'Cannot publish question: Not a teacher, no active session or not connected'); + return null; + } + + // 创建题目 + const question = new QuestionModel(title, options, correctOption, duration, this.currentClassCode); + + try { + // 发送发布题目请求 + const response = await this.postRequest('publishQuestion', question); + + if (response.success && response.data) { + // 使用服务器返回的题目 + const publishedQuestion = response.data as QuestionModel; + + // 添加到当前会话 + this.currentSession.questions.push(publishedQuestion); + + // 设置题目结束倒计时 + setTimeout(() => { + this.checkQuestionStatus(publishedQuestion.questionId); + }, duration * 1000); + + // 通知题目发布 + this.notifyEvent(WebSocketEventType.PUBLISH_QUESTION, publishedQuestion); + + return publishedQuestion; + } else { + logManager.error(LogCategory.CLASS, `Publish question failed: ${response.message}`); + return null; + } + } catch (error) { + logManager.error(LogCategory.CLASS, `Publish question error: ${error}`); + return null; + } + } + + // 提交答案(学生) + public async submitAnswer(questionId: string, selectedOption: number): Promise { + if (this.isTeacher || !this.currentSession || !this.connected || !this.currentClassCode) { + logManager.error(LogCategory.CLASS, 'Cannot submit answer: Is a teacher, no active session or not connected'); + return false; + } + + // 查找题目 + const question = this.currentSession.questions.find(q => q.questionId === questionId); + if (!question || question.status === QuestionStatus.ENDED) { + logManager.error(LogCategory.CLASS, 'Cannot submit answer: Question not found or already ended'); + return false; + } + + const currentAccount = settingsService.getCurrentAccount(); + const studentName = this.dbService.getUserNickname(currentAccount) || currentAccount; + + // 创建答案 + const answer = new QuestionAnswer( + currentAccount, + studentName, + selectedOption, + question.correctOption + ); + + try { + // 发送提交答案请求 + const request: SubmitAnswerRequest = { + questionId: questionId, + classCode: this.currentClassCode, + answer: answer + }; + + const response = await this.postRequest('submitAnswer', request); + + if (response.success) { + // 检查是否已答题 + const existingAnswerIndex = question.answers.findIndex(a => a.studentAccount === currentAccount); + if (existingAnswerIndex !== -1) { + // 更新已有答案 + question.answers[existingAnswerIndex] = answer; + } else { + // 添加新答案 + question.answers.push(answer); + } + + // 通知答案提交 + const answerEvent: QuestionAnswerEvent = { + questionId: questionId, + answer: answer + }; + + this.notifyEvent(WebSocketEventType.SUBMIT_ANSWER, answerEvent); + + return true; + } else { + logManager.error(LogCategory.CLASS, `Submit answer failed: ${response.message}`); + return false; + } + } catch (error) { + logManager.error(LogCategory.CLASS, `Submit answer error: ${error}`); + return false; + } + } + + // 结束课堂(教师) + public async endClassSession(): Promise { + if (!this.isTeacher || !this.currentSession || !this.connected || !this.currentClassCode) { + logManager.error(LogCategory.CLASS, 'Cannot end class: Not a teacher, no active session or not connected'); + return false; + } + + try { + // 发送结束课堂请求 + const request: EndSessionRequest = { + classCode: this.currentClassCode + }; + + const response = await this.postRequest('endSession', request); + + if (response.success) { + // 更新课堂状态 + this.currentSession.status = ClassSessionStatus.ENDED; + this.currentSession.endTime = new Date(); + + // 通知课堂结束 + this.notifyEvent(WebSocketEventType.END_CLASS, this.currentSession); + + // 停止轮询 + this.stopPolling(); + + // 清空当前会话 + const endedSession = this.currentSession; + this.currentSession = null; + this.currentClassCode = ''; + + return true; + } else { + logManager.error(LogCategory.CLASS, `End class failed: ${response.message}`); + return false; + } + } catch (error) { + logManager.error(LogCategory.CLASS, `End class error: ${error}`); + return false; + } + } + + // 获取当前课堂 + public getCurrentSession(): ClassSessionModel | null { + return this.currentSession; + } + + // 获取当前题目(如果有) + public getCurrentQuestion(): QuestionModel | null { + if (!this.currentSession) return null; + + // 查找最新的活动题目 + const activeQuestions = this.currentSession.questions.filter(q => q.status === QuestionStatus.ACTIVE); + if (activeQuestions.length === 0) return null; + + // 返回最新发布的题目 + return activeQuestions[activeQuestions.length - 1]; + } + + // 注册事件回调 + public addEventListener(event: string, callback: EventCallback): void { + if (!this.eventCallbacks.has(event)) { + this.eventCallbacks.set(event, []); + } + + const callbacks = this.eventCallbacks.get(event); + if (callbacks) { + callbacks.push(callback); + } + } + + // 移除事件回调 + public removeEventListener(event: string, callback: EventCallback): void { + if (!this.eventCallbacks.has(event)) return; + + const callbacks = this.eventCallbacks.get(event); + if (callbacks) { + const index = callbacks.indexOf(callback); + if (index !== -1) { + callbacks.splice(index, 1); + } + } + } + + // 通知事件 + private notifyEvent(event: string, data: EventData): void { + if (!this.eventCallbacks.has(event)) return; + + const callbacks = this.eventCallbacks.get(event); + if (callbacks) { + callbacks.forEach(callback => { + try { + callback(data); + } catch (error) { + logManager.error(LogCategory.ERROR, `Error in event callback: ${error}`); + } + }); + } + } +} + +// 导出单例实例 +export default ClassRoomService.getInstance(); \ No newline at end of file diff --git a/entry/src/main/ets/common/SettingsService.ets b/entry/src/main/ets/common/SettingsService.ets index 6dd2d4c..b4381b5 100644 --- a/entry/src/main/ets/common/SettingsService.ets +++ b/entry/src/main/ets/common/SettingsService.ets @@ -256,6 +256,11 @@ export class SettingsService { this.refreshUserInfo(); } + // 获取当前用户账号 + public getCurrentAccount(): string { + return this.currentAccount; + } + // 刷新用户信息 private refreshUserInfo(): void { if (this.currentAccount) { diff --git a/entry/src/main/ets/common/logtext.ets b/entry/src/main/ets/common/logtext.ets index 11f40bb..d6595b7 100644 --- a/entry/src/main/ets/common/logtext.ets +++ b/entry/src/main/ets/common/logtext.ets @@ -155,7 +155,8 @@ export class LogManager { new VersionLogItem("1.0.0", "2025-2-10", ["初始版本发布", "实现基本功能界面", "添加用户信息管理", "支持主题色切换", "支持中英文切换"]), new VersionLogItem("1.1.0", "2025-3-31", ["添加用户信息编辑功能", "增加版本日志功能", "改进主题颜色切换效果","添加公告栏","修复已知问题"]), new VersionLogItem("1.1.5","2025-4-1",["增加远程测试数据库","优化登录页面","优化用户信息设置","增加退出按钮"]), - new VersionLogItem("1.1.7","2025-4-1",["添加测试按钮","修复已知问题"]) + new VersionLogItem("1.1.7","2025-4-1",["添加测试按钮","修复已知问题"]), + new VersionLogItem("1.2.0","2025-4-2",["添加课堂功能","修改上课页面逻辑"]) ]; } diff --git a/entry/src/main/ets/pages/ClassLivePage.ets b/entry/src/main/ets/pages/ClassLivePage.ets new file mode 100644 index 0000000..8ad49a4 --- /dev/null +++ b/entry/src/main/ets/pages/ClassLivePage.ets @@ -0,0 +1,885 @@ +import { router } from '@kit.ArkUI'; +import { hilog } from '@kit.PerformanceAnalysisKit'; +import settingsService, { SettingsModel, TextResources } from '../common/SettingsService'; +import logManager, { LogCategory, LogEventType } from '../common/logtext'; +import classRoomService, { + ClassSessionModel, + MessageModel, + QuestionModel, + QuestionOption, + QuestionAnswer, + SenderRole, + WebSocketEventType, + QuestionStatus, + EventData +} from '../common/ClassRoomService'; + +// 路由参数接口 +interface RouteParams { + mode?: string; +} + +// 课堂界面模式 +enum ClassLiveMode { + TEACHER = 'teacher', + STUDENT = 'student' +} + +// 对话框按钮定义 +interface DialogButton { + value: string; + action: () => void; +} + +// 对话框配置 +interface AlertDialogConfig { + title: string; + message: string; + primaryButton: DialogButton; + secondaryButton?: DialogButton; +} + +// 选项统计结果接口 +interface OptionStats { + count: number; + percentage: number; +} + +// 正确率统计结果接口 +interface CorrectRateStats { + correctCount: number; + totalCount: number; + percentage: number; +} + +@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 = () => {}; + + 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 = classRoomService.getCurrentSession(); + + if (!this.classSession) { + // 如果没有课堂会话,返回上课页面 + this.showErrorAndReturn('未找到课堂会话,请重新加入或创建课堂'); + return; + } + + // 获取当前消息列表 + this.messages = this.classSession.messages || []; + + // 获取当前活动题目 + this.currentQuestion = 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) { + this.messages.push(message); + } + }; + 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); + } + }; + 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; + } + }; + 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' + }); + } + }; + + const alertConfig: AlertDialogConfig = { + title: '课堂已结束', + message: '教师已结束当前课堂', + primaryButton: primaryButton + }; + AlertDialog.show(alertConfig); + } + }; + classRoomService.addEventListener(WebSocketEventType.END_CLASS, this.endClassCallback); + } + + // 发送消息 + private async sendMessage() { + if (!this.messageText || this.messageText.trim() === '') { + return; + } + + try { + const result = await classRoomService.sendMessage(this.messageText); + + if (result) { + // 清空消息 + this.messageText = ''; + } + } catch (error) { + // 处理错误 + logManager.error(LogCategory.CLASS, `Send message error: ${error}`); + } + } + + // 开始倒计时 + private startCountdown(seconds: number) { + // 停止之前的计时器 + this.stopCountdown(); + + // 设置初始倒计时 + this.countdownSeconds = seconds; + + // 创建新计时器 + this.countdownTimer = setInterval(() => { + if (this.countdownSeconds > 0) { + this.countdownSeconds--; + } else { + // 倒计时结束 + this.stopCountdown(); + } + }, 1000); + } + + // 停止倒计时 + 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 classRoomService.publishQuestion( + this.questionTitle, + this.questionOptions, + this.correctOption, + this.questionDuration + ); + + if (question) { + // 清空题目编辑器 + this.resetQuestionEditor(); + // 关闭题目编辑器 + this.showQuestionEditor = false; + } else { + this.errorMessage = '发布题目失败,请稍后再试'; + } + } catch (error) { + this.errorMessage = '发布题目过程中发生错误'; + logManager.error(LogCategory.CLASS, `Publish question error: ${error}`); + } finally { + this.isLoading = false; + } + } + + // 提交答案 + private async submitAnswer() { + if (this.selectedOption < 0 || !this.currentQuestion) { + this.errorMessage = '请选择一个选项'; + return; + } + + this.isLoading = true; + + try { + const result = await classRoomService.submitAnswer( + this.currentQuestion.questionId, + this.selectedOption + ); + + if (result) { + // 关闭答题窗口 + this.showAnswerDialog = false; + // 清空错误消息 + this.errorMessage = ''; + } else { + this.errorMessage = '提交答案失败,请稍后再试'; + } + } catch (error) { + this.errorMessage = '提交答案过程中发生错误'; + logManager.error(LogCategory.CLASS, `Submit answer error: ${error}`); + } finally { + this.isLoading = false; + } + } + + // 结束课堂 + private async endClass() { + this.isLoading = true; + + try { + const result = await classRoomService.endClassSession(); + + if (result) { + // 返回上课页面 - 使用明确的导航而不是back() + router.pushUrl({ + url: 'pages/ClassPage' + }); + } 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); + logManager.error(LogCategory.CLASS, `End class error: ${error}`); + } finally { + this.isLoading = false; + } + } + + // 显示错误并返回 + private showErrorAndReturn(message: string) { + const primaryButton: DialogButton = { + value: '确定', + action: () => { + // 使用明确的导航而不是back() + router.pushUrl({ + url: 'pages/ClassPage' + }); + } + }; + + const alertConfig: AlertDialogConfig = { + title: '错误', + message: message, + primaryButton: primaryButton + }; + AlertDialog.show(alertConfig); + } + + // 创建问题编辑器内的选项 + private addOption() { + const newIndex = this.questionOptions.length; + const newOption = new QuestionOption(newIndex, ''); + 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); + + // 重新编号 + this.questionOptions.forEach((option, i) => { + option.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) { + return { count: 0, percentage: 0 }; + } + + // 使用普通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; + + return { count: optionCount, percentage: percentage }; + } + + // 计算正确率 + private getCorrectRate(): CorrectRateStats { + if (!this.currentQuestion || this.currentQuestion.answers.length === 0) { + return { correctCount: 0, totalCount: 0, percentage: 0 }; + } + + // 使用普通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; + + return { correctCount, totalCount, percentage }; + } + + // 获取学生自己的答案 + 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 = ''; + } + + build() { + Column() { + // 顶部导航栏 + Row() { + Row() { + Image($r('app.media.back')) + .width(24) + .height(24) + .fillColor(Color.White) + .margin({ right: 16 }) + .onClick(() => { + // 返回确认 + 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' + }); + } + }; + + const secondaryButton: DialogButton = { + value: '取消', + action: () => {} + }; + + const alertConfig: AlertDialogConfig = { + title: '离开课堂', + message: '确定要离开当前课堂吗?', + primaryButton: primaryButton, + secondaryButton: secondaryButton + }; + AlertDialog.show(alertConfig); + } + }) + + 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)) + .fontSize(12) + .fontColor('#999') + .margin({ top: 4 }) + .alignSelf(ItemAlign.End) + } + .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 }) + + // 功能按钮区域 + Row({ space: 20 }) { + if (this.mode === ClassLiveMode.TEACHER && !this.showQuestionEditor) { + Button("发布题目") + .width(150) + .height(44) + .fontSize(16) + .backgroundColor(this.settings.themeColor) + .borderRadius(22) + .fontColor(Color.White) + .onClick(() => { + this.showQuestionEditor = true; + + // 初始化题目选项 + if (this.questionOptions.length === 0) { + this.addOption(); + this.addOption(); + } + }) + } + + if (this.mode === ClassLiveMode.STUDENT && this.currentQuestion && !this.showAnswerDialog) { + Button("查看题目") + .width(150) + .height(44) + .fontSize(16) + .backgroundColor(this.settings.themeColor) + .borderRadius(22) + .fontColor(Color.White) + .onClick(() => { + this.showAnswerDialog = true; + }) + } + } + .width('100%') + .justifyContent(FlexAlign.Center) + .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') + } + .margin({ top: 10 }) + } + } + .width('100%') + .height('100%') + .padding(16) + } + .width('100%') + .height('100%') + } + + // 格式化时间 + private formatTime(timestamp: Date): 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) { + classRoomService.removeEventListener(WebSocketEventType.SEND_MESSAGE, this.messageCallback); + } + + if (this.questionCallback) { + classRoomService.removeEventListener(WebSocketEventType.PUBLISH_QUESTION, this.questionCallback); + } + + if (this.endQuestionCallback) { + classRoomService.removeEventListener(WebSocketEventType.END_QUESTION, this.endQuestionCallback); + } + + if (this.endClassCallback) { + 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' + }); + } + }; + + const secondaryButton: DialogButton = { + value: '取消', + action: () => {} + }; + + const alertConfig: AlertDialogConfig = { + title: '离开课堂', + message: '确定要离开当前课堂吗?', + primaryButton: primaryButton, + secondaryButton: secondaryButton + }; + AlertDialog.show(alertConfig); + } + + // 返回true表示已处理返回事件 + return true; + } +} \ No newline at end of file diff --git a/entry/src/main/ets/pages/ClassPage.ets b/entry/src/main/ets/pages/ClassPage.ets index c9eb933..2f92d9c 100644 --- a/entry/src/main/ets/pages/ClassPage.ets +++ b/entry/src/main/ets/pages/ClassPage.ets @@ -3,6 +3,15 @@ import { hilog } from '@kit.PerformanceAnalysisKit'; import { MonitorDataType } from './HomePage'; import settingsService, { SettingsModel, TextResources } from '../common/SettingsService'; import logManager, { LogCategory, LogEventType } from '../common/logtext'; +import { DatabaseService } from '../common/DatabaseService'; +import classRoomService, { ClassSessionModel, QuestionOption } from '../common/ClassRoomService'; + +// 用户角色类型 +enum UserRole { + STUDENT = 'student', + TEACHER = 'teacher', + UNKNOWN = 'unknown' +} @Entry @Component @@ -13,7 +22,19 @@ struct ClassPage { time: '', content: '', status: false - }; // Initialize with empty values + }; + + // 新增状态 + @State userRole: UserRole = UserRole.UNKNOWN; + @State classCode: string = ''; + @State className: string = ''; + @State showCreateClassDialog: boolean = false; + @State showJoinClassDialog: boolean = false; + @State errorMessage: string = ''; + @State isLoading: boolean = false; + + // 数据库服务 + private dbService: DatabaseService = DatabaseService.getInstance(); // 从设置服务获取设置 @State settings: SettingsModel = settingsService.getSettings(); @@ -31,60 +52,658 @@ struct ClassPage { this.settings = settingsService.getSettings(); }); + // 检查用户角色 + const currentAccount = settingsService.getCurrentAccount(); + const category = this.dbService.getUserCategory(currentAccount); + + if (category === 'student') { + this.userRole = UserRole.STUDENT; + } else if (category === 'teacher') { + this.userRole = UserRole.TEACHER; + } else { + this.userRole = UserRole.UNKNOWN; + } + + // 同步设置到课堂服务 + classRoomService.checkIsTeacher(); + this.Log_Event('aboutToAppear'); } build() { - Column() { - // 顶部导航栏 - Row() { - Text(this.texts.classPageTitle).fontSize(22).fontColor(Color.White).fontWeight(FontWeight.Bold) - } - .width('100%') - .backgroundColor(this.settings.themeColor) - .height(60) - .justifyContent(FlexAlign.Center) - .padding({ left: 20 }) - - // 页面内容区 - Scroll() { - Column() { - // 当前课程信息 - Column() { - Text(this.currentClassInfo.name).fontSize(24).fontWeight(FontWeight.Bold).margin({ bottom: 10 }) - Text(`${this.texts.teacher}: ${this.currentClassInfo.teacher}`).fontSize(16).fontColor('#666').margin({ bottom: 8 }) - Text(`${this.texts.classroom}: ${this.currentClassInfo.room}`).fontSize(16).fontColor('#666') - } - .width('100%') - .backgroundColor(Color.White) - .borderRadius(12) - .padding(20) - .shadow({ radius: 6, color: '#eeeeee' }) - .margin({ top: 15, bottom: 15 }) - - // 上课数据卡片 - Card({ - title: this.texts.classData, - data: this.classData, - themeColor: this.settings.themeColor - }) + Stack({ alignContent: Alignment.TopStart }) { + Column() { + // 顶部导航栏 + Row() { + Text(this.texts.classPageTitle).fontSize(22).fontColor(Color.White).fontWeight(FontWeight.Bold) } .width('100%') - .padding({ left: 16, right: 16 }) + .backgroundColor(this.settings.themeColor) + .height(60) + .justifyContent(FlexAlign.Center) + .padding({ left: 20 }) + + // 页面内容区 + Scroll() { + Column() { + // 根据用户角色显示不同界面 + if (this.userRole === UserRole.STUDENT) { + this.StudentView() + } else if (this.userRole === UserRole.TEACHER) { + this.TeacherView() + } else { + this.UnknownRoleView() + } + } + .width('100%') + .padding({ left: 16, right: 16 }) + } + .layoutWeight(1) + .scrollBar(BarState.Off) + + // 底部导航栏 + BottomNavigation({ + activePage: 'class', + themeColor: this.settings.themeColor, + texts: this.texts + }) + } + .width('100%') + .height('100%') + + // 创建课堂对话框 + if (this.showCreateClassDialog) { + this.CreateClassDialog() + } + + // 加入课堂对话框 + if (this.showJoinClassDialog) { + this.JoinClassDialog() } - .layoutWeight(1) - .scrollBar(BarState.Off) - - // 底部导航栏 - BottomNavigation({ - activePage: 'class', - themeColor: this.settings.themeColor, - texts: this.texts - }) } .width('100%') .height('100%') } + + // 学生视图 + @Builder + StudentView() { + Column() { + // 学生上课指引卡片 + Column() { + Text("学生上课") + .fontSize(24) + .fontWeight(FontWeight.Bold) + .margin({ bottom: 16 }) + + Text("请输入老师提供的课堂暗号进入课堂") + .fontSize(16) + .fontColor('#666') + .margin({ bottom: 20 }) + + // 暗号输入框 + Column({ space: 8 }) { + Text("课堂暗号") + .fontSize(16) + .fontColor('#666666') + .alignSelf(ItemAlign.Start) + + TextInput({ placeholder: '请输入4-6位数字暗号' }) + .type(InputType.Number) + .maxLength(6) + .placeholderColor('#999999') + .placeholderFont({ size: 16 }) + .width('100%') + .height(50) + .fontSize(16) + .fontColor('#333333') + .backgroundColor('#F5F5F5') + .borderRadius(8) + .padding({ left: 16, right: 16 }) + .onChange((value: string) => { + this.classCode = value; + this.errorMessage = ''; + }) + } + .width('100%') + .margin({ bottom: 16 }) + + // 错误信息 + if (this.errorMessage !== '') { + Text(this.errorMessage) + .fontSize(14) + .fontColor('#FF0000') + .width('100%') + .margin({ bottom: 16 }) + } + + // 加入课堂按钮 + Button("加入课堂") + .width('100%') + .height(50) + .fontSize(18) + .fontWeight(FontWeight.Medium) + .backgroundColor(this.settings.themeColor) + .borderRadius(8) + .fontColor(Color.White) + .enabled(!this.isLoading) + .opacity(this.isLoading ? 0.6 : 1) + .onClick(() => { + this.joinClass(); + }) + } + .width('100%') + .backgroundColor(Color.White) + .borderRadius(12) + .padding(20) + .shadow({ radius: 6, color: '#eeeeee' }) + .margin({ top: 20, bottom: 20 }) + + // 当前课程信息卡片(只在没有正在进行的课程时显示) + Column() { + Text("近期课程").fontSize(20).fontWeight(FontWeight.Bold).margin({ bottom: 16 }) + + Row() { + Column() { + Text(this.currentClassInfo.name) + .fontSize(18) + .fontWeight(FontWeight.Bold) + Text(`${this.texts.teacher}: ${this.currentClassInfo.teacher}`) + .fontSize(14) + .fontColor('#666') + .margin({ top: 8 }) + Text(`${this.texts.classroom}: ${this.currentClassInfo.room}`) + .fontSize(14) + .fontColor('#666') + .margin({ top: 4 }) + } + .layoutWeight(1) + .alignItems(HorizontalAlign.Start) + + Column() { + Text("未开始") + .fontSize(14) + .fontColor('#999') + .backgroundColor('#f5f5f5') + .borderRadius(12) + .padding({ left: 12, right: 12, top: 6, bottom: 6 }) + } + .justifyContent(FlexAlign.Center) + } + .width('100%') + .padding(16) + .borderRadius(8) + .backgroundColor('#f9f9f9') + } + .width('100%') + .backgroundColor(Color.White) + .borderRadius(12) + .padding(20) + .shadow({ radius: 6, color: '#eeeeee' }) + } + } + + // 教师视图 + @Builder + TeacherView() { + Column() { + // 教师上课指引卡片 + Column() { + Text("教师上课") + .fontSize(24) + .fontWeight(FontWeight.Bold) + .margin({ bottom: 16 }) + + Text("点击下方按钮创建新课堂") + .fontSize(16) + .fontColor('#666') + .margin({ bottom: 20 }) + + // 创建课堂按钮 + Button("创建新课堂") + .width('100%') + .height(50) + .fontSize(18) + .fontWeight(FontWeight.Medium) + .backgroundColor(this.settings.themeColor) + .borderRadius(8) + .fontColor(Color.White) + .enabled(!this.isLoading) + .opacity(this.isLoading ? 0.6 : 1) + .onClick(() => { + this.showCreateClassDialog = true; + }) + } + .width('100%') + .backgroundColor(Color.White) + .borderRadius(12) + .padding(20) + .shadow({ radius: 6, color: '#eeeeee' }) + .margin({ top: 20, bottom: 20 }) + + // 历史课堂记录 + Column() { + Text("历史课堂").fontSize(20).fontWeight(FontWeight.Bold).margin({ bottom: 16 }) + + // 示例历史课堂卡片 + Row() { + Column() { + Text("高等数学") + .fontSize(18) + .fontWeight(FontWeight.Bold) + Text("课堂暗号: 123456") + .fontSize(14) + .fontColor('#666') + .margin({ top: 8 }) + Text("2025-04-01 10:30") + .fontSize(14) + .fontColor('#666') + .margin({ top: 4 }) + } + .layoutWeight(1) + .alignItems(HorizontalAlign.Start) + + Column() { + Text("已结束") + .fontSize(14) + .fontColor('#999') + .backgroundColor('#f5f5f5') + .borderRadius(12) + .padding({ left: 12, right: 12, top: 6, bottom: 6 }) + } + .justifyContent(FlexAlign.Center) + } + .width('100%') + .padding(16) + .borderRadius(8) + .backgroundColor('#f9f9f9') + .margin({ bottom: 12 }) + + // 示例历史课堂卡片 + Row() { + Column() { + Text("线性代数") + .fontSize(18) + .fontWeight(FontWeight.Bold) + Text("课堂暗号: 654321") + .fontSize(14) + .fontColor('#666') + .margin({ top: 8 }) + Text("2025-03-30 14:30") + .fontSize(14) + .fontColor('#666') + .margin({ top: 4 }) + } + .layoutWeight(1) + .alignItems(HorizontalAlign.Start) + + Column() { + Text("已结束") + .fontSize(14) + .fontColor('#999') + .backgroundColor('#f5f5f5') + .borderRadius(12) + .padding({ left: 12, right: 12, top: 6, bottom: 6 }) + } + .justifyContent(FlexAlign.Center) + } + .width('100%') + .padding(16) + .borderRadius(8) + .backgroundColor('#f9f9f9') + } + .width('100%') + .backgroundColor(Color.White) + .borderRadius(12) + .padding(20) + .shadow({ radius: 6, color: '#eeeeee' }) + } + } + + // 未知角色视图 + @Builder + UnknownRoleView() { + Column() { + Text("用户角色未知") + .fontSize(24) + .fontWeight(FontWeight.Bold) + .margin({ bottom: 16 }) + + Text("系统无法识别您的用户角色,请联系管理员或重新登录") + .fontSize(16) + .fontColor('#666') + .margin({ bottom: 20 }) + + Button("返回登录") + .width('60%') + .height(50) + .fontSize(18) + .fontWeight(FontWeight.Medium) + .backgroundColor(this.settings.themeColor) + .borderRadius(8) + .fontColor(Color.White) + .onClick(() => { + // 返回登录页 + router.replaceUrl({ + url: 'pages/login' + }); + }) + } + .width('100%') + .backgroundColor(Color.White) + .borderRadius(12) + .padding(20) + .shadow({ radius: 6, color: '#eeeeee' }) + .margin({ top: 20 }) + } + + // 创建课堂对话框 + @Builder + CreateClassDialog() { + // 遮罩层 + Column() { + // 对话框内容 + Column() { + // 标题 + Text("创建新课堂") + .fontSize(22) + .fontWeight(FontWeight.Bold) + .margin({ bottom: 24 }) + + // 课堂名称 + Column({ space: 8 }) { + Text("课堂名称") + .fontSize(16) + .fontColor('#666666') + .alignSelf(ItemAlign.Start) + + TextInput({ placeholder: '请输入课堂名称' }) + .maxLength(20) + .placeholderColor('#999999') + .placeholderFont({ size: 16 }) + .width('100%') + .height(50) + .fontSize(16) + .fontColor('#333333') + .backgroundColor('#F5F5F5') + .borderRadius(8) + .padding({ left: 16, right: 16 }) + .onChange((value: string) => { + this.className = value; + }) + } + .width('100%') + .margin({ bottom: 16 }) + + // 课堂暗号 + Column({ space: 8 }) { + Text("课堂暗号") + .fontSize(16) + .fontColor('#666666') + .alignSelf(ItemAlign.Start) + + TextInput({ placeholder: '请输入4-6位数字暗号' }) + .type(InputType.Number) + .maxLength(6) + .placeholderColor('#999999') + .placeholderFont({ size: 16 }) + .width('100%') + .height(50) + .fontSize(16) + .fontColor('#333333') + .backgroundColor('#F5F5F5') + .borderRadius(8) + .padding({ left: 16, right: 16 }) + .onChange((value: string) => { + this.classCode = value; + }) + } + .width('100%') + .margin({ bottom: 16 }) + + // 错误信息 + if (this.errorMessage !== '') { + Text(this.errorMessage) + .fontSize(14) + .fontColor('#FF0000') + .width('100%') + .margin({ bottom: 16 }) + } + + // 按钮组 + Row({ space: 20 }) { + Button("取消") + .width('45%') + .height(45) + .fontSize(16) + .fontWeight(FontWeight.Medium) + .backgroundColor('#f5f5f5') + .borderRadius(8) + .fontColor('#333') + .onClick(() => { + this.showCreateClassDialog = false; + this.errorMessage = ''; + }) + + Button("创建") + .width('45%') + .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.createClass(); + }) + } + .width('100%') + } + .width('85%') + .backgroundColor(Color.White) + .borderRadius(16) + .padding(24) + } + .width('100%') + .height('100%') + .backgroundColor('rgba(0,0,0,0.5)') + .justifyContent(FlexAlign.Center) + .onClick(() => { + // 点击遮罩不关闭对话框 + // 只能通过按钮关闭 + }) + } + + // 加入课堂对话框 + @Builder + JoinClassDialog() { + // 遮罩层 + Column() { + // 对话框内容 + Column() { + // 标题 + Text("加入课堂") + .fontSize(22) + .fontWeight(FontWeight.Bold) + .margin({ bottom: 24 }) + + // 课堂暗号 + Column({ space: 8 }) { + Text("课堂暗号") + .fontSize(16) + .fontColor('#666666') + .alignSelf(ItemAlign.Start) + + TextInput({ placeholder: '请输入4-6位数字暗号' }) + .type(InputType.Number) + .maxLength(6) + .placeholderColor('#999999') + .placeholderFont({ size: 16 }) + .width('100%') + .height(50) + .fontSize(16) + .fontColor('#333333') + .backgroundColor('#F5F5F5') + .borderRadius(8) + .padding({ left: 16, right: 16 }) + .onChange((value: string) => { + this.classCode = value; + }) + } + .width('100%') + .margin({ bottom: 16 }) + + // 错误信息 + if (this.errorMessage !== '') { + Text(this.errorMessage) + .fontSize(14) + .fontColor('#FF0000') + .width('100%') + .margin({ bottom: 16 }) + } + + // 按钮组 + Row({ space: 20 }) { + Button("取消") + .width('45%') + .height(45) + .fontSize(16) + .fontWeight(FontWeight.Medium) + .backgroundColor('#f5f5f5') + .borderRadius(8) + .fontColor('#333') + .onClick(() => { + this.showJoinClassDialog = false; + this.errorMessage = ''; + }) + + Button("加入") + .width('45%') + .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.joinClass(); + }) + } + .width('100%') + } + .width('85%') + .backgroundColor(Color.White) + .borderRadius(16) + .padding(24) + } + .width('100%') + .height('100%') + .backgroundColor('rgba(0,0,0,0.5)') + .justifyContent(FlexAlign.Center) + .onClick(() => { + // 点击遮罩不关闭对话框 + // 只能通过按钮关闭 + }) + } + + // 创建课堂方法 + private async createClass() { + // 参数验证 + if (!this.className || this.className.trim() === '') { + this.errorMessage = '请输入课堂名称'; + return; + } + + if (!this.classCode || this.classCode.trim() === '') { + this.errorMessage = '请输入课堂暗号'; + return; + } + + if (!/^\d{4,6}$/.test(this.classCode)) { + this.errorMessage = '课堂暗号必须是4-6位数字'; + return; + } + + this.isLoading = true; + + // 连接服务 + await classRoomService.connect(); + + // 创建课堂 + try { + const classSession = await classRoomService.createClassSession(this.className, this.classCode); + + if (classSession) { + // 创建成功 + this.showCreateClassDialog = false; + this.errorMessage = ''; + + // 跳转到教师上课页面 + router.pushUrl({ + url: 'pages/ClassLivePage', + params: { + mode: 'teacher' + } + }); + } else { + this.errorMessage = '创建课堂失败,请稍后再试'; + } + } catch (error) { + this.errorMessage = '创建过程中发生错误,请稍后再试'; + logManager.error(LogCategory.CLASS, `Create class error: ${error}`); + } finally { + this.isLoading = false; + } + } + + // 加入课堂方法 + private async joinClass() { + // 参数验证 + if (!this.classCode || this.classCode.trim() === '') { + this.errorMessage = '请输入课堂暗号'; + return; + } + + if (!/^\d{4,6}$/.test(this.classCode)) { + this.errorMessage = '课堂暗号必须是4-6位数字'; + return; + } + + this.isLoading = true; + + // 连接服务 + await classRoomService.connect(); + + // 加入课堂 + try { + const result = await classRoomService.joinClassSession(this.classCode); + + if (result) { + // 加入成功 + this.showJoinClassDialog = false; + this.errorMessage = ''; + + // 跳转到学生上课页面 + router.pushUrl({ + url: 'pages/ClassLivePage', + params: { + mode: 'student' + } + }); + } else { + this.errorMessage = '加入课堂失败,请检查暗号是否正确'; + } + } catch (error) { + this.errorMessage = '加入过程中发生错误,请稍后再试'; + logManager.error(LogCategory.CLASS, `Join class error: ${error}`); + } finally { + this.isLoading = false; + } + } // 页面生命周期方法 onPageShow(): void { @@ -256,10 +875,7 @@ struct BottomNavigation { .onClick(() => { if (this.activePage !== 'settings') { router.replaceUrl({ - url: 'pages/SettingsPage', - params: { - direction: 'right' // 从上课页向右切换到设置页 - } + url: 'pages/SettingsPage' }); } }) @@ -267,7 +883,9 @@ struct BottomNavigation { .width('100%') .height(60) .backgroundColor(Color.White) - .border({ color: '#eeeeee', width: 1, style: BorderStyle.Solid }) + .borderWidth({ top: 0.5 }) + .borderColor('#eeeeee') + .padding({ top: 8, bottom: 8 }) } } diff --git a/entry/src/main/resources/base/media/add.png b/entry/src/main/resources/base/media/add.png new file mode 100644 index 0000000000000000000000000000000000000000..de12102935f9990e0b9ea8653febf3cb402eb9db GIT binary patch literal 3439 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 CcYy)` literal 0 HcmV?d00001 diff --git a/entry/src/main/resources/base/media/back.png b/entry/src/main/resources/base/media/back.png new file mode 100644 index 0000000000000000000000000000000000000000..415b590bacbbcd7a1b16f622e39a984748af2b59 GIT binary patch literal 1362 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k4M?tyST~P>fmPDe#WAE}&fB|&c_HBvZ4Zli zL>6*dGe{Mc&HK~m;KjKA z*}1*OcF#ZATvC_aUjF5jom}QJu?Le`m^d39oD?PqaHtF-rt#+8)!)7LteNg@QNB)Y zf7RDFsWOq19nu~h>s_C}ulo19<4=orhW}fCZSmZvpLW~F%iHwLm+(+9E|5BYyZ+uk zvE$ww`&U_8^NV*s|J~}ya%pGiqaO*&r{w=tlXO#%l@>Z)|LFYM!-vJgA0O$;>fz(< zd%X9v___Na)nG#|Oh*J$DrElBO;-_mm z7T12vvE+XA!r3N%o3G5@j`TBTos0E9&bc{l>!=N$eWJ=+bPshc@ z_22ES`T69%#pXn&n@ojYycML?`u1_A0ByBz%;~Ur=BOa8(Dzf|#1XZg#}oS%xAR;f=3MNnIb1!*zq$(9&Rc8;c?*@XUYQ2 z#~(OV-U^>^xW4}W&(%89!%ulUdeQ8_R`LJ#)p6$ zW8)kl&-kn!QQvR*aT7}xACpM<{%h{yr$v>PeTzGm@S7fEh>zV}`+v!l_(vAAJZ^<2 z^K1d8nS-C6JXvnhr#5NZRf%kYlg}3(i3Z2d6_N0rZE$vrBQey%c9Q@@Z_8c9>2npb7pw_3Qf*w^!OE$oYU#?D>yl4 zqQ|eGD_oBeW zElZDO^myC~Og8aQ$z5S6Wtyw{WW(p3I9_swC>CCrmugI-^J_^^9* zVUnw<8~d`ZCx2Q$Z;#ts_06bnje~;hOr=S?HW%k_toXNNgHU5hT8;`6C^1V3d$JUn zGFfV^aZ#8Xr9LU@^yQ-u55^^?GQGTUz@zVR)4u0Tc0CSO+p|cYlwitM)8wF_ mDB!`tBE;0#GMJeEnfRs8KfidFlLc5-GI+ZBxvX9VGHLETrAL`B82%gDA!P@_WGvlWD4W@cT0Qgd{0M`h z+OyD*P;z?Zqc+f4<6Ta##T|cad}qL7su<5>B$2 z^T<`-_al{&?(X12wm-tzzgFx871=UlevmD!=B_tlU7a!F?j58u@A6DULVKB=m|PAq zXJp2&9^@m8tz2wtcQM3#CNnbA3ood-J|APx9e^Qbhs+ooRG6>kLPn=^pJ^dxgUlEb zG@kLMmpgna@m2L!+pWK}u`WXIRfkr7H(W}5G=~{@FSwUoo;F*}QvFr`No>SUN4WP^ z0yt5wI_`!`mR%8+i@oGDcA$sh#omnmb|Ok@;wU#t4%g40HQ@p6vAMtPr%xExM=kfk zv>m72I`_Bp6Hj#S1jU1jc|68347`!j|KL7o&CjpK%?`5(n$L0;$8w7B8zm>}$AtBO zHNUX3rD2l*3CM+ddA<~g#_5x<`bCC=3n@RCe?|yTE6EM!0*{T7rTU2R<(GrW6HUtR zkLxn8t0K9N4 zLw&p}EKOx3yPpc`bglRDSY4G=c7HfHKcL{@nu-O8h62tH%7I#&;4cV^qZ(gtv(=}y zdBLX$i!zOGsQJUpk?gWr{Hd3|w1;5$ou11<;E*;_0=n zx*ZdEaQ{!^&5XcxhQn={fmG|fBYBhA0|*WsE7v#41+f55^JJTNiaKH6=%`tl|rNnuu7%zDwFAEvIYEHJ(`lTc^!mN>- z2Hv8K>KEF(#;&Pmf9I4*2_H=;u~!_yrYfLP@(0qMXi^(d=sPP>k17Feh`$^28204H zd?;fxUMA;!n`gokz1Ai7M-g;G%;zRFF#s`W1l?e4lF8sjMvR>V-9ShZ_Vqp_QxBD1 z&KV4BN)bDWmC0K!KlB$1M=iG_C64CoZvSJo(Uedud^{ zQq@kRHz2e-pA#m&TsYan2$jG3O2FL)G*K9cpyQDAMP2h!&Gs|UpU%}4B-In*o<)Ry zh4%A6`n=Lcw-fB^izQ4OZ$B7~Gdwa(6os&HuO0E+0-+gqYAkbPtD0SV5Y5!~oxLyXq+Y4Zj&= literal 0 HcmV?d00001 diff --git a/entry/src/main/resources/base/media/delete.png b/entry/src/main/resources/base/media/delete.png new file mode 100644 index 0000000000000000000000000000000000000000..5990164c5ad184cc533f201c0ae3bc58b771971b GIT binary patch literal 5197 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 MhJkt+!Y<?TG{`ft6%r zWPt8&EY3dp!(mY#WRa9D<&>}5dOE=4_a>L+L9Z-(*PEVHkP3CE!Qp5iYX$+&wlO8Z## z{RIvOuls9XMZrK&GU6&g_6K!EfE)CC1p&DSz39Y?Plt3JB%h}T8X~Q5S|{+2o;-=J z5^NI$Y)U&9v)tFvu12;aNxm*XLcWGr;ig`iRbom4MwplCK1S7joI`XW2bduxqG^F& zL+Bk;CG5I_5phqN>3gQ>7}I1dXX}gH=FsivvEkta2CCg)mz(@+VQ$4>|B0c#0uu^r z$Rr~_@>>I{jSe==LYQLF=*pyogalMZk`*qZ9UI ziifU5M$Q=Q+eYcpl$A8u*;FEOub8^%)AjWS@?EsKcB5m`Mq_Pzd8ZA3zmU9YsL^f< z$eo}6t35i~f``GvJKf$eCyiUSZXvchCCtYU+v%LcEvO6(e(Jh&z zT-P0z*=Vl}2(ao7DR+c*`_w}x@IFO+@asQIY@;sWE@iP5DuC-GkB)_s<^IiTI!lc> zs7O&s1FrKiwN1?G_Z{FeJlT+%Gd|dVz|=i1Cvz4y?V!@Y!Kj!#NGOhli!msHE=xvD zb0J4{yS(AZ2CKT=2PsgaRP#b6$g?oCGLwTzTd$E^Qw;*YA>LO`uE3WGYE7SHZq_W$ zEv_$3Qt4mgR=(veeBhgz4WHe&+6K??#lTbZeMwJ+5{*G#>lnVvHfrnx#6C&m6PxXd zN@=6BUxsl%9+HGq?IL5a)|_Q$GNXO&9J@L5BvfqaZRaB>k!=eaDm>f z(7>se@eB+9^V&c87z%AesPDdb0c{iH{0BY~!kn(Fxx{5ZE*c&1PeX^8zhDKM3rpHx zy?QmcereH{l+!A|+kv*;BBHB~PEi*w#l$qtHFZ5xnzYT-laKzH;8rdd(9aIW_is%- zffW@FK`5`Mn0sT3cH7WSj2l9XL0 zBg&?(yYlJN!9Ww5IoZw;npmfmRAsLszZiN(S_|;;OW+>#-DFq_kp2JO{1$oTTOkEn)n_wiJ z3r&=_>^uA1jf^R6wk1gX@^gClMdm37WZnDLr?A7^`RMxuK3C`D4J}>lQZ=6hX@>ui z_6_jB1qv){LGDE&+dVoqk(r3pzE(8p^LD8`WO|m%(@`Y@YV2HZqR(WEjTzBeydSrY}G2B>G{|1x5=eXPrNS1COu8K0uS_C0DAT%e;}4#2);9*?_CjZRK9Ip`tT&t^6|rIt`^swmU4~ zTuhY8BxU%V72Op|jo9Dy*cGKKn2lhYfo|x{*r$J9D%}XqoCVMM`uTk`uI)ieleFF9 z2eidZ3tpBo6oiaXN^4;YK{qOk0gK+fHX8}e78Vj4S$yLJ!r$|ks|X#FLo~~*96AnT z-5D*eQ|#cT${kSI0T{