|
|
|
|
@ -3,19 +3,21 @@
|
|
|
|
|
<!-- 顶部操作区 -->
|
|
|
|
|
<div class="admin-header">
|
|
|
|
|
<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="calendar-panel">
|
|
|
|
|
<div class="calendar-wrapper">
|
|
|
|
|
<el-calendar v-model="calendarDate" :first-day-of-week="1">
|
|
|
|
|
<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>
|
|
|
|
|
<div class="event-list">
|
|
|
|
|
<div
|
|
|
|
|
v-for="ev in eventsForDate(date)"
|
|
|
|
|
:key="ev._id"
|
|
|
|
|
:key="ev.id"
|
|
|
|
|
:class="['event-item', getEventClass(ev, date)]"
|
|
|
|
|
:style="getEventItemStyle(ev)"
|
|
|
|
|
:title="getEventTooltip(ev, date)"
|
|
|
|
|
@ -27,19 +29,17 @@
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
</el-calendar>
|
|
|
|
|
<!-- 连续事件覆盖层 -->
|
|
|
|
|
<div class="continuous-events-overlay">
|
|
|
|
|
<div
|
|
|
|
|
v-for="event in getContinuousEvents()"
|
|
|
|
|
:key="`continuous-${event.id}-${event.segStartISO}`"
|
|
|
|
|
:class="['continuous-event', getContinuousEventClass(event)]"
|
|
|
|
|
:style="getContinuousEventStyle(event)"
|
|
|
|
|
:title="getEventTooltip(event, new Date(event.segStartISO))"
|
|
|
|
|
@click.stop="openCreateModal('editor', event.id)"
|
|
|
|
|
>
|
|
|
|
|
{{ event.title }}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<!-- 跨天事件条:直接作为 wrapper 的绝对定位子元素渲染,避免覆盖层拦截点击 -->
|
|
|
|
|
<div
|
|
|
|
|
v-for="event in getContinuousEvents()"
|
|
|
|
|
:key="`continuous-${event.id}-${event.segStartISO}`"
|
|
|
|
|
:class="['continuous-event', getContinuousEventClass(event)]"
|
|
|
|
|
:style="getContinuousEventStyle(event)"
|
|
|
|
|
:title="getEventTooltip(event, new Date(event.segStartISO))"
|
|
|
|
|
@click.stop="openCreateModal('editor', event.id)"
|
|
|
|
|
>
|
|
|
|
|
{{ event.title }}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
@ -53,6 +53,7 @@ import addCalendar from './components/addCalendar.vue'
|
|
|
|
|
import {
|
|
|
|
|
index
|
|
|
|
|
} from '@/api/calendars/index.js'
|
|
|
|
|
import { getToken } from '@/utils/auth';
|
|
|
|
|
export default {
|
|
|
|
|
components: {
|
|
|
|
|
addCalendar
|
|
|
|
|
@ -60,7 +61,9 @@ import addCalendar from './components/addCalendar.vue'
|
|
|
|
|
data() {
|
|
|
|
|
return {
|
|
|
|
|
list: [],
|
|
|
|
|
calendarDate: new Date()
|
|
|
|
|
calendarDate: new Date(),
|
|
|
|
|
// 记录每一周行在容器内的实际像素 top,解决不等高行导致定位偏差
|
|
|
|
|
weekRowTops: []
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
computed: {
|
|
|
|
|
@ -73,75 +76,200 @@ import addCalendar from './components/addCalendar.vue'
|
|
|
|
|
},
|
|
|
|
|
watch: {
|
|
|
|
|
calendarDate: {
|
|
|
|
|
handler() {
|
|
|
|
|
this.getList()
|
|
|
|
|
},
|
|
|
|
|
deep: true
|
|
|
|
|
handler(newVal, oldVal) {
|
|
|
|
|
if (!oldVal) return
|
|
|
|
|
const n = newVal instanceof Date ? newVal : new Date(newVal)
|
|
|
|
|
const o = oldVal instanceof Date ? oldVal : new Date(oldVal)
|
|
|
|
|
if (n.getFullYear() !== o.getFullYear() || n.getMonth() !== o.getMonth()) {
|
|
|
|
|
this.getList()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
created() {
|
|
|
|
|
this.getList()
|
|
|
|
|
this.generateDynamicStyles()
|
|
|
|
|
},
|
|
|
|
|
mounted() {
|
|
|
|
|
this.$nextTick(() => this.measureWeekRowTops())
|
|
|
|
|
window.addEventListener('resize', this.measureWeekRowTops)
|
|
|
|
|
},
|
|
|
|
|
beforeDestroy() {
|
|
|
|
|
window.removeEventListener('resize', this.measureWeekRowTops)
|
|
|
|
|
},
|
|
|
|
|
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() {
|
|
|
|
|
const res = await index({
|
|
|
|
|
month: this.selectMonth
|
|
|
|
|
})
|
|
|
|
|
this.list = res
|
|
|
|
|
// 统一规范化 id,避免 _id 与 id 不一致导致点击与定位异常
|
|
|
|
|
this.list = (res || []).map(e => ({ ...e, id: e.id || e._id }))
|
|
|
|
|
// 重新生成动态样式
|
|
|
|
|
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) {
|
|
|
|
|
// 调试日志:记录点击行为与入参
|
|
|
|
|
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') {
|
|
|
|
|
this.$refs.addCalendar.id = id
|
|
|
|
|
addRef.id = finalId
|
|
|
|
|
}
|
|
|
|
|
this.$refs.addCalendar.type = type
|
|
|
|
|
this.$refs.addCalendar.isShow = true
|
|
|
|
|
addRef.type = type
|
|
|
|
|
addRef.isShow = true
|
|
|
|
|
try { console.log('[calendar] modal state set:', { id: addRef.id, type: addRef.type, isShow: addRef.isShow }) } catch (e) {}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
eventsForDate(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)
|
|
|
|
|
|
|
|
|
|
// 如果没有end_time,只在start_time的日期显示
|
|
|
|
|
if (!ev.end_time) {
|
|
|
|
|
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()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const endDate = this.parseDateTime(ev.end_time)
|
|
|
|
|
|
|
|
|
|
// 判断是否是跨天事件
|
|
|
|
|
const isMultiDay = startDate.getDate() !== endDate.getDate() ||
|
|
|
|
|
startDate.getMonth() !== endDate.getMonth() ||
|
|
|
|
|
startDate.getFullYear() !== endDate.getFullYear()
|
|
|
|
|
|
|
|
|
|
// 跨天事件不在单元格中显示,只在覆盖层显示
|
|
|
|
|
if (isMultiDay) {
|
|
|
|
|
return false
|
|
|
|
|
const startOnly = new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate())
|
|
|
|
|
const hasEnd = !!ev.end_time
|
|
|
|
|
const endDate = hasEnd ? this.parseDateTime(ev.end_time) : startDate
|
|
|
|
|
const endOnly = new Date(endDate.getFullYear(), endDate.getMonth(), endDate.getDate())
|
|
|
|
|
|
|
|
|
|
// 仅单天事件在格子内渲染;跨天交给覆盖层
|
|
|
|
|
const isMultiDay = hasEnd && (startOnly.getTime() !== endOnly.getTime())
|
|
|
|
|
if (isMultiDay) return
|
|
|
|
|
|
|
|
|
|
if (currentDate.getTime() === startOnly.getTime()) {
|
|
|
|
|
oneDayEvents.push({
|
|
|
|
|
...ev,
|
|
|
|
|
id: ev.id || ev._id
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 单天事件正常显示
|
|
|
|
|
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) {
|
|
|
|
|
if (!events || events.length === 0) return events
|
|
|
|
|
|
|
|
|
|
// 按开始时间排序
|
|
|
|
|
// 按开始时间排序(片段无具体时间则按标题/ID兜底)
|
|
|
|
|
const sortedEvents = events.sort((a, b) => {
|
|
|
|
|
const timeA = this.parseDateTime(a.start_time)
|
|
|
|
|
const timeB = this.parseDateTime(b.start_time)
|
|
|
|
|
return timeA.getTime() - timeB.getTime()
|
|
|
|
|
const aStart = this.parseDateTime(a.start_time)
|
|
|
|
|
const bStart = this.parseDateTime(b.start_time)
|
|
|
|
|
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 endTime = event.end_time ? this.parseDateTime(event.end_time) : startTime
|
|
|
|
|
|
|
|
|
|
// 计算时间范围(以小时为单位)
|
|
|
|
|
const startHour = startTime.getHours() + startTime.getMinutes() / 60
|
|
|
|
|
const endHour = endTime.getHours() + endTime.getMinutes() / 60
|
|
|
|
|
// 片段(跨天按全天)采用 0..24,单天按具体时间
|
|
|
|
|
const isSegment = !!event.isSegment
|
|
|
|
|
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
|
|
|
|
|
@ -189,8 +318,8 @@ import addCalendar from './components/addCalendar.vue'
|
|
|
|
|
|
|
|
|
|
// 为事件添加垂直位置信息
|
|
|
|
|
event.verticalPosition = laneIndex
|
|
|
|
|
// 使用bottom定位时,需要从底部开始计算偏移
|
|
|
|
|
event.topOffset = (lanes.length - 1 - laneIndex) * (eventHeight + eventSpacing)
|
|
|
|
|
// 自上而下堆叠:从顶部开始计算偏移
|
|
|
|
|
event.topOffset = laneIndex * (eventHeight + eventSpacing)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return sortedEvents
|
|
|
|
|
@ -229,8 +358,8 @@ import addCalendar from './components/addCalendar.vue'
|
|
|
|
|
if (event.topOffset !== undefined) {
|
|
|
|
|
return {
|
|
|
|
|
position: 'relative',
|
|
|
|
|
bottom: `${event.topOffset}px`,
|
|
|
|
|
zIndex: 70 + (event.verticalPosition || 0) // 确保单天事件在跨天事件之上
|
|
|
|
|
top: `${Math.max(0, event.topOffset - 8)}px`,
|
|
|
|
|
zIndex: 70 + (event.verticalPosition || 0) // 单天事件保持在顶层
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return {}
|
|
|
|
|
@ -329,6 +458,8 @@ import addCalendar from './components/addCalendar.vue'
|
|
|
|
|
const jsDow = d.getDay() // 0..6 (Sun..Sat)
|
|
|
|
|
const offset = (jsDow - FIRST_DOW + 7) % 7
|
|
|
|
|
d.setDate(d.getDate() - offset)
|
|
|
|
|
// 规范化到 00:00:00,避免不同时间导致同一周被分到不同key
|
|
|
|
|
d.setHours(0, 0, 0, 0)
|
|
|
|
|
return d
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -413,37 +544,28 @@ import addCalendar from './components/addCalendar.vue'
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
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)))
|
|
|
|
|
const laneEndCols = [] // 每个轨道当前占据的最后列(显示列)
|
|
|
|
|
const placedSegs = []
|
|
|
|
|
// 使用严格区间冲突检测的多区间轨道
|
|
|
|
|
const lanes = [] // 每个元素是该轨道内的区间数组 [{start,end}]
|
|
|
|
|
segs.forEach(seg => {
|
|
|
|
|
let laneIndex = 0
|
|
|
|
|
let placed = false
|
|
|
|
|
for (let i = 0; i < laneEndCols.length; i += 1) {
|
|
|
|
|
// 只要前一个轨道的结束列 < 当前分段的开始列,就可复用该轨道
|
|
|
|
|
if (laneEndCols[i] < seg.displayStartCol) {
|
|
|
|
|
seg.laneIndex = i
|
|
|
|
|
laneEndCols[i] = seg.displayEndCol
|
|
|
|
|
for (let i = 0; i < lanes.length; i += 1) {
|
|
|
|
|
const intervals = lanes[i]
|
|
|
|
|
const overlaps = intervals.some(it => !(seg.displayEndCol < it.start || seg.displayStartCol > it.end))
|
|
|
|
|
if (!overlaps) {
|
|
|
|
|
intervals.push({ start: seg.displayStartCol, end: seg.displayEndCol })
|
|
|
|
|
laneIndex = i
|
|
|
|
|
placed = true
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (!placed) {
|
|
|
|
|
seg.laneIndex = laneEndCols.length
|
|
|
|
|
laneEndCols.push(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 }
|
|
|
|
|
laneIndex = lanes.length
|
|
|
|
|
lanes.push([{ start: seg.displayStartCol, end: seg.displayEndCol }])
|
|
|
|
|
}
|
|
|
|
|
seg.laneIndex = laneIndex
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
@ -480,27 +602,35 @@ import addCalendar from './components/addCalendar.vue'
|
|
|
|
|
: (adjStart.getDay() - FIRST_DOW + 7) % 7
|
|
|
|
|
|
|
|
|
|
const cellWidth = 100 / 7
|
|
|
|
|
const cellHeight = 120 // 调整格子高度,确保有足够空间
|
|
|
|
|
const headerHeight = 50
|
|
|
|
|
const dateNumberHeight = 40 // 调整日期数字高度,确保有足够空间
|
|
|
|
|
const eventHeight = 16
|
|
|
|
|
const eventSpacing = 2
|
|
|
|
|
const cellHeight = 120 // 兜底高度
|
|
|
|
|
const overlayBaseTop = 50 // 与 CSS .continuous-events-overlay 保持一致
|
|
|
|
|
const dateNumberHeight = 8 // 进一步压缩日期占位
|
|
|
|
|
const eventHeight = 20
|
|
|
|
|
const eventSpacing = 3
|
|
|
|
|
|
|
|
|
|
// 获取该日期所有事件的垂直位置分配
|
|
|
|
|
const dayEvents = this.getDayEventsWithPositions(adjStart)
|
|
|
|
|
const currentEvent = dayEvents.find(ev => ev.id === event.id)
|
|
|
|
|
const verticalOffset = currentEvent ? currentEvent.topOffset : 0
|
|
|
|
|
// 使用跨天分段在本周内的 laneIndex 做垂直堆叠,避免同一周同一行的跨天事件重叠
|
|
|
|
|
const verticalOffset = (event.laneIndex || 0) * (eventHeight + eventSpacing)
|
|
|
|
|
|
|
|
|
|
// 基于实际测量的行 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 {
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
left: `calc(${startColAdjusted * cellWidth}% + 2px)`,
|
|
|
|
|
top: `${headerHeight + weekRow * cellHeight + dateNumberHeight + 25 + verticalOffset}px`, // 增加偏移量到25px,确保课程标题完全在日期数字下方
|
|
|
|
|
width: `calc(${event.spanCols * cellWidth}% - 4px)`,
|
|
|
|
|
left: `calc(${startColAdjusted * cellWidth}% + 1px)`,
|
|
|
|
|
top: `${overlayBaseTop + measuredRowTop + dateNumberHeight + safeGap + singleDayStackPx + verticalOffset}px`, // 预留单天事件空间+安全间距
|
|
|
|
|
width: `calc(${event.spanCols * cellWidth}% - 2px)`,
|
|
|
|
|
height: `${eventHeight}px`,
|
|
|
|
|
zIndex: 50, // 降低跨天事件的层级
|
|
|
|
|
zIndex: 500, // 置于表格之上,结合预留空隙不遮挡单天,确保可点
|
|
|
|
|
background: `linear-gradient(90deg, ${this.getEventTypeColor(event.type)} 0%, ${this.darkenColor(this.getEventTypeColor(event.type))} 100%)`,
|
|
|
|
|
borderRadius: '3px',
|
|
|
|
|
fontSize: '11px',
|
|
|
|
|
fontSize: '12px',
|
|
|
|
|
lineHeight: `${eventHeight}px`,
|
|
|
|
|
color: 'white',
|
|
|
|
|
padding: '0 4px',
|
|
|
|
|
@ -761,26 +891,18 @@ import addCalendar from './components/addCalendar.vue'
|
|
|
|
|
position: relative;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.continuous-events-overlay {
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 0;
|
|
|
|
|
left: 0;
|
|
|
|
|
right: 0;
|
|
|
|
|
bottom: 0;
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
z-index: 50; /* 降低跨天事件的层级 */
|
|
|
|
|
}
|
|
|
|
|
/* 不再需要覆盖层,跨天条直接渲染为 wrapper 的绝对定位子元素 */
|
|
|
|
|
|
|
|
|
|
.continuous-event {
|
|
|
|
|
pointer-events: auto;
|
|
|
|
|
pointer-events: auto; /* 条目本身可点击(覆盖父层 none) */
|
|
|
|
|
transition: all 0.2s ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.continuous-event:hover {
|
|
|
|
|
transform: translateY(-1px);
|
|
|
|
|
filter: brightness(1.1);
|
|
|
|
|
z-index: 51; /* 调整悬停时的层级 */
|
|
|
|
|
}
|
|
|
|
|
.continuous-event:hover {
|
|
|
|
|
transform: translateY(-1px);
|
|
|
|
|
filter: brightness(1.1);
|
|
|
|
|
z-index: 501; /* 悬停时略高于默认 */
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Element UI 日历样式覆盖 */
|
|
|
|
|
.calendar-panel ::v-deep .el-calendar-table {
|
|
|
|
|
@ -795,6 +917,12 @@ import addCalendar from './components/addCalendar.vue'
|
|
|
|
|
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 {
|
|
|
|
|
position: relative;
|
|
|
|
|
overflow: visible;
|
|
|
|
|
@ -820,10 +948,17 @@ import addCalendar from './components/addCalendar.vue'
|
|
|
|
|
|
|
|
|
|
.date-number {
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
color: #333;
|
|
|
|
|
margin-bottom: 2px;
|
|
|
|
|
position: relative;
|
|
|
|
|
z-index: 1;
|
|
|
|
|
color: #ccc;
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 50%;
|
|
|
|
|
left: 50%;
|
|
|
|
|
transform: translate(-50%, -50%);
|
|
|
|
|
margin: 0;
|
|
|
|
|
width: 100%;
|
|
|
|
|
font-size: 24px;
|
|
|
|
|
text-align: center;
|
|
|
|
|
z-index: 1; /* 位于事件之下,不影响点击 */
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.event-list {
|
|
|
|
|
|