From 010c3cb947c3766cfcf1a691db6bce44e979e154 Mon Sep 17 00:00:00 2001 From: lion <120344285@qq.com> Date: Wed, 18 Mar 2026 15:19:37 +0800 Subject: [PATCH] =?UTF-8?q?=E8=AE=A1=E5=88=92=E8=AF=BE=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.development.local | 8 + src/api/scheduleOverview/index.js | 48 + .../components/CourseFormDialog.vue | 71 ++ .../components/LocationManagerDialog.vue | 160 +++ .../components/MemberOverview.vue | 38 +- .../components/MonthlyHeatmap.vue | 59 +- .../components/OwnerManagerDialog.vue | 160 +++ .../components/PlanMatrix.vue | 356 ++++++- .../components/ScheduleFormDialog.vue | 201 ++++ .../components/ScheduleManagerDialog.vue | 63 ++ .../components/SystemCourseManagerDialog.vue | 160 +++ .../components/SystemFormDialog.vue | 66 ++ src/views/scheduleOverview/index.vue | 953 +++++++++++------- vue.config.js | 3 +- 14 files changed, 1870 insertions(+), 476 deletions(-) create mode 100644 .env.development.local create mode 100644 src/views/scheduleOverview/components/CourseFormDialog.vue create mode 100644 src/views/scheduleOverview/components/LocationManagerDialog.vue create mode 100644 src/views/scheduleOverview/components/OwnerManagerDialog.vue create mode 100644 src/views/scheduleOverview/components/ScheduleFormDialog.vue create mode 100644 src/views/scheduleOverview/components/ScheduleManagerDialog.vue create mode 100644 src/views/scheduleOverview/components/SystemCourseManagerDialog.vue create mode 100644 src/views/scheduleOverview/components/SystemFormDialog.vue diff --git a/.env.development.local b/.env.development.local new file mode 100644 index 0000000..2523a2d --- /dev/null +++ b/.env.development.local @@ -0,0 +1,8 @@ +## 本地开发环境覆盖(不会提交到线上配置,优先级高于 .env.development) +ENV='development' + +# 后端本地 Laravel(php artisan serve 默认 8000) +VUE_APP_PRO_API = http://127.0.0.1:8000 +VUE_APP_BASE_API = http://127.0.0.1:8000 +VUE_APP_UPLOAD_API = http://127.0.0.1:8000/api/admin/upload-file + diff --git a/src/api/scheduleOverview/index.js b/src/api/scheduleOverview/index.js index c8ddcb1..0536f78 100644 --- a/src/api/scheduleOverview/index.js +++ b/src/api/scheduleOverview/index.js @@ -79,3 +79,51 @@ export function destroySchedule(params) { params }) } + +export function getLocationList(params) { + return request({ + method: 'get', + url: '/api/admin/schedule-overview/locations/index', + params + }) +} + +export function saveLocation(data) { + return request({ + method: 'post', + url: '/api/admin/schedule-overview/locations/save', + data + }) +} + +export function destroyLocation(params) { + return request({ + method: 'get', + url: '/api/admin/schedule-overview/locations/destroy', + params + }) +} + +export function getOwnerList(params) { + return request({ + method: 'get', + url: '/api/admin/schedule-overview/owners/index', + params + }) +} + +export function saveOwner(data) { + return request({ + method: 'post', + url: '/api/admin/schedule-overview/owners/save', + data + }) +} + +export function destroyOwner(params) { + return request({ + method: 'get', + url: '/api/admin/schedule-overview/owners/destroy', + params + }) +} diff --git a/src/views/scheduleOverview/components/CourseFormDialog.vue b/src/views/scheduleOverview/components/CourseFormDialog.vue new file mode 100644 index 0000000..9acff6f --- /dev/null +++ b/src/views/scheduleOverview/components/CourseFormDialog.vue @@ -0,0 +1,71 @@ + + + + diff --git a/src/views/scheduleOverview/components/LocationManagerDialog.vue b/src/views/scheduleOverview/components/LocationManagerDialog.vue new file mode 100644 index 0000000..d27da6a --- /dev/null +++ b/src/views/scheduleOverview/components/LocationManagerDialog.vue @@ -0,0 +1,160 @@ + + + + + + diff --git a/src/views/scheduleOverview/components/MemberOverview.vue b/src/views/scheduleOverview/components/MemberOverview.vue index ea801f6..035ca56 100644 --- a/src/views/scheduleOverview/components/MemberOverview.vue +++ b/src/views/scheduleOverview/components/MemberOverview.vue @@ -1,30 +1,19 @@ @@ -40,4 +29,3 @@ export default { } } - diff --git a/src/views/scheduleOverview/components/MonthlyHeatmap.vue b/src/views/scheduleOverview/components/MonthlyHeatmap.vue index a603bad..f6d825b 100644 --- a/src/views/scheduleOverview/components/MonthlyHeatmap.vue +++ b/src/views/scheduleOverview/components/MonthlyHeatmap.vue @@ -1,32 +1,29 @@ @@ -47,7 +44,15 @@ export default { type: Function, default: () => () => 'heat-empty' } + }, + methods: { + cellClassName({ row, columnIndex }) { + if (columnIndex >= 1 && columnIndex < 1 + this.monthLabels.length) { + const val = row.months && row.months[columnIndex - 1] + return ['heat-cell', this.heatClass(val)].filter(Boolean).join(' ') + } + return '' + } } } - diff --git a/src/views/scheduleOverview/components/OwnerManagerDialog.vue b/src/views/scheduleOverview/components/OwnerManagerDialog.vue new file mode 100644 index 0000000..ad95d0c --- /dev/null +++ b/src/views/scheduleOverview/components/OwnerManagerDialog.vue @@ -0,0 +1,160 @@ + + + + + + diff --git a/src/views/scheduleOverview/components/PlanMatrix.vue b/src/views/scheduleOverview/components/PlanMatrix.vue index 67d30c5..386f985 100644 --- a/src/views/scheduleOverview/components/PlanMatrix.vue +++ b/src/views/scheduleOverview/components/PlanMatrix.vue @@ -1,48 +1,79 @@ + + diff --git a/src/views/scheduleOverview/components/ScheduleFormDialog.vue b/src/views/scheduleOverview/components/ScheduleFormDialog.vue new file mode 100644 index 0000000..b3f2a2c --- /dev/null +++ b/src/views/scheduleOverview/components/ScheduleFormDialog.vue @@ -0,0 +1,201 @@ + + + + + + diff --git a/src/views/scheduleOverview/components/ScheduleManagerDialog.vue b/src/views/scheduleOverview/components/ScheduleManagerDialog.vue new file mode 100644 index 0000000..66271c2 --- /dev/null +++ b/src/views/scheduleOverview/components/ScheduleManagerDialog.vue @@ -0,0 +1,63 @@ + + + + + diff --git a/src/views/scheduleOverview/components/SystemCourseManagerDialog.vue b/src/views/scheduleOverview/components/SystemCourseManagerDialog.vue new file mode 100644 index 0000000..68e20f4 --- /dev/null +++ b/src/views/scheduleOverview/components/SystemCourseManagerDialog.vue @@ -0,0 +1,160 @@ + + + + diff --git a/src/views/scheduleOverview/components/SystemFormDialog.vue b/src/views/scheduleOverview/components/SystemFormDialog.vue new file mode 100644 index 0000000..fe80bb7 --- /dev/null +++ b/src/views/scheduleOverview/components/SystemFormDialog.vue @@ -0,0 +1,66 @@ + + + + diff --git a/src/views/scheduleOverview/index.vue b/src/views/scheduleOverview/index.vue index 7a72695..8cfc497 100644 --- a/src/views/scheduleOverview/index.vue +++ b/src/views/scheduleOverview/index.vue @@ -12,8 +12,10 @@ style="width: 120px;" @change="handleYearChange" /> - 体系课程管理 - 编排管理 + 计划体系管理 + 带班管理 + 新增地点 + 新增负责人 @@ -28,281 +30,88 @@ - -
-
-
-
体系课程树
-
- 新增体系 - 新增课程 -
-
-
- -
-
- - {{ data.nodeType === 'system' ? '体系' : '课程' }} - - {{ data.name }} -
-
- - 新增课程 - - - 编辑 - - - 删除 - -
-
-
-
-
- -
-
-
节点信息
-
- {{ selectedTreeNode ? selectedTreeNode.name : '请在左侧选择节点' }} -
-
-
-
- 类型 - {{ selectedTreeNode.nodeType === 'system' ? '体系' : '课程' }} -
-
- 名称 - {{ selectedTreeNode.name }} -
-
- 年份 - {{ selectedTreeNode.year }} -
-
- 排序 - {{ selectedTreeNode.sort }} -
-
- 所属体系 - {{ selectedTreeNode.systemName }} -
-
- - 新增课程 - - - 编辑 - - - 删除 - -
-
-
- 请在左侧树中选择体系或课程 -
-
-
-
+ :tree-data="systemCourseTreeData" + :selected-system="selectedSystem" + :selected-tree-node="selectedTreeNode" + :current-key="selectedTreeNodeKey" + @tree-node-select="handleTreeNodeSelect" + @open-system-form="openSystemForm" + @open-course-form="handleOpenCourseFormFromDialog" + @delete-system="handleDeleteSystem" + @delete-course="handleDeleteCourse" + /> - -
-
编排列表
-
- 新增编排 -
-
- - - - - - - - - - - - -
- - + + - - - - - - - - - - - - - 取消 - 保存 - - - - + + - - - - - - - - - - - - - - - 取消 - 保存 - - - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 删除 - - 取消 - 保存 - - + :mode="scheduleFormMode" + :form="scheduleForm" + :rules="scheduleRules" + :systems="scheduleFormSystems" + :course-options="scheduleFormCourseOptions" + :month-options="monthOptions" + :locations="locations" + :owners="owners" + @year-change="handleScheduleYearChange" + @system-change="handleScheduleSystemChange" + @open-location-manager="openLocationManager" + @open-owner-manager="openOwnerManager" + @cancel="handleScheduleDialogCancel" + @delete="handleDeleteSchedule" + @submit="submitScheduleForm" + /> + + + + @@ -311,12 +120,27 @@ import SummaryPanel from './components/SummaryPanel.vue' import MonthlyHeatmap from './components/MonthlyHeatmap.vue' import MemberOverview from './components/MemberOverview.vue' import PlanMatrix from './components/PlanMatrix.vue' +import SystemCourseManagerDialog from './components/SystemCourseManagerDialog.vue' +import ScheduleManagerDialog from './components/ScheduleManagerDialog.vue' +import SystemFormDialog from './components/SystemFormDialog.vue' +import CourseFormDialog from './components/CourseFormDialog.vue' +import ScheduleFormDialog from './components/ScheduleFormDialog.vue' +import LocationManagerDialog from './components/LocationManagerDialog.vue' +import OwnerManagerDialog from './components/OwnerManagerDialog.vue' +import { index as courseTypeIndex } from '@/api/course/courseType.js' import { destroyCourse, destroySchedule, destroySystem, getOverview, + getLocationList, + getOwnerList, saveCourse, + saveLocation, + saveOwner, + destroyLocation, + destroyOwner, + getScheduleList, saveSchedule, saveSystem } from '@/api/scheduleOverview/index' @@ -333,6 +157,7 @@ const createSystemForm = () => ({ const createCourseForm = (systemId = '') => ({ id: '', system_id: systemId || '', + course_type_id: '', name: '', sort: 1 }) @@ -342,11 +167,8 @@ const createScheduleForm = () => ({ year: getCurrentYear(), system_id: '', course_id: '', - month: [], title: '', - owner: '', - location: '', - count_text: '' + modules: [] }) const TONE_CLASSES = ['tone-green', 'tone-blue', 'tone-purple', 'tone-sand', 'tone-cyan', 'tone-cream'] @@ -364,13 +186,24 @@ export default { SummaryPanel, MonthlyHeatmap, MemberOverview, - PlanMatrix + PlanMatrix, + SystemCourseManagerDialog, + ScheduleManagerDialog, + SystemFormDialog, + CourseFormDialog, + ScheduleFormDialog, + LocationManagerDialog, + OwnerManagerDialog }, data() { return { systems: [], courses: [], schedules: [], + scheduleGroups: [], + courseTypes: [], + locations: [], + owners: [], loading: false, monthLabels: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'], monthOptions: Array.from({ length: 12 }, (_, i) => ({ value: i + 1, label: `${i + 1}月` })), @@ -380,6 +213,8 @@ export default { scheduleFormCourses: [], systemCourseDialogVisible: false, scheduleManagerDialogVisible: false, + locationManagerDialogVisible: false, + ownerManagerDialogVisible: false, systemFormVisible: false, courseFormVisible: false, scheduleFormVisible: false, @@ -391,36 +226,29 @@ export default { scheduleForm: createScheduleForm(), systemRules: { year: [{ required: true, message: '请选择年份', trigger: 'change' }], - name: [{ required: true, message: '请输入体系名称', trigger: 'blur' }], + name: [{ required: true, message: '请输入计划体系名称', trigger: 'blur' }], sort: [{ required: true, message: '请输入排序', trigger: 'change' }] }, courseRules: { - system_id: [{ required: true, message: '请选择所属体系', trigger: 'change' }], - name: [{ required: true, message: '请输入课程名称', trigger: 'blur' }], + system_id: [{ required: true, message: '请选择所属计划体系', trigger: 'change' }], + course_type_id: [{ required: true, message: '请选择课程体系', trigger: 'change' }], sort: [{ required: true, message: '请输入排序', trigger: 'change' }] }, scheduleRules: { year: [{ required: true, message: '请选择年份', trigger: 'change' }], - system_id: [{ required: true, message: '请选择体系', trigger: 'change' }], - course_id: [{ required: true, message: '请选择课程', trigger: 'change' }], - month: [{ + system_id: [{ required: true, message: '请选择计划体系', trigger: 'change' }], + modules: [{ trigger: 'change', validator: (rule, value, callback) => { - if (Array.isArray(value)) { - if (!value.length) { - callback(new Error('请选择月份')) - return - } - } else if (!value) { - callback(new Error('请选择月份')) + if (!Array.isArray(value) || !value.length) { + callback(new Error('模块/期数至少需要一条数据')) return } callback() } }], - title: [{ required: true, message: '请输入编排标题', trigger: 'blur' }], - owner: [{ required: true, message: '请输入负责人', trigger: 'blur' }], - location: [{ required: true, message: '请输入地点', trigger: 'blur' }] + title: [{ required: true, message: '请输入课程名称', trigger: 'blur' }], + // modules 内部必填项由子组件在提交前做校验提示 }, currentYear: getCurrentYear() } @@ -576,66 +404,274 @@ export default { }, {}) }, scheduleTableRows() { - return this.schedules.map((item) => ({ - ...item, - systemName: (this.systemMap[item.system_id] || {}).name || '-', - courseName: (this.courseMap[item.course_id] || {}).name || '-', - monthLabel: `${item.month}月` - })).sort((a, b) => { - const monthDiff = Number(a.month) - Number(b.month) - if (monthDiff !== 0) { - return monthDiff + const groups = Array.isArray(this.scheduleGroups) ? this.scheduleGroups : [] + const rows = groups.reduce((list, group) => { + const modules = Array.isArray(group.modules) ? group.modules : [] + if (!modules.length) { + list.push({ + group_id: group.id, + year: group.year, + system_id: group.system_id, + course_id: group.course_id, + title: group.title, + moduleName: '-', + month: '', + monthLabel: '-', + location: '-', + owner: '-', + count_text: '', + systemName: (this.systemMap[group.system_id] || {}).name || '-', + courseName: (this.courseMap[group.course_id] || {}).name || '-' + }) + return list } - return String(a.title || '').localeCompare(String(b.title || ''), 'zh-CN') + modules.forEach((m) => { + list.push({ + id: m.id, + group_id: group.id, + year: group.year, + system_id: group.system_id, + course_id: group.course_id, + title: group.title, + moduleName: m.name || '-', + month: m.month, + monthLabel: m.month ? `${m.month}月` : '-', + location: (m.location || {}).name || '-', + owner: (m.owner || {}).name || '-', + count_text: m.count_text || '', + systemName: (this.systemMap[group.system_id] || {}).name || '-', + courseName: (this.courseMap[group.course_id] || {}).name || '-' + }) + }) + return list + }, []) + + return rows.sort((a, b) => { + const systemA = this.systemMap[a.system_id] || {} + const systemB = this.systemMap[b.system_id] || {} + const systemSortDiff = Number(systemA.sort || 0) - Number(systemB.sort || 0) + if (systemSortDiff !== 0) { + return systemSortDiff + } + const systemNameDiff = String(systemA.name || '').localeCompare(String(systemB.name || ''), 'zh-CN') + if (systemNameDiff !== 0) { + return systemNameDiff + } + const courseA = this.courseMap[a.course_id] || {} + const courseB = this.courseMap[b.course_id] || {} + const courseSortDiff = Number(courseA.sort || 0) - Number(courseB.sort || 0) + if (courseSortDiff !== 0) { + return courseSortDiff + } + const courseNameDiff = String(courseA.name || '').localeCompare(String(courseB.name || ''), 'zh-CN') + if (courseNameDiff !== 0) { + return courseNameDiff + } + // 先按课程名称(group_id)聚在一起,以便相同课程名称合并单元格;再按月份排序 + const groupDiff = String(a.group_id || '').localeCompare(String(b.group_id || '')) + if (groupDiff !== 0) { + return groupDiff + } + return Number(a.month || 0) - Number(b.month || 0) }) }, + scheduleSpanMeta() { + const rows = this.scheduleTableRows + const systemMeta = rows.map(() => ({ rowspan: 1, colspan: 1 })) + const courseMeta = rows.map(() => ({ rowspan: 1, colspan: 1 })) + const titleMeta = rows.map(() => ({ rowspan: 1, colspan: 1 })) + const opMeta = rows.map(() => ({ rowspan: 1, colspan: 1 })) + + // 计划体系列:按 system_id 合并连续行 + let i = 0 + while (i < rows.length) { + const systemKey = String(rows[i].system_id) + let j = i + 1 + while (j < rows.length && String(rows[j].system_id) === systemKey) { + j += 1 + } + const span = j - i + systemMeta[i] = { rowspan: span, colspan: 1 } + for (let k = i + 1; k < j; k += 1) { + systemMeta[k] = { rowspan: 0, colspan: 0 } + } + i = j + } + + // 课程体系列、课程名称列:在同一 system_id 分组内,按 course_id 合并;课程名称再按 group_id 合并 + i = 0 + while (i < rows.length) { + const systemKey = String(rows[i].system_id) + let j = i + 1 + while (j < rows.length && String(rows[j].system_id) === systemKey) { + j += 1 + } + + let p = i + while (p < j) { + const courseKey = String(rows[p].course_id) + let q = p + 1 + while (q < j && String(rows[q].course_id) === courseKey) { + q += 1 + } + const courseSpan = q - p + courseMeta[p] = { rowspan: courseSpan, colspan: 1 } + for (let k = p + 1; k < q; k += 1) { + courseMeta[k] = { rowspan: 0, colspan: 0 } + } + + // 课程名称列、操作列:在同一 course 分组内,按 group_id 合并连续行 + let u = p + while (u < q) { + const groupKey = String(rows[u].group_id || '') + let v = u + 1 + while (v < q && String(rows[v].group_id || '') === groupKey) { + v += 1 + } + const span = v - u + titleMeta[u] = { rowspan: span, colspan: 1 } + opMeta[u] = { rowspan: span, colspan: 1 } + for (let k = u + 1; k < v; k += 1) { + titleMeta[k] = { rowspan: 0, colspan: 0 } + opMeta[k] = { rowspan: 0, colspan: 0 } + } + u = v + } + + p = q + } + + i = j + } + + return { systemMeta, courseMeta, titleMeta, opMeta } + }, planRows() { const rows = [] const sortedSystems = [...this.systems].sort((a, b) => Number(a.sort || 0) - Number(b.sort || 0)) + const groups = Array.isArray(this.scheduleGroups) ? this.scheduleGroups : [] sortedSystems.forEach((system, systemIndex) => { - const relatedCourses = this.courses - .filter((item) => String(item.system_id) === String(system.id)) - .sort((a, b) => Number(a.sort || 0) - Number(b.sort || 0)) + const systemGroups = groups + .filter((g) => String(g.system_id) === String(system.id)) + .sort((a, b) => { + const courseA = a.course || {} + const courseB = b.course || {} + const nameA = String(courseA.name || '') + const nameB = String(courseB.name || '') + + // 先按是否有课程体系排序:有名称的排在前面,没有名称的排在后面 + const hasCourseA = nameA !== '' + const hasCourseB = nameB !== '' + if (hasCourseA !== hasCourseB) { + return hasCourseA ? -1 : 1 + } + + const sortDiff = Number(courseA.sort || 0) - Number(courseB.sort || 0) + if (sortDiff !== 0) return sortDiff + + const nameDiff = nameA.localeCompare(nameB, 'zh-CN') + if (nameDiff !== 0) return nameDiff + return String(a.title || '').localeCompare(String(b.title || ''), 'zh-CN') + }) - const safeCourses = relatedCourses.length ? relatedCourses : [{ id: `empty-${system.id}`, name: '未配置课程', system_id: system.id }] const groupClass = TONE_CLASSES[systemIndex % TONE_CLASSES.length] - safeCourses.forEach((course, courseIndex) => { - const courseSchedules = this.schedules.filter((item) => String(item.course_id) === String(course.id)) - const plan = this.monthLabels.reduce((map, monthLabel) => { + // 计划体系下所有课程体系(来自 courses 表)都要显示 + const allCoursesUnderSystem = this.courses + .filter((c) => String(c.system_id) === String(system.id)) + .sort((a, b) => { + const sortDiff = Number(a.sort || 0) - Number(b.sort || 0) + if (sortDiff !== 0) return sortDiff + return String(a.name || '').localeCompare(String(b.name || ''), 'zh-CN') + }) + + // 有带班的课程体系:按 group 展开为多行(同月份多模块换行) + systemGroups.forEach((group) => { + const modules = Array.isArray(group.modules) ? group.modules : [] + const monthMap = this.monthLabels.reduce((map, monthLabel) => { const monthNumber = Number(monthLabel.replace('月', '')) - map[monthLabel] = courseSchedules - .filter((item) => Number(item.month) === monthNumber) - .sort((a, b) => String(a.title || '').localeCompare(String(b.title || ''), 'zh-CN')) - .map((item) => ({ - id: item.id, - year: item.year, - system_id: item.system_id, - course_id: item.course_id, - month: item.month, - title: item.title, - owner: item.owner, - location: item.location, - count_text: item.count_text, - ownerLocation: `${item.owner || '-'} / ${item.location || '-'}`, - countText: item.count_text || '-' + map[monthLabel] = modules + .filter((m) => Number(m.month) === monthNumber) + .sort((a, b) => String(a.name || '').localeCompare(String(b.name || ''), 'zh-CN')) + .map((m) => ({ + id: m.id, + group_id: group.id, + year: group.year, + system_id: group.system_id, + course_id: group.course_id, + month: m.month, + title: group.title || '', + moduleName: m.name || '', + owner: (m.owner || {}).name || '', + location: (m.location || {}).name || '', + count_text: m.count_text || '', + ownerLocation: `${(m.owner || {}).name || '-'} / ${(m.location || {}).name || '-'}`, + countText: m.count_text || '' })) return map }, {}) + const maxModules = Math.max( + 1, + ...this.monthLabels.map((monthLabel) => (monthMap[monthLabel] || []).length || 0) + ) + + for (let idx = 0; idx < maxModules; idx += 1) { + const plan = this.monthLabels.reduce((map, monthLabel) => { + const list = monthMap[monthLabel] || [] + map[monthLabel] = list[idx] ? [list[idx]] : [] + return map + }, {}) + + rows.push({ + rowKey: `${system.id}-${group.course_id || 'none'}-${group.id}-${idx}`, + systemId: system.id, + group: system.name, + groupClass, + courseId: group.course_id || '', + course: (group.course || {}).name || '-', + plan + }) + } + }) + + // 追加:尚未有带班的课程体系也各占一行(空数据) + const appearedCourseIds = new Set(systemGroups.map((g) => String(g.course_id || '')).filter(Boolean)) + allCoursesUnderSystem.forEach((course) => { + if (!appearedCourseIds.has(String(course.id))) { + const emptyPlan = this.monthLabels.reduce((map, monthLabel) => { + map[monthLabel] = [] + return map + }, {}) + rows.push({ + rowKey: `${system.id}-${course.id}-empty`, + systemId: system.id, + group: system.name, + groupClass, + courseId: course.id, + course: course.name || '-', + plan: emptyPlan + }) + } + }) + + // 若该计划体系下没有任何课程体系配置,至少显示一行空占位 + if (!allCoursesUnderSystem.length && !systemGroups.length) { + const emptyPlan = this.monthLabels.reduce((map, monthLabel) => { + map[monthLabel] = [] + return map + }, {}) rows.push({ - rowKey: `${system.id}-${course.id}`, + rowKey: `${system.id}-empty`, systemId: system.id, group: system.name, groupClass, - groupSpan: safeCourses.length, - showGroup: courseIndex === 0, - courseId: course.id, - course: course.name, - plan + courseId: '', + course: '-', + plan: emptyPlan }) - }) + } }) return rows @@ -643,8 +679,133 @@ export default { }, created() { this.loadData() + this.loadCourseTypes() + this.loadLocations() + this.loadOwners() }, methods: { + async loadLocations() { + try { + const res = await getLocationList() + this.locations = Array.isArray(res) ? res : (res.data || res || []) + } catch (e) { + this.locations = [] + } + }, + async loadOwners() { + try { + const res = await getOwnerList() + this.owners = Array.isArray(res) ? res : (res.data || res || []) + } catch (e) { + this.owners = [] + } + }, + async handleAddLocation(payload) { + await saveLocation({ name: payload.name, status: payload.status, sort: payload.sort }) + await this.loadLocations() + this.$message.success('地点新增成功') + }, + async handleEditLocation(payload) { + await saveLocation({ id: payload.id, name: payload.name, status: payload.status, sort: payload.sort }) + await this.loadLocations() + this.$message.success('地点保存成功') + }, + handleDeleteLocation(row) { + this.$confirm(`确定删除地点“${row.name}”吗?`, '提示', { type: 'warning' }) + .then(async() => { + await destroyLocation({ id: row.id }) + await this.loadLocations() + this.$message.success('地点删除成功') + }) + .catch(() => {}) + }, + async handleAddOwner(payload) { + await saveOwner({ name: payload.name, status: payload.status, sort: payload.sort }) + await this.loadOwners() + this.$message.success('负责人新增成功') + }, + async handleEditOwner(payload) { + await saveOwner({ id: payload.id, name: payload.name, status: payload.status, sort: payload.sort }) + await this.loadOwners() + this.$message.success('负责人保存成功') + }, + handleDeleteOwner(row) { + this.$confirm(`确定删除负责人“${row.name}”吗?`, '提示', { type: 'warning' }) + .then(async() => { + await destroyOwner({ id: row.id }) + await this.loadOwners() + this.$message.success('负责人删除成功') + }) + .catch(() => {}) + }, + openLocationManager() { + this.locationManagerDialogVisible = true + }, + openOwnerManager() { + this.ownerManagerDialogVisible = true + }, + handleOpenCourseFormFromDialog(payload = {}) { + const mode = payload.mode || 'add' + const row = payload.row || null + const targetSystem = payload.targetSystem || null + this.openCourseForm(mode, row, targetSystem) + }, + scheduleSpanMethod({ rowIndex, columnIndex, column }) { + const spanMeta = this.scheduleSpanMeta + + // 操作列:固定列时右侧表格中 columnIndex 为 0,需优先用 label 识别 + if (column && column.label === '操作') { + const meta = spanMeta.opMeta[rowIndex] + return meta ? { rowspan: meta.rowspan, colspan: meta.colspan } : { rowspan: 1, colspan: 1 } + } + + // 第1列:计划体系(按 system_id 合并) + if (columnIndex === 0) { + const meta = spanMeta.systemMeta[rowIndex] + return meta ? { rowspan: meta.rowspan, colspan: meta.colspan } : { rowspan: 1, colspan: 1 } + } + + // 第2列:课程体系(同一计划体系内按 course_id 合并) + if (columnIndex === 1) { + const meta = spanMeta.courseMeta[rowIndex] + return meta ? { rowspan: meta.rowspan, colspan: meta.colspan } : { rowspan: 1, colspan: 1 } + } + + // 第3列:课程名称(同一课程体系内按 group_id 合并) + if (columnIndex === 2) { + const meta = spanMeta.titleMeta[rowIndex] + return meta ? { rowspan: meta.rowspan, colspan: meta.colspan } : { rowspan: 1, colspan: 1 } + } + + // 第9列:操作(主表体中的列索引) + if (columnIndex === 8) { + const meta = spanMeta.opMeta[rowIndex] + return meta ? { rowspan: meta.rowspan, colspan: meta.colspan } : { rowspan: 1, colspan: 1 } + } + return { rowspan: 1, colspan: 1 } + }, + async loadCourseTypes() { + try { + const res = await courseTypeIndex({ + page: 1, + page_size: 9999, + sort_name: 'sort', + sort_type: 'ASC', + filter: [{ + key: 'status', + op: 'like', + value: 1 + }] + }, true) + this.courseTypes = (res && res.data) ? res.data : [] + } catch (e) { + this.courseTypes = [] + } + }, + getCourseTypeNameById(id) { + const hit = this.courseTypes.find((item) => String(item.id) === String(id)) + return hit ? hit.name : '' + }, async loadData() { this.loading = true try { @@ -652,6 +813,8 @@ export default { this.systems = data.systems || [] this.courses = data.courses || [] this.schedules = data.schedules || [] + // 带班管理:使用 group 列表用于编辑/回显 modules + this.scheduleGroups = await getScheduleList({ year: this.currentYear }) || [] if (!this.selectedSystemId || !this.systems.find((item) => String(item.id) === String(this.selectedSystemId))) { this.selectedSystemId = this.systems[0] ? this.systems[0].id : '' @@ -703,11 +866,6 @@ export default { if (!this.selectedTreeNodeKey && this.selectedSystemId) { this.selectedTreeNodeKey = `system-${this.selectedSystemId}` } - this.$nextTick(() => { - if (this.$refs.systemCourseTree && this.selectedTreeNodeKey) { - this.$refs.systemCourseTree.setCurrentKey(this.selectedTreeNodeKey) - } - }) }, openScheduleManager() { this.scheduleManagerDialogVisible = true @@ -720,17 +878,9 @@ export default { this.systemFormMode = mode this.systemForm = row ? { id: row.id, year: row.year || this.currentYear, name: row.name, sort: row.sort } : { ...createSystemForm(), year: this.currentYear } this.systemFormVisible = true - this.$nextTick(() => { - if (this.$refs.systemFormRef) { - this.$refs.systemFormRef.clearValidate() - } - }) }, submitSystemForm() { - this.$refs.systemFormRef.validate(async(valid) => { - if (!valid) { - return - } + ;(async() => { await saveSystem(this.systemForm) await this.loadData() if (this.systemForm.id) { @@ -743,17 +893,17 @@ export default { this.selectedTreeNodeKey = `system-${this.selectedSystemId}` } this.systemFormVisible = false - this.$message.success('体系保存成功') - }) + this.$message.success('计划体系保存成功') + })() }, handleDeleteSystem(row) { - this.$confirm(`删除体系“${row.name}”后,其下课程和编排也会一并删除,是否继续?`, '提示', { + this.$confirm(`删除计划体系“${row.name}”后,其下课程体系和带班也会一并删除,是否继续?`, '提示', { type: 'warning' }).then(async() => { await destroySystem({ id: row.id }) await this.loadData() this.selectedTreeNodeKey = this.selectedSystemId ? `system-${this.selectedSystemId}` : '' - this.$message.success('体系删除成功') + this.$message.success('计划体系删除成功') }).catch(() => {}) }, openCourseForm(mode, row, targetSystem) { @@ -762,6 +912,7 @@ export default { this.courseForm = { id: row.id, system_id: row.system_id, + course_type_id: row.course_type_id || (row.course_type ? row.course_type.id : ''), name: row.name, sort: row.sort } @@ -769,16 +920,12 @@ export default { this.courseForm = createCourseForm(targetSystem ? targetSystem.id : this.selectedSystemId) } this.courseFormVisible = true - this.$nextTick(() => { - if (this.$refs.courseFormRef) { - this.$refs.courseFormRef.clearValidate() - } - }) }, submitCourseForm() { - this.$refs.courseFormRef.validate(async(valid) => { - if (!valid) { - return + ;(async() => { + const selectedName = this.getCourseTypeNameById(this.courseForm.course_type_id) + if (selectedName) { + this.courseForm.name = selectedName } await saveCourse(this.courseForm) await this.loadData() @@ -787,17 +934,17 @@ export default { this.selectedTreeNodeKey = `course-${this.courseForm.id}` } this.courseFormVisible = false - this.$message.success('课程保存成功') - }) + this.$message.success('课程体系保存成功') + })() }, handleDeleteCourse(row) { - this.$confirm(`删除课程“${row.name}”后,其下编排也会一并删除,是否继续?`, '提示', { + this.$confirm(`删除课程体系“${row.name}”后,其下带班也会一并删除,是否继续?`, '提示', { type: 'warning' }).then(async() => { await destroyCourse({ id: row.id }) await this.loadData() this.selectedTreeNodeKey = this.selectedSystemId ? `system-${this.selectedSystemId}` : '' - this.$message.success('课程删除成功') + this.$message.success('课程体系删除成功') }).catch(() => {}) }, handleTreeNodeSelect(data) { @@ -819,26 +966,27 @@ export default { if (!exists) { this.selectedTreeNodeKey = this.selectedSystemId ? `system-${this.selectedSystemId}` : (this.systemCourseTreeData[0] ? this.systemCourseTreeData[0].treeKey : '') } - - this.$nextTick(() => { - if (this.$refs.systemCourseTree && this.selectedTreeNodeKey) { - this.$refs.systemCourseTree.setCurrentKey(this.selectedTreeNodeKey) - } - }) }, async openScheduleForm(mode, row, preset = {}) { this.scheduleFormMode = mode if (row) { + const groupId = row.group_id || row.id + const group = (Array.isArray(this.scheduleGroups) ? this.scheduleGroups : []).find((g) => String(g.id) === String(groupId)) + const modules = group && Array.isArray(group.modules) ? group.modules : [] this.scheduleForm = { - id: row.id, - year: row.year || getCurrentYear(), - system_id: row.system_id, - course_id: row.course_id, - month: row.month, - title: row.title, - owner: row.owner, - location: row.location, - count_text: row.count_text + id: group ? group.id : groupId, + year: (group ? group.year : row.year) || getCurrentYear(), + system_id: group ? group.system_id : row.system_id, + course_id: group ? group.course_id : row.course_id, + title: group ? group.title : row.title, + modules: modules.map((m) => ({ + id: m.id, + name: m.name || '', + month: m.month, + location_id: m.location_id || (m.location || {}).id || '', + owner_id: m.owner_id || (m.owner || {}).id || '', + count_text: m.count_text || '' + })) } await this.loadScheduleFormOptions(this.scheduleForm.year) } else { @@ -850,16 +998,19 @@ export default { ...createScheduleForm(), year: defaultYear, system_id: defaultSystemId, - month: preset.month ? [preset.month] : [], course_id: preset.course_id || (defaultCourse ? defaultCourse.id : '') } + if (preset.month) { + this.scheduleForm.modules = [{ + name: '', + month: preset.month, + location_id: '', + owner_id: '', + count_text: '' + }] + } } this.scheduleFormVisible = true - this.$nextTick(() => { - if (this.$refs.scheduleFormRef) { - this.$refs.scheduleFormRef.clearValidate() - } - }) }, handlePlanCellClick(row, monthLabel, items) { if (items && items.length) { @@ -903,20 +1054,18 @@ export default { this.loadData() }, submitScheduleForm() { - this.$refs.scheduleFormRef.validate(async(valid) => { - if (!valid) { - return - } + ;(async() => { await saveSchedule(this.scheduleForm) - await this.refreshAfterScheduleAction('编排保存成功') - }) + await this.refreshAfterScheduleAction('带班保存成功') + })() }, handleDeleteSchedule(row) { - this.$confirm(`确定删除编排“${row.title}”吗?`, '提示', { + const groupId = row.group_id || row.id + this.$confirm(`确定删除带班“${row.title}”吗?`, '提示', { type: 'warning' }).then(async() => { - await destroySchedule({ id: row.id }) - await this.refreshAfterScheduleAction('编排删除成功') + await destroySchedule({ id: groupId }) + await this.refreshAfterScheduleAction('带班删除成功') }).catch(() => {}) } } @@ -996,7 +1145,7 @@ export default { } .panel-title { - font-size: 13px; + font-size: 16px; font-weight: 600; color: #303133; margin-bottom: 10px; @@ -1084,7 +1233,7 @@ export default { } .plan-table { - min-width: 1280px; + min-width: 1600px; } .group-cell, @@ -1093,14 +1242,15 @@ export default { } .group-cell { - width: 110px; + width: 140px; } .course-cell { - width: 100px; + width: 140px; } .plan-month-cell { + width: 140px; height: 74px; vertical-align: top; background: #fff; @@ -1108,6 +1258,29 @@ export default { transition: background-color 0.2s ease; } +.plan-table thead th { + position: sticky; + top: 0; + z-index: 3; + background: #f8f8f9; +} + +.plan-table .sticky-col { + position: sticky; + left: 0; + z-index: 4; + background: #fff; +} + +.plan-table .sticky-col-2 { + left: 140px; +} + +.plan-table thead .sticky-col-1, +.plan-table thead .sticky-col-2 { + z-index: 5; +} + .plan-month-cell:hover { background: #f5f7fa; } diff --git a/vue.config.js b/vue.config.js index b69e529..04f5077 100644 --- a/vue.config.js +++ b/vue.config.js @@ -29,9 +29,10 @@ module.exports = { publicPath: process.env.ENV === 'staging' ? '/admin' : '/admin', // 前台打包输出到当前机器的 /Users/apple/www/wx.sstbc.com/public/admin 目录下 // 正式 - outputDir: '/Users/mac/Documents/朗业/2024/s-苏州科技商学院/wx.sstbc.com/public/admin', + // outputDir: '/Users/mac/Documents/朗业/2024/s-苏州科技商学院/wx.sstbc.com/public/admin', // 测试 // outputDir: '/Users/mac/Documents/朗业/2025/s-苏州科技商学院/wx.sstbc.com/public/admin', + outputDir: '/Users/mac/Documents/朗业/2026/s-苏州科技商学院/code/wx.sstbc.com/public/admin', assetsDir: 'static', css: { loaderOptions: { // 向 CSS 相关的 loader 传递选项