前言
该项目是使用Vue3+Node.js+mongodb数据库实现的,主要实现了用户登录验证、时段选择、教室选择以及座位选择功能
项目实现效果
选座流程:登录 → 选座 → 选座成功
登录页面,输入学号和姓名,点击登录按钮进行登录,这里的登录的信息是存储在数据库中的,如果输入的学号和姓名信息与数据库中存储的信息不一致,则会有一个弹窗,提示学号必须是23503开头的9位或9-12位数字字符串,只有当输入的学号和姓名信息和数据库中的信息一致时,才能登录成功,并跳转到选座页面,进行选座!
选座页面,最上面有一个退出按钮,用户点击退出,则会退出登录,返回到登录页面,然后下面是一个选座时间段和教室号的提示,当然每个教室的座位信息也是不同的,它们是实时更新的,在不同的时间段登录,它显示的就是不一样的。用户可以点击不同的教室进行选座,点击座位后,可以点击确认锁定座位,同时会向后端发送选座请求,并对你的token值进行验证,之后会跳转到选择成功的页面,这样选座就成功了。

在选座成功的页面中,你也可以清楚的看到,具体的教室、时间段、以及日期和星期,你选择的座位在哪里,点击搜索可以进行刷新,如果别人也选座成功了,你也可以看到他的选座情况。点击右侧的学生名单,可以看到在这个时间段上课的学生名单,再次点击就可以恢复原样。

前端逻辑代码
接下来,我们来看看是如何实现选座的,它的逻辑是怎样的吧!
- 退出按钮功能 :用户点击退出按钮,就会触发
logout
方法,实现用户退出登录 - 信息展示 :使用
<pre>
标签展示课程开设时间和可选座时段信息 - 时段选择 :遍历
ampmArr
数组,渲染选座时段选项 - 教室选择 :遍历
vmRoomArr
数组渲染教室选项,点击触发selectRoom
方法加载对应教室数据。 - 座位渲染 :通过两层
v-for
循环渲染教室座位布局,根据seat
的值判断是座位还是过道(1表示座位,0表示过道),点击座位触发handleSeatClick
方法。
vue
<template>
<div>
<button @click="logout">退出</button>
<!-- 添加导航链接 -->
<pre>
Exprsss开设时间:周一、周五全天 。Python周四 上午
当天可选座时段:
上午 08:00-12:00
下午 13:00-17:00
晚上 19:00-23:00
其他时间 记录为 非选座时段
</pre>
<!-- 上午、下午、晚上 -->
<section>
<ul class="ampm">
<li v-for="(item, index) in ampmArr
:class="{ 'pick': ampm === item }"
:key="index"
>
{{ item }}
</li>
</ul>
</section>
<section>
<ul class="room">
<li v-for="roomID in vmRoomArr"
:class="{ 'pick': vmRoom === roomID }"
:key="roomID"
@click="selectRoom(roomID)"
>
{{ roomID }}
</li>
</ul>
</section>
<!-- 讲台 -->
<div class="blackboard">讲台</div>
<div class="classroom">
<!-- 遍历每一排 -->
<div v-for="(row, rowIndex) in 教室数据.arr" :key="rowIndex" class="row">
<!-- 遍历每一个座位或过道 -->
<div v-for="(seat, colIndex) in row" :key="`${rowIndex}-${colIndex}`"
:class="{ 'seat': seat === 1, 'aisle': seat === 0, 'selected': getSeatById(教室数据.layout[rowIndex][colIndex])?.isSelected }"
@click="seat === 1 && handleSeatClick(教室数据.layout[rowIndex][colIndex])">
<span v-if="seat === 1">{{ 教室数据.layout[rowIndex][colIndex] }}</span>
</div>
</div>
</div>
</div>
</template>
登录验证
调用 验证token
方法,检查 localStorage
中是否存在 token
,若不存在则跳转到登录页
vue
// 判断是否有 token
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');
}
};
教室数据获取
用户选择教室时,通过使用async
异步获取数据,调用 fetchClassroomData
方法,发送 GET
请求,获取对应教室的座位布局数据。
vue
// 获取教室数据
const fetchClassroomData = async () => {
try {
// 获取本地存储的 token(身份验证凭证
const token = localStorage.getItem('token');
let URL = `${BASE_URL}/api/room/want?roomID=${vmRoom.value}`
// 发送 GET 请求(携带 token 进行身份验证)
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);
}
};
onMounted(() => {
console.log('组件已挂载,检查 token...');
验证token();
// 组件挂载后获取教室数据
fetchClassroomData();
});
选座功能
用户点击座位就会触发 handleSeatClick
方法,先更新本地座位选中状态,弹出确认框,确认后发送 POST
请求提交选座信息
选中状态管理
- 先将所有座位的
isSelected
设为false
,再将当前点击的座位设为true
- 这确保同一时间只有一个座位被选中
座位锁定流程
- 弹出确认对话框,用户确认后发送 POST 请求,锁定座位。
- 根据服务器返回的状态码(
code
)处理结果: - 成功(
code === 10000
):显示提示并跳转到查看页面。 - 失败:显示错误信息,锁定座位出错
vue
// 根据 seat_id 获取座位信息
const getSeatById = (seatId) => {
return 教室数据.value.data.find(seat => seat.seat_id === seatId);
};
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;
}
}
};
连接MongoDB数据库
需要用到的数据都存储在MongoDB数据库中,为了方便调用
javascript
// db.js
const { MongoClient } = require('mongodb');
// 数据库连接配置
class DB {
constructor(数据库名="云上教室") {
this.uri = "mongodb://localhost:27017/云上教室";
this.uri = "mongodb://'这里放你的IPV4地址':27017/云上教室";
//this.uri = "mongodb://127.0.0.1:27017/云上教室"; //上线设置为本机IP 速度更快
this.client = new MongoClient(this.uri);
this.db = this.client.db(数据库名);
return this.db;
}
// 封装连接数据库的函数
async connect() {
try {
await this.client.connect();
console.log('已连接到 MongoDB');
return this.db;
} catch (error) {
console.error('连接到 MongoDB 时出错:', error);
throw error;
}
}
// 封装关闭数据库连接的函数
async close() {
try {
await this.client.close();
console.log('已断开与 MongoDB 的连接');
} catch (error) {
console.error('断开与 MongoDB 的连接时出错:', error);
throw error;
}
}
}
module.exports = new DB();
后端逻辑代码
类文件
用于获取当前的时间信息(上午/下午、星期和日期)
node
class 工具 {
constructor() {
}
获取上午下午() {
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 '非选座时段';
}
}
获取星期() {
const daysOfWeek = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
const now = new Date();
const dayIndex = now.getDay();
return daysOfWeek[dayIndex];
}
//获取当前日期,格式为 YYYY-MM-DD
获取当前日期() {
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}`;
}
}// 定义 工具 类
// 导出 工具 类
module.exports = 工具;
路由处理函数
/api/room/list
,用于连接数据库,获取所有教室的数据
/api/room/want
,用于获取指定教室在特定日期和上下午时段的座位信息
node
路由.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
,用于查看指定教室在特定日期和上下午时段的座位详情
node
//查看教室座位 详情
路由.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)
});
});
获取座位列表和选择座位
用于处理时间信息和座位状态的更新
- 定义了一个输入验证函数
validateInput
,用于验证token
和seat_id
是否有效 - 使用
validateSeat
函数验证座位号是否在有效范围内(1-154)。 - 使用
validateToken
函数验证token
的格式是否正确。
node
// 封装输入验证逻辑
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;
};
总结
以上就是云上课堂选座的内容,这个项目提供了一个线上可视化选座的平台,操作步骤简单,不仅为学生提供了自由选座,也让老师能够清楚地看出教室的选座情况。