forked from liweijie/education
1 line
36 KiB
JSON
1 line
36 KiB
JSON
{"remainingRequest":"C:\\Users\\Admin\\Desktop\\education\\frontend\\node_modules\\vue-loader\\lib\\index.js??vue-loader-options!C:\\Users\\Admin\\Desktop\\education\\frontend\\src\\views\\teachBusiness\\calendar\\components\\SchoolCalendarEditor.vue?vue&type=style&index=0&id=2308c5ca&scoped=true&lang=scss","dependencies":[{"path":"C:\\Users\\Admin\\Desktop\\education\\frontend\\src\\views\\teachBusiness\\calendar\\components\\SchoolCalendarEditor.vue","mtime":1787627704322},{"path":"C:\\Users\\Admin\\Desktop\\education\\frontend\\node_modules\\css-loader\\dist\\cjs.js","mtime":1787309792478},{"path":"C:\\Users\\Admin\\Desktop\\education\\frontend\\node_modules\\vue-loader\\lib\\loaders\\stylePostLoader.js","mtime":1787309794547},{"path":"C:\\Users\\Admin\\Desktop\\education\\frontend\\node_modules\\postcss-loader\\src\\index.js","mtime":1787309793084},{"path":"C:\\Users\\Admin\\Desktop\\education\\frontend\\node_modules\\sass-loader\\dist\\cjs.js","mtime":1787309791955},{"path":"C:\\Users\\Admin\\Desktop\\education\\frontend\\node_modules\\cache-loader\\dist\\cjs.js","mtime":1787309791955},{"path":"C:\\Users\\Admin\\Desktop\\education\\frontend\\node_modules\\vue-loader\\lib\\index.js","mtime":1787309793790}],"contextDependencies":[],"result":[{"type":"Buffer","data":"base64:CgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCg0KLnNjaG9vbC1jYWxlbmRhci1lZGl0b3Igew0KICBkaXNwbGF5OiBmbGV4Ow0KICBmbGV4LWRpcmVjdGlvbjogY29sdW1uOw0KICBoZWlnaHQ6IDEwMCU7DQogIGJhY2tncm91bmQ6ICNmZmY7DQogIGJvcmRlcjogMXB4IHNvbGlkICNlNGU3ZWQ7DQogIGJvcmRlci1yYWRpdXM6IDZweDsNCiAgb3ZlcmZsb3c6IGhpZGRlbjsNCg0KICAvKiDmoIfpopjmoI8gKi8NCiAgLnNjZS10aXRsZWJhciB7DQogICAgZGlzcGxheTogZmxleDsNCiAgICBhbGlnbi1pdGVtczogY2VudGVyOw0KICAgIGhlaWdodDogNDRweDsNCiAgICBwYWRkaW5nOiAwIDE0cHg7DQogICAgYmFja2dyb3VuZDogdmFyKC0tZWR1LWdyZWVuLXByaW1hcnksICMwMDg3NWEpOw0KICAgIGNvbG9yOiAjZmZmOw0KDQogICAgLnNjZS10aXRsZS1sZWZ0IHsgZmxleDogMTsgZm9udC1zaXplOiAxNXB4OyBmb250LXdlaWdodDogNjAwOyB9DQogICAgLnNjZS10aXRsZS1jZW50ZXIgeyBmbGV4OiAxOyB0ZXh0LWFsaWduOiBjZW50ZXI7IGZvbnQtc2l6ZTogMTNweDsgb3BhY2l0eTogMC45OyB9DQogICAgLnNjZS10aXRsZS1yaWdodCB7IGZsZXg6IDAgMCBhdXRvOyB0ZXh0LWFsaWduOiByaWdodDsgfQ0KDQogICAgLnNjZS10aXRsZS1yaWdodCBpIHsNCiAgICAgIG1hcmdpbi1sZWZ0OiAxNHB4Ow0KICAgICAgY3Vyc29yOiBwb2ludGVyOw0KICAgICAgZm9udC1zaXplOiAxOHB4Ow0KICAgICAgb3BhY2l0eTogMC44NTsNCiAgICAgICY6aG92ZXIgeyBvcGFjaXR5OiAxOyB9DQogICAgfQ0KICB9DQoNCiAgLyog6K6+572u6Z2i5p2/ICovDQogIC5zY2UtcGFuZWwgew0KICAgIHBhZGRpbmc6IDEwcHggMTRweDsNCiAgICBib3JkZXItYm90dG9tOiAxcHggc29saWQgI2ViZWVmNTsNCiAgICBiYWNrZ3JvdW5kOiAjZmFmYWZhOw0KDQogICAgLnNjZS1wYW5lbC1yb3cgew0KICAgICAgZGlzcGxheTogZmxleDsNCiAgICAgIGFsaWduLWl0ZW1zOiBjZW50ZXI7DQogICAgICBmbGV4LXdyYXA6IHdyYXA7DQogICAgICBtYXJnaW4tYm90dG9tOiA4cHg7DQoNCiAgICAgICY6bGFzdC1jaGlsZCB7IG1hcmdpbi1ib3R0b206IDA7IH0NCiAgICB9DQoNCiAgICAuc2NlLWxhYmVsIHsgZm9udC1zaXplOiAxM3B4OyBjb2xvcjogIzYwNjI2NjsgbWFyZ2luLXJpZ2h0OiA2cHg7IHdoaXRlLXNwYWNlOiBub3dyYXA7IH0NCiAgICAuc2NlLWV2ZW50LWlucHV0IHsgd2lkdGg6IDIwMHB4OyBtYXJnaW4tcmlnaHQ6IDEycHg7IH0NCiAgICAuc2NlLXJlbWFyay1pbnB1dCB7IHdpZHRoOiAyNDBweDsgbWFyZ2luLWxlZnQ6IDZweDsgfQ0KICAgIC5zY2UtY2IgeyBtYXJnaW4tcmlnaHQ6IDE0cHg7IH0NCiAgICAuc2NlLWRpdmlkZXIgeyB3aWR0aDogMXB4OyBoZWlnaHQ6IDE4cHg7IGJhY2tncm91bmQ6ICNkY2RmZTY7IG1hcmdpbjogMCAxNHB4OyB9DQogIH0NCg0KICAvKiDml7bpl7TnvJbmjpLljLogKi8NCiAgLnNjZS1ncmlkLXdyYXAgew0KICAgIGZsZXg6IDE7DQogICAgb3ZlcmZsb3c6IGF1dG87DQogICAgcGFkZGluZzogOHB4IDEwcHg7DQogICAgYmFja2dyb3VuZDogI2ZmZjsNCiAgfQ0KDQogIC5zY2UtZ3JpZCB7DQogICAgYm9yZGVyLWNvbGxhcHNlOiBjb2xsYXBzZTsNCiAgICB0YWJsZS1sYXlvdXQ6IGZpeGVkOw0KICAgIHdpZHRoOiAxMDAlOw0KICAgIGZvbnQtc2l6ZTogMTJweDsNCiAgICB1c2VyLXNlbGVjdDogbm9uZTsNCg0KICAgIHRoLCB0ZCB7DQogICAgICBib3JkZXI6IDFweCBzb2xpZCAjZTRlN2VkOw0KICAgICAgdGV4dC1hbGlnbjogY2VudGVyOw0KICAgICAgcGFkZGluZzogMDsNCiAgICB9DQoNCiAgICAuc2NlLWNvcm5lciB7DQogICAgICBiYWNrZ3JvdW5kOiAjZjVmN2ZhOw0KICAgICAgZm9udC13ZWlnaHQ6IDYwMDsNCiAgICAgIGNvbG9yOiAjMzAzMTMzOw0KICAgICAgdmVydGljYWwtYWxpZ246IG1pZGRsZTsNCiAgICB9DQoNCiAgICAuc2NlLXRoLXdlZWtubyB7IHdpZHRoOiA1MnB4OyB9DQogICAgLnNjZS10aC13ZWVrcmFuZ2UgeyB3aWR0aDogMTEwcHg7IH0NCg0KICAgIC5zY2UtdGgtZGF5IHsNCiAgICAgIGJhY2tncm91bmQ6IHZhcigtLWVkdS1ncmVlbi1wcmltYXJ5LCAjMDA4NzVhKTsNCiAgICAgIGNvbG9yOiAjZmZmOw0KICAgICAgZm9udC13ZWlnaHQ6IDYwMDsNCiAgICAgIGhlaWdodDogMjZweDsNCiAgICB9DQoNCiAgICAuc2NlLXRoLXNsb3Qgew0KICAgICAgYmFja2dyb3VuZDogI2VjZjVmMDsNCiAgICAgIGNvbG9yOiAjMDA4NzVhOw0KICAgICAgaGVpZ2h0OiAyMnB4Ow0KICAgICAgZm9udC13ZWlnaHQ6IDUwMDsNCiAgICB9DQoNCiAgICAuc2NlLXdlZWstY2VsbCB7DQogICAgICBiYWNrZ3JvdW5kOiAjZjVmN2ZhOw0KICAgICAgdmVydGljYWwtYWxpZ246IG1pZGRsZTsNCiAgICAgIHdoaXRlLXNwYWNlOiBub3dyYXA7DQogICAgfQ0KDQogICAgLnNjZS13ZWVrbm8tY2VsbCB7DQogICAgICBmb250LXdlaWdodDogNjAwOw0KICAgICAgY29sb3I6ICMzMDMxMzM7DQogICAgICBmb250LXNpemU6IDEzcHg7DQogICAgfQ0KDQogICAgLnNjZS13ZWVrcmFuZ2UtY2VsbCB7DQogICAgICBmb250LXNpemU6IDExcHg7DQogICAgICBjb2xvcjogIzkwOTM5OTsNCiAgICB9DQoNCiAgICAuc2NlLWNlbGwgew0KICAgICAgaGVpZ2h0OiAzMHB4Ow0KICAgICAgcG9zaXRpb246IHJlbGF0aXZlOw0KICAgICAgY3Vyc29yOiBwb2ludGVyOw0KICAgICAgYmFja2dyb3VuZDogI2ZmZjsNCiAgICAgIHRyYW5zaXRpb246IGJhY2tncm91bmQgMC4xNXM7DQoNCiAgICAgIC5zY2UtZGF0ZS1udW0geyBjb2xvcjogI2MwYzRjYzsgfQ0KDQogICAgICAvLyDmmJ/mnJ/kuqTmm7/lupXoibLvvJrlkajkuIAv5LiJL+S6lC/ml6XkuLrmtYXnu7/vvIzlkajkuowv5ZubL+WFreS4uueZve+8jOS+v+S6juWMuuWIhuaYn+acnw0KICAgICAgJi5zY2UtY2VsbC1vZGQgeyBiYWNrZ3JvdW5kOiAjZWVmNmYxOyB9DQogICAgICAmLnNjZS1jZWxsLWV2ZW4geyBiYWNrZ3JvdW5kOiAjZmZmZmZmOyB9DQogICAgICAmLmhhcy1ldmVudCB7IGJhY2tncm91bmQ6ICNlNmY0ZWE7IH0NCiAgICAgICYuaGFzLWV2ZW50IC5zY2UtZXZlbnQtbmFtZSB7IGNvbG9yOiAjMDA2NjNlOyBmb250LXdlaWdodDogNTAwOyB9DQogICAgICAmLmhhcy1ldmVudCAuc2NlLWV2ZW50LW5hbWUuaXMtYm9sZCB7IGZvbnQtd2VpZ2h0OiA3MDA7IH0NCiAgICAgICYubm8tc2NoZWR1bGUgeyBiYWNrZ3JvdW5kOiAjZmRlMmUyOyB9DQoNCiAgICAgICYuaXMtc2VsZWN0ZWQgew0KICAgICAgICBvdXRsaW5lOiAycHggc29saWQgdmFyKC0tZWR1LWdyZWVuLXByaW1hcnksICMwMDg3NWEpOw0KICAgICAgICBvdXRsaW5lLW9mZnNldDogLTJweDsNCiAgICAgICAgYmFja2dyb3VuZDogI2Q0ZWNkZiAhaW1wb3J0YW50Ow0KICAgICAgfQ0KICAgIH0NCiAgfQ0KDQogIC5zY2Utc3RhdHVzIHsNCiAgICBwYWRkaW5nOiA2cHggMTRweDsNCiAgICBmb250LXNpemU6IDEycHg7DQogICAgY29sb3I6ICM5MDkzOTk7DQogICAgYm9yZGVyLXRvcDogMXB4IHNvbGlkICNlYmVlZjU7DQogICAgYmFja2dyb3VuZDogI2ZhZmFmYTsNCg0KICAgIC5zY2UtYWJzb3JiLXRpcCB7IG1hcmdpbi1sZWZ0OiAxMnB4OyBjb2xvcjogIzAwODc1YTsgfQ0KICB9DQp9DQo="},{"version":3,"sources":["SchoolCalendarEditor.vue"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0lBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA","file":"SchoolCalendarEditor.vue","sourceRoot":"src/views/teachBusiness/calendar/components","sourcesContent":["<template>\r\n <div class=\"school-calendar-editor\">\r\n <!-- 标题栏 -->\r\n <div class=\"sce-titlebar\">\r\n <div class=\"sce-title-left\">\r\n <span class=\"sce-semester-name\">{{ semesterName }}</span>\r\n </div>\r\n <div class=\"sce-title-center\">\r\n <span class=\"sce-date-range\">从 {{ dateRangeText }} ,共 {{ totalWeeks }} 周</span>\r\n </div>\r\n <div class=\"sce-title-right\">\r\n <i class=\"el-icon-close\" title=\"关闭\" @click=\"handleClose\" />\r\n </div>\r\n </div>\r\n\r\n <!-- 设置面板 -->\r\n <div class=\"sce-panel\">\r\n <div class=\"sce-panel-row\">\r\n <span class=\"sce-label\">事件名称:</span>\r\n <el-input v-model=\"toolbar.eventName\" size=\"small\" class=\"sce-event-input\" placeholder=\"请输入校历事件名称\" />\r\n <el-checkbox v-model=\"toolbar.bold\" class=\"sce-cb\">加粗显示</el-checkbox>\r\n <el-checkbox v-model=\"toolbar.schedulable\" class=\"sce-cb\">可排课</el-checkbox>\r\n <el-checkbox v-model=\"toolbar.mainCourse\" class=\"sce-cb\">正课</el-checkbox>\r\n <el-checkbox v-model=\"toolbar.remarkShow\" class=\"sce-cb\">备注显示</el-checkbox>\r\n <span class=\"sce-label\">备注:</span>\r\n <el-input\r\n v-model=\"toolbar.remark\"\r\n size=\"small\"\r\n type=\"textarea\"\r\n :rows=\"1\"\r\n class=\"sce-remark-input\"\r\n resize=\"none\"\r\n placeholder=\"备注信息\"\r\n />\r\n </div>\r\n <div class=\"sce-panel-row\">\r\n <el-button type=\"primary\" size=\"small\" icon=\"el-icon-check\" @click=\"applyEvent\">设定</el-button>\r\n <el-button type=\"danger\" size=\"small\" icon=\"el-icon-delete\" @click=\"deleteEvent\">删除</el-button>\r\n <el-button size=\"small\" icon=\"el-icon-brush\" @click=\"absorbEvent\">吸取</el-button>\r\n <el-button size=\"small\" icon=\"el-icon-refresh\" @click=\"refreshGrid\">刷新</el-button>\r\n <span class=\"sce-divider\" />\r\n <el-checkbox v-model=\"show78\" class=\"sce-cb\">显示78节</el-checkbox>\r\n <el-checkbox v-model=\"showNight\" class=\"sce-cb\">显示晚上</el-checkbox>\r\n <el-checkbox v-model=\"showLateNight\" class=\"sce-cb\">显示夜间</el-checkbox>\r\n </div>\r\n </div>\r\n\r\n <!-- 时间编排区域 -->\r\n <div ref=\"gridWrap\" class=\"sce-grid-wrap\" @mousedown=\"onGridMouseDown\">\r\n <table class=\"sce-grid\" :class=\"{ 'is-selecting': selecting }\">\r\n <thead>\r\n <tr>\r\n <th class=\"sce-corner sce-th-weekno\" :rowspan=\"2\">周次</th>\r\n <th class=\"sce-corner sce-th-weekrange\" :rowspan=\"2\">日期段</th>\r\n <th v-for=\"col in visibleColumns\" :key=\"'h1-' + col.colIndex\" :colspan=\"1\" class=\"sce-th-day\">\r\n {{ weekDayLabel(col.dayIndex) }}\r\n </th>\r\n </tr>\r\n <tr>\r\n <th v-for=\"col in visibleColumns\" :key=\"'h2-' + col.colIndex\" class=\"sce-th-slot\">\r\n {{ col.slotLabel }}\r\n </th>\r\n </tr>\r\n </thead>\r\n <tbody>\r\n <tr v-for=\"(week, wIdx) in weeks\" :key=\"'w-' + wIdx\">\r\n <td class=\"sce-week-cell sce-weekno-cell\">{{ wIdx + 1 }}</td>\r\n <td class=\"sce-week-cell sce-weekrange-cell\">{{ week.rangeText }}</td>\r\n <td\r\n v-for=\"col in visibleColumns\"\r\n :key=\"'c-' + wIdx + '-' + col.colIndex\"\r\n class=\"sce-cell\"\r\n :class=\"cellClass(wIdx, col)\"\r\n :data-key=\"cellKey(wIdx, col.colIndex)\"\r\n @dblclick=\"onCellDblClick(wIdx, col)\"\r\n >\r\n <span v-if=\"getEvent(wIdx, col.colIndex)\" class=\"sce-event-name\" :class=\"{ 'is-bold': getEvent(wIdx, col.colIndex).bold }\">{{ getEvent(wIdx, col.colIndex).name }}</span>\r\n <span v-else class=\"sce-date-num\">{{ dateNumberOf(wIdx, col.dayIndex) }}</span>\r\n </td>\r\n </tr>\r\n </tbody>\r\n </table>\r\n </div>\r\n\r\n <div class=\"sce-status\">\r\n 已选 {{ selectedKeys.length }} 个时间格\r\n <span v-if=\"absorbPreview\" class=\"sce-absorb-tip\">已吸取:{{ absorbPreview }}</span>\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script>\r\nimport { getSemester } from '@/api/teachBusiness/semester'\r\nimport { listXqxlb, updateXqxlb } from '@/api/teachBusiness/xqxlb'\r\n\r\n// 节次定义:12/34/56 常显,78/晚上/夜间由开关控制可见\r\nconst SLOT_DEFS = [\r\n { key: '12', label: '1-2', ctrl: null },\r\n { key: '34', label: '3-4', ctrl: null },\r\n { key: '56', label: '5-6', ctrl: null },\r\n { key: '78', label: '7-8', ctrl: 'show78' },\r\n { key: 'night', label: '晚上', ctrl: 'showNight' },\r\n { key: 'late', label: '夜间', ctrl: 'showLateNight' }\r\n]\r\n\r\n// 前端节次 key -> 后端 xqxlb.courseClass 节次范围(如 12 节 -> \"1-2\"、夜间 -> \"11-12\")\r\nconst SLOT_COURSE_MAP = {\r\n '12': '1-2',\r\n '34': '3-4',\r\n '56': '5-6',\r\n '78': '7-8',\r\n night: '9-10',\r\n late: '11-12'\r\n}\r\n\r\nconst WEEK_DAYS = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']\r\n\r\nfunction pad2(n) {\r\n return n < 10 ? '0' + n : '' + n\r\n}\r\n\r\nfunction fmtDate(d) {\r\n return d.getFullYear() + '-' + pad2(d.getMonth() + 1) + '-' + pad2(d.getDate())\r\n}\r\n\r\nexport default {\r\n name: 'SchoolCalendarEditor',\r\n props: {\r\n nd: { type: [String, Number], default: '' },\r\n semesterName: { type: String, default: '' }\r\n },\r\n data() {\r\n return {\r\n // 学期日期范围\r\n startDate: null,\r\n endDate: null,\r\n totalWeeks: 0,\r\n dateRangeText: '',\r\n weeks: [],\r\n // 显示开关(默认不勾选,每天仅显示 1-2/3-4/5-6 三个节次)\r\n show78: false,\r\n showNight: false,\r\n showLateNight: false,\r\n // 工具栏表单\r\n toolbar: {\r\n eventName: '',\r\n bold: false,\r\n schedulable: false,\r\n mainCourse: false,\r\n remarkShow: false,\r\n remark: ''\r\n },\r\n // 事件存储:key = cellKey -> event\r\n events: {},\r\n // 暂存被隐藏节次列上的事件:key = 周-星期-节次 -> event,列再次显示时恢复\r\n hiddenEvents: {},\r\n // 选择集(用数组以保证 Vue2 响应式)\r\n selectedKeys: [],\r\n // 拖拽状态\r\n selecting: false,\r\n dragStart: null,\r\n dragStartSelected: false, // 按下起点在按下前是否已选中\r\n moved: false, // 是否发生移动(区分点击与拖拽)\r\n dragAddMode: false, // ctrl 拖拽为追加\r\n // 窗口控制\r\n absorbPreview: ''\r\n }\r\n },\r\n computed: {\r\n // 当前可见列(由开关过滤,默认每天三个节次)\r\n visibleColumns() {\r\n const cols = []\r\n let colIndex = 0\r\n WEEK_DAYS.forEach((dayLabel, dayIndex) => {\r\n SLOT_DEFS.forEach(slot => {\r\n if (slot.ctrl && !this[slot.ctrl]) return\r\n cols.push({ colIndex: colIndex, dayIndex: dayIndex, slotKey: slot.key, slotLabel: slot.label, dayLabel: dayLabel })\r\n colIndex++\r\n })\r\n })\r\n return cols\r\n },\r\n // xqxlb 的年度(nd)为年份(如 2026),由 6 位学期代号(如 202601)截取前 4 位得出\r\n xqxlbNd() {\r\n return Number(String(this.nd).slice(0, 4))\r\n }\r\n },\r\n watch: {\r\n nd: {\r\n immediate: true,\r\n handler(val) {\r\n if (val) this.loadSemester()\r\n }\r\n },\r\n visibleColumns(newCols, oldCols) {\r\n // 节次列显隐会改变列索引,按「周期-星期-节次」重定位事件,避免事件随时间格列偏移\r\n if (oldCols && oldCols.length) {\r\n const oldPosOf = index => (oldCols[index] || null)\r\n const newIndex = (dayIndex, slotKey) => {\r\n const c = newCols.find(x => x.dayIndex === dayIndex && x.slotKey === slotKey)\r\n return c ? c.colIndex : -1\r\n }\r\n const next = {}\r\n Object.keys(this.events).forEach(key => {\r\n const m = key.match(/^w(\\d+)-c(\\d+)$/)\r\n if (!m) return\r\n const wIdx = +m[1]\r\n const old = oldPosOf(+m[2])\r\n if (!old) return\r\n const newC = newIndex(old.dayIndex, old.slotKey)\r\n if (newC < 0) {\r\n // 该节次列被隐藏:事件暂存,待列再次显示时恢复\r\n this.hiddenEvents[this.cellStableKey(wIdx, old.dayIndex, old.slotKey)] = this.events[key]\r\n return\r\n }\r\n next['w' + wIdx + '-c' + newC] = this.events[key]\r\n })\r\n // 恢复重新显示的隐藏节次事件\r\n Object.keys(this.hiddenEvents).forEach(stable => {\r\n const p = stable.split('-')\r\n const wIdx = +p[0]\r\n const dayIndex = +p[1]\r\n const slotKey = p[2]\r\n const newC = newIndex(dayIndex, slotKey)\r\n if (newC >= 0) {\r\n next['w' + wIdx + '-c' + newC] = this.hiddenEvents[stable]\r\n delete this.hiddenEvents[stable]\r\n }\r\n })\r\n this.events = next\r\n }\r\n // 列变化后清空无效选择(列索引含义会变)\r\n this.selectedKeys = []\r\n }\r\n },\r\n beforeDestroy() {\r\n this.removeGlobalListeners()\r\n },\r\n methods: {\r\n /* ---------- 数据加载 ---------- */\r\n loadSemester() {\r\n if (!this.nd) return\r\n getSemester(this.nd)\r\n .then(res => {\r\n const data = res.data || res || {}\r\n const kx = (data.kxrq || '').toString().slice(0, 10)\r\n const jx = (data.jsrq || '').toString().slice(0, 10)\r\n if (kx && jx) {\r\n this.startDate = new Date(kx.replace(/-/g, '/'))\r\n this.endDate = new Date(jx.replace(/-/g, '/'))\r\n this.buildWeeks()\r\n }\r\n this.loadXqxlbEvents()\r\n })\r\n .catch(() => {\r\n this.$message.warning('学期详情获取失败,请确认学期数据')\r\n })\r\n },\r\n // 从后端拉取本学期校历事件,渲染到对应时间格(年度由 6 位学期代号截取前 4 位)\r\n loadXqxlbEvents() {\r\n listXqxlb({ nd: this.xqxlbNd }).then(res => {\r\n const data = res.data\r\n const list = Array.isArray(data) ? data : (data && data.records) || []\r\n return list\r\n }).then(list => {\r\n const loaded = {}\r\n list.forEach(item => {\r\n const pos = this.locateXqxlb(item)\r\n if (!pos) return\r\n const ev = {\r\n bh: item.bh,\r\n name: item.jqmc || '',\r\n bold: !!item.jc,\r\n schedulable: !!item.kpk,\r\n mainCourse: !!item.zdpk,\r\n remarkShow: !!item.bzxs,\r\n remark: item.bz || ''\r\n }\r\n const colIndex = this.colIndexOf(pos.dayIndex, pos.slotKey)\r\n if (colIndex >= 0) {\r\n loaded[this.cellKey(pos.wIdx, colIndex)] = ev\r\n } else {\r\n // 该节次列当前隐藏:暂存(含 bh),待列显示时由 visibleColumns 监听恢复\r\n this.hiddenEvents[this.cellStableKey(pos.wIdx, pos.dayIndex, pos.slotKey)] = ev\r\n }\r\n })\r\n // 合并到现有格子(后端数据覆盖已有状态)\r\n this.events = Object.assign({}, this.events, loaded)\r\n }).catch(() => {\r\n this.$message.warning('校历事件加载失败')\r\n })\r\n },\r\n // 根据假期记录反推时间格位置(jqsj 日期 + courseClass 节次),与列是否可见无关\r\n locateXqxlb(item) {\r\n if (!this.startDate || !item.jqsj) return null\r\n const d = new Date(String(item.jqsj).slice(0, 10).replace(/-/g, '/'))\r\n if (isNaN(d.getTime())) return null\r\n const offset = Math.round((d - this.startDate) / (24 * 3600 * 1000))\r\n if (offset < 0) return null\r\n const wIdx = Math.floor(offset / 7)\r\n const dayIndex = offset % 7\r\n const slotKey = this.courseClassToSlotKey(item.courseClass)\r\n if (slotKey === null) return null\r\n return { wIdx, dayIndex, slotKey }\r\n },\r\n // 后端 courseClass 节次范围 -> 前端节次 key\r\n courseClassToSlotKey(courseClass) {\r\n if (!courseClass) return null\r\n const s = String(courseClass).trim()\r\n for (const key in SLOT_COURSE_MAP) {\r\n if (s === SLOT_COURSE_MAP[key]) return key\r\n }\r\n // 兼容 \"910\"/\"1112\" 等紧凑写法\r\n if (s === '910') return 'night'\r\n if (s === '1112') return 'late'\r\n return null\r\n },\r\n buildWeeks() {\r\n if (!this.startDate || !this.endDate) return\r\n const start = new Date(this.startDate)\r\n const end = new Date(this.endDate)\r\n const days = Math.round((end - start) / (24 * 3600 * 1000)) + 1\r\n this.totalWeeks = Math.ceil(days / 7)\r\n this.dateRangeText = fmtDate(start) + ' 到 ' + fmtDate(end)\r\n const weeks = []\r\n for (let w = 0; w < this.totalWeeks; w++) {\r\n const wkStart = new Date(start)\r\n wkStart.setDate(start.getDate() + w * 7)\r\n const wkEnd = new Date(wkStart)\r\n wkEnd.setDate(wkStart.getDate() + 6)\r\n weeks.push({\r\n rangeText: pad2(wkStart.getMonth() + 1) + '-' + pad2(wkStart.getDate()) +\r\n '至' + pad2(wkEnd.getMonth() + 1) + '-' + pad2(wkEnd.getDate())\r\n })\r\n }\r\n this.weeks = weeks\r\n },\r\n colIndexOf(dayIndex, slotKey) {\r\n const col = this.visibleColumns.find(c => c.dayIndex === dayIndex && c.slotKey === slotKey)\r\n return col ? col.colIndex : -1\r\n },\r\n /* ---------- 单元格工具 ---------- */\r\n cellKey(wIdx, colIndex) {\r\n return 'w' + wIdx + '-c' + colIndex\r\n },\r\n // 稳定的单元格身份:不随列显隐变化的周-星期-节次\r\n cellStableKey(wIdx, dayIndex, slotKey) {\r\n return wIdx + '-' + dayIndex + '-' + slotKey\r\n },\r\n getEvent(wIdx, colIndex) {\r\n return this.events[this.cellKey(wIdx, colIndex)] || null\r\n },\r\n weekDayLabel(dayIndex) {\r\n return WEEK_DAYS[dayIndex]\r\n },\r\n // 单元格内显示的日期数字(如 0703)\r\n dateNumberOf(wIdx, dayIndex) {\r\n if (!this.startDate) return ''\r\n const d = new Date(this.startDate)\r\n d.setDate(this.startDate.getDate() + wIdx * 7 + dayIndex)\r\n return pad2(d.getMonth() + 1) + pad2(d.getDate())\r\n },\r\n cellClass(wIdx, col) {\r\n const classes = []\r\n // 星期交替背景(单双日不同底色)\r\n if (col.dayIndex % 2 === 0) classes.push('sce-cell-odd')\r\n else classes.push('sce-cell-even')\r\n if (this.selectedKeys.includes(this.cellKey(wIdx, col.colIndex))) classes.push('is-selected')\r\n const ev = this.getEvent(wIdx, col.colIndex)\r\n if (ev) {\r\n classes.push('has-event')\r\n if (!ev.schedulable) classes.push('no-schedule')\r\n }\r\n return classes\r\n },\r\n /* ---------- 点击 / 拖拽多选 ---------- */\r\n onGridMouseDown(e) {\r\n const cell = e.target.closest('.sce-cell')\r\n if (!cell) {\r\n // 点击空白区(非单元格,含表头、周次列等)取消所有选中\r\n if (this.selectedKeys.length) this.selectedKeys = []\r\n return\r\n }\r\n e.preventDefault()\r\n const key = cell.getAttribute('data-key')\r\n this.dragStart = key\r\n this.dragAddMode = e.ctrlKey || e.metaKey\r\n // 记录起点格在按下时的选中状态,用于松手时判定\"纯点击\"的切换行为\r\n this.dragStartSelected = this.selectedKeys.includes(key)\r\n this.moved = false\r\n this.selecting = true\r\n // 非 ctrl 模式下,若起点本身已选中(可能准备拖拽扩展),先不清除,避免误清空\r\n if (!this.dragAddMode && !this.dragStartSelected) this.selectedKeys = []\r\n this.addGlobalListeners()\r\n },\r\n addGlobalListeners() {\r\n document.addEventListener('mousemove', this.onGridMouseMove)\r\n document.addEventListener('mouseup', this.onGridMouseUp)\r\n },\r\n removeGlobalListeners() {\r\n document.removeEventListener('mousemove', this.onGridMouseMove)\r\n document.removeEventListener('mouseup', this.onGridMouseUp)\r\n },\r\n onGridMouseMove(e) {\r\n if (!this.selecting || !this.dragStart) return\r\n if (!this.moved) this.moved = true\r\n const el = document.elementFromPoint(e.clientX, e.clientY)\r\n const cell = el && el.closest ? el.closest('.sce-cell') : null\r\n if (!cell) return\r\n const curKey = cell.getAttribute('data-key')\r\n this.selectRectangle(this.dragStart, curKey, this.dragAddMode)\r\n },\r\n onGridMouseUp() {\r\n if (!this.selecting) {\r\n this.removeGlobalListeners()\r\n return\r\n }\r\n // 纯点击(未移动)时,做单选 / 切换\r\n if (!this.moved) {\r\n const key = this.dragStart\r\n if (this.dragAddMode) {\r\n // Ctrl + 单击:切换该格选中\r\n if (this.selectedKeys.includes(key)) {\r\n this.selectedKeys = this.selectedKeys.filter(k => k !== key)\r\n } else {\r\n this.selectedKeys.push(key)\r\n }\r\n } else {\r\n // 普通单击:已选中则取消该格,未选中则仅选中该格\r\n if (this.dragStartSelected) {\r\n this.selectedKeys = this.selectedKeys.filter(k => k !== key)\r\n } else {\r\n this.selectedKeys = [key]\r\n }\r\n }\r\n }\r\n this.selecting = false\r\n this.dragStart = null\r\n this.dragStartSelected = false\r\n this.removeGlobalListeners()\r\n },\r\n // 根据起点终点 key 选中矩形范围内所有格\r\n selectRectangle(startKey, endKey, addMode) {\r\n const parse = k => {\r\n const m = k.match(/^w(\\d+)-c(\\d+)$/)\r\n return m ? { w: +m[1], c: +m[2] } : null\r\n }\r\n const s = parse(startKey)\r\n const t = parse(endKey)\r\n if (!s || !t) return\r\n if (!addMode) this.selectedKeys = []\r\n const minW = Math.min(s.w, t.w)\r\n const maxW = Math.max(s.w, t.w)\r\n const minC = Math.min(s.c, t.c)\r\n const maxC = Math.max(s.c, t.c)\r\n for (let w = minW; w <= maxW; w++) {\r\n for (let c = minC; c <= maxC; c++) {\r\n const key = this.cellKey(w, c)\r\n if (this.selectedKeys.indexOf(key) === -1) this.selectedKeys.push(key)\r\n }\r\n }\r\n },\r\n /* ---------- 事件操作 ---------- */\r\n applyEvent() {\r\n if (!this.selectedKeys.length) {\r\n this.$message.warning('请先选择时间格')\r\n return\r\n }\r\n const name = this.toolbar.eventName.trim()\r\n if (!name) {\r\n this.$message.warning('请输入事件名称')\r\n return\r\n }\r\n const keys = [...this.selectedKeys]\r\n keys.forEach(key => {\r\n const prev = this.events[key]\r\n this.$set(this.events, key, {\r\n bh: prev ? prev.bh : undefined,\r\n name: name,\r\n bold: this.toolbar.bold,\r\n schedulable: this.toolbar.schedulable,\r\n mainCourse: this.toolbar.mainCourse,\r\n remarkShow: this.toolbar.remarkShow,\r\n remark: this.toolbar.remark\r\n })\r\n })\r\n this.persistEvents(keys)\r\n },\r\n deleteEvent() {\r\n if (!this.selectedKeys.length) {\r\n this.$message.warning('请先选择时间格')\r\n return\r\n }\r\n const keys = [...this.selectedKeys]\r\n keys.forEach(key => {\r\n this.$delete(this.events, key)\r\n })\r\n this.$message.success('已删除 ' + keys.length + ' 个时间格事件')\r\n },\r\n absorbEvent() {\r\n // 吸取:取选择中第一个有事件的格\r\n let target = null\r\n for (const key of this.selectedKeys) {\r\n if (this.events[key]) { target = this.events[key]; break }\r\n }\r\n if (!target) {\r\n this.$message.warning('所选时间格中无事件可吸取')\r\n return\r\n }\r\n this.toolbar.eventName = target.name\r\n this.toolbar.bold = !!target.bold\r\n this.toolbar.schedulable = !!target.schedulable\r\n this.toolbar.mainCourse = !!target.mainCourse\r\n this.toolbar.remarkShow = !!target.remarkShow\r\n this.toolbar.remark = target.remark || ''\r\n this.absorbPreview = target.name\r\n },\r\n onCellDblClick(wIdx, col) {\r\n const ev = this.getEvent(wIdx, col.colIndex)\r\n if (ev) {\r\n this.toolbar.eventName = ev.name\r\n this.toolbar.bold = !!ev.bold\r\n this.toolbar.schedulable = !!ev.schedulable\r\n this.toolbar.mainCourse = !!ev.mainCourse\r\n this.toolbar.remarkShow = !!ev.remarkShow\r\n this.toolbar.remark = ev.remark || ''\r\n this.absorbPreview = ev.name\r\n this.$message.info('已吸取事件:' + ev.name)\r\n }\r\n },\r\n refreshGrid() {\r\n this.selectedKeys = []\r\n this.absorbPreview = ''\r\n this.events = {}\r\n this.hiddenEvents = {}\r\n this.loadXqxlbEvents()\r\n this.$message.info('已刷新')\r\n },\r\n /* ---------- 后端持久化(xqxlb 接口) ---------- */\r\n // 批量保存选中格事件,返回各格子保存成功/失败的个数\r\n persistEvents(keys) {\r\n const tasks = keys\r\n .filter(key => this.events[key])\r\n .map(key => {\r\n const payload = this.buildXqxlbPayload(key, this.events[key])\r\n if (!payload) return Promise.resolve(false)\r\n return updateXqxlb(payload)\r\n .then(() => true)\r\n .catch(() => false)\r\n })\r\n return Promise.all(tasks).then(results => {\r\n const ok = results.filter(r => r === true).length\r\n const fail = results.length - ok\r\n if (fail > 0) {\r\n this.$message.error(fail + ' 个事件保存失败,请检查后端接口')\r\n } else if (ok > 0) {\r\n this.$message.success('已设定 ' + ok + ' 个时间格')\r\n }\r\n // 保存成功后重新拉取,回写各记录主键,避免重复新增\r\n this.loadXqxlbEvents()\r\n return { ok: ok, fail: fail }\r\n })\r\n },\r\n // 构造 xqxlb 提交数据\r\n buildXqxlbPayload(key, ev) {\r\n const pos = this.parseCellKey(key)\r\n if (!pos) return null\r\n const d = new Date(this.startDate)\r\n d.setDate(this.startDate.getDate() + pos.wIdx * 7 + pos.dayIndex)\r\n return {\r\n delFlag: 0,\r\n bh: ev.bh || undefined,\r\n nd: this.xqxlbNd,\r\n jqsj: fmtDate(d),\r\n jqmc: ev.name || '',\r\n jc: !!ev.bold,\r\n bz: ev.remark || null,\r\n kpk: !!ev.schedulable,\r\n bzxs: !!ev.remarkShow,\r\n zdpk: !!ev.mainCourse,\r\n courseClass: SLOT_COURSE_MAP[pos.slotKey] || null\r\n }\r\n },\r\n // 时间格 key -> { wIdx, dayIndex, slotKey }\r\n parseCellKey(key) {\r\n const m = key.match(/^w(\\d+)-c(\\d+)$/)\r\n if (!m) return null\r\n const colIndex = +m[2]\r\n const col = this.visibleColumns.find(c => c.colIndex === colIndex)\r\n if (!col) return null\r\n return { wIdx: +m[1], dayIndex: col.dayIndex, slotKey: col.slotKey }\r\n },\r\n /* ---------- 返回学期管理 ---------- */\r\n handleClose() {\r\n this.$emit('close')\r\n this.$router.replace({ path: '/teachBusiness/semester' })\r\n }\r\n }\r\n}\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.school-calendar-editor {\r\n display: flex;\r\n flex-direction: column;\r\n height: 100%;\r\n background: #fff;\r\n border: 1px solid #e4e7ed;\r\n border-radius: 6px;\r\n overflow: hidden;\r\n\r\n /* 标题栏 */\r\n .sce-titlebar {\r\n display: flex;\r\n align-items: center;\r\n height: 44px;\r\n padding: 0 14px;\r\n background: var(--edu-green-primary, #00875a);\r\n color: #fff;\r\n\r\n .sce-title-left { flex: 1; font-size: 15px; font-weight: 600; }\r\n .sce-title-center { flex: 1; text-align: center; font-size: 13px; opacity: 0.9; }\r\n .sce-title-right { flex: 0 0 auto; text-align: right; }\r\n\r\n .sce-title-right i {\r\n margin-left: 14px;\r\n cursor: pointer;\r\n font-size: 18px;\r\n opacity: 0.85;\r\n &:hover { opacity: 1; }\r\n }\r\n }\r\n\r\n /* 设置面板 */\r\n .sce-panel {\r\n padding: 10px 14px;\r\n border-bottom: 1px solid #ebeef5;\r\n background: #fafafa;\r\n\r\n .sce-panel-row {\r\n display: flex;\r\n align-items: center;\r\n flex-wrap: wrap;\r\n margin-bottom: 8px;\r\n\r\n &:last-child { margin-bottom: 0; }\r\n }\r\n\r\n .sce-label { font-size: 13px; color: #606266; margin-right: 6px; white-space: nowrap; }\r\n .sce-event-input { width: 200px; margin-right: 12px; }\r\n .sce-remark-input { width: 240px; margin-left: 6px; }\r\n .sce-cb { margin-right: 14px; }\r\n .sce-divider { width: 1px; height: 18px; background: #dcdfe6; margin: 0 14px; }\r\n }\r\n\r\n /* 时间编排区 */\r\n .sce-grid-wrap {\r\n flex: 1;\r\n overflow: auto;\r\n padding: 8px 10px;\r\n background: #fff;\r\n }\r\n\r\n .sce-grid {\r\n border-collapse: collapse;\r\n table-layout: fixed;\r\n width: 100%;\r\n font-size: 12px;\r\n user-select: none;\r\n\r\n th, td {\r\n border: 1px solid #e4e7ed;\r\n text-align: center;\r\n padding: 0;\r\n }\r\n\r\n .sce-corner {\r\n background: #f5f7fa;\r\n font-weight: 600;\r\n color: #303133;\r\n vertical-align: middle;\r\n }\r\n\r\n .sce-th-weekno { width: 52px; }\r\n .sce-th-weekrange { width: 110px; }\r\n\r\n .sce-th-day {\r\n background: var(--edu-green-primary, #00875a);\r\n color: #fff;\r\n font-weight: 600;\r\n height: 26px;\r\n }\r\n\r\n .sce-th-slot {\r\n background: #ecf5f0;\r\n color: #00875a;\r\n height: 22px;\r\n font-weight: 500;\r\n }\r\n\r\n .sce-week-cell {\r\n background: #f5f7fa;\r\n vertical-align: middle;\r\n white-space: nowrap;\r\n }\r\n\r\n .sce-weekno-cell {\r\n font-weight: 600;\r\n color: #303133;\r\n font-size: 13px;\r\n }\r\n\r\n .sce-weekrange-cell {\r\n font-size: 11px;\r\n color: #909399;\r\n }\r\n\r\n .sce-cell {\r\n height: 30px;\r\n position: relative;\r\n cursor: pointer;\r\n background: #fff;\r\n transition: background 0.15s;\r\n\r\n .sce-date-num { color: #c0c4cc; }\r\n\r\n // 星期交替底色:周一/三/五/日为浅绿,周二/四/六为白,便于区分星期\r\n &.sce-cell-odd { background: #eef6f1; }\r\n &.sce-cell-even { background: #ffffff; }\r\n &.has-event { background: #e6f4ea; }\r\n &.has-event .sce-event-name { color: #00663e; font-weight: 500; }\r\n &.has-event .sce-event-name.is-bold { font-weight: 700; }\r\n &.no-schedule { background: #fde2e2; }\r\n\r\n &.is-selected {\r\n outline: 2px solid var(--edu-green-primary, #00875a);\r\n outline-offset: -2px;\r\n background: #d4ecdf !important;\r\n }\r\n }\r\n }\r\n\r\n .sce-status {\r\n padding: 6px 14px;\r\n font-size: 12px;\r\n color: #909399;\r\n border-top: 1px solid #ebeef5;\r\n background: #fafafa;\r\n\r\n .sce-absorb-tip { margin-left: 12px; color: #00875a; }\r\n }\r\n}\r\n</style>\r\n"]}]} |