添加测试按钮,修改上课页面逻辑

This commit is contained in:
cc 2025-04-02 15:15:20 +08:00
parent f1e7589344
commit 7faa9f1b35
13 changed files with 2548 additions and 111 deletions

103
README.md
View File

@ -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://<Your computer's IP address>: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"
This is a development server intended for local testing only. For production use, additional security measures would be required.

View File

@ -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<MessageModel> | Array<QuestionModel> | 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<string, Array<EventCallback>> = 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<ChannelApiResponse> {
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<ChannelApiResponse> {
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<void> {
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<void> {
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<boolean> {
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<ClassSessionModel | null> {
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<boolean> {
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<boolean> {
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<QuestionModel | null> {
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<boolean> {
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<boolean> {
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();

View File

@ -256,6 +256,11 @@ export class SettingsService {
this.refreshUserInfo();
}
// 获取当前用户账号
public getCurrentAccount(): string {
return this.currentAccount;
}
// 刷新用户信息
private refreshUserInfo(): void {
if (this.currentAccount) {

View File

@ -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",["添加课堂功能","修改上课页面逻辑"])
];
}

View File

@ -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;
}
}

View File

@ -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 })
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -5,6 +5,7 @@
"pages/HomePage",
"pages/SettingsPage",
"pages/ClassPage",
"pages/TransitionPage"
"pages/TransitionPage",
"pages/ClassLivePage"
]
}

14
package.json Normal file
View File

@ -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"
}
}