什么是云上教室?
为方便归纳高校课堂教室的使用状况以及师生之间明确双方动向的状态。
云上教室
通过学生选座的形式记录学生上课动向,并且方便教师对学生学习状态的掌控。
通过登录页面验证学生的身份,进入选座页面,学生根据实际情况选择相应座位,进入总览页面可以看到自己的所在的座位信息。

云上教室项目目录结构
项目的实现通过前端利用vue3、后端使用node.js以及MongoDB数据库。
项目结构严格按照前后端分离的要求进行。


数据库方面学生信息按班级为单位创建,并且还专门设置了活跃和座位记录集合,便于了解到学生上课经常选择的座位并分析学习状况。

云上后端文件夹拆分
工具文件夹
myFun.js
文件将其他文件内经常需要用的函数封装到一个js文件中,便于其他模块的调用。

创建getCurrentTime()和getCurrentDate()函数
,获取中国时区当前时间的字符串并且规范当前时间格式。

为保证学生信息的唯一性,在输入信息登录进去之后会为用户生成一个随机的token,作为身份认证的唯一证明。
创建generateRandomToken()函数
,为生成的token生成加密格式xxxx-xxxx-xxxx
的形式
js
function generateRandomToken() {
let A = crypto.randomBytes(2).toString('hex');
let B = crypto.randomBytes(2).toString('hex');
let C = crypto.randomBytes(2).toString('hex');
let token = `${A}-${B}-${C}`;
return token;
}
同样,在获得token的值之后,还需要常见一个validateToken(token)函数
来校验token格式是否符合规范。
校验方式通过正则表达式来校验每一部分的格式是否正确。

getIP()函数
获取本机的IPv4地址。
js
function getIP() {
const interfaces = os.networkInterfaces();
let IP = 0;
Object.keys(interfaces).forEach(interface => {
interfaces[interface].forEach(alias => {
if (alias.family === 'IPv4' && interface == 'WLAN') {
IP = alias.address;
}
});
});
return IP;
}
validateSeat()函数
用于验证座位号是否有效。
js
function validateSeat(str) {
// 定义正则表达式,匹配纯数字字符串
const positiveIntegerPattern = /^\d+$/;
// 检查输入字符串是否符合纯数字格式
if (positiveIntegerPattern.test(str)) {
// 将字符串转换为整数
const num = parseInt(str, 10);
// 检查数字是否小于等于154
if (num <= 154) {
return true; // 验证通过
}
}
// 如果不符合任何条件则返回false
return false;
}
将创建的函数封装成一个模块便于其他文件导入使用。
js
module.exports = {
getCurrentTime,
generateRandomToken,
getIP,
validateSeat,
report,
validateToken,
validateChinese,
getCurrentDate
};
类文件夹

工具.js
主要将时间、日期等相关信息创建成函数,并封装成一个类,用于获取当前具体时间。
获取上午还是下午时间,通过if...else...
来判定当前小时数在哪一个范围进行。
js
获取上午下午() {
const now = new Date();
const hours = now.getHours();
if (hours >= 8 && hours < 12) {
return '上午';
} else if (hours >= 13 && hours < 17) {
return '下午';
} else if (hours >= 19 && hours < 23) {
return '晚上';
} else {
return '非选座时段';
}
}
创建获取星期()函数
,利用getDay()返回当前具体星期日期。
js
获取星期() {
const daysOfWeek = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
const now = new Date();
const dayIndex = now.getDay();
return daysOfWeek[dayIndex];
}
创建获取当前日期()
函数,返回当前具体日期。
js
获取当前日期() {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
以上这些函数都被封装在了一个类名为工具
的class类里,将函数导出便于其他页面使用时直接导出类即可。

路由文件夹
搭建了学生、座位、教室以及数据库连接的相关接口,按各部分功能划分便于代码的维护并且独立测试各功能模块。

DB.js文件
封装 MongoDB 数据库连接、操作和关闭的模块,后端连接了MongoDB数据库,便于前端从后台获数据并展示。
专门创建了一个constructor(数据库名="云上教室")
的构造函数,默认连接到名为"云上教室"
的数据库。
js
constructor(数据库名="云上教室") {
this.uri = "mongodb://112.124.62.199:27017/云上教室";
this.uri = "mongodb://localhost:27017/云上教室";
//this.uri = "mongodb://127.0.0.1:27017/云上教室"; //上线设置为本机IP 速度更快
this.client = new MongoClient(this.uri);
this.db = this.client.db(数据库名);
return this.db;
}
接下来通过.client.connect()
的方式连接数据库,以及.client.close()
的方式断开与数据库的连接。

room.js文件
文件创建了多个接口来处理查询座位状态和教室信息。
代码引入了自定义模块,为后续获取时间以及数据库数据做出了准备。

/api/room/list 接口获取了数据库教室集合
的所有数据,并且通过时间进行升序排序。
js
路由.get('/api/room/list', async (req, res,next ) => {
// await 芒果.connect(); // 连接数据库
const 教室 = await DB.collection("教室");
const 数据 = await 教室.find().sort({ time: 1 }).toArray();
res.json(数据);
});
/api/room/want接口根据roomID来获取某一个教室座位的占用状况。
js
路由.get('/api/room/want', async (req, res ) => {
//一些参数 动态生成 Data
let { roomID = 506 } = req.query;
//发起请求时必须在有效时段
//服务器设置时间
let ampm = tool.获取上午下午();
let week = tool.获取星期();
let date = tool.获取当前日期();
// 数据校验 注意严格 区分数据类型 、大小写
// RoomID roomID ='501'
// 转换数字类型 在 Express 里,通过 req.query 获取的查询字符串参数默认都是字符串类型,这是因为 HTTP 请求中的查询字符串是以文本形式传输的,Express 不会自动对这些参数进行类型转换
roomID = parseInt(roomID);
//查看教室座位 数量
const 教室 =await DB.collection("教室");
let room = await 教室.findOne(
{ roomID }, // 查询条件
{ projection: { _id: 0 } } // 投影参数
);
// 关联数据 根据的应该是 date 决定,不是由Week决定的
const 座位记录 =await DB.collection("座位记录");
let 参数 = {
roomID,
date,
ampm
}
let 投影参数 = {
projection: { _id: 0,roomID:0,date:0,ampm:0 }
}
const seatArr = await 座位记录.find(参数,投影参数).sort({ time: 1 }).toArray();
console.table(seatArr)
// 确保 room 不为 null 再进行操作
const 合并数据 =room ? room.data?.map(座位 => {
const 记录 = seatArr.find(记录 => 记录.seat_id === 座位.seat_id);
return {
...座位,
...记录, // 保留原始记录数据
is_free: 记录 ? false : 座位.is_free // 动态设置状态
};
}) : [];
console.log( `--------------------------------------------------` )
console.log( 合并数据 )
res.json({
...(room || {}),
// 过滤完全无记录的座位,先检查元素是否为对象,再检查属性数量
data: 合并数据?.filter(item => typeof item === 'object' && item!== null && Object.keys(item).length > 3)
});
});
/api/room/look 接口更加详细的查看到某一个教室座位在具体时间段的使用情况。与want接口的查询逻辑类似,但是相较之下look接口允许更加灵活的时间控制。
js
路由.get('/api/room/look', async (req, res ) => {
let { roomID: rawRoomID, ampm: rawAmpm,date:dateFE } = req.query;
// 参数有效性校验
roomID = parseInt(rawRoomID);
if (isNaN(roomID)) roomID = 506; // 无效值时使用默认值
ampm = ['上午', '下午'].includes(rawAmpm) ? rawAmpm : tool.获取上午下午();
date =dateFE ?? tool.获取当前日期();
// 数据校验 注意严格 区分数据类型 、大小写
// RoomID roomID ='501'
// 转换数字类型 在 Express 里,通过 req.query 获取的查询字符串参数默认都是字符串类型,这是因为 HTTP 请求中的查询字符串是以文本形式传输的,Express 不会自动对这些参数进行类型转换
roomID = parseInt(roomID);
const 集合 =await DB.collection("教室");
let room = await 集合.findOne(
{ roomID }, // 查询条件
{ projection: { _id: 0 } } // 投影参数
);
// 新增字段过滤
// const { _id, ...教室数据 } = room ? room : {}; // 处理空值情况
console.log('room',room); // 现在输出已移除 _id 的数据
// 关联数据 根据的应该是 date 决定,不是由Week决定的
const 集合2 =await DB.collection("座位记录");
let 参数 = {
roomID,
date,
ampm
}
let 投影参数 = {
projection: { _id: 0,roomID:0,date:0,ampm:0 }
}
const seatArr = await 集合2.find(参数,投影参数).sort({ time: 1 }).toArray();
console.table(seatArr)
// 确保 room 不为 null 再进行操作
const 合并数据 =room ? room.data?.map(座位 => {
const 记录 = seatArr.find(记录 => 记录.seat_id === 座位.seat_id);
return {
...座位,
...记录, // 保留原始记录数据
is_free: 记录 ? false : 座位.is_free // 动态设置状态
};
}) : [];
console.log( `--------------------------------------------------` )
console.log( 合并数据 )
res.json({
...(room || {}),
// 过滤完全无记录的座位,先检查元素是否为对象,再检查属性数量
data: 合并数据?.filter(item => typeof item === 'object' && item!== null && Object.keys(item).length > 3)
});
});
seat.js文件
该页面从后台获取座位列表,用户选择相应的座位并且验证用户登录的信息以及token,处理座位的占用。
创建一个箭头函数来判断用户的token是否规范。如果格式有出入,那么此时调用report()
函数记录异常,并返回相应的错误信息。
js
const validateInput = (token, seat_id, ip) => {
if (!token || !seat_id) {
const msg = { text: `🐮,🐯,🐰,🐱,🐲,🐳,🐴,🐵 。 token和seat_id都是必需的!👀 🐶,🐷,🐸,🐹,🐺,🐻,🐼,🐽,🐾,🐿️, ` };
report(ip, msg);
return msg;
}
if (!validateSeat(seat_id)) {
const msg = { text: `🐱座位号应该在 154(含)以内!请核对真实座位,使用数字 ` };
report(ip, msg);
return msg;
}
if (!validateToken(token)) {
const msg = { text: `🐲🐲🐲🐲门票(token)应通过node 1获得,格式为 XXXX-XXXX-XXXX` };
report(ip, msg);
return msg;
}
return null;
};
创建一个 /api/seat/pick 选择座位接口,通过POST请求来处理用户选择座位。
js
路由.post('/api/seat/pick', async (req, res) => {
提示 = { text: '🐯🐯🐯' }
try {
const { token, roomID, seat_id } = req.body;
let ip = req.ip.slice(7);
console.log(`/api/seat/pick被访问!${ip}`);
const 验证结果 = validateInput(token, seat_id, ip);
if (验证结果) {
return res.json(验证结果);
}
//验证token有效
const one = await DB.collection("活跃").findOne({ "token": token, "ip": ip });
if (one) {
console.log(`你的token有效`);
console.log(one);
const 数据 = {
roomID,
seat_id,
name: one.name,
num: one.num,
token,
time: getCurrentTime(),
ip: one.ip,
};
// 更新座位状态
锁定时段(数据)
console.log(数据)
// if (数据.ampm == '非工作时段') {
// 提示 = { code: -1, text: `🐺🐺🐺 ${one.name} 登记操作不在有效时段` };
// return res.json(提示);
// }
await 判断座位(数据) //掉了await
} else {
提示 = { code: -1, text: `🐺🐺🐺你的门票(token)不存在或和ip不匹配${ip}` };
}
return res.json(提示);
} catch (error) {
console.log(error)
handleError(res, '选择座位接口出错: ' + error.message);
} finally {
// await client.close(); // 关闭数据库连接
}
});
创建一个判断座位状态的函数
,判定用户所选择的座位是否已经被选择过了,并且检查用户在这一个教室这一时段是否已经选择过座位了。
js
function 锁定时段(数据) {
// 获取时间字段的值
const 时间字符串 = 数据.time;
// 将时间字符串转换为 Date 对象
const 日期对象 = new Date(时间字符串.replace(/\//g, '-'));
// 获取日期
const 年 = 日期对象.getFullYear();
const 月 = String(日期对象.getMonth() + 1).padStart(2, '0');
const 日 = String(日期对象.getDate()).padStart(2, '0');
const 日期 = `${年}-${月}-${日}`;
// 获取星期
const 星期列表 = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
const 星期 = 星期列表[日期对象.getDay()];
// 判断时段
let 时段 = tool.获取上午下午();
// 将日期、星期和时段信息添加到数据对象中
数据.date = 日期;
数据.week = 星期;
数据.ampm = 时段;
return 数据;
}//锁定时段
stu.js文件
创建了多个函数或者是接口,处理学生信息登录认证,以班级为单位展示班级学生信息以及学生在特定时间段内选座的状态。
/api/stu/login创建登录接口,此处主要是验证学生姓名和学号,信息匹配成功之后就回将数据记录在活跃集合当中。
js
路由.get('/api/stu/login', async (req, res) => {
try {
let code = -1;
const { name, num } = req.query;
console.dir(req.query);
if(!name || !num) {
const message = {
text: `姓名name、学号num都是必需的!👀 🐮,🐯,🐰,🐱,🐲,🐳,🐴,🐵,🐶,🐷,🐸,🐹,🐺,🐻,🐼,🐽,🐾,🐿️ `,
code
};
return res.json(message);
}
let queryData = { name, num };
const result = await DB.collection('1班').aggregate([
{ $match: queryData },
{
$unionWith: {
coll: "2班",
pipeline: [{ $match: queryData }]
}
},
{
$unionWith: {
coll: "3班",
pipeline: [{ $match: queryData }]
}
},
{
$unionWith: {
coll: "4班",
pipeline: [{ $match: queryData }]
}
},
{
$unionWith: {
coll: "24级运维2班",
pipeline: [{ $match: queryData }]
}
},
{ $limit: 1 }
]).toArray();
console.log('__________________');
console.log(result);
let ip = req.ip.slice(7);
let time = getCurrentTime();
if (result.length >= 1) {
code = 10000;
let token = generateRandomToken();
let one = { ...queryData, ip, time, token };
await DB.collection("活跃").insertOne(one);
const text = `登录成功,欢迎 ${name} 同学!`;
res.json({ code, token, ip, time, text });
} else {
code = -1;
let token = -1;
ip = req.ip;
const text = `登录失败,${name} 同学!`;
res.json({ code, token, ip, time, text });
}
} catch (error) {
console.error('登录接口出错:', error);
res.status(500).json({ text: '服务器内部错误', code: -1 });
}
});
/api/stu/list/:id这个接口是以班级为单位获取到对应的数据库之后将班级学生姓名展示在页面列表之上,实现了学生信息可视化的效果。
js
路由.get('/api/stu/list/:id', async (req, res) => {
try {
const id = req.params.id;
if (['1', '2', '3', '4'].includes(id)) {
const collection = DB.collection(`${id}班`);
const data = await collection.find().sort({ num: 1 }).toArray();
return res.json(data);
} else {
const message = { text: `班级仅有数字 1、2、3、4 ` };
report(req.ip, message);
return res.json(message);
}
} catch (error) {
console.error('查看学生列表接口出错:', error);
res.status(500).json({ text: '服务器内部错误', code: -1 });
}
});
/api/stu/listdesk接口实时监控在某一时段内教室座位的占用状态,从数据库座位记录集合中查找用户是否已经在这一时段内的某教室选择了座位。
js
路由.get('/api/stu/listdesk', async (req, res) => {
let ampm = '上午';
let date = '2025-03-14'
const result = await DB.collection('2班').aggregate([
{
$lookup: {
from: "座位记录",
let: { studentNum: "$num" },
pipeline: [
{
$match: {
date, // 日期类型转换
ampm,
$expr: { $eq: ["$num", "$$studentNum"] }
}
}
],
as: "seat_data"
}
},
{
$addFields: {
isPick: { $gt: [{ $size: "$seat_data" }, 0] }
}
},
{
$project: {
_id: 0,
num: 1,
name: 1,
isPick: 1,
ampm: { $arrayElemAt: ["$seat_data.ampm", 0] } // 展示时段信息
}
}
]).toArray();
res.json({ code: 0, data: result });
});
芒果文件夹
从文件夹的名字中就可以看出来,主要是整理需要放置进MongoDB数据库的JSON文件
。

ADB.js文件
文件主要是为了连接mongoDB数据库所创建,并且初识化教室座位的数据,将所得到的数据插入到
教室集合
当中。

运用二维数组
表示教室的布局,1代表座位,0代表教室中非座位的地方。
为了生成用二维数组表示的教室布局,代码专门创建了一个函数实现效果。
js
function 生成数据(roomID, arr) {
let seatId = 1;
const layout = [];
const data = [];
// 生成 layout 数组
for (let i = 0; i < arr.length; i++) {
const row = [];
for (let j = 0; j < arr[i].length; j++) {
if (arr[i][j] === 1) {
row.push(seatId);
data.push({
seat_id: seatId,
remark: '备注',
is_free: false,
reArr: []
});
seatId++;
} else {
row.push(0);
}
}
layout.push(row);
}
return {roomID, arr, layout,data };
}
API.js文件
API.js相当于结合上述文件中所搭建的基础,搭建了一个后端服务器。
将模块路由挂载到/路径之下。

创建HTTPS的服务器。
js
https.createServer(sslOptions, app).listen(httpsPort, () => {
const interfaces = os.networkInterfaces();
let text = 'HTTPS 服务器已启动\n';
Object.keys(interfaces).forEach(interface => {
interfaces[interface].forEach(alias => {
if (alias.family === 'IPv4') {
text += `网络接口: ${interface}, 地址: ${alias.address} \n`;
}
});
});
text += `https://${getIP()}:${httpsPort}/`;
console.log(text);
}).on('error', (err) => {
if (err.code === 'EADDRINUSE') {
console.error(`🚷 HTTPS端口 ${httpsPort} 已被占用`);
} else {
console.error(`HTTPS服务器启动错误: ${err.message}`);
}
});
为了实现HTTPS服务器,需要加载SSL证书文件(.key
和 .pem
)

云上教室前端文件夹拆分
前端代码部分使用了vue3实现的页面可视化效果,将不同部分分成了不同的组件,方便代码的维护和页面的管理。

文件夹的路径如上,主要部分集中在方框内部。
App.vue文件
此文件负责了整个页面的布局。

<RouterView />
这个组件实现了动态渲染匹配到的路由组件的效果。
配置.js

文件代码仅仅只有一行单独的路径,由于目前本机即作为服务器有座位客户端使用,那么对于本机路径要求就十分严格。
index.js
该文件主要配置路由,严格按照页面路径创建和管理路由。
首先导入页面组件。
js
import Login from '../components/Login.vue';
import 选座 from '../components/选座.vue';
import 座位查看 from '../components/座位查看.vue';
import 个人 from '../components/个人.vue';
接下来需要定义路由规则。

最后创建路由实例。
js
const router = createRouter({
history: createWebHistory(),
routes
});
components文件夹
个人.vue
这个页面主要是记录用户的个人信息,根据游戏个人主页所迸发的灵感,包括学生姓名,学习状态,科目战斗统计等等。
代码通过路由查询参数在其中提取到了 seatName
和 seatId
,来显示不同的用户信息。

选座.vue

页面实现了在指定的时间段内选择教室和座位。
代码需要对页面教室布局的结构进行初始化,从服务器中获取存储的教室布局和座位状态。
js
const 教室数据 = ref({
roomID: null,
arr: [],
layout: [],
data: []
});
在用户登录之后成功获取到唯一的token才能进入教室选座这个页面,那么此时就需要对token
进行验证。
js
const hasToken = ref(false);
const 验证token = () => {
const token = localStorage.getItem('token');
console.log('token=', token);
hasToken.value = !!token;
console.log(hasToken.value);
if (!hasToken.value) {
console.log('跳转登录页');
router.push('/login');
}
};
运用异步函数
向后端发送请求获取教室的座位布局和状态。
js
const fetchClassroomData = async () => {
try {
const token = localStorage.getItem('token');
let URL = `${BASE_URL}/api/room/want?roomID=${vmRoom.value}`
const response = await fetch(URL, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (!response.ok) {
throw new Error(`HTTP 错误!状态:${response.status}`);
}
const data = await response.json();
教室数据.value = data;
} catch (error) {
console.error('获取教室数据出错:', error);
}
};
同样,这个部分需要对每个座位设置一个点击事件
,点击之后向后台发送请求来锁定当前的座位,座位没有被占用就成功跳转至首页,如果座位被占用那么此时页面则提示跳转失败。
js
const handleSeatClick = async (seatId) => {
// 先将所有座位的选中状态置为 false
教室数据.value.data.forEach(seat => {
seat.isSelected = false;
});
// 将当前点击的座位选中状态置为 true
const currentSeat = getSeatById(seatId);
if (currentSeat) {
currentSeat.isSelected = true;
}
console.log(`你点击了座位 ID 为 ${seatId} 的座位`);
if (confirm(`确认锁定座位 ID 为 ${seatId} 的座位吗?`)) {
let 请求参数 = {
seat_id: seatId,
roomID: 教室数据.value.roomID,
token: localStorage.getItem('token'),
}
console.log(请求参数)
try {
const 响应 = await fetch(`${BASE_URL}/api/seat/pick`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(请求参数)
});
if (!响应.ok) {
throw new Error(`HTTP 错误!状态:${响应.status}`);
}
// 处理响应数据
const { code, text } = await 响应.json(); //数据
if (code === 10000) {
alert(text);
let 参数 = {
name:'look',
query:{
roomID:vmRoom.value,
ampm:ampm.value,
}
}
router.push( 参数 );
} else {
alert(text);
}
// 更新本地座位状态
if (currentSeat) {
currentSeat.isLocked = true;
}
} catch (error) {
console.error('锁定座位出错:', error);
}
} else {
// 如果取消锁定,将当前座位的选中状态置为 false
if (currentSeat) {
currentSeat.isSelected = false;
}
}
};
既然在指定时间段内选择教室和座位,那么时间的要求也十分严苛,获取当前时间段来判定所处时间段。

座位查看.vue
页面把用户选择的座位显示出来,实现了教室座位的查看效果。

代码根据seat_id
从教室数据中查找对应的座位信息。
js
const getSeatById = (room, seat_id) => {
return room.data.find(seat => seat.seat_id === seat_id);
};
为搜索按钮处理点击事件,需要向后台发起请求获取数据,时刻更新加载状态。
js
const handleSearch = async () => {
try {
loading.value = true;
error.value = null;
let date = selectedDate.value
vmWeek.value = vmWeekArr[new Date().getDay()];
let URL = `${BASE_URL}/api/room/look?roomID=${vmRoom.value}&m=${vmAmpm.value}&date=${date}`;
const 响应 = await fetch(URL);
if (!响应.ok) {
throw new Error(`HTTP错误!状态码:${响应.status}`);
}
rooms.value = [await 响应.json()];
} catch (err) {
error.value = `数据加载失败:${err.message}`;
} finally {
loading.value = false;
}
};
login.vue

这个页面框架主要是登录页面,用户输入学号和姓名向后端发起请求获取数据,得到对应的数据之后就可以生成token进入选座页面。
定义了一个异步函数 handleLogin
,通过fecth的方式向后端发起请求获得响应。
js
const handleLogin = async () => {
try {
// 定义请求参数对象
const params = {
num: num.value,
name: name.value,
n: n.value
};
// 将参数对象转换为查询字符串
const queryString = new URLSearchParams(params).toString();
// 目标 API 的基础 URL
const baseUrl = `${BASE_URL}/api/stu/login`;
// 拼接完整的请求 URL
const apiUrl = `${baseUrl}?${queryString}`;
// 发起 Fetch 请求
const response = await fetch(apiUrl);
// 检查响应状态
if (!response.ok) {
throw new Error(`HTTP 错误! 状态码: ${response.status}`);
}
// 解析响应数据
const data = await response.json();
console.log(data);
if (data.code === 10000) {
// 存储 token 到 localStorage
localStorage.setItem('token', data.token);
console.log('登录成功,token 已存储');
// 登录成功后跳转到 App.vue
await router.push('/');
return;
} else {
// 显示服务器返回的错误信息
alert(data.text)
}
} catch (error) {
console.error('登录失败:', error);
alert('登录失败');
}
};
页面响应成功就会跳转至选座页面,相反,页面如果响应失败页面也会弹出错误信息。
换位思考,于我而言该如何制作这个云上教室项目
以上项目已经实现了一个基础的上课选座的功能,
但同样,页面功能其实还有许多需要完善的地方,就比如:
- 可以创建一个页面实现在某一时间段内教室占用情况,合理利用教室资源;
- 添加选座时间段的限制,严格规范选座时间;
- 个人页面可以新设一个学生出勤率的功能效果,使学生上课的出勤更加直观。
在技术代码方面,我还是会利用MongoDB数据库、node.js以及vue3实现页面效果。使用UI组件库让页面样式更加美观。