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 0000000..de12102 Binary files /dev/null and b/entry/src/main/resources/base/media/add.png differ 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 0000000..415b590 Binary files /dev/null and b/entry/src/main/resources/base/media/back.png differ diff --git a/entry/src/main/resources/base/media/close.png b/entry/src/main/resources/base/media/close.png new file mode 100644 index 0000000..44e1aad Binary files /dev/null and b/entry/src/main/resources/base/media/close.png differ 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 0000000..5990164 Binary files /dev/null and b/entry/src/main/resources/base/media/delete.png differ diff --git a/entry/src/main/resources/base/media/question.png b/entry/src/main/resources/base/media/question.png new file mode 100644 index 0000000..5e9dcab Binary files /dev/null and b/entry/src/main/resources/base/media/question.png differ diff --git a/entry/src/main/resources/base/profile/main_pages.json b/entry/src/main/resources/base/profile/main_pages.json index 8c521d4..d70ca8f 100644 --- a/entry/src/main/resources/base/profile/main_pages.json +++ b/entry/src/main/resources/base/profile/main_pages.json @@ -5,6 +5,7 @@ "pages/HomePage", "pages/SettingsPage", "pages/ClassPage", - "pages/TransitionPage" + "pages/TransitionPage", + "pages/ClassLivePage" ] } \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..2ffa3df --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{ + "name": "classmg-server", + "version": "1.0.0", + "description": "Communication server for ClassMG application", + "main": "server.js", + "scripts": { + "start": "node server.js" + }, + "dependencies": { + "express": "^4.18.2", + "body-parser": "^1.20.2", + "cors": "^2.8.5" + } +} \ No newline at end of file