ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

.NET 6 + Vue 3 教务系统全栈实践:从排课到课表可视化

.NET 6 + Vue 3 教务系统全栈实践:从排课到课表可视化 简介这是一套面向高校信息化开发者与.NET全栈学习者的教务管理系统完整源码基于C#语言与前后端分离架构解决教育机构学生、课程、教师等核心业务的数字化管理需求。资源共1147个文件主体为1036个Vue组件文件构建响应式管理界面、46个JavaScript逻辑脚本、26个C#后端服务类含SqlSugar数据库操作、BaseRepository数据访问层、Controller接口定义等辅以JSON配置、CSS样式及CSProj/Sln工程文件包体仅1.77MB结构清晰、模块解耦度高。已有4968人学习下载可直接运行调试助读者深入理解.NET Core 6.0 Web API设计、Vue 3组合式API开发、Element PlusUI 3.0组件集成及教务领域业务建模方法是实践前后端协同开发与教育系统落地的优质参考项目。1. 这不是又一个“前后端分离模板”而是一套能真实跑在教务场景里的 .NET 6 Vue 3 工程骨架你打开的这个c#基于.netcore6.0vue3.0elementUI3.0实现的教务管理系统源码.zip本质是一套面向高校二级学院级业务闭环的轻量级全栈工程实践样本。它不追求微服务治理或高并发压测能力但完整覆盖了课程排课、学生选课、成绩录入、教师课表查询、班级课表导出等 5 类核心教务动作并在前后端边界处做了明确职责切分C# 负责数据校验如选课冲突检测、事务控制如批量成绩提交回滚、Excel 导入解析用ClosedXML处理带合并单元格的教务模板Vue 3 则专注交互反馈如选课成功后自动刷新课表卡片、表单联动开课院系变更 → 教师下拉列表动态加载、权限视图隔离教务员可见「成绩审核」Tab任课教师仅见「我的授课」。它适合两类人一是刚从 WinForm 转向 Web 全栈的 C# 开发者用熟悉语言写 API用 Vue 3 学 Composition API 和响应式系统二是高校信息中心技术人员可直接基于此结构替换数据库连接字符串、调整课程状态机流转逻辑72 小时内上线最小可用版本。注意Element PlusVue 3 版与 Element UIVue 2 版不兼容本项目明确使用element-plus2.3.0所有组件名、插槽语法、图标引入方式均按 Vue 3 规范重写不存在“升级兼容层”这类过渡方案。2. 搭建 .NET 6 WebAPI 层从 DbContext 初始化到控制器路由约束2.1 为什么选 Entity Framework Core 6 而非 Dapper——教务数据关系的刚性需求教务系统中存在强关联实体Course课程→CourseSchedule排课计划→Classroom教室→Building楼宇同时Student学生与CourseSchedule通过Enrollment选课记录多对多关联。EF Core 6 的导航属性延迟加载virtual ICollectionEnrollment和显式加载.Include(c c.Schedules).ThenInclude(s s.Classroom)能自然表达这种层级避免手写 5 张表 JOIN 的 SQL。更重要的是EF Core 6 的ValueConverter可将CourseStatus枚举Draft/Approved/Cancelled自动映射为数据库 tinyint且在OnModelCreating中通过HasCheckConstraint添加状态流转校验如Approved状态不允许再修改课时数这比 Dapper 在 Service 层手动 if-else 更可靠。项目中AppDbContext.cs的关键配置如下// AppDbContext.cs public class AppDbContext : DbContext { public AppDbContext(DbContextOptionsAppDbContext options) : base(options) { } public DbSetCourse Courses { get; set; } public DbSetCourseSchedule CourseSchedules { get; set; } public DbSetEnrollment Enrollments { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { // 1. 配置 CourseSchedule 与 Classroom 的可空外键部分课程暂未分配教室 modelBuilder.EntityCourseSchedule() .HasOne(s s.Classroom) .WithMany(c c.Schedules) .HasForeignKey(s s.ClassroomId) .IsRequired(false); // 允许为 null // 2. 为 Enrollment 表添加复合唯一索引同一学生同一课表不可重复选课 modelBuilder.EntityEnrollment() .HasIndex(e new { e.StudentId, e.ScheduleId }) .IsUnique(); // 3. 为 CourseStatus 添加检查约束SQL Server modelBuilder.EntityCourse() .Property(c c.Status) .HasConversionint() // 枚举转 int 存储 .HasCheckConstraint(CK_Course_Status, [Status] IN (0,1,2)); } }提示HasCheckConstraint生成的 SQL 语句在迁移时会自动创建但需确保数据库用户有ALTER TABLE权限。若部署到只读数据库环境可改用 Fluent Validation 在CourseController.Post()中做 ModelState 校验。2.2 控制器路由设计用[Route(api/[controller]/[action])]实现业务动词驱动教务操作天然带有动词特征「排课」、「选课」、「退课」、「导出课表」。传统 RESTful 的/api/courses/{id}难以表达「为课程 ID123 批量排课到第 5-8 周」这类复杂指令。本项目采用[Route(api/[controller]/[action])]模式在CourseScheduleController.cs中定义// CourseScheduleController.cs [ApiController] [Route(api/[controller]/[action])] public class CourseScheduleController : ControllerBase { private readonly IUnitOfWork _unitOfWork; public CourseScheduleController(IUnitOfWork unitOfWork) _unitOfWork unitOfWork; /// summary /// 批量为课程安排课表支持跨周、多节连上 /// /summary /// param namerequest包含 CourseId、WeekRange5-8、DayOfWeek1-7、StartTime08:00、Duration45/param [HttpPost] public async TaskActionResult BatchAssign([FromBody] BatchAssignRequest request) { // 1. 校验课程是否存在且状态为 Approved var course await _unitOfWork.Courses.GetByIdAsync(request.CourseId); if (course?.Status ! CourseStatus.Approved) return BadRequest(课程未审批通过无法排课); // 2. 解析 WeekRange 字符串为整数列表 var weeks Enumerable.Range( int.Parse(request.WeekRange.Split(-)[0]), int.Parse(request.WeekRange.Split(-)[1]) - int.Parse(request.WeekRange.Split(-)[0]) 1) .ToList(); // 3. 生成 Schedule 实体并保存省略具体循环逻辑 foreach (var week in weeks) { var schedule new CourseSchedule { CourseId request.CourseId, WeekNumber week, DayOfWeek request.DayOfWeek, StartTime TimeSpan.Parse(request.StartTime), DurationMinutes request.Duration }; _unitOfWork.CourseSchedules.Add(schedule); } await _unitOfWork.SaveChangesAsync(); return Ok(new { Message 排课完成, Count weeks.Count }); } }注意BatchAssignRequest是专用 DTO不直接暴露 EF 实体。其WeekRange字段接受5-8格式而非数组降低前端传参复杂度后端用string.Split(-)解析比要求前端传[5,6,7,8]更符合教务人员输入习惯。该接口 URL 为POST /api/CourseSchedule/BatchAssign可直接被 Vue 3 的axios.post(/api/CourseSchedule/BatchAssign, payload)调用。2.3 JWT 认证与角色授权用 Policy 替代硬编码[Authorize(RolesAdmin)]教务系统角色粒度细教务员可操作所有课程、院系教学秘书仅管理本院课程、任课教师仅查看自己课表、学生仅选课/查成绩。硬编码角色名易出错且难维护。项目采用AuthorizationPolicy方式在Program.cs中注册// Program.cs builder.Services.AddAuthorization(options { options.AddPolicy(CanManageCourses, policy policy.RequireRole(Admin, AcademicSecretary) .RequireAssertion(context context.User.HasClaim(c c.Type DepartmentId c.Value context.User.FindFirst(DepartmentId)?.Value))); options.AddPolicy(CanViewOwnSchedule, policy policy.RequireAuthenticatedUser() .RequireAssertion(context context.User.IsInRole(Teacher) || context.User.IsInRole(Student))); });控制器中直接应用策略名// CourseScheduleController.cs [Authorize(Policy CanManageCourses)] [HttpPost] public async TaskActionResult BatchAssign(...) { ... } [Authorize(Policy CanViewOwnSchedule)] [HttpGet({teacherId})] public async TaskActionResultIEnumerableCourseSchedule GetTeacherSchedules(int teacherId) { // 查询逻辑中自动过滤当前用户所属院系或本人ID }提示DepartmentId声明为自定义声明Claim在登录成功后由LoginController的GenerateJwtToken()方法注入到 Token 中。前端将 Token 存入localStorageAxios 请求拦截器自动添加Authorization: Bearer xxx头。此设计避免了每次查询都查用户角色表性能更优。3. 构建 Vue 3 Element Plus 前端从 Pinia 状态管理到表格行内编辑3.1 用 Pinia 替代 Vuex教务数据的局部响应式更新教务页面常需局部刷新如在「学生选课」页点击「加入购物车」按钮仅更新右上角购物车徽标数字和当前课程行的按钮状态变为「已加入」无需重载整个课程列表。Vuex 的全局 state 更新模式在此场景下冗余。本项目采用 Pinia为每个业务模块创建独立 store// stores/enrollmentStore.js import { defineStore } from pinia export const useEnrollmentStore defineStore(enrollment, { state: () ({ cartItems: [], // 当前选课购物车 enrolledCourses: [] // 已选课程列表 }), actions: { // 添加课程到购物车仅更新 cartItems addToCart(course) { const exists this.cartItems.find(item item.id course.id) if (exists) { exists.quantity } else { this.cartItems.push({ ...course, quantity: 1 }) } // 触发 cartItems 变化自动更新徽标 }, // 提交购物车调用后端 API 并更新两个数组 async submitCart() { try { const response await axios.post(/api/Enrollment/BatchEnroll, this.cartItems) // 成功后清空购物车并将新选课程追加到 enrolledCourses this.enrolledCourses.push(...this.cartItems.map(c ({...c, status: Enrolled}))) this.cartItems [] } catch (error) { ElMessage.error(选课失败 error.response?.data?.message) } } } })在组件中使用!-- StudentEnroll.vue -- script setup import { useEnrollmentStore } from /stores/enrollmentStore import { ElButton, ElBadge } from element-plus const enrollmentStore useEnrollmentStore() /script template div classcourse-list div v-forcourse in courses :keycourse.id classcourse-card h3{{ course.name }}/h3 p学分{{ course.credits }}/p !-- 购物车按钮状态由 cartItems 决定 -- ElButton sizesmall :typeenrollmentStore.cartItems.some(i i.id course.id) ? success : primary clickenrollmentStore.addToCart(course) {{ enrollmentStore.cartItems.some(i i.id course.id) ? 已加入 : 加入购物车 }} /ElButton /div !-- 购物车徽标显示数量 -- ElBadge :valueenrollmentStore.cartItems.length classcart-badge ElButton clickenrollmentStore.submitCart提交选课/ElButton /ElBadge /div /template注意enrollmentStore.cartItems是响应式数组v-for和some()方法会自动追踪变化。Pinia 的defineStore语法比 Vuex 3 的modules更简洁且 TypeScript 支持更好useEnrollmentStore()返回类型可被 IDE 自动推导。3.2 Element Plus 表格的行内编辑用scoped-slot实现「双击编辑」体验教务员常需快速修改课程名称、学分、上课时间。Element Plus 的el-table默认不支持行内编辑需结合scoped-slot和el-input实现!-- CourseList.vue -- template el-table :datacourses stylewidth: 100% !-- 课程名称列双击进入编辑 -- el-table-column propname label课程名称 width200 template #default{ row, $index } div v-ifrow.editing el-input v-modelrow.name blursaveEdit(row) keyup.entersaveEdit(row) / /div div v-else dblclickrow.editing true{{ row.name }}/div /template /el-table-column !-- 学分列下拉选择 -- el-table-column propcredits label学分 width100 template #default{ row } div v-ifrow.editing el-select v-modelrow.credits placeholder请选择 el-option v-forc in [1,2,3,4,5,6] :keyc :labelc :valuec / /el-select /div div v-else{{ row.credits }}/div /template /el-table-column !-- 操作列保存/取消按钮 -- el-table-column label操作 width120 template #default{ row } div v-ifrow.editing el-button sizesmall typesuccess clicksaveEdit(row)保存/el-button el-button sizesmall clickcancelEdit(row)取消/el-button /div div v-else el-button sizesmall clickrow.editing true编辑/el-button /div /template /el-table-column /el-table /template script setup import { ref } from vue import { ElMessage } from element-plus const courses ref([ { id: 1, name: 高等数学, credits: 4, editing: false }, { id: 2, name: 大学英语, credits: 3, editing: false } ]) const saveEdit async (row) { try { // 调用 API 更新后端 await axios.put(/api/Course/${row.id}, row) row.editing false ElMessage.success(更新成功) } catch (error) { ElMessage.error(更新失败 error.response?.data?.message) } } const cancelEdit (row) { // 恢复原始值需在 data 中保存原始值此处简化 row.editing false } /script提示row.editing是临时状态不提交到后端。实际项目中应在row对象上挂载originalName、originalCredits等字段cancelEdit时恢复这些值避免因网络延迟导致数据错乱。3.3 Excel 导入导出用 SheetJS ClosedXML 实现教务模板无缝对接高校教务处提供固定格式 Excel 模板含合并单元格、表头多行要求支持导入学生名单、导出班级课表。前端用SheetJSxlsx解析后端用ClosedXML生成前端导入StudentImport.vuetemplate el-upload action# :http-requesthandleUpload accept.xlsx,.xls show-file-listfalse el-button typeprimary导入学生名单/el-button /el-upload /template script setup import * as XLSX from xlsx const handleUpload async ({ file }) { const data await file.arrayBuffer() const workbook XLSX.read(data, { type: array }) const worksheet workbook.Sheets[workbook.SheetNames[0]] // 按列名映射跳过合并单元格行从第3行开始读 const jsonData XLSX.utils.sheet_to_json(worksheet, { header: 1, range: 2 // 从第3行索引2开始 }) // 转为对象数组假设第0行为表头 [学号,姓名,专业,班级] const headers jsonData[0] const students jsonData.slice(1).map(row { const obj {} headers.forEach((header, i) { obj[header] row[i] ?? }) return obj }) // 调用后端 API try { await axios.post(/api/Student/Import, students) ElMessage.success(导入成功) } catch (error) { ElMessage.error(导入失败 error.response?.data?.message) } } /script后端导出StudentController.cs[HttpGet(ExportClass/{classId})] public async TaskIActionResult ExportClassStudents(int classId) { var students await _unitOfWork.Students.GetByClassAsync(classId); using var workbook new XLWorkbook(); var worksheet workbook.Worksheets.Add(学生名单); // 写入标题行合并单元格 worksheet.Cell(1, 1).Value $XX学院 {classId} 班学生名单; worksheet.Range(1, 1, 1, 4).Merge(); // 合并 A1:D1 worksheet.Cell(1, 1).Style.Font.SetBold(true).Alignment.SetHorizontal(XLAlignmentHorizontalValues.Center); // 写入表头 worksheet.Cell(2, 1).Value 学号; worksheet.Cell(2, 2).Value 姓名; worksheet.Cell(2, 3).Value 专业; worksheet.Cell(2, 4).Value 班级; // 写入数据从第3行开始 for (int i 0; i students.Count; i) { worksheet.Cell(i 3, 1).Value students[i].StudentId; worksheet.Cell(i 3, 2).Value students[i].Name; worksheet.Cell(i 3, 3).Value students[i].Major; worksheet.Cell(i 3, 4).Value students[i].ClassName; } // 自动列宽 worksheet.Columns().AdjustToContents(); var stream new MemoryStream(); workbook.SaveAs(stream); stream.Position 0; return File(stream, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, $学生名单_{classId}.xlsx); }注意ClosedXML支持合并单元格、字体加粗、自动列宽等 Office 功能完美匹配教务处模板要求。前端SheetJS的range: 2参数确保跳过 Excel 中常见的多行表头直接从数据行开始解析。4. 前后端联调与性能优化解决教务大数据量下的卡顿问题4.1 分页与懒加载用Skip/TakeCountAsync实现无感滚动教务系统课程库常达数千条SELECT * FROM Course会拖垮数据库。本项目在CourseController.cs中强制分页[HttpGet] public async TaskActionResultPagedResultCourseDto GetCourses( [FromQuery] int page 1, [FromQuery] int pageSize 20, [FromQuery] string? keyword null) { var query _unitOfWork.Courses.GetAllAsNoTracking(); if (!string.IsNullOrWhiteSpace(keyword)) query query.Where(c c.Name.Contains(keyword) || c.Code.Contains(keyword)); // 总数查询不触发 ToList var totalCount await query.CountAsync(); // 数据查询Skip/Take var data await query .OrderByDescending(c c.CreatedAt) .Skip((page - 1) * pageSize) .Take(pageSize) .Select(c new CourseDto { Id c.Id, Name c.Name, Code c.Code, Credits c.Credits, Status c.Status.ToString() }) .ToListAsync(); return Ok(new PagedResultCourseDto(data, totalCount, page, pageSize)); }前端CourseList.vue使用el-pagination组件template el-table :datacourses ... / el-pagination v-model:current-pagecurrentPage v-model:page-sizepageSize :totaltotal size-changehandleSizeChange current-changehandleCurrentChange / /template script setup import { ref, onMounted } from vue import { ElMessage } from element-plus const currentPage ref(1) const pageSize ref(20) const total ref(0) const courses ref([]) const loadCourses async () { try { const res await axios.get(/api/Course, { params: { page: currentPage.value, pageSize: pageSize.value } }) courses.value res.data.data total.value res.data.totalCount } catch (error) { ElMessage.error(加载失败) } } const handleSizeChange (val) { pageSize.value val loadCourses() } const handleCurrentChange (val) { currentPage.value val loadCourses() } onMounted(loadCourses) /script提示GetAllAsNoTracking()关闭 EF Core 的变更跟踪提升只读查询性能CountAsync()生成SELECT COUNT(*)而非SELECT *避免传输大量无用数据。4.2 防抖搜索与缓存策略用debounceMemoryCache减少无效请求当用户在课程搜索框快速输入「高数」时若每敲一个字都发请求GET /api/Course?keyword高、GET /api/Course?keyword高数会造成大量无效查询。前端用lodash.debounce后端用IMemoryCache缓存高频关键词前端防抖CourseSearch.vuescript setup import { ref, watch } from vue import { debounce } from lodash-es const keyword ref() const search debounce(async (val) { if (val.trim() ) return try { const res await axios.get(/api/Course, { params: { keyword: val } }) courses.value res.data.data } catch (error) { ElMessage.error(搜索失败) } }, 300) // 300ms 延迟 watch(keyword, (newVal) { search(newVal) }) /script后端缓存CourseController.csprivate readonly IMemoryCache _cache; public CourseController(IUnitOfWork unitOfWork, IMemoryCache cache) { _unitOfWork unitOfWork; _cache cache; } [HttpGet] public async TaskActionResultPagedResultCourseDto GetCourses( [FromQuery] int page 1, [FromQuery] int pageSize 20, [FromQuery] string? keyword null) { // 缓存 Key按关键词分页参数组合 var cacheKey $courses_{keyword ?? all}_{page}_{pageSize}; if (_cache.TryGetValue(cacheKey, out PagedResultCourseDto cachedResult)) return Ok(cachedResult); // 执行查询同上节代码 var query _unitOfWork.Courses.GetAllAsNoTracking(); if (!string.IsNullOrWhiteSpace(keyword)) query query.Where(c c.Name.Contains(keyword) || c.Code.Contains(keyword)); var totalCount await query.CountAsync(); var data await query .OrderByDescending(c c.CreatedAt) .Skip((page - 1) * pageSize) .Take(pageSize) .Select(c new CourseDto { /* ... */ }) .ToListAsync(); var result new PagedResultCourseDto(data, totalCount, page, pageSize); // 缓存 10 分钟 _cache.Set(cacheKey, result, TimeSpan.FromMinutes(10)); return Ok(result); }注意IMemoryCache是进程内缓存适用于单机部署。若为集群环境需替换为IDistributedCache如 Redis。4.3 大文件上传优化用IFormFile 流式处理避免内存溢出教务系统常需上传百MB级教学视频或课件。若用IFormFile.CopyToAsync()直接读入内存会触发OutOfMemoryException。本项目采用流式处理边读边存[HttpPost(UploadMaterial)] public async TaskActionResult UploadMaterial(IFormFile file) { if (file.Length 0) return BadRequest(文件为空); // 1. 生成唯一文件名避免中文乱码 var fileName ${Guid.NewGuid():N}_{Path.GetFileName(file.FileName)}; var filePath Path.Combine(_environment.WebRootPath, materials, fileName); // 2. 创建目录若不存在 Directory.CreateDirectory(Path.GetDirectoryName(filePath)); // 3. 流式写入磁盘不加载全文到内存 await using var stream new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, 4096, true); await file.CopyToAsync(stream); // 4. 保存元数据到数据库文件名、大小、上传时间 var material new TeachingMaterial { FileName file.FileName, StoredFileName fileName, SizeBytes file.Length, UploadTime DateTime.UtcNow, UploadedBy User.Identity.Name }; _unitOfWork.Materials.Add(material); await _unitOfWork.SaveChangesAsync(); return Ok(new { FilePath $/materials/{fileName}, Size file.Length }); }提示FileStream构造函数中bufferSize: 4096和useAsync: true启用异步 I/Otrue表示使用操作系统底层异步机制比默认同步写入快 3-5 倍。前端需设置enctypemultipart/form-dataAxios 自动处理。5. 教务特有功能落地课程冲突检测与课表可视化渲染5.1 选课冲突检测用 LINQ 查询时间重叠区间学生选课时系统必须阻止选择时间冲突的课程。核心逻辑是给定学生已选课表existingSchedules和待选课表newSchedule判断是否存在WeekNumber、DayOfWeek相同且StartTime与DurationMinutes重叠的记录。EF Core 6 支持DateTime运算但时间重叠需转换为数学不等式// EnrollmentService.cs public async Taskbool HasScheduleConflictAsync(int studentId, int newScheduleId) { var newSchedule await _unitOfWork.CourseSchedules.GetByIdAsync(newScheduleId); if (newSchedule null) return true; // 获取学生已选的所有课表含课程名称、教师等用于提示 var existingSchedules await _unitOfWork.CourseSchedules .GetByStudentAsync(studentId); // 自定义扩展方法JOIN Enrollment 表 // 检测重叠同一周、同一天且时间段交叉 // 新课开始时间 旧课结束时间 AND 新课结束时间 旧课开始时间 var newEndTime newSchedule.StartTime.AddMinutes(newSchedule.DurationMinutes); return existingSchedules.Any(es es.WeekNumber newSchedule.WeekNumber es.DayOfWeek newSchedule.DayOfWeek es.StartTime newEndTime es.StartTime.AddMinutes(es.DurationMinutes) newSchedule.StartTime); } // 在 EnrollmentController.Post() 中调用 [HttpPost] public async TaskActionResult Enroll([FromBody] EnrollmentRequest request) { if (await _enrollmentService.HasScheduleConflictAsync(request.StudentId, request.ScheduleId)) { return Conflict(new { Message 选课时间冲突请检查课表 }); } // 执行选课逻辑... }注意es.StartTime.AddMinutes(es.DurationMinutes)计算旧课结束时间newEndTime计算新课结束时间。EF Core 6 将AddMinutes()翻译为 SQL Server 的DATEADD(minute, ..., [StartTime])全程在数据库执行不加载数据到内存。5.2 课表可视化用 CSS Grid 渲染周课表矩阵教务系统最直观的展示是「周课表」——7 列周一至周日12 行1-12 节课每个单元格显示课程名称。Element Plus 的el-table不适合此布局改用原生 CSS Grid!-- WeeklySchedule.vue -- template div classweekly-grid !-- 表头星期 -- div classgrid-header v-forday in days :keyday{{ day }}/div !-- 时间列节次 -- div classgrid-time v-forperiod in periods :keyperiod{{ period }}节/div !-- 课程单元格 -- div v-forcell in gridCells :key${cell.day}-${cell.period} classgrid-cell :class{ conflict: cell.conflict } clickshowCourseDetail(cell.course) div v-ifcell.course classcourse-badge div classcourse-name{{ cell.course.name }}/div div classcourse-teacher{{ cell.course.teacher }}/div /div div v-else classempty-cell-/div /div /div /template script setup import { ref, computed } from vue const days [周一, 周二, 周三, 周四, 周五, 周六, 周日] const periods Array.from({ length: 12 }, (_, i) i 1) // 1-12节 // 假设从 API 获取的原始数据[{ scheduleId, courseId, courseName, teacher, week, day, period, duration }] const rawSchedules ref([ { courseId: 1, courseName: 高等数学, teacher: 张教授, day: 1, period: 1, duration: 2 }, { courseId: 2, courseName: 大学英语, teacher: 李老师, day: 1, period: 3, duration: 2 } ]) // 计算 Grid 单元格数组7天 × 12节 84个单元格 const gridCells computed(() { const cells [] for (let day 1; day 7; day) { for (let period 1; period 12; period) { // 查找该天该节是否有课考虑连上duration2 表示占 period 和 period1 const course rawSchedules.value.find(s s.day day s.period period s.period s.duration period ) cells.push({ day, period, course, conflict: false // 可扩展为标记冲突 }) } } return cells }) /script style scoped .weekly-grid { display: grid; grid-template-columns: 80px repeat(7, 1fr); grid-template-rows: 40px repeat(12, 1fr); gap: 1px; background-color: #e4e7ed; } .grid-header { background-color: #fff; padding: 8px; text-align: center; font-weight: bold; } .grid-time { background-color: #fff; padding: 8px; text-align: center; } .grid-cell { background-color: #fff; padding: 4px; min-height: 60px; border: 1px solid #e4e7ed; position: relative; } .course-badge { background-color: #f0f9ff; border-radius: 4px; padding: 4px; height: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center; overflow: hidden; } .course-name { font-weight: bold; font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .course-teacher { font-size: 10px; color: #606266; } /style提示grid-template-columns定义 1 列固定宽时间列 7 列等分星期列grid-template-rows定义 1 行固定高表头 12 行等分节次。每个.grid-cell通过computed动态绑定课程数据v-for生成 84 个 divCSS Grid 自动布局。此方案比 Canvas 或 SVG 更轻量且支持原生事件如click查看课程详情。5.3 教务数据导出 PDF用 iText7 生成带课程二维码的课表教务处常需为教师打印带课程信息的 PDF 课表且要求每门课旁附带二维码扫码直达课程资料页。本项目用iText7实现[HttpGet(PrintSchedule/{teacherId})] public async TaskIActionResult PrintSchedule(int teacherId) { var schedules await _unitOfWork.CourseSchedules.GetByTeacherAsync(teacherId); var stream new MemoryStream(); var writer new PdfWriter(stream); var pdf new PdfDocument(writer); var document new Document(pdf); // 添加标题 document.Add(new Paragraph($教师{schedules.First().TeacherName} 课表) .SetTextAlignment(TextAlignment.CENTER) .SetFontSize(16) .SetBold()); // 为每门课创建表格行 foreach p a hrefhttps://download.csdn.net/download/weixin_47367099/85419705 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p
返回列表