在校园管理领域,“规模化运营”与“个性化服务”的矛盾、“管理效率”与“服务体验”的平衡始终是技术团队的核心挑战。传统开发模式下,一套覆盖校园管理、师生服务、资源调度的智慧校园系统需投入35人团队开发16个月以上,且频繁面临“流程繁琐”“数据孤岛”“服务响应滞后”等问题。飞算JavaAI通过校园场景深度适配,构建了从智能管理到精准服务的全栈解决方案,将核心系统开发周期缩短70%的同时,实现师生服务满意度提升55%,为校园数字化转型提供技术支撑。本文聚焦智慧校园领域的技术实践,解析飞算JavaAI如何重塑校园系统开发范式。

请添加图片描述

作为每天在不同教学楼间狂奔的大学生,试过不少课表APP,要么广告弹窗不断,要么功能复杂到需要看教程。干脆自己动手做了个纯前端课表网页,支持手动添加、修改课程,数据存在浏览器里不用担心丢失。记录下开发过程和使用体验,说不定能帮到同样需要的同学。

先明确核心需求

其实我的需求很简单:

  • 能按星期和上课时间清晰展示课程
  • 随时能添加新课程(课程名、老师、教室这几个核心信息必须有)
  • 填错了能改,停课了能删,操作要简单
  • 关掉网页再打开,之前的课程还在
  • 界面干净点,别搞花里胡哨的装饰

技术选型

纯前端实现足够了,毕竟只是个人用,没必要搞服务器和数据库:

  • HTML + Tailwind CSS 搭页面框架
  • JavaScript 处理添加、修改这些逻辑
  • localStorage 存课程数据(简单直接,不用复杂配置)
  • Font Awesome 加几个必要的小图标(编辑、删除这些)

核心功能实现过程

在这里插入图片描述

1. 先设计数据结构

首先得想清楚一个课程需要包含哪些信息,用JavaScript对象来存:

// 课程数据结构设计
const course = {
  id: '唯一标识', // 用来区分不同课程,避免冲突
  name: '大学物理', // 课程名称
  teacher: '李教授', // 授课教师
  classroom: '2号实验楼302', // 上课地点
  day: 3, // 星期几(1-7对应周一到周日)
  section: 5, // 第几节课(1-10)
  color: '#10b981' // 课程卡片颜色(随机生成,方便区分)
}

2. 课表渲染逻辑

用表格来展示星期和时间段,然后动态生成课程卡片:

<!-- 课表表格结构 -->
<table class="w-full border-collapse">
  <thead>
    <tr>
      <th class="border border-gray-200 p-3 bg-gray-50">时间段</th>
      <th class="border border-gray-200 p-3 bg-gray-50">周一</th>
      <th class="border border-gray-200 p-3 bg-gray-50">周二</th>
      <!-- 其他星期的表头... -->
    </tr>
  </thead>
  <tbody id="timetable-body">
    <!-- 这里用JS动态生成课程行 -->
  </tbody>
</table>

渲染课程的核心代码:

// 渲染课表函数
function renderTimetable() {
  const tbody = document.getElementById('timetable-body');
  tbody.innerHTML = ''; // 清空现有内容
  
  // 循环生成10节课的行(假设每天最多10节课)
  for (let section = 1; section <= 10; section++) {
    const row = document.createElement('tr');
    
    // 添加时间段单元格(第几节课)
    const timeCell = document.createElement('td');
    timeCell.className = 'border border-gray-200 p-2 bg-gray-50';
    timeCell.textContent = `${section}`;
    row.appendChild(timeCell);
    
    // 循环生成星期列
    for (let day = 1; day <= 7; day++) {
      const cell = document.createElement('td');
      cell.className = 'border border-gray-200 p-1 min-h-[100px]';
      
      // 查找这个时间段的课程
      const courses = getCoursesByDayAndSection(day, section);
      
      // 生成课程卡片并添加到单元格
      courses.forEach(course => {
        const courseCard = createCourseCard(course);
        cell.appendChild(courseCard);
      });
      
      row.appendChild(cell);
    }
    
    tbody.appendChild(row);
  }
}

// 创建课程卡片
function createCourseCard(course) {
  const card = document.createElement('div');
  card.className = 'rounded-md p-2 mb-1 text-white relative';
  card.style.backgroundColor = course.color; // 设置卡片颜色
  
  // 卡片内容
  card.innerHTML = `
    <div class="text-sm font-bold">${course.name}</div>
    <div class="text-xs">${course.teacher || '无教师'}</div>
    <div class="text-xs">${course.classroom || '无教室'}</div>
    <!-- 编辑和删除按钮 -->
    <div class="absolute top-1 right-1 flex gap-1">
      <i class="fa fa-edit text-xs" onclick="editCourse('${course.id}')"></i>
      <i class="fa fa-trash text-xs" onclick="deleteCourse('${course.id}')"></i>
    </div>
  `;
  
  return card;
}

3. 添加和编辑课程功能

用一个弹窗表单来处理添加和编辑操作:

<!-- 添加/编辑课程弹窗 -->
<div id="course-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center hidden">
  <div class="bg-white p-6 rounded-lg w-full max-w-md">
    <h3 id="modal-title" class="text-xl font-bold mb-4">添加课程</h3>
    <form id="course-form">
      <input type="hidden" id="course-id"> <!-- 用于编辑时存储课程ID -->
      
      <div class="mb-3">
        <label class="block text-sm">课程名 *</label>
        <input type="text" id="course-name" class="w-full p-2 border border-gray-300 rounded" required>
      </div>
      
      <div class="mb-3">
        <label class="block text-sm">教师</label>
        <input type="text" id="course-teacher" class="w-full p-2 border border-gray-300 rounded">
      </div>
      
      <div class="mb-3">
        <label class="block text-sm">教室</label>
        <input type="text" id="course-classroom" class="w-full p-2 border border-gray-300 rounded">
      </div>
      
      <div class="grid grid-cols-2 gap-4 mb-3">
        <div>
          <label class="block text-sm">星期 *</label>
          <select id="course-day" class="w-full p-2 border border-gray-300 rounded" required>
            <option value="1">周一</option>
            <option value="2">周二</option>
            <!-- 其他星期选项... -->
          </select>
        </div>
        <div>
          <label class="block text-sm">节次 *</label>
          <select id="course-section" class="w-full p-2 border border-gray-300 rounded" required>
            <option value="1">1节 (8:00-8:45)</option>
            <option value="2">2节 (8:55-9:40)</option>
            <!-- 其他节次选项... -->
          </select>
        </div>
      </div>
      
      <div class="flex justify-end gap-2">
        <button type="button" id="cancel-btn" class="px-4 py-2 border border-gray-300 rounded">取消</button>
        <button type="submit" class="px-4 py-2 bg-blue-600 text-white rounded">保存</button>
      </div>
    </form>
  </div>
</div>

处理表单提交的逻辑:

// 处理表单提交
document.getElementById('course-form').addEventListener('submit', function(e) {
  e.preventDefault(); // 阻止表单默认提交行为
  
  // 收集表单数据
  const courseData = {
    id: document.getElementById('course-id').value || generateId(), // 编辑时用现有ID,新增时生成新ID
    name: document.getElementById('course-name').value,
    teacher: document.getElementById('course-teacher').value,
    classroom: document.getElementById('course-classroom').value,
    day: parseInt(document.getElementById('course-day').value),
    section: parseInt(document.getElementById('course-section').value),
    // 编辑时保留原有颜色,新增时随机生成
    color: document.getElementById('course-id').value ? 
      getCourseById(document.getElementById('course-id').value).color : 
      getRandomColor()
  };
  
  // 保存到本地存储
  saveCourse(courseData);
  
  // 刷新课表显示
  renderTimetable();
  
  // 关闭弹窗
  closeModal();
});

4. 本地数据存储

用localStorage简单处理数据存储:

// 保存课程到本地存储
function saveCourse(course) {
  // 从本地存储获取现有课程列表,没有则为空数组
  let courses = JSON.parse(localStorage.getItem('myTimetable') || '[]');
  
  // 查找是否已有该课程(编辑模式)
  const index = courses.findIndex(c => c.id === course.id);
  if (index > -1) {
    // 替换现有课程
    courses[index] = course;
  } else {
    // 添加新课程
    courses.push(course);
  }
  
  // 保存回本地存储
  localStorage.setItem('myTimetable', JSON.stringify(courses));
}

// 删除课程
function deleteCourse(id) {
  if (confirm('确定要删除这门课吗?')) {
    let courses = JSON.parse(localStorage.getItem('myTimetable') || '[]');
    // 过滤掉要删除的课程
    courses = courses.filter(c => c.id !== id);
    localStorage.setItem('myTimetable', JSON.stringify(courses));
    // 刷新课表
    renderTimetable();
  }
}

// 页面加载时初始化
function init() {
  renderTimetable(); // 渲染课表
  
  // 绑定添加课程按钮事件
  document.getElementById('add-course-btn').addEventListener('click', function() {
    openModal(); // 打开添加课程弹窗
  });
  
  // 绑定取消按钮事件
  document.getElementById('cancel-btn').addEventListener('click', closeModal);
}

使用体验和待优化的点

测试了几次,基本满足我的日常需求,比之前用Excel记课表方便太多。但用着用着也发现一些可以改进的地方:

  1. 现在只能添加单节课程,没法设置连堂(比如2-3节这种情况),只能手动添加两节相同的课程
  2. 没有拖拽功能,想调整课程时间只能先删除再添加
  3. 换浏览器或者清理缓存会丢失数据,虽然概率不高但确实有点麻烦
  4. 手机上查看时表格会横向滚动,虽然能用但体验一般

下一步打算先加上连堂功能,这个需求最迫切。然后优化下移动端显示,毕竟上课前基本都是用手机看课表。

整体来说,这个小工具虽然简单,但完全是按自己的使用习惯做的,没有多余功能,打开就能用。这种"量身定制"的感觉真的很好,开发过程也学到了不少前端小技巧。如果你也在为课表管理烦恼,不妨试试自己动手做一个,其实没想象中那么难~

<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>极简课表</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
<style>
.course-card {
transition: all 0.2s;
}
.course-card:hover {
transform: translateY(-2px);
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
</style>
</head>
<body class="bg-gray-50 min-h-screen">
<div class="container mx-auto p-4 max-w-6xl">
<header class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-800">我的课表</h1>
<button id="add-course-btn" class="bg-blue-600 text-white px-4 py-2 rounded-md flex items-center gap-2 hover:bg-blue-700 transition-colors">
<i class="fa fa-plus"></i> 添加课程
</button>
</header>
    <!-- 课表表格 -->

    <div class="bg-white rounded-lg shadow-sm overflow-hidden">
        <table class="w-full border-collapse">
            <thead>
                <tr class="bg-gray-50">
                    <th class="border border-gray-200 p-3 text-left">时间段</th>
                    <th class="border border-gray-200 p-3 text-center">周一</th>
                    <th class="border border-gray-200 p-3 text-center">周二</th>
                    <th class="border border-gray-200 p-3 text-center">周三</th>
                    <th class="border border-gray-200 p-3 text-center">周四</th>
                    <th class="border border-gray-200 p-3 text-center">周五</th>
                    <th class="border border-gray-200 p-3 text-center">周六</th>
                    <th class="border border-gray-200 p-3 text-center">周日</th>
                </tr>
            </thead>
            <tbody id="timetable-body">
                <!-- 课程内容将通过JS动态生成 -->
            </tbody>
        </table>
    </div>

    <!-- 使用提示 -->

    <div class="mt-4 text-sm text-gray-500">
        <p><i class="fa fa-lightbulb-o"></i> 提示:点击课程卡片可编辑,数据保存在浏览器本地</p>
    </div>
</div>

<!-- 添加/编辑课程弹窗 -->

<div id="course-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center hidden z-50">
    <div class="bg-white p-6 rounded-lg w-full max-w-md mx-4">
        <h3 id="modal-title" class="text-xl font-bold mb-4 text-gray-800">添加课程</h3>
        <form id="course-form">
            <input type="hidden" id="course-id">

            <div class="mb-4">
                <label for="course-name" class="block text-sm font-medium text-gray-700 mb-1">课程名 <span class="text-red-500">*</span></label>
                <input type="text" id="course-name" class="w-full p-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500/50" required>
            </div>

            <div class="mb-4">
                <label for="course-teacher" class="block text-sm font-medium text-gray-700 mb-1">教师</label>
                <input type="text" id="course-teacher" class="w-full p-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500/50">
            </div>

            <div class="mb-4">
                <label for="course-classroom" class="block text-sm font-medium text-gray-700 mb-1">教室</label>
                <input type="text" id="course-classroom" class="w-full p-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500/50">
            </div>

            <div class="grid grid-cols-2 gap-4 mb-6">
                <div>
                    <label for="course-day" class="block text-sm font-medium text-gray-700 mb-1">星期 <span class="text-red-500">*</span></label>
                    <select id="course-day" class="w-full p-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500/50" required>
                        <option value="1">周一</option>
                        <option value="2">周二</option>
                        <option value="3">周三</option>
                        <option value="4">周四</option>
                        <option value="5">周五</option>
                        <option value="6">周六</option>
                        <option value="7">周日</option>
                    </select>
                </div>
                <div>
                    <label for="course-section" class="block text-sm font-medium text-gray-700 mb-1">节次 <span class="text-red-500">*</span></label>
                    <select id="course-section" class="w-full p-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500/50" required>
                        <option value="1">1节 (8:00-8:45)</option>
                        <option value="2">2节 (8:55-9:40)</option>
                        <option value="3">3节 (10:00-10:45)</option>
                        <option value="4">4节 (10:55-11:40)</option>
                        <option value="5">5节 (14:00-14:45)</option>
                        <option value="6">6节 (14:55-15:40)</option>
                        <option value="7">7节 (16:00-16:45)</option>
                        <option value="8">8节 (16:55-17:40)</option>
                        <option value="9">9节 (19:00-19:45)</option>
                        <option value="10">10节 (19:55-20:40)</option>
                    </select>
                </div>
            </div>

            <div class="flex justify-end gap-3">
                <button type="button" id="cancel-btn" class="px-4 py-2 border border-gray-300 rounded hover:bg-gray-100 transition-colors">取消</button>
                <button type="submit" class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors">保存</button>
            </div>
        </form>
    </div>
</div>

<script>
    // 页面加载完成后初始化
    document.addEventListener('DOMContentLoaded', init);

    // 初始化函数
    function init() {
        renderTimetable(); // 渲染课表
        
        // 绑定按钮事件
        document.getElementById('add-course-btn').addEventListener('click', () => openModal());
        document.getElementById('cancel-btn').addEventListener('click', () => closeModal());
        document.getElementById('course-form').addEventListener('submit', handleFormSubmit);
    }

    // 渲染课表
    function renderTimetable() {
        const tbody = document.getElementById('timetable-body');
        tbody.innerHTML = ''; // 清空现有内容
        
        // 生成10节课的行
        for (let section = 1; section <= 10; section++) {
            const row = document.createElement('tr');
            
            // 时间段单元格
            const timeCell = document.createElement('td');
            timeCell.className = 'border border-gray-200 p-2 bg-gray-50 font-medium';
            timeCell.textContent = `${section}`;
            row.appendChild(timeCell);
            
            // 生成星期列
            for (let day = 1; day <= 7; day++) {
                const cell = document.createElement('td');
                cell.className = 'border border-gray-200 p-1 min-h-[100px] vertical-align-top';
                
                // 查找该时间段的课程
                const courses = getCoursesByDayAndSection(day, section);
                
                // 添加课程卡片
                courses.forEach(course => {
                    const courseCard = createCourseCard(course);
                    cell.appendChild(courseCard);
                });
                
                row.appendChild(cell);
            }
            
            tbody.appendChild(row);
        }
    }

    // 创建课程卡片
    function createCourseCard(course) {
        const card = document.createElement('div');
        card.className = 'course-card rounded-md p-2 mb-1 text-white relative';
        card.style.backgroundColor = course.color;
        
        // 课程卡片内容
        card.innerHTML = `
            <div class="text-sm font-bold truncate">${course.name}</div>
            <div class="text-xs truncate">${course.teacher || '无教师'}</div>
            <div class="text-xs truncate">${course.classroom || '无教室'}</div>
            <div class="absolute top-1 right-1 flex gap-1 opacity-70">
                <i class="fa fa-edit text-xs" onclick="editCourse('${course.id}', event)"></i>
                <i class="fa fa-trash text-xs" onclick="deleteCourse('${course.id}', event)"></i>
            </div>
        `;
        
        return card;
    }

    // 打开弹窗
    function openModal(courseId = null) {
        const modal = document.getElementById('course-modal');
        const title = document.getElementById('modal-title');
        
        // 重置表单
        document.getElementById('course-form').reset();
        document.getElementById('course-id').value = '';
        
        // 如果是编辑模式
        if (courseId) {
            title.textContent = '编辑课程';
            const course = getCourseById(courseId);
            if (course) {
                document.getElementById('course-id').value = course.id;
                document.getElementById('course-name').value = course.name;
                document.getElementById('course-teacher').value = course.teacher || '';
                document.getElementById('course-classroom').value = course.classroom || '';
                document.getElementById('course-day').value = course.day;
                document.getElementById('course-section').value = course.section;
            }
        } else {
            title.textContent = '添加课程';
        }
        
        modal.classList.remove('hidden');
    }

    // 关闭弹窗
    function closeModal() {
        const modal = document.getElementById('course-modal');
        modal.classList.add('hidden');
    }

    // 处理表单提交
    function handleFormSubmit(e) {
        e.preventDefault();
        
        // 收集表单数据
        const courseData = {
            id: document.getElementById('course-id').value || generateId(),
            name: document.getElementById('course-name').value,
            teacher: document.getElementById('course-teacher').value,
            classroom: document.getElementById('course-classroom').value,
            day: parseInt(document.getElementById('course-day').value),
            section: parseInt(document.getElementById('course-section').value),
            color: document.getElementById('course-id').value ? 
                getCourseById(document.getElementById('course-id').value).color : 
                getRandomColor()
        };
        
        // 保存课程
        saveCourse(courseData);
        
        // 刷新课表
        renderTimetable();
        
        // 关闭弹窗
        closeModal();
    }

    // 编辑课程
    function editCourse(courseId, e) {
        e.stopPropagation(); // 防止事件冒泡
        openModal(courseId);
    }

    // 删除课程
    function deleteCourse(courseId, e) {
        e.stopPropagation(); // 防止事件冒泡
        if (confirm('确定要删除这门课程吗?')) {
            let courses = getCourses();
            courses = courses.filter(course => course.id !== courseId);
            localStorage.setItem('myTimetable', JSON.stringify(courses));
            renderTimetable();
        }
    }

    // 生成唯一ID
    function generateId() {
        return Date.now().toString(36) + Math.random().toString(36).substr(2, 5);
    }

    // 获取随机颜色
    function getRandomColor() {
        const colors = [
            '#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', 
            '#ec4899', '#06b6d4', '#6366f1', '#14b8a6', '#f97316'
        ];
        return colors[Math.floor(Math.random() * colors.length)];
    }

    // 从本地存储获取所有课程
    function getCourses() {
        return JSON.parse(localStorage.getItem('myTimetable') || '[]');
    }

    // 根据ID获取课程
    function getCourseById(id) {
        return getCourses().find(course => course.id === id);
    }

    // 根据星期和节次获取课程
    function getCoursesByDayAndSection(day, section) {
        return getCourses().filter(course => course.day === day && course.section === section);
    }

    // 保存课程到本地存储
    function saveCourse(course) {
        let courses = getCourses();
        const index = courses.findIndex(c => c.id === course.id);
        
        if (index > -1) {
            // 更新现有课程
            courses[index] = course;
        } else {
            // 添加新课程
            courses.push(course);
        }
        
        localStorage.setItem('myTimetable', JSON.stringify(courses));
    }
</script>
</body>
</html>

这个课表工具用下来最舒服的点就是"轻量",打开网页就能用,不用安装APP,也不用注册登录。
在这里插入图片描述
开发的时候特意做了这些小细节:

  • 课程卡片用不同颜色区分,视觉上更清晰
  • 手机上也能正常操作,虽然表格会横向滚动但不影响使用
  • 输入框只做必要的验证,避免填写时太繁琐
  • 卡片内容会自动省略过长文本,不会出现排版错乱
    在这里插入图片描述

目前遇到的最大问题是如果同一时间有两门课,卡片会叠在一起,之后打算加个自动换行的功能。另外考虑加个"课程导出"功能,能生成图片分享给同学,这样小组讨论时就不用一个个报时间了。

总的来说,花了大半天时间做的这个小工具,解决了我实际的痛点,这种自己动手解决问题的感觉真的很棒~

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐