《云上选座》项目分析

前言

该项目是使用Vue3+Node.js+mongodb数据库实现的,主要实现了用户登录验证、时段选择、教室选择以及座位选择功能

项目实现效果

选座流程:登录 → 选座 → 选座成功

登录页面,输入学号和姓名,点击登录按钮进行登录,这里的登录的信息是存储在数据库中的,如果输入的学号和姓名信息与数据库中存储的信息不一致,则会有一个弹窗,提示学号必须是23503开头的9位或9-12位数字字符串,只有当输入的学号和姓名信息和数据库中的信息一致时,才能登录成功,并跳转到选座页面,进行选座!

选座页面,最上面有一个退出按钮,用户点击退出,则会退出登录,返回到登录页面,然后下面是一个选座时间段和教室号的提示,当然每个教室的座位信息也是不同的,它们是实时更新的,在不同的时间段登录,它显示的就是不一样的。用户可以点击不同的教室进行选座,点击座位后,可以点击确认锁定座位,同时会向后端发送选座请求,并对你的token值进行验证,之后会跳转到选择成功的页面,这样选座就成功了。

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

前端逻辑代码

接下来,我们来看看是如何实现选座的,它的逻辑是怎样的吧!

  1. 退出按钮功能 :用户点击退出按钮,就会触发 logout 方法,实现用户退出登录
  2. 信息展示 :使用 <pre> 标签展示课程开设时间和可选座时段信息
  3. 时段选择 :遍历 ampmArr 数组,渲染选座时段选项
  4. 教室选择 :遍历 vmRoomArr 数组渲染教室选项,点击触发 selectRoom 方法加载对应教室数据。
  5. 座位渲染 :通过两层 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)
    });
});

获取座位列表和选择座位

用于处理时间信息和座位状态的更新

  1. 定义了一个输入验证函数 validateInput,用于验证 tokenseat_id 是否有效
  2. 使用 validateSeat 函数验证座位号是否在有效范围内(1-154)。
  3. 使用 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;
};

总结

以上就是云上课堂选座的内容,这个项目提供了一个线上可视化选座的平台,操作步骤简单,不仅为学生提供了自由选座,也让老师能够清楚地看出教室的选座情况。

相关推荐
IT瘾君30 分钟前
JavaWeb:前端工程化-Vue
前端·javascript·vue.js
zhang988000030 分钟前
JavaScript 核心原理深度解析-不停留于表面的VUE等的使用!
开发语言·javascript·vue.js
恸流失31 分钟前
DJango项目
后端·python·django
potender32 分钟前
前端框架Vue
前端·vue.js·前端框架
Mr Aokey3 小时前
Spring MVC参数绑定终极手册:单&多参/对象/集合/JSON/文件上传精讲
java·后端·spring
地藏Kelvin4 小时前
Spring Ai 从Demo到搭建套壳项目(二)实现deepseek+MCP client让高德生成昆明游玩4天攻略
人工智能·spring boot·后端
拉不动的猪4 小时前
都25年啦,还有谁分不清双向绑定原理,响应式原理、v-model实现原理
前端·javascript·vue.js
菠萝014 小时前
共识算法Raft系列(1)——什么是Raft?
c++·后端·算法·区块链·共识算法
长勺4 小时前
Spring中@Primary注解的作用与使用
java·后端·spring
小奏技术5 小时前
基于 Spring AI 和 MCP:用自然语言查询 RocketMQ 消息
后端·aigc·mcp