master
lion 7 months ago
parent 2ff8b73777
commit 7a3f6a1d86

@ -73,6 +73,7 @@
:disabled="canSelect" :disabled="canSelect"
@change="changeCourse" @change="changeCourse"
placeholder="请选择课程" placeholder="请选择课程"
filterable
clearable clearable
style="width: 100%" style="width: 100%"
> >

@ -3,19 +3,21 @@
<!-- 顶部操作区 --> <!-- 顶部操作区 -->
<div class="admin-header"> <div class="admin-header">
<el-button type="success" icon="el-icon-plus" @click="openCreateModal('add')"></el-button> <el-button type="success" icon="el-icon-plus" @click="openCreateModal('add')"></el-button>
</div> <el-button type="primary" icon="el-icon-plus" @click="exportCalendar"></el-button>
</div>
<!-- 日历预览区 --> <!-- 日历预览区 -->
<div class="admin-main"> <div class="admin-main">
<div class="calendar-panel"> <div class="calendar-panel">
<div class="calendar-wrapper"> <div class="calendar-wrapper">
<el-calendar v-model="calendarDate" :first-day-of-week="1"> <el-calendar v-model="calendarDate" :first-day-of-week="1">
<template slot="dateCell" slot-scope="{date}"> <template slot="dateCell" slot-scope="{date}">
<div class="cell-content"> <div class="cell-content" @click.stop.prevent="onDateCellClick(date)">
<span class="date-number">{{ date.getDate() }}</span> <span class="date-number">{{ date.getDate() }}</span>
<div class="event-list"> <div class="event-list">
<div <div
v-for="ev in eventsForDate(date)" v-for="ev in eventsForDate(date)"
:key="ev._id" :key="ev.id"
:class="['event-item', getEventClass(ev, date)]" :class="['event-item', getEventClass(ev, date)]"
:style="getEventItemStyle(ev)" :style="getEventItemStyle(ev)"
:title="getEventTooltip(ev, date)" :title="getEventTooltip(ev, date)"
@ -27,19 +29,17 @@
</div> </div>
</template> </template>
</el-calendar> </el-calendar>
<!-- 连续事件覆盖层 --> <!-- 跨天事件条直接作为 wrapper 的绝对定位子元素渲染避免覆盖层拦截点击 -->
<div class="continuous-events-overlay"> <div
<div v-for="event in getContinuousEvents()"
v-for="event in getContinuousEvents()" :key="`continuous-${event.id}-${event.segStartISO}`"
:key="`continuous-${event.id}-${event.segStartISO}`" :class="['continuous-event', getContinuousEventClass(event)]"
:class="['continuous-event', getContinuousEventClass(event)]" :style="getContinuousEventStyle(event)"
:style="getContinuousEventStyle(event)" :title="getEventTooltip(event, new Date(event.segStartISO))"
:title="getEventTooltip(event, new Date(event.segStartISO))" @click.stop="openCreateModal('editor', event.id)"
@click.stop="openCreateModal('editor', event.id)" >
> {{ event.title }}
{{ event.title }} </div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -53,6 +53,7 @@ import addCalendar from './components/addCalendar.vue'
import { import {
index index
} from '@/api/calendars/index.js' } from '@/api/calendars/index.js'
import { getToken } from '@/utils/auth';
export default { export default {
components: { components: {
addCalendar addCalendar
@ -60,7 +61,9 @@ import addCalendar from './components/addCalendar.vue'
data() { data() {
return { return {
list: [], list: [],
calendarDate: new Date() calendarDate: new Date(),
// top
weekRowTops: []
} }
}, },
computed: { computed: {
@ -73,75 +76,200 @@ import addCalendar from './components/addCalendar.vue'
}, },
watch: { watch: {
calendarDate: { calendarDate: {
handler() { handler(newVal, oldVal) {
this.getList() if (!oldVal) return
}, const n = newVal instanceof Date ? newVal : new Date(newVal)
deep: true const o = oldVal instanceof Date ? oldVal : new Date(oldVal)
if (n.getFullYear() !== o.getFullYear() || n.getMonth() !== o.getMonth()) {
this.getList()
}
}
} }
}, },
created() { created() {
this.getList() this.getList()
this.generateDynamicStyles() this.generateDynamicStyles()
}, },
mounted() {
this.$nextTick(() => this.measureWeekRowTops())
window.addEventListener('resize', this.measureWeekRowTops)
},
beforeDestroy() {
window.removeEventListener('resize', this.measureWeekRowTops)
},
methods: { methods: {
async exportCalendar() {
console.log('导出日历事件')
const res = await index({
month: this.selectMonth,
is_export: 1,
'export_fields[is_publish_text]':'是否对外展示',
'export_fields[type_text]': '日程类型',
'export_fields[course.name]': '课程名称',
'export_fields[introduce]': '具体说明',
'export_fields[title]': '标题',
'export_fields[url]': '资讯链接',
'export_fields[start_time]': '开始时间',
'export_fields[end_time]': '截止时间',
'export_fields[address]': '地址',
'export_fields[color]': '主题颜色',
'export_fields[content]': '内容'
})
var url = process.env.VUE_APP_BASE_API + '/api/admin/calendars/index?month=' + this.selectMonth + '&is_export=1&export_fields[is_publish_text]=是否对外展示&export_fields[type_text]=日程类型&export_fields[course.name]=课程名称&export_fields[introduce]=具体说明&export_fields[title]=标题&export_fields[url]=资讯链接&export_fields[start_time]=开始时间&export_fields[end_time]=截止时间&export_fields[address]=地址&export_fields[color]=主题颜色&export_fields[content]=内容&token=' + getToken()
window.open(url, '_blank')
console.log(res)
},
onDateCellClick() {
// el-calendar
return false
},
async getList() { async getList() {
const res = await index({ const res = await index({
month: this.selectMonth month: this.selectMonth
}) })
this.list = res // id _id id
this.list = (res || []).map(e => ({ ...e, id: e.id || e._id }))
// //
this.generateDynamicStyles() this.generateDynamicStyles()
//
this.$nextTick(() => this.measureWeekRowTops())
}, },
//
getSingleDayLaneCount(date) {
const d = new Date(date)
const target = new Date(d.getFullYear(), d.getMonth(), d.getDate())
const dayEvents = (this.list || []).filter(ev => {
if (!ev) return false
const s = this.parseDateTime(ev.start_time)
if (!s) return false
const sOnly = new Date(s.getFullYear(), s.getMonth(), s.getDate())
const hasEnd = !!ev.end_time
const e = hasEnd ? this.parseDateTime(ev.end_time) : s
const eOnly = new Date(e.getFullYear(), e.getMonth(), e.getDate())
//
const isMulti = hasEnd && (sOnly.getTime() !== eOnly.getTime())
if (isMulti) return false
return sOnly.getTime() === target.getTime()
})
if (dayEvents.length === 0) return 0
// arrangeEventsVertically
const sorted = dayEvents.sort((a, b) => {
const as = this.parseDateTime(a.start_time)
const bs = this.parseDateTime(b.start_time)
return (as ? as.getTime() : 0) - (bs ? bs.getTime() : 0)
})
const lanes = []
sorted.forEach(ev => {
const s = this.parseDateTime(ev.start_time)
const e = ev.end_time ? this.parseDateTime(ev.end_time) : s
const sh = s.getHours() + s.getMinutes() / 60
const eh = e.getHours() + e.getMinutes() / 60
let placed = false
for (let i = 0; i < lanes.length; i += 1) {
const lane = lanes[i]
const conflict = lane.some(r => !(eh <= r.start || sh >= r.end))
if (!conflict) {
lane.push({ start: sh, end: eh })
placed = true
break
}
}
if (!placed) {
lanes.push([{ start: sh, end: eh }])
}
})
return lanes.length
},
//
getMaxSingleDayLaneCountBetween(startISO, endISO) {
if (!startISO || !endISO) return 0
const start = new Date(startISO)
const end = new Date(endISO)
const cursor = new Date(start)
let maxCount = 0
while (cursor <= end) {
maxCount = Math.max(maxCount, this.getSingleDayLaneCount(cursor))
cursor.setDate(cursor.getDate() + 1)
}
return maxCount
},
// () top
measureWeekRowTops() {
try {
const wrapper = this.$el.querySelector('.calendar-wrapper')
const wrapperTop = wrapper ? wrapper.getBoundingClientRect().top : 0
const tableRows = this.$el.querySelectorAll('.el-calendar-table tbody tr')
if (!tableRows || !tableRows.length) return
const tops = []
tableRows.forEach(row => {
const rect = row.getBoundingClientRect()
const relTop = rect.top - wrapperTop
tops.push(relTop)
})
this.weekRowTops = tops
} catch (e) {
//
}
},
openCreateModal(type, id) { openCreateModal(type, id) {
//
try { console.log('[calendar] openCreateModal called with:', { type, id }) } catch (e) {}
// id/_id
const finalId = (id && typeof id === 'object') ? (id.id || id._id) : (id || null)
try { console.log('[calendar] resolved finalId:', finalId) } catch (e) {}
const addRef = this.$refs && this.$refs.addCalendar
if (!addRef) {
try { console.warn('[calendar] addCalendar ref not found') } catch (e) {}
return
}
if (type === 'editor') { if (type === 'editor') {
this.$refs.addCalendar.id = id addRef.id = finalId
} }
this.$refs.addCalendar.type = type addRef.type = type
this.$refs.addCalendar.isShow = true addRef.isShow = true
try { console.log('[calendar] modal state set:', { id: addRef.id, type: addRef.type, isShow: addRef.isShow }) } catch (e) {}
}, },
eventsForDate(date) { eventsForDate(date) {
const d = new Date(date) const d = new Date(date)
const events = this.list.filter(ev => { const currentDate = new Date(d.getFullYear(), d.getMonth(), d.getDate())
const oneDayEvents = [];
(this.list || []).forEach(ev => {
const startDate = this.parseDateTime(ev.start_time) const startDate = this.parseDateTime(ev.start_time)
const startOnly = new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate())
// end_timestart_time const hasEnd = !!ev.end_time
if (!ev.end_time) { const endDate = hasEnd ? this.parseDateTime(ev.end_time) : startDate
const currentDate = new Date(d.getFullYear(), d.getMonth(), d.getDate()) const endOnly = new Date(endDate.getFullYear(), endDate.getMonth(), endDate.getDate())
const eventStartDate = new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate())
return currentDate.getTime() === eventStartDate.getTime() //
} const isMultiDay = hasEnd && (startOnly.getTime() !== endOnly.getTime())
if (isMultiDay) return
const endDate = this.parseDateTime(ev.end_time)
if (currentDate.getTime() === startOnly.getTime()) {
// oneDayEvents.push({
const isMultiDay = startDate.getDate() !== endDate.getDate() || ...ev,
startDate.getMonth() !== endDate.getMonth() || id: ev.id || ev._id
startDate.getFullYear() !== endDate.getFullYear() })
//
if (isMultiDay) {
return false
} }
//
const currentDate = new Date(d.getFullYear(), d.getMonth(), d.getDate())
const eventStartDate = new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate())
return currentDate.getTime() === eventStartDate.getTime()
}) })
// //
return this.arrangeEventsVertically(events) return this.arrangeEventsVertically(oneDayEvents)
}, },
// //
arrangeEventsVertically(events) { arrangeEventsVertically(events) {
if (!events || events.length === 0) return events if (!events || events.length === 0) return events
// // /ID
const sortedEvents = events.sort((a, b) => { const sortedEvents = events.sort((a, b) => {
const timeA = this.parseDateTime(a.start_time) const aStart = this.parseDateTime(a.start_time)
const timeB = this.parseDateTime(b.start_time) const bStart = this.parseDateTime(b.start_time)
return timeA.getTime() - timeB.getTime() const aTs = aStart ? aStart.getTime() : 0
const bTs = bStart ? bStart.getTime() : 0
const diff = aTs - bTs
if (diff !== 0) return diff
return String(a.id).localeCompare(String(b.id))
}) })
// //
@ -153,9 +281,10 @@ import addCalendar from './components/addCalendar.vue'
const startTime = this.parseDateTime(event.start_time) const startTime = this.parseDateTime(event.start_time)
const endTime = event.end_time ? this.parseDateTime(event.end_time) : startTime const endTime = event.end_time ? this.parseDateTime(event.end_time) : startTime
// // 0..24
const startHour = startTime.getHours() + startTime.getMinutes() / 60 const isSegment = !!event.isSegment
const endHour = endTime.getHours() + endTime.getMinutes() / 60 const startHour = isSegment && event.isMultiDay ? 0 : (startTime.getHours() + startTime.getMinutes() / 60)
const endHour = isSegment && event.isMultiDay ? 24 : (endTime.getHours() + endTime.getMinutes() / 60)
// //
let laneIndex = 0 let laneIndex = 0
@ -189,8 +318,8 @@ import addCalendar from './components/addCalendar.vue'
// //
event.verticalPosition = laneIndex event.verticalPosition = laneIndex
// 使bottom //
event.topOffset = (lanes.length - 1 - laneIndex) * (eventHeight + eventSpacing) event.topOffset = laneIndex * (eventHeight + eventSpacing)
}) })
return sortedEvents return sortedEvents
@ -229,8 +358,8 @@ import addCalendar from './components/addCalendar.vue'
if (event.topOffset !== undefined) { if (event.topOffset !== undefined) {
return { return {
position: 'relative', position: 'relative',
bottom: `${event.topOffset}px`, top: `${Math.max(0, event.topOffset - 8)}px`,
zIndex: 70 + (event.verticalPosition || 0) // zIndex: 70 + (event.verticalPosition || 0) //
} }
} }
return {} return {}
@ -329,6 +458,8 @@ import addCalendar from './components/addCalendar.vue'
const jsDow = d.getDay() // 0..6 (Sun..Sat) const jsDow = d.getDay() // 0..6 (Sun..Sat)
const offset = (jsDow - FIRST_DOW + 7) % 7 const offset = (jsDow - FIRST_DOW + 7) % 7
d.setDate(d.getDate() - offset) d.setDate(d.getDate() - offset)
// 00:00:00key
d.setHours(0, 0, 0, 0)
return d return d
} }
@ -413,37 +544,28 @@ import addCalendar from './components/addCalendar.vue'
}) })
Object.values(byWeek).forEach(segs => { Object.values(byWeek).forEach(segs => {
// id // id
segs.sort((a, b) => (a.displayStartCol - b.displayStartCol) || (b.spanCols - a.spanCols) || String(a.id).localeCompare(String(b.id))) segs.sort((a, b) => (a.displayStartCol - b.displayStartCol) || (b.spanCols - a.spanCols) || String(a.id).localeCompare(String(b.id)))
const laneEndCols = [] // // 使
const placedSegs = [] const lanes = [] // [{start,end}]
segs.forEach(seg => { segs.forEach(seg => {
let laneIndex = 0
let placed = false let placed = false
for (let i = 0; i < laneEndCols.length; i += 1) { for (let i = 0; i < lanes.length; i += 1) {
// < const intervals = lanes[i]
if (laneEndCols[i] < seg.displayStartCol) { const overlaps = intervals.some(it => !(seg.displayEndCol < it.start || seg.displayStartCol > it.end))
seg.laneIndex = i if (!overlaps) {
laneEndCols[i] = seg.displayEndCol intervals.push({ start: seg.displayStartCol, end: seg.displayEndCol })
laneIndex = i
placed = true placed = true
break break
} }
} }
if (!placed) { if (!placed) {
seg.laneIndex = laneEndCols.length laneIndex = lanes.length
laneEndCols.push(seg.displayEndCol) lanes.push([{ start: seg.displayStartCol, end: seg.displayEndCol }])
}
placedSegs.push(seg)
})
//
const seen = {}
placedSegs.forEach(seg => {
const k = `${seg.displayStartCol}-${seg.displayEndCol}-${seg.laneIndex}`
if (seen[k]) {
//
seg.laneIndex = ++seen[k].maxLane
} else {
seen[k] = { maxLane: seg.laneIndex }
} }
seg.laneIndex = laneIndex
}) })
}) })
@ -480,27 +602,35 @@ import addCalendar from './components/addCalendar.vue'
: (adjStart.getDay() - FIRST_DOW + 7) % 7 : (adjStart.getDay() - FIRST_DOW + 7) % 7
const cellWidth = 100 / 7 const cellWidth = 100 / 7
const cellHeight = 120 // const cellHeight = 120 //
const headerHeight = 50 const overlayBaseTop = 50 // CSS .continuous-events-overlay
const dateNumberHeight = 40 // const dateNumberHeight = 8 //
const eventHeight = 16 const eventHeight = 20
const eventSpacing = 2 const eventSpacing = 3
// // 使 laneIndex
const dayEvents = this.getDayEventsWithPositions(adjStart) const verticalOffset = (event.laneIndex || 0) * (eventHeight + eventSpacing)
const currentEvent = dayEvents.find(ev => ev.id === event.id)
const verticalOffset = currentEvent ? currentEvent.topOffset : 0 // top退
const measuredRowTop = (this.weekRowTops && this.weekRowTops[weekRow] != null)
? this.weekRowTops[weekRow]
: (weekRow * cellHeight)
//
const singleDayLanes = this.getMaxSingleDayLaneCountBetween(event.segStartISO, event.segEndISO)
const singleDayStackPx = singleDayLanes * (16 + 2) //
const safeGap = 0 // 使
return { return {
position: 'absolute', position: 'absolute',
left: `calc(${startColAdjusted * cellWidth}% + 2px)`, left: `calc(${startColAdjusted * cellWidth}% + 1px)`,
top: `${headerHeight + weekRow * cellHeight + dateNumberHeight + 25 + verticalOffset}px`, // 25px top: `${overlayBaseTop + measuredRowTop + dateNumberHeight + safeGap + singleDayStackPx + verticalOffset}px`, // +
width: `calc(${event.spanCols * cellWidth}% - 4px)`, width: `calc(${event.spanCols * cellWidth}% - 2px)`,
height: `${eventHeight}px`, height: `${eventHeight}px`,
zIndex: 50, // zIndex: 500, //
background: `linear-gradient(90deg, ${this.getEventTypeColor(event.type)} 0%, ${this.darkenColor(this.getEventTypeColor(event.type))} 100%)`, background: `linear-gradient(90deg, ${this.getEventTypeColor(event.type)} 0%, ${this.darkenColor(this.getEventTypeColor(event.type))} 100%)`,
borderRadius: '3px', borderRadius: '3px',
fontSize: '11px', fontSize: '12px',
lineHeight: `${eventHeight}px`, lineHeight: `${eventHeight}px`,
color: 'white', color: 'white',
padding: '0 4px', padding: '0 4px',
@ -761,26 +891,18 @@ import addCalendar from './components/addCalendar.vue'
position: relative; position: relative;
} }
.continuous-events-overlay { /* 不再需要覆盖层,跨天条直接渲染为 wrapper 的绝对定位子元素 */
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
z-index: 50; /* 降低跨天事件的层级 */
}
.continuous-event { .continuous-event {
pointer-events: auto; pointer-events: auto; /* 条目本身可点击(覆盖父层 none */
transition: all 0.2s ease; transition: all 0.2s ease;
} }
.continuous-event:hover { .continuous-event:hover {
transform: translateY(-1px); transform: translateY(-1px);
filter: brightness(1.1); filter: brightness(1.1);
z-index: 51; /* 调整悬停时的层级 */ z-index: 501; /* 悬停时略高于默认 */
} }
/* Element UI 日历样式覆盖 */ /* Element UI 日历样式覆盖 */
.calendar-panel ::v-deep .el-calendar-table { .calendar-panel ::v-deep .el-calendar-table {
@ -795,6 +917,12 @@ import addCalendar from './components/addCalendar.vue'
padding: 4px; padding: 4px;
} }
/* 禁止点击当月视图中“上月/下月”的日期格子,避免触发月份切换请求 */
.calendar-panel ::v-deep .el-calendar-table td.is-prev-month .el-calendar-day,
.calendar-panel ::v-deep .el-calendar-table td.is-next-month .el-calendar-day {
pointer-events: none;
}
.calendar-panel ::v-deep .el-calendar-table td { .calendar-panel ::v-deep .el-calendar-table td {
position: relative; position: relative;
overflow: visible; overflow: visible;
@ -820,10 +948,17 @@ import addCalendar from './components/addCalendar.vue'
.date-number { .date-number {
font-weight: bold; font-weight: bold;
color: #333; color: #ccc;
margin-bottom: 2px; position: absolute;
position: relative; top: 50%;
z-index: 1; left: 50%;
transform: translate(-50%, -50%);
margin: 0;
width: 100%;
font-size: 24px;
text-align: center;
z-index: 1; /* 位于事件之下,不影响点击 */
pointer-events: none;
} }
.event-list { .event-list {

@ -166,6 +166,7 @@
import { import {
save as saveCourseContent save as saveCourseContent
} from '@/api/course/courseContent.js' } from '@/api/course/courseContent.js'
import { getToken } from '@/utils/auth';
export default { export default {
components: { components: {
addTeacher, addTeacher,
@ -262,29 +263,32 @@
value: this.select.name value: this.select.name
}], }],
page: 1, page: 1,
page_size: 9999 page_size: 9999,
is_export: 1
}) })
if (res.data) { var url = process.env.VUE_APP_BASE_API + '/api/admin/teachers/index?is_export=1&page=1&page_size=9999&filter[0][key]=name&filter[0][op]=like&filter[0][value]=' + this.select.name + '&token=' + getToken()
let headers = this.table_item.map(i => { window.open(url, '_blank')
return { // if (res.data) {
key: i.prop, // let headers = this.table_item.map(i => {
title: i.label // return {
} // key: i.prop,
}) // title: i.label
const data = res.data.map(row => headers.map(header => row[header.key])); // }
data.unshift(headers.map(header => header.title)); // })
const wb = XLSX.utils.book_new(); // const data = res.data.map(row => headers.map(header => row[header.key]));
const ws = XLSX.utils.aoa_to_sheet(data); // data.unshift(headers.map(header => header.title));
XLSX.utils.book_append_sheet(wb, ws, sheetName); // const wb = XLSX.utils.book_new();
const wbout = XLSX.write(wb, { // const ws = XLSX.utils.aoa_to_sheet(data);
bookType: 'xlsx', // XLSX.utils.book_append_sheet(wb, ws, sheetName);
bookSST: true, // const wbout = XLSX.write(wb, {
type: 'array' // bookType: 'xlsx',
}); // bookSST: true,
saveAs(new Blob([wbout], { // type: 'array'
type: 'application/octet-stream' // });
}), `${sheetName}.xlsx`); // saveAs(new Blob([wbout], {
} // type: 'application/octet-stream'
// }), `${sheetName}.xlsx`);
// }
}, },
editTeacher(type, id) { editTeacher(type, id) {
if (id) { if (id) {

Loading…
Cancel
Save