forked from liweijie/education
前端代码
This commit is contained in:
@@ -0,0 +1,509 @@
|
||||
<template>
|
||||
<div class="teach-calendar app-container">
|
||||
<!-- 顶部标题:学期名称-校历 -->
|
||||
<div class="cal-titlebar">
|
||||
<span class="cal-title">{{ titleText }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 显示控制栏 -->
|
||||
<div class="cal-panel">
|
||||
<el-checkbox v-model="showDate" class="cal-cb">显示日期</el-checkbox>
|
||||
<div class="cal-panel-right">
|
||||
<el-button size="mini" type="primary" icon="el-icon-download" @click="exportExcel">另存为Excel</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 学期校历表格 -->
|
||||
<div class="cal-grid-wrap">
|
||||
<table class="cal-grid">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="cal-corner cal-th-weekno" :rowspan="2">周次</th>
|
||||
<th class="cal-corner cal-th-weekrange" :rowspan="2">日期段</th>
|
||||
<th v-for="day in weekDayHeaders" :key="'h1-' + day.dayIndex" :colspan="day.colCount" class="cal-th-day">
|
||||
{{ day.label }}
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th v-for="col in visibleColumns" :key="'h2-' + col.colIndex" class="cal-th-slot">
|
||||
{{ col.slotLabel }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(week, wIdx) in weeks" :key="'w-' + wIdx">
|
||||
<td class="cal-week-cell cal-weekno-cell">{{ wIdx + 1 }}</td>
|
||||
<td class="cal-week-cell cal-weekrange-cell">{{ week.rangeText }}</td>
|
||||
<td
|
||||
v-for="col in visibleColumns"
|
||||
:key="'c-' + wIdx + '-' + col.colIndex"
|
||||
class="cal-cell"
|
||||
:class="cellClass(wIdx, col)"
|
||||
>
|
||||
<template v-if="getEvent(wIdx, col.colIndex)">
|
||||
<span class="cal-event-name">{{ getEvent(wIdx, col.colIndex).name }}</span>
|
||||
<span
|
||||
v-if="!getEvent(wIdx, col.colIndex).schedulable"
|
||||
class="cal-flag cal-flag-noschedule"
|
||||
title="全校不允许排课"
|
||||
/>
|
||||
<span
|
||||
v-if="getEvent(wIdx, col.colIndex).mainCourse"
|
||||
class="cal-flag cal-flag-maincourse"
|
||||
title="正课"
|
||||
/>
|
||||
</template>
|
||||
<span v-else-if="showDate" class="cal-date-num">{{ dateNumberOf(wIdx, col.dayIndex) }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 底部状态栏 -->
|
||||
<div class="cal-status">
|
||||
<span v-if="nd">从 {{ dateRangeText }} ,共 {{ totalWeeks }} 周</span>
|
||||
<span v-else>请选择学期</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { saveAs } from 'file-saver'
|
||||
import { listSemester, getSemester } from '@/api/teachBusiness/semester'
|
||||
import { listXqxlb } from '@/api/teachBusiness/xqxlb'
|
||||
|
||||
// 节次定义:全部常显
|
||||
const SLOT_DEFS = [
|
||||
{ key: '12', label: '1-2' },
|
||||
{ key: '34', label: '3-4' },
|
||||
{ key: '56', label: '5-6' },
|
||||
{ key: '78', label: '7-8' },
|
||||
{ key: 'night', label: '晚上' },
|
||||
{ key: 'late', label: '夜间' }
|
||||
]
|
||||
|
||||
// 前端节次 key -> 后端 xqxlb.courseClass 节次范围
|
||||
const SLOT_COURSE_MAP = {
|
||||
'12': '1-2',
|
||||
'34': '3-4',
|
||||
'56': '5-6',
|
||||
'78': '7-8',
|
||||
night: '9-10',
|
||||
late: '11-12'
|
||||
}
|
||||
|
||||
const WEEK_DAYS = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
|
||||
|
||||
function pad2(n) {
|
||||
return n < 10 ? '0' + n : '' + n
|
||||
}
|
||||
|
||||
function fmtDate(d) {
|
||||
return d.getFullYear() + '-' + pad2(d.getMonth() + 1) + '-' + pad2(d.getDate())
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'SemesterCalendar',
|
||||
data() {
|
||||
return {
|
||||
// 当前学期代号(跟随顶栏切换学期,自动更新为当前学期)
|
||||
nd: '',
|
||||
// 学期日期范围
|
||||
startDate: null,
|
||||
endDate: null,
|
||||
totalWeeks: 0,
|
||||
dateRangeText: '',
|
||||
weeks: [],
|
||||
// 显示控制:默认勾选显示日期;节次全部展示
|
||||
showDate: true,
|
||||
// 事件存储:key = cellKey -> event
|
||||
events: {},
|
||||
loading: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 页面标题:学期名称-校历
|
||||
titleText() {
|
||||
if (!this.nd) return '学期校历'
|
||||
return this.getSemesterName(this.nd) + '-校历'
|
||||
},
|
||||
// 当前可见列(全部节次常显)
|
||||
visibleColumns() {
|
||||
const cols = []
|
||||
let colIndex = 0
|
||||
WEEK_DAYS.forEach((dayLabel, dayIndex) => {
|
||||
SLOT_DEFS.forEach(slot => {
|
||||
cols.push({ colIndex: colIndex, dayIndex: dayIndex, slotKey: slot.key, slotLabel: slot.label, dayLabel: dayLabel })
|
||||
colIndex++
|
||||
})
|
||||
})
|
||||
return cols
|
||||
},
|
||||
// 表头第一行星期:每个星期合并其下全部节次列
|
||||
weekDayHeaders() {
|
||||
return WEEK_DAYS.map((label, dayIndex) => ({
|
||||
dayIndex: dayIndex,
|
||||
label: label,
|
||||
colCount: SLOT_DEFS.length
|
||||
}))
|
||||
}
|
||||
},
|
||||
created() {
|
||||
// 初始加载当前学期
|
||||
this.loadCurrentSemester()
|
||||
// 顶栏切换学期后自动刷新为当前学期的校历
|
||||
this.$root.$on('semester-current-changed', this.loadCurrentSemester)
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.$root.$off('semester-current-changed', this.loadCurrentSemester)
|
||||
},
|
||||
methods: {
|
||||
/* ---------- 加载当前学期 ---------- */
|
||||
loadCurrentSemester() {
|
||||
this.loading = true
|
||||
listSemester({ pageNum: 1, pageSize: 100 }).then(response => {
|
||||
const data = response.data || {}
|
||||
const list = data.records || []
|
||||
// 跟随顶栏当前学期(dqxq=true),无则取第一条
|
||||
const current = list.find(i => i.dqxq) || list[0]
|
||||
this.loading = false
|
||||
if (current) {
|
||||
if (String(this.nd) !== String(current.nd)) {
|
||||
this.nd = current.nd
|
||||
this.loadSemester()
|
||||
}
|
||||
}
|
||||
}).catch(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
/* ---------- 数据加载 ---------- */
|
||||
loadSemester() {
|
||||
if (!this.nd) return
|
||||
this.startDate = null
|
||||
this.endDate = null
|
||||
getSemester(this.nd)
|
||||
.then(res => {
|
||||
const data = res.data || res || {}
|
||||
const kx = (data.kxrq || '').toString().slice(0, 10)
|
||||
const jx = (data.jsrq || '').toString().slice(0, 10)
|
||||
if (kx && jx) {
|
||||
this.startDate = new Date(kx.replace(/-/g, '/'))
|
||||
this.endDate = new Date(jx.replace(/-/g, '/'))
|
||||
this.buildWeeks()
|
||||
}
|
||||
this.loadXqxlbEvents()
|
||||
})
|
||||
.catch(() => {
|
||||
this.$message.warning('学期详情获取失败,请确认学期数据')
|
||||
})
|
||||
},
|
||||
// 从后端拉取本学期校历假期,渲染到对应时间格(nd 为 6 位学期代号,如 202601)
|
||||
loadXqxlbEvents() {
|
||||
listXqxlb({ nd: this.nd }).then(res => {
|
||||
const data = res.data
|
||||
const list = Array.isArray(data) ? data : (data && data.records) || []
|
||||
const loaded = {}
|
||||
list.forEach(item => {
|
||||
const pos = this.locateXqxlb(item)
|
||||
if (pos) {
|
||||
loaded[pos.key] = {
|
||||
bh: item.bh,
|
||||
name: item.jqmc || '',
|
||||
schedulable: !!item.kpk,
|
||||
mainCourse: !!item.zdpk,
|
||||
remarkShow: !!item.bzxs,
|
||||
remark: item.bz || ''
|
||||
}
|
||||
}
|
||||
})
|
||||
this.events = loaded
|
||||
}).catch(() => {
|
||||
this.$message.warning('校历事件加载失败')
|
||||
})
|
||||
},
|
||||
// 根据假期记录反推时间格 key(jqsj 日期 + courseClass 节次)
|
||||
locateXqxlb(item) {
|
||||
if (!this.startDate || !item.jqsj) return null
|
||||
const d = new Date(String(item.jqsj).slice(0, 10).replace(/-/g, '/'))
|
||||
if (isNaN(d.getTime())) return null
|
||||
const offset = Math.round((d - this.startDate) / (24 * 3600 * 1000))
|
||||
if (offset < 0) return null
|
||||
const wIdx = Math.floor(offset / 7)
|
||||
const dayIndex = offset % 7
|
||||
const slotKey = this.courseClassToSlotKey(item.courseClass)
|
||||
if (slotKey === null) return null
|
||||
const colIndex = this.colIndexOf(dayIndex, slotKey)
|
||||
if (colIndex < 0) return null
|
||||
return { key: this.cellKey(wIdx, colIndex) }
|
||||
},
|
||||
// 后端 courseClass 节次范围 -> 前端节次 key
|
||||
courseClassToSlotKey(courseClass) {
|
||||
if (!courseClass) return null
|
||||
const s = String(courseClass).trim()
|
||||
for (const key in SLOT_COURSE_MAP) {
|
||||
if (s === SLOT_COURSE_MAP[key]) return key
|
||||
}
|
||||
// 兼容 "910"/"1112" 等紧凑写法
|
||||
if (s === '910') return 'night'
|
||||
if (s === '1112') return 'late'
|
||||
return null
|
||||
},
|
||||
buildWeeks() {
|
||||
if (!this.startDate || !this.endDate) return
|
||||
const start = new Date(this.startDate)
|
||||
const end = new Date(this.endDate)
|
||||
const days = Math.round((end - start) / (24 * 3600 * 1000)) + 1
|
||||
this.totalWeeks = Math.ceil(days / 7)
|
||||
this.dateRangeText = fmtDate(start) + ' 到 ' + fmtDate(end)
|
||||
const weeks = []
|
||||
for (let w = 0; w < this.totalWeeks; w++) {
|
||||
const wkStart = new Date(start)
|
||||
wkStart.setDate(start.getDate() + w * 7)
|
||||
const wkEnd = new Date(wkStart)
|
||||
wkEnd.setDate(wkStart.getDate() + 6)
|
||||
weeks.push({
|
||||
rangeText: pad2(wkStart.getMonth() + 1) + '-' + pad2(wkStart.getDate()) +
|
||||
'至' + pad2(wkEnd.getMonth() + 1) + '-' + pad2(wkEnd.getDate())
|
||||
})
|
||||
}
|
||||
this.weeks = weeks
|
||||
},
|
||||
colIndexOf(dayIndex, slotKey) {
|
||||
const col = this.visibleColumns.find(c => c.dayIndex === dayIndex && c.slotKey === slotKey)
|
||||
return col ? col.colIndex : -1
|
||||
},
|
||||
/* ---------- 单元格工具 ---------- */
|
||||
cellKey(wIdx, colIndex) {
|
||||
return 'w' + wIdx + '-c' + colIndex
|
||||
},
|
||||
getEvent(wIdx, colIndex) {
|
||||
return this.events[this.cellKey(wIdx, colIndex)] || null
|
||||
},
|
||||
weekDayLabel(dayIndex) {
|
||||
return WEEK_DAYS[dayIndex]
|
||||
},
|
||||
// 单元格内显示的日期数字(如 0703)
|
||||
dateNumberOf(wIdx, dayIndex) {
|
||||
if (!this.startDate) return ''
|
||||
const d = new Date(this.startDate)
|
||||
d.setDate(this.startDate.getDate() + wIdx * 7 + dayIndex)
|
||||
return pad2(d.getMonth() + 1) + pad2(d.getDate())
|
||||
},
|
||||
cellClass(wIdx, col) {
|
||||
const classes = []
|
||||
// 星期交替背景(单双日不同底色)
|
||||
if (col.dayIndex % 2 === 0) classes.push('cal-cell-odd')
|
||||
else classes.push('cal-cell-even')
|
||||
const ev = this.getEvent(wIdx, col.colIndex)
|
||||
if (ev) {
|
||||
classes.push('has-event')
|
||||
if (!ev.schedulable) classes.push('no-schedule')
|
||||
}
|
||||
return classes
|
||||
},
|
||||
/** 学期代号 -> 学期名称(如 202601 -> 2026年春季学期) */
|
||||
getSemesterName(nd) {
|
||||
if (!nd) return ''
|
||||
const s = String(nd)
|
||||
const xn = s.slice(0, 4)
|
||||
const xq = s.slice(4)
|
||||
const map = { '01': '春季学期', '02': '夏季学期', '03': '秋季学期' }
|
||||
return xn + '年' + (map[xq] || '第' + xq + '学期')
|
||||
},
|
||||
/* ---------- 另存为Excel ---------- */
|
||||
exportExcel() {
|
||||
if (!this.weeks.length) {
|
||||
this.$message.warning('暂无校历数据可导出')
|
||||
return
|
||||
}
|
||||
// 按星期分组节次列,用于表头合并
|
||||
const dayCols = []
|
||||
this.visibleColumns.forEach(col => {
|
||||
if (!dayCols[col.dayIndex]) dayCols[col.dayIndex] = []
|
||||
dayCols[col.dayIndex].push(col)
|
||||
})
|
||||
let html = '<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel">'
|
||||
html += '<head><meta charset="utf-8"></head><body>'
|
||||
html += '<table border="1" cellspacing="0" cellpadding="4" style="border-collapse:collapse;">'
|
||||
// 标题行
|
||||
html += '<tr><td colspan="' + (this.visibleColumns.length + 2) + '" style="font-size:15px;font-weight:bold;text-align:center;">' + this.titleText + '</td></tr>'
|
||||
// 表头第一行:星期(合并同类天)
|
||||
html += '<tr><th rowspan="2">周次</th><th rowspan="2">日期段</th>'
|
||||
WEEK_DAYS.forEach((dayLabel, dayIndex) => {
|
||||
const cols = dayCols[dayIndex] || []
|
||||
if (cols.length) html += '<th colspan="' + cols.length + '">' + dayLabel + '</th>'
|
||||
})
|
||||
html += '</tr>'
|
||||
// 表头第二行:节次
|
||||
html += '<tr>'
|
||||
this.visibleColumns.forEach(col => {
|
||||
html += '<th>' + col.slotLabel + '</th>'
|
||||
})
|
||||
html += '</tr>'
|
||||
// 数据行
|
||||
this.weeks.forEach((week, wIdx) => {
|
||||
html += '<tr>'
|
||||
html += '<td>' + (wIdx + 1) + '</td>'
|
||||
html += '<td>' + week.rangeText + '</td>'
|
||||
this.visibleColumns.forEach(col => {
|
||||
const ev = this.getEvent(wIdx, col.colIndex)
|
||||
if (ev) {
|
||||
const remark = ev.remark && ev.remarkShow ? '(' + ev.remark + ')' : ''
|
||||
html += '<td>' + ev.name + remark + '</td>'
|
||||
} else if (this.showDate) {
|
||||
html += '<td>' + this.dateNumberOf(wIdx, col.dayIndex) + '</td>'
|
||||
} else {
|
||||
html += '<td></td>'
|
||||
}
|
||||
})
|
||||
html += '</tr>'
|
||||
})
|
||||
html += '</table></body></html>'
|
||||
const blob = new Blob(['\ufeff' + html], { type: 'application/vnd.ms-excel;charset=utf-8' })
|
||||
saveAs(blob, (this.titleText || '学期校历') + '.xls')
|
||||
this.$message.success('已导出Excel')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.teach-calendar {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 12px;
|
||||
box-sizing: border-box;
|
||||
background: #fff;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
|
||||
/* 顶部标题栏 */
|
||||
.cal-titlebar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 44px;
|
||||
background: var(--edu-green-primary, #00875a);
|
||||
color: #fff;
|
||||
border-radius: 4px 4px 0 0;
|
||||
|
||||
.cal-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
/* 显示控制栏 */
|
||||
.cal-panel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
padding: 8px 14px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
background: #fafafa;
|
||||
|
||||
.cal-cb { margin-right: 14px; }
|
||||
.cal-panel-right { margin-left: auto; }
|
||||
}
|
||||
|
||||
/* 表格区域 */
|
||||
.cal-grid-wrap {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 8px 10px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.cal-grid {
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
|
||||
th, td {
|
||||
border: 1px solid #e4e7ed;
|
||||
text-align: center;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.cal-corner {
|
||||
background: #f5f7fa;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.cal-th-weekno { width: 52px; }
|
||||
.cal-th-weekrange { width: 110px; }
|
||||
|
||||
.cal-th-day {
|
||||
background: var(--edu-green-primary, #00875a);
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
height: 26px;
|
||||
}
|
||||
|
||||
.cal-th-slot {
|
||||
background: #ecf5f0;
|
||||
color: #00875a;
|
||||
height: 22px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.cal-week-cell {
|
||||
background: #f5f7fa;
|
||||
vertical-align: middle;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.cal-weekno-cell {
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.cal-weekrange-cell {
|
||||
font-size: 11px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.cal-cell {
|
||||
height: 30px;
|
||||
position: relative;
|
||||
background: #fff;
|
||||
|
||||
.cal-date-num { color: #c0c4cc; }
|
||||
|
||||
// 星期交替底色:周一/三/五/日为浅绿,周二/四/六为白
|
||||
&.cal-cell-odd { background: #eef6f1; }
|
||||
&.cal-cell-even { background: #ffffff; }
|
||||
&.has-event { background: #e6f4ea; }
|
||||
&.has-event .cal-event-name { color: #00663e; font-weight: 500; }
|
||||
&.no-schedule { background: #fde2e2; }
|
||||
}
|
||||
|
||||
/* 状态标记小方块 */
|
||||
.cal-flag {
|
||||
position: absolute;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 1px;
|
||||
}
|
||||
.cal-flag-noschedule { left: 2px; top: 2px; background: #909399; }
|
||||
.cal-flag-maincourse { right: 2px; top: 2px; background: #409eff; }
|
||||
}
|
||||
|
||||
/* 底部状态栏 */
|
||||
.cal-status {
|
||||
padding: 6px 14px;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
border-top: 1px solid #ebeef5;
|
||||
background: #fafafa;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user