|
|
<template>
|
|
|
<div class="schedule-overview-page" v-loading="loading">
|
|
|
<lx-header icon="md-apps" :text="$route.meta.title" style="margin-bottom: 10px; border: 0; margin-top: 15px;">
|
|
|
<div slot="content" class="header-actions">
|
|
|
<el-date-picker
|
|
|
v-model="currentYear"
|
|
|
type="year"
|
|
|
value-format="yyyy"
|
|
|
format="yyyy"
|
|
|
placeholder="选择年份"
|
|
|
size="small"
|
|
|
style="width: 120px;"
|
|
|
@change="handleYearChange"
|
|
|
/>
|
|
|
<el-button size="small" plain @click="openSystemCourseManager">计划体系管理</el-button>
|
|
|
<el-button size="small" type="primary" plain @click="openScheduleManager">带班管理</el-button>
|
|
|
<el-button size="small" plain @click="openLocationManager">新增地点</el-button>
|
|
|
<el-button size="small" plain @click="openOwnerManager">新增负责人</el-button>
|
|
|
</div>
|
|
|
</lx-header>
|
|
|
|
|
|
<summary-panel :summary-cards="summaryCards" />
|
|
|
|
|
|
<monthly-heatmap
|
|
|
:month-labels="monthLabels"
|
|
|
:monthly-stats="monthlyStats"
|
|
|
:heat-class="heatClass"
|
|
|
/>
|
|
|
|
|
|
<member-overview :member-analysis="memberAnalysis" />
|
|
|
|
|
|
<plan-matrix
|
|
|
:year="currentYear"
|
|
|
:month-labels="monthLabels"
|
|
|
:plan-rows="planRows"
|
|
|
@cell-click="handlePlanCellClick"
|
|
|
/>
|
|
|
|
|
|
<system-course-manager-dialog
|
|
|
:visible.sync="systemCourseDialogVisible"
|
|
|
: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"
|
|
|
/>
|
|
|
|
|
|
<schedule-manager-dialog
|
|
|
:visible.sync="scheduleManagerDialogVisible"
|
|
|
:rows="scheduleTableRows"
|
|
|
:span-method="scheduleSpanMethod"
|
|
|
@open-schedule-form="openScheduleForm"
|
|
|
@delete-schedule="handleDeleteSchedule"
|
|
|
/>
|
|
|
|
|
|
<system-form-dialog
|
|
|
:visible.sync="systemFormVisible"
|
|
|
:mode="systemFormMode"
|
|
|
:form="systemForm"
|
|
|
:rules="systemRules"
|
|
|
@submit="submitSystemForm"
|
|
|
/>
|
|
|
|
|
|
<course-form-dialog
|
|
|
:visible.sync="courseFormVisible"
|
|
|
:mode="courseFormMode"
|
|
|
:form="courseForm"
|
|
|
:rules="courseRules"
|
|
|
:systems="systems"
|
|
|
:course-types="courseTypes"
|
|
|
@course-type-change="(val) => { courseForm.name = getCourseTypeNameById(val) || courseForm.name }"
|
|
|
@submit="submitCourseForm"
|
|
|
/>
|
|
|
|
|
|
<schedule-form-dialog
|
|
|
:visible.sync="scheduleFormVisible"
|
|
|
: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"
|
|
|
/>
|
|
|
|
|
|
<location-manager-dialog
|
|
|
:visible.sync="locationManagerDialogVisible"
|
|
|
:list="locations"
|
|
|
@open="loadLocations"
|
|
|
@add="handleAddLocation"
|
|
|
@edit="handleEditLocation"
|
|
|
@delete="handleDeleteLocation"
|
|
|
/>
|
|
|
|
|
|
<owner-manager-dialog
|
|
|
:visible.sync="ownerManagerDialogVisible"
|
|
|
:list="owners"
|
|
|
@open="loadOwners"
|
|
|
@add="handleAddOwner"
|
|
|
@edit="handleEditOwner"
|
|
|
@delete="handleDeleteOwner"
|
|
|
/>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script>
|
|
|
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'
|
|
|
|
|
|
const getCurrentYear = () => String(new Date().getFullYear())
|
|
|
|
|
|
const createSystemForm = () => ({
|
|
|
id: '',
|
|
|
year: getCurrentYear(),
|
|
|
name: '',
|
|
|
sort: 1
|
|
|
})
|
|
|
|
|
|
const createCourseForm = (systemId = '') => ({
|
|
|
id: '',
|
|
|
system_id: systemId || '',
|
|
|
course_type_id: '',
|
|
|
name: '',
|
|
|
sort: 1
|
|
|
})
|
|
|
|
|
|
const createScheduleForm = () => ({
|
|
|
id: '',
|
|
|
year: getCurrentYear(),
|
|
|
system_id: '',
|
|
|
course_id: '',
|
|
|
title: '',
|
|
|
modules: []
|
|
|
})
|
|
|
|
|
|
const TONE_CLASSES = ['tone-green', 'tone-blue', 'tone-purple', 'tone-sand', 'tone-cyan', 'tone-cream']
|
|
|
const HEAT_LEVELS = [
|
|
|
{ min: 0, class: 'heat-empty' },
|
|
|
{ min: 1, class: 'heat-level-1' },
|
|
|
{ min: 2, class: 'heat-level-2' },
|
|
|
{ min: 3, class: 'heat-level-3' },
|
|
|
{ min: 4, class: 'heat-level-4' }
|
|
|
]
|
|
|
|
|
|
export default {
|
|
|
name: 'ScheduleOverview',
|
|
|
components: {
|
|
|
SummaryPanel,
|
|
|
MonthlyHeatmap,
|
|
|
MemberOverview,
|
|
|
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}月` })),
|
|
|
selectedSystemId: '',
|
|
|
selectedTreeNodeKey: '',
|
|
|
scheduleFormSystems: [],
|
|
|
scheduleFormCourses: [],
|
|
|
systemCourseDialogVisible: false,
|
|
|
scheduleManagerDialogVisible: false,
|
|
|
locationManagerDialogVisible: false,
|
|
|
ownerManagerDialogVisible: false,
|
|
|
systemFormVisible: false,
|
|
|
courseFormVisible: false,
|
|
|
scheduleFormVisible: false,
|
|
|
systemFormMode: 'add',
|
|
|
courseFormMode: 'add',
|
|
|
scheduleFormMode: 'add',
|
|
|
systemForm: createSystemForm(),
|
|
|
courseForm: createCourseForm(),
|
|
|
scheduleForm: createScheduleForm(),
|
|
|
systemRules: {
|
|
|
year: [{ required: true, message: '请选择年份', trigger: 'change' }],
|
|
|
name: [{ required: true, message: '请输入计划体系名称', trigger: 'blur' }],
|
|
|
sort: [{ required: true, message: '请输入排序', trigger: 'change' }]
|
|
|
},
|
|
|
courseRules: {
|
|
|
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' }],
|
|
|
modules: [{
|
|
|
trigger: 'change',
|
|
|
validator: (rule, value, callback) => {
|
|
|
if (!Array.isArray(value) || !value.length) {
|
|
|
callback(new Error('模块/期数至少需要一条数据'))
|
|
|
return
|
|
|
}
|
|
|
callback()
|
|
|
}
|
|
|
}],
|
|
|
title: [{ required: true, message: '请输入课程名称', trigger: 'blur' }],
|
|
|
// modules 内部必填项由子组件在提交前做校验提示
|
|
|
},
|
|
|
currentYear: getCurrentYear()
|
|
|
}
|
|
|
},
|
|
|
computed: {
|
|
|
selectedSystem() {
|
|
|
return this.systems.find((item) => String(item.id) === String(this.selectedSystemId)) || null
|
|
|
},
|
|
|
selectedTreeNode() {
|
|
|
const key = String(this.selectedTreeNodeKey || '')
|
|
|
if (!key) {
|
|
|
return null
|
|
|
}
|
|
|
if (key.indexOf('system-') === 0) {
|
|
|
const systemId = key.replace('system-', '')
|
|
|
const system = this.systems.find((item) => String(item.id) === String(systemId))
|
|
|
return system ? {
|
|
|
treeKey: key,
|
|
|
nodeType: 'system',
|
|
|
name: system.name,
|
|
|
year: system.year,
|
|
|
sort: system.sort,
|
|
|
raw: system
|
|
|
} : null
|
|
|
}
|
|
|
if (key.indexOf('course-') === 0) {
|
|
|
const courseId = key.replace('course-', '')
|
|
|
const course = this.courses.find((item) => String(item.id) === String(courseId))
|
|
|
const system = course ? this.systemMap[course.system_id] : null
|
|
|
return course ? {
|
|
|
treeKey: key,
|
|
|
nodeType: 'course',
|
|
|
name: course.name,
|
|
|
year: system ? system.year : '-',
|
|
|
sort: course.sort,
|
|
|
systemName: system ? system.name : '-',
|
|
|
raw: course
|
|
|
} : null
|
|
|
}
|
|
|
return null
|
|
|
},
|
|
|
selectedSystemCourses() {
|
|
|
return this.courses.filter((item) => String(item.system_id) === String(this.selectedSystemId))
|
|
|
},
|
|
|
systemCourseTreeData() {
|
|
|
return this.systems.map((system) => ({
|
|
|
treeKey: `system-${system.id}`,
|
|
|
id: system.id,
|
|
|
year: system.year,
|
|
|
name: system.name,
|
|
|
sort: system.sort,
|
|
|
nodeType: 'system',
|
|
|
raw: system,
|
|
|
children: this.courses
|
|
|
.filter((item) => String(item.system_id) === String(system.id))
|
|
|
.sort((a, b) => Number(a.sort || 0) - Number(b.sort || 0))
|
|
|
.map((course) => ({
|
|
|
treeKey: `course-${course.id}`,
|
|
|
id: course.id,
|
|
|
name: course.name,
|
|
|
sort: course.sort,
|
|
|
nodeType: 'course',
|
|
|
systemName: system.name,
|
|
|
raw: course
|
|
|
}))
|
|
|
}))
|
|
|
},
|
|
|
scheduleFormCourseOptions() {
|
|
|
return this.scheduleFormCourses.filter((item) => String(item.system_id) === String(this.scheduleForm.system_id))
|
|
|
},
|
|
|
summaryCards() {
|
|
|
const totalCount = this.schedules.length
|
|
|
const owners = [...new Set(this.schedules.map((item) => item.owner).filter(Boolean))]
|
|
|
const activeMonths = [...new Set(this.schedules.map((item) => Number(item.month)).filter(Boolean))]
|
|
|
const monthCounter = this.buildCounter(this.schedules, (item) => `${item.month}月`)
|
|
|
const ownerCounter = this.buildCounter(this.schedules, (item) => item.owner || '未分配')
|
|
|
const peakMonth = this.getTopCounterEntry(monthCounter)
|
|
|
const peakOwner = this.getTopCounterEntry(ownerCounter)
|
|
|
|
|
|
return [
|
|
|
{ label: '总开班次数(周期)', value: String(totalCount), unit: '次' },
|
|
|
{ label: '参与人员', value: String(owners.length), unit: '人' },
|
|
|
{ label: '活跃月度', value: String(activeMonths.length), unit: '个月' },
|
|
|
{ label: '峰值月度', value: peakMonth ? `${peakMonth.label}/${peakMonth.value}` : '-', unit: peakMonth ? '次' : '' },
|
|
|
{ label: '峰值人员', value: peakOwner ? `${peakOwner.label}/${peakOwner.value}` : '-', unit: peakOwner ? '次' : '' }
|
|
|
]
|
|
|
},
|
|
|
monthlyStats() {
|
|
|
const ownerStatsMap = {}
|
|
|
|
|
|
this.schedules.forEach((item) => {
|
|
|
const name = item.owner || '未分配'
|
|
|
if (!ownerStatsMap[name]) {
|
|
|
ownerStatsMap[name] = {
|
|
|
name,
|
|
|
months: Array(12).fill(0),
|
|
|
total: 0
|
|
|
}
|
|
|
}
|
|
|
const monthIndex = Number(item.month) - 1
|
|
|
if (monthIndex >= 0 && monthIndex < 12) {
|
|
|
ownerStatsMap[name].months[monthIndex] += 1
|
|
|
ownerStatsMap[name].total += 1
|
|
|
}
|
|
|
})
|
|
|
|
|
|
return Object.values(ownerStatsMap).sort((a, b) => {
|
|
|
if (b.total !== a.total) {
|
|
|
return b.total - a.total
|
|
|
}
|
|
|
return a.name.localeCompare(b.name, 'zh-CN')
|
|
|
})
|
|
|
},
|
|
|
memberAnalysis() {
|
|
|
const systemMap = this.systemMap
|
|
|
const statsMap = {}
|
|
|
|
|
|
this.schedules.forEach((item) => {
|
|
|
const name = item.owner || '未分配'
|
|
|
if (!statsMap[name]) {
|
|
|
statsMap[name] = {
|
|
|
total: 0,
|
|
|
monthCounter: {},
|
|
|
locationCounter: {},
|
|
|
systemCounter: {}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
statsMap[name].total += 1
|
|
|
this.incrementCounter(statsMap[name].monthCounter, `${item.month}月`)
|
|
|
this.incrementCounter(statsMap[name].locationCounter, item.location || '-')
|
|
|
this.incrementCounter(statsMap[name].systemCounter, (systemMap[item.system_id] || {}).name || '-')
|
|
|
})
|
|
|
|
|
|
return Object.keys(statsMap).map((name) => ({
|
|
|
name,
|
|
|
total: `${statsMap[name].total}次`,
|
|
|
month: this.getTopCounterEntry(statsMap[name].monthCounter)?.label || '-',
|
|
|
location: this.getTopCounterEntry(statsMap[name].locationCounter)?.label || '-',
|
|
|
tag: this.getTopCounterEntry(statsMap[name].systemCounter)?.label || '-'
|
|
|
})).sort((a, b) => Number(b.total.replace('次', '')) - Number(a.total.replace('次', '')))
|
|
|
},
|
|
|
systemMap() {
|
|
|
return this.systems.reduce((map, item) => {
|
|
|
map[item.id] = item
|
|
|
return map
|
|
|
}, {})
|
|
|
},
|
|
|
courseMap() {
|
|
|
return this.courses.reduce((map, item) => {
|
|
|
map[item.id] = item
|
|
|
return map
|
|
|
}, {})
|
|
|
},
|
|
|
scheduleTableRows() {
|
|
|
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
|
|
|
}
|
|
|
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 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 groupClass = TONE_CLASSES[systemIndex % TONE_CLASSES.length]
|
|
|
|
|
|
// 计划体系下所有课程体系(来自 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] = 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}-empty`,
|
|
|
systemId: system.id,
|
|
|
group: system.name,
|
|
|
groupClass,
|
|
|
courseId: '',
|
|
|
course: '-',
|
|
|
plan: emptyPlan
|
|
|
})
|
|
|
}
|
|
|
})
|
|
|
|
|
|
return rows
|
|
|
}
|
|
|
},
|
|
|
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 {
|
|
|
const data = await getOverview({ year: this.currentYear })
|
|
|
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 : ''
|
|
|
}
|
|
|
this.syncSelectedTreeNode()
|
|
|
} finally {
|
|
|
this.loading = false
|
|
|
}
|
|
|
},
|
|
|
async loadScheduleFormOptions(year) {
|
|
|
const data = await getOverview({ year: year || this.currentYear })
|
|
|
this.scheduleFormSystems = data.systems || []
|
|
|
this.scheduleFormCourses = data.courses || []
|
|
|
},
|
|
|
buildCounter(list, getter) {
|
|
|
return list.reduce((map, item) => {
|
|
|
const label = getter(item)
|
|
|
this.incrementCounter(map, label)
|
|
|
return map
|
|
|
}, {})
|
|
|
},
|
|
|
incrementCounter(counter, key) {
|
|
|
const safeKey = key || '-'
|
|
|
counter[safeKey] = (counter[safeKey] || 0) + 1
|
|
|
},
|
|
|
getTopCounterEntry(counter) {
|
|
|
return Object.keys(counter || {}).map((label) => ({
|
|
|
label,
|
|
|
value: counter[label]
|
|
|
})).sort((a, b) => {
|
|
|
if (b.value !== a.value) {
|
|
|
return b.value - a.value
|
|
|
}
|
|
|
return a.label.localeCompare(b.label, 'zh-CN')
|
|
|
})[0]
|
|
|
},
|
|
|
heatClass(value) {
|
|
|
const n = Number(value) || 0
|
|
|
for (let i = HEAT_LEVELS.length - 1; i >= 0; i--) {
|
|
|
if (n >= HEAT_LEVELS[i].min) return HEAT_LEVELS[i].class
|
|
|
}
|
|
|
return 'heat-empty'
|
|
|
},
|
|
|
openSystemCourseManager() {
|
|
|
this.systemCourseDialogVisible = true
|
|
|
if (!this.selectedSystemId && this.systems[0]) {
|
|
|
this.selectedSystemId = this.systems[0].id
|
|
|
}
|
|
|
if (!this.selectedTreeNodeKey && this.selectedSystemId) {
|
|
|
this.selectedTreeNodeKey = `system-${this.selectedSystemId}`
|
|
|
}
|
|
|
},
|
|
|
openScheduleManager() {
|
|
|
this.scheduleManagerDialogVisible = true
|
|
|
},
|
|
|
handleYearChange(value) {
|
|
|
this.currentYear = value || getCurrentYear()
|
|
|
this.loadData()
|
|
|
},
|
|
|
openSystemForm(mode, row) {
|
|
|
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
|
|
|
},
|
|
|
submitSystemForm() {
|
|
|
;(async() => {
|
|
|
await saveSystem(this.systemForm)
|
|
|
await this.loadData()
|
|
|
if (this.systemForm.id) {
|
|
|
this.selectedTreeNodeKey = `system-${this.systemForm.id}`
|
|
|
}
|
|
|
if (!this.selectedSystemId && this.systems[0]) {
|
|
|
this.selectedSystemId = this.systems[0].id
|
|
|
}
|
|
|
if (!this.systemForm.id && this.selectedSystemId) {
|
|
|
this.selectedTreeNodeKey = `system-${this.selectedSystemId}`
|
|
|
}
|
|
|
this.systemFormVisible = false
|
|
|
this.$message.success('计划体系保存成功')
|
|
|
})()
|
|
|
},
|
|
|
handleDeleteSystem(row) {
|
|
|
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('计划体系删除成功')
|
|
|
}).catch(() => {})
|
|
|
},
|
|
|
openCourseForm(mode, row, targetSystem) {
|
|
|
this.courseFormMode = mode
|
|
|
if (row) {
|
|
|
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
|
|
|
}
|
|
|
} else {
|
|
|
this.courseForm = createCourseForm(targetSystem ? targetSystem.id : this.selectedSystemId)
|
|
|
}
|
|
|
this.courseFormVisible = true
|
|
|
},
|
|
|
submitCourseForm() {
|
|
|
;(async() => {
|
|
|
const selectedName = this.getCourseTypeNameById(this.courseForm.course_type_id)
|
|
|
if (selectedName) {
|
|
|
this.courseForm.name = selectedName
|
|
|
}
|
|
|
await saveCourse(this.courseForm)
|
|
|
await this.loadData()
|
|
|
this.selectedSystemId = this.courseForm.system_id
|
|
|
if (this.courseForm.id) {
|
|
|
this.selectedTreeNodeKey = `course-${this.courseForm.id}`
|
|
|
}
|
|
|
this.courseFormVisible = false
|
|
|
this.$message.success('课程体系保存成功')
|
|
|
})()
|
|
|
},
|
|
|
handleDeleteCourse(row) {
|
|
|
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('课程体系删除成功')
|
|
|
}).catch(() => {})
|
|
|
},
|
|
|
handleTreeNodeSelect(data) {
|
|
|
this.selectedTreeNodeKey = data.treeKey
|
|
|
if (data.nodeType === 'system') {
|
|
|
this.selectedSystemId = data.id
|
|
|
} else if (data.raw && data.raw.system_id) {
|
|
|
this.selectedSystemId = data.raw.system_id
|
|
|
}
|
|
|
},
|
|
|
syncSelectedTreeNode() {
|
|
|
const exists = this.systemCourseTreeData.some((systemNode) => {
|
|
|
if (systemNode.treeKey === this.selectedTreeNodeKey) {
|
|
|
return true
|
|
|
}
|
|
|
return (systemNode.children || []).some((courseNode) => courseNode.treeKey === this.selectedTreeNodeKey)
|
|
|
})
|
|
|
|
|
|
if (!exists) {
|
|
|
this.selectedTreeNodeKey = this.selectedSystemId ? `system-${this.selectedSystemId}` : (this.systemCourseTreeData[0] ? this.systemCourseTreeData[0].treeKey : '')
|
|
|
}
|
|
|
},
|
|
|
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: 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 {
|
|
|
const defaultYear = preset.year || this.currentYear
|
|
|
await this.loadScheduleFormOptions(defaultYear)
|
|
|
const defaultSystemId = preset.system_id || this.selectedSystemId || (this.scheduleFormSystems[0] ? this.scheduleFormSystems[0].id : '')
|
|
|
const defaultCourse = this.scheduleFormCourses.find((item) => String(item.system_id) === String(defaultSystemId))
|
|
|
this.scheduleForm = {
|
|
|
...createScheduleForm(),
|
|
|
year: defaultYear,
|
|
|
system_id: defaultSystemId,
|
|
|
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
|
|
|
},
|
|
|
handlePlanCellClick(row, monthLabel, items) {
|
|
|
if (items && items.length) {
|
|
|
this.openScheduleForm('edit', items[0])
|
|
|
return
|
|
|
}
|
|
|
const month = Number(String(monthLabel).replace('月', ''))
|
|
|
this.openScheduleForm('add', null, {
|
|
|
year: this.currentYear,
|
|
|
system_id: row.systemId,
|
|
|
course_id: row.courseId,
|
|
|
month
|
|
|
})
|
|
|
},
|
|
|
async handleScheduleYearChange(value) {
|
|
|
const selectedYear = value || getCurrentYear()
|
|
|
this.scheduleForm.year = selectedYear
|
|
|
await this.loadScheduleFormOptions(selectedYear)
|
|
|
const firstSystem = this.scheduleFormSystems[0]
|
|
|
const firstCourse = this.scheduleFormCourses.find((item) => firstSystem && String(item.system_id) === String(firstSystem.id))
|
|
|
this.scheduleForm.system_id = firstSystem ? firstSystem.id : ''
|
|
|
this.scheduleForm.course_id = firstCourse ? firstCourse.id : ''
|
|
|
this.scheduleForm.month = []
|
|
|
},
|
|
|
handleScheduleSystemChange(value) {
|
|
|
const currentCourseExists = this.scheduleFormCourses.find((item) => String(item.id) === String(this.scheduleForm.course_id) && String(item.system_id) === String(value))
|
|
|
if (!currentCourseExists) {
|
|
|
const firstCourse = this.scheduleFormCourses.find((item) => String(item.system_id) === String(value))
|
|
|
this.scheduleForm.course_id = firstCourse ? firstCourse.id : ''
|
|
|
}
|
|
|
},
|
|
|
async refreshAfterScheduleAction(message) {
|
|
|
this.scheduleFormVisible = false
|
|
|
await this.loadData()
|
|
|
if (message) {
|
|
|
this.$message.success(message)
|
|
|
}
|
|
|
},
|
|
|
handleScheduleDialogCancel() {
|
|
|
this.scheduleFormVisible = false
|
|
|
this.loadData()
|
|
|
},
|
|
|
submitScheduleForm() {
|
|
|
;(async() => {
|
|
|
await saveSchedule(this.scheduleForm)
|
|
|
await this.refreshAfterScheduleAction('带班保存成功')
|
|
|
})()
|
|
|
},
|
|
|
handleDeleteSchedule(row) {
|
|
|
const groupId = row.group_id || row.id
|
|
|
this.$confirm(`确定删除带班“${row.title}”吗?`, '提示', {
|
|
|
type: 'warning'
|
|
|
}).then(async() => {
|
|
|
await destroySchedule({ id: groupId })
|
|
|
await this.refreshAfterScheduleAction('带班删除成功')
|
|
|
}).catch(() => {})
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
</script>
|
|
|
|
|
|
<style lang="scss">
|
|
|
.schedule-overview-page {
|
|
|
padding: 0 16px 24px;
|
|
|
background: #eff2f9;
|
|
|
min-height: calc(100vh - 84px);
|
|
|
}
|
|
|
|
|
|
.header-actions {
|
|
|
display: flex;
|
|
|
justify-content: flex-end;
|
|
|
gap: 12px;
|
|
|
}
|
|
|
|
|
|
.summary-panel,
|
|
|
.summary-panel {
|
|
|
padding: 18px;
|
|
|
margin-bottom: 14px;
|
|
|
background: #fff;
|
|
|
border: 1px solid #ebeef5;
|
|
|
border-radius: 10px;
|
|
|
box-shadow: 0 2px 8px rgba(31, 45, 61, 0.04);
|
|
|
}
|
|
|
|
|
|
.summary-grid {
|
|
|
display: grid;
|
|
|
grid-template-columns: repeat(5, minmax(0, 1fr));
|
|
|
gap: 12px;
|
|
|
}
|
|
|
|
|
|
.summary-card {
|
|
|
min-height: 72px;
|
|
|
padding: 14px 16px;
|
|
|
background: #fff;
|
|
|
border: 1px solid #ebeef5;
|
|
|
border-radius: 8px;
|
|
|
}
|
|
|
|
|
|
.summary-label {
|
|
|
font-size: 12px;
|
|
|
color: #96876d;
|
|
|
margin-bottom: 8px;
|
|
|
white-space: nowrap;
|
|
|
}
|
|
|
|
|
|
.summary-value {
|
|
|
display: flex;
|
|
|
align-items: baseline;
|
|
|
flex-wrap: nowrap;
|
|
|
color: #285b5c;
|
|
|
font-weight: 700;
|
|
|
white-space: nowrap;
|
|
|
}
|
|
|
|
|
|
.summary-number {
|
|
|
font-size: 28px;
|
|
|
line-height: 1;
|
|
|
}
|
|
|
|
|
|
.summary-unit {
|
|
|
font-size: 14px;
|
|
|
margin-left: 4px;
|
|
|
}
|
|
|
|
|
|
.panel {
|
|
|
padding: 14px;
|
|
|
margin-bottom: 14px;
|
|
|
background: #fff;
|
|
|
border: 1px solid #ebeef5;
|
|
|
border-radius: 10px;
|
|
|
box-shadow: 0 2px 8px rgba(31, 45, 61, 0.04);
|
|
|
}
|
|
|
|
|
|
.panel-title {
|
|
|
font-size: 16px;
|
|
|
font-weight: 600;
|
|
|
color: #303133;
|
|
|
margin-bottom: 10px;
|
|
|
text-align: center;
|
|
|
}
|
|
|
|
|
|
.table-scroll {
|
|
|
overflow-x: auto;
|
|
|
}
|
|
|
|
|
|
.table-scroll-y {
|
|
|
overflow-y: auto;
|
|
|
}
|
|
|
|
|
|
.limit-rows-8 {
|
|
|
max-height: 314px;
|
|
|
}
|
|
|
|
|
|
.data-table,
|
|
|
.plan-table {
|
|
|
width: 100%;
|
|
|
min-width: 920px;
|
|
|
border-collapse: collapse;
|
|
|
table-layout: fixed;
|
|
|
background: #fff;
|
|
|
}
|
|
|
|
|
|
.data-table th,
|
|
|
.data-table td,
|
|
|
.plan-table th,
|
|
|
.plan-table td {
|
|
|
border: 1px solid #ebeef5;
|
|
|
padding: 8px 10px;
|
|
|
font-size: 12px;
|
|
|
color: #606266;
|
|
|
text-align: center;
|
|
|
}
|
|
|
|
|
|
.data-table thead th,
|
|
|
.plan-table thead th {
|
|
|
background: #f8f8f9;
|
|
|
color: #606266;
|
|
|
font-weight: 600;
|
|
|
}
|
|
|
|
|
|
.data-table tbody tr:hover td,
|
|
|
.plan-table tbody tr:hover td {
|
|
|
background: #f5f7fa;
|
|
|
}
|
|
|
|
|
|
.name-cell,
|
|
|
.total-cell {
|
|
|
font-weight: 600;
|
|
|
}
|
|
|
|
|
|
.empty-cell {
|
|
|
color: #909399;
|
|
|
background: #fff;
|
|
|
}
|
|
|
|
|
|
.heat-cell {
|
|
|
transition: background-color 0.2s ease;
|
|
|
}
|
|
|
|
|
|
.heat-empty {
|
|
|
background: #fff;
|
|
|
color: #c0c4cc;
|
|
|
}
|
|
|
|
|
|
.heat-level-1 {
|
|
|
background: #edf7f5;
|
|
|
}
|
|
|
|
|
|
.heat-level-2 {
|
|
|
background: #d7eeea;
|
|
|
}
|
|
|
|
|
|
.heat-level-3 {
|
|
|
background: #b7ddd5;
|
|
|
}
|
|
|
|
|
|
.heat-level-4 {
|
|
|
background: #8bc3b8;
|
|
|
color: #fff;
|
|
|
}
|
|
|
|
|
|
.plan-table {
|
|
|
min-width: 1600px;
|
|
|
}
|
|
|
|
|
|
.group-cell,
|
|
|
.course-cell {
|
|
|
font-weight: 600;
|
|
|
}
|
|
|
|
|
|
.group-cell {
|
|
|
width: 140px;
|
|
|
}
|
|
|
|
|
|
.course-cell {
|
|
|
width: 140px;
|
|
|
}
|
|
|
|
|
|
.plan-month-cell {
|
|
|
width: 140px;
|
|
|
height: 74px;
|
|
|
vertical-align: top;
|
|
|
background: #fff;
|
|
|
cursor: pointer;
|
|
|
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;
|
|
|
}
|
|
|
|
|
|
.plan-chip-list {
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
gap: 6px;
|
|
|
}
|
|
|
|
|
|
.plan-chip {
|
|
|
background: #ffffff;
|
|
|
border: 1px solid #dcdfe6;
|
|
|
border-radius: 6px;
|
|
|
padding: 6px 4px;
|
|
|
line-height: 1.45;
|
|
|
box-shadow: 0 1px 2px rgba(31, 45, 61, 0.06);
|
|
|
}
|
|
|
|
|
|
.tone-green {
|
|
|
background: #dfead6;
|
|
|
}
|
|
|
|
|
|
.tone-blue {
|
|
|
background: #dbe6f7;
|
|
|
}
|
|
|
|
|
|
.tone-purple {
|
|
|
background: #eadff0;
|
|
|
}
|
|
|
|
|
|
.tone-sand {
|
|
|
background: #efe4d2;
|
|
|
}
|
|
|
|
|
|
.tone-cyan {
|
|
|
background: #d8edf0;
|
|
|
}
|
|
|
|
|
|
.tone-cream {
|
|
|
background: #f0e5d7;
|
|
|
}
|
|
|
|
|
|
.manager-toolbar {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: space-between;
|
|
|
gap: 12px;
|
|
|
margin-bottom: 12px;
|
|
|
}
|
|
|
|
|
|
.second-toolbar {
|
|
|
margin-top: 18px;
|
|
|
}
|
|
|
|
|
|
.manager-title {
|
|
|
font-size: 14px;
|
|
|
font-weight: 600;
|
|
|
color: #303133;
|
|
|
}
|
|
|
|
|
|
.manager-actions {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: 10px;
|
|
|
}
|
|
|
|
|
|
.selected-tip {
|
|
|
color: #909399;
|
|
|
font-size: 12px;
|
|
|
}
|
|
|
|
|
|
.danger-text {
|
|
|
color: #d94b4b;
|
|
|
}
|
|
|
|
|
|
.tree-manager {
|
|
|
display: grid;
|
|
|
grid-template-columns: minmax(360px, 1fr) minmax(280px, 360px);
|
|
|
gap: 16px;
|
|
|
}
|
|
|
|
|
|
.tree-panel,
|
|
|
.tree-detail-panel {
|
|
|
border: 1px solid #ebeef5;
|
|
|
border-radius: 10px;
|
|
|
background: #fff;
|
|
|
padding: 14px;
|
|
|
}
|
|
|
|
|
|
.tree-wrap {
|
|
|
max-height: 520px;
|
|
|
overflow: auto;
|
|
|
padding-right: 4px;
|
|
|
}
|
|
|
|
|
|
.tree-node {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: space-between;
|
|
|
gap: 12px;
|
|
|
width: 100%;
|
|
|
padding: 4px 0;
|
|
|
}
|
|
|
|
|
|
.tree-node-main {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: 8px;
|
|
|
min-width: 0;
|
|
|
}
|
|
|
|
|
|
.tree-node-tag {
|
|
|
display: inline-flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
min-width: 34px;
|
|
|
height: 20px;
|
|
|
padding: 0 6px;
|
|
|
border-radius: 10px;
|
|
|
font-size: 11px;
|
|
|
line-height: 20px;
|
|
|
}
|
|
|
|
|
|
.system-tag {
|
|
|
background: #ecf5ff;
|
|
|
color: #409eff;
|
|
|
}
|
|
|
|
|
|
.course-tag {
|
|
|
background: #f0f9eb;
|
|
|
color: #67c23a;
|
|
|
}
|
|
|
|
|
|
.tree-node-label {
|
|
|
overflow: hidden;
|
|
|
text-overflow: ellipsis;
|
|
|
white-space: nowrap;
|
|
|
color: #606266;
|
|
|
}
|
|
|
|
|
|
.tree-node-actions {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
flex-shrink: 0;
|
|
|
}
|
|
|
|
|
|
.node-detail-card {
|
|
|
min-height: 220px;
|
|
|
padding: 16px;
|
|
|
border-radius: 8px;
|
|
|
background: #fff;
|
|
|
border: 1px solid #ebeef5;
|
|
|
}
|
|
|
|
|
|
.node-detail-row {
|
|
|
display: flex;
|
|
|
align-items: flex-start;
|
|
|
gap: 12px;
|
|
|
padding: 10px 0;
|
|
|
border-bottom: 1px dashed #ebeef5;
|
|
|
}
|
|
|
|
|
|
.node-detail-row:last-of-type {
|
|
|
border-bottom: 0;
|
|
|
}
|
|
|
|
|
|
.node-detail-label {
|
|
|
width: 72px;
|
|
|
color: #909399;
|
|
|
}
|
|
|
|
|
|
.node-detail-value {
|
|
|
flex: 1;
|
|
|
color: #606266;
|
|
|
word-break: break-all;
|
|
|
}
|
|
|
|
|
|
.node-detail-actions {
|
|
|
display: flex;
|
|
|
gap: 10px;
|
|
|
margin-top: 16px;
|
|
|
}
|
|
|
|
|
|
.tree-empty-state {
|
|
|
min-height: 220px;
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
color: #909399;
|
|
|
background: #fff;
|
|
|
border: 1px dashed #dcdfe6;
|
|
|
border-radius: 8px;
|
|
|
}
|
|
|
|
|
|
::v-deep .el-tree {
|
|
|
background: transparent;
|
|
|
}
|
|
|
|
|
|
::v-deep .el-tree-node__content {
|
|
|
height: auto;
|
|
|
min-height: 36px;
|
|
|
border-radius: 8px;
|
|
|
margin-bottom: 4px;
|
|
|
padding-right: 8px;
|
|
|
}
|
|
|
|
|
|
::v-deep .el-tree--highlight-current .el-tree-node.is-current > .el-tree-node__content {
|
|
|
background: #f5f7fa;
|
|
|
}
|
|
|
|
|
|
::v-deep .overview-dialog .el-dialog {
|
|
|
background: #eff2f9;
|
|
|
border-radius: 12px;
|
|
|
margin: 10vh auto 0;
|
|
|
}
|
|
|
|
|
|
::v-deep .overview-dialog .el-dialog__header {
|
|
|
background: #eff2f9;
|
|
|
border-bottom: 1px solid #e7dccb;
|
|
|
border-radius: 12px 12px 0 0;
|
|
|
}
|
|
|
|
|
|
::v-deep .overview-dialog .el-dialog__body {
|
|
|
background: #eff2f9;
|
|
|
}
|
|
|
|
|
|
@media (max-width: 1200px) {
|
|
|
.summary-grid {
|
|
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
|
}
|
|
|
|
|
|
.tree-manager {
|
|
|
grid-template-columns: 1fr;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@media (max-width: 768px) {
|
|
|
.schedule-overview-page {
|
|
|
padding: 0 10px 18px;
|
|
|
}
|
|
|
|
|
|
.header-actions {
|
|
|
justify-content: flex-start;
|
|
|
flex-wrap: wrap;
|
|
|
}
|
|
|
|
|
|
.summary-grid {
|
|
|
grid-template-columns: 1fr;
|
|
|
}
|
|
|
|
|
|
.manager-toolbar,
|
|
|
.manager-actions {
|
|
|
flex-direction: column;
|
|
|
align-items: flex-start;
|
|
|
}
|
|
|
}
|
|
|
</style>
|