|
|
|
|
@ -13,29 +13,18 @@
|
|
|
|
|
<div class="admin-main">
|
|
|
|
|
<div class="calendar-panel">
|
|
|
|
|
<div class="calendar-wrapper">
|
|
|
|
|
<el-calendar v-model="calendarDate" :first-day-of-week="1">
|
|
|
|
|
<el-calendar ref="mainCalendar" v-model="calendarDate" :first-day-of-week="1">
|
|
|
|
|
<template slot="dateCell" slot-scope="{date}">
|
|
|
|
|
<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"
|
|
|
|
|
:class="['event-item', getEventClass(ev, date)]"
|
|
|
|
|
:style="getEventItemStyle(ev)"
|
|
|
|
|
:title="getEventTooltip(ev, date)"
|
|
|
|
|
@click.stop="openCreateModal('editor', ev.id)"
|
|
|
|
|
>
|
|
|
|
|
<span class="event-title">{{ ev.title }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="event-list"></div>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
</el-calendar>
|
|
|
|
|
<!-- 跨天事件条:直接作为 wrapper 的绝对定位子元素渲染,避免覆盖层拦截点击 -->
|
|
|
|
|
<!-- 统一事件条:单天/跨天都在同一层按轨道排布 -->
|
|
|
|
|
<div
|
|
|
|
|
v-for="event in getContinuousEvents()"
|
|
|
|
|
:key="`continuous-${event.id}-${event.segStartISO}`"
|
|
|
|
|
v-for="event in getCalendarSegments()"
|
|
|
|
|
:key="`segment-${event.id}-${event.segStartISO}`"
|
|
|
|
|
:class="['continuous-event', getContinuousEventClass(event)]"
|
|
|
|
|
:style="getContinuousEventStyle(event)"
|
|
|
|
|
:title="getEventTooltip(event, new Date(event.segStartISO))"
|
|
|
|
|
@ -47,6 +36,50 @@
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<addCalendar ref="addCalendar" @refresh="getList"></addCalendar>
|
|
|
|
|
<el-dialog
|
|
|
|
|
title="本月事件"
|
|
|
|
|
:visible.sync="monthEventsDialogVisible"
|
|
|
|
|
width="760px"
|
|
|
|
|
append-to-body
|
|
|
|
|
>
|
|
|
|
|
<div class="month-events-summary">
|
|
|
|
|
本月开课场次:{{ monthCourseCount }},
|
|
|
|
|
本月开课天数:{{ monthDayCalendarDisplay }}天
|
|
|
|
|
</div>
|
|
|
|
|
<el-table
|
|
|
|
|
:data="monthEvents"
|
|
|
|
|
stripe
|
|
|
|
|
border
|
|
|
|
|
max-height="460"
|
|
|
|
|
@row-click="handleMonthEventRowClick"
|
|
|
|
|
>
|
|
|
|
|
<el-table-column prop="title" label="标题" min-width="220" show-overflow-tooltip />
|
|
|
|
|
<el-table-column prop="start_time" label="开始时间" min-width="180" />
|
|
|
|
|
<el-table-column prop="end_time" label="截止时间" min-width="180">
|
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
{{ scope.row.end_time || '-' }}
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column prop="is_count_days_text" label="是否显示天数" min-width="120">
|
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
{{ scope.row.is_count_days_text || '-' }}
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column prop="is_count_people_text" label="是否统计人数" min-width="120">
|
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
{{ scope.row.is_count_people_text || '-' }}
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
<el-table-column prop="days" label="天数" min-width="80">
|
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
{{ scope.row.days || '-' }}
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
</el-table>
|
|
|
|
|
<span slot="footer" class="dialog-footer">
|
|
|
|
|
<el-button @click="monthEventsDialogVisible = false">关闭</el-button>
|
|
|
|
|
</span>
|
|
|
|
|
</el-dialog>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
@ -70,7 +103,8 @@ import { getToken } from '@/utils/auth';
|
|
|
|
|
monthDayCalendar: 0,
|
|
|
|
|
yearDayCalendar: 0,
|
|
|
|
|
monthCourseCount: 0,
|
|
|
|
|
yearCourseCount: 0
|
|
|
|
|
yearCourseCount: 0,
|
|
|
|
|
monthEventsDialogVisible: false
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
computed: {
|
|
|
|
|
@ -79,6 +113,21 @@ import { getToken } from '@/utils/auth';
|
|
|
|
|
const month = now.getMonth() + 1 < 10 ? '0' + (now.getMonth() + 1) : now.getMonth() + 1
|
|
|
|
|
const year = now.getFullYear()
|
|
|
|
|
return year + '-' + month
|
|
|
|
|
},
|
|
|
|
|
monthEvents() {
|
|
|
|
|
return (this.list || [])
|
|
|
|
|
.slice()
|
|
|
|
|
.sort((a, b) => {
|
|
|
|
|
const aStart = this.parseDateTime(a.start_time)
|
|
|
|
|
const bStart = this.parseDateTime(b.start_time)
|
|
|
|
|
const diff = (aStart ? aStart.getTime() : 0) - (bStart ? bStart.getTime() : 0)
|
|
|
|
|
if (diff !== 0) return diff
|
|
|
|
|
return String(a.id || '').localeCompare(String(b.id || ''))
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
monthDayCalendarDisplay() {
|
|
|
|
|
const val = Number(this.monthDayCalendar || 0)
|
|
|
|
|
return Number.isFinite(val) ? val.toFixed(1) : '0.0'
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
watch: {
|
|
|
|
|
@ -99,10 +148,12 @@ import { getToken } from '@/utils/auth';
|
|
|
|
|
},
|
|
|
|
|
mounted() {
|
|
|
|
|
this.$nextTick(() => this.measureWeekRowTops())
|
|
|
|
|
this.$nextTick(() => this.ensureMonthEventsButton())
|
|
|
|
|
window.addEventListener('resize', this.measureWeekRowTops)
|
|
|
|
|
},
|
|
|
|
|
beforeDestroy() {
|
|
|
|
|
window.removeEventListener('resize', this.measureWeekRowTops)
|
|
|
|
|
this.removeMonthEventsButton()
|
|
|
|
|
},
|
|
|
|
|
methods: {
|
|
|
|
|
async exportCalendar() {
|
|
|
|
|
@ -148,6 +199,44 @@ import { getToken } from '@/utils/auth';
|
|
|
|
|
// 阻止 el-calendar 默认点击日期触发的月份切换
|
|
|
|
|
return false
|
|
|
|
|
},
|
|
|
|
|
ensureMonthEventsButton() {
|
|
|
|
|
this.$nextTick(() => {
|
|
|
|
|
const root = this.$el
|
|
|
|
|
if (!root) return
|
|
|
|
|
const buttonGroup = root.querySelector('.el-calendar__button-group')
|
|
|
|
|
if (!buttonGroup) return
|
|
|
|
|
if (buttonGroup.querySelector('.month-events-btn')) return
|
|
|
|
|
|
|
|
|
|
const btn = document.createElement('button')
|
|
|
|
|
btn.type = 'button'
|
|
|
|
|
btn.className = 'el-button el-button--primary el-button--mini month-events-btn'
|
|
|
|
|
btn.innerHTML = '<span>本月事件</span>'
|
|
|
|
|
btn.addEventListener('click', this.openMonthEventsDialog)
|
|
|
|
|
buttonGroup.appendChild(btn)
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
removeMonthEventsButton() {
|
|
|
|
|
const root = this.$el
|
|
|
|
|
if (!root) return
|
|
|
|
|
const btn = root.querySelector('.month-events-btn')
|
|
|
|
|
if (!btn) return
|
|
|
|
|
btn.removeEventListener('click', this.openMonthEventsDialog)
|
|
|
|
|
btn.remove()
|
|
|
|
|
},
|
|
|
|
|
selectCalendarDate(type) {
|
|
|
|
|
const calendarRef = this.$refs && this.$refs.mainCalendar
|
|
|
|
|
if (!calendarRef || typeof calendarRef.selectDate !== 'function') return
|
|
|
|
|
calendarRef.selectDate(type)
|
|
|
|
|
this.$nextTick(() => this.measureWeekRowTops())
|
|
|
|
|
},
|
|
|
|
|
openMonthEventsDialog() {
|
|
|
|
|
this.monthEventsDialogVisible = true
|
|
|
|
|
},
|
|
|
|
|
handleMonthEventRowClick(row) {
|
|
|
|
|
if (!row) return
|
|
|
|
|
this.monthEventsDialogVisible = false
|
|
|
|
|
this.openCreateModal('editor', row.id)
|
|
|
|
|
},
|
|
|
|
|
async getList() {
|
|
|
|
|
const res = await index({
|
|
|
|
|
month: this.selectMonth
|
|
|
|
|
@ -161,7 +250,10 @@ import { getToken } from '@/utils/auth';
|
|
|
|
|
// 重新生成动态样式
|
|
|
|
|
this.generateDynamicStyles()
|
|
|
|
|
// 渲染后测量行位置信息
|
|
|
|
|
this.$nextTick(() => this.measureWeekRowTops())
|
|
|
|
|
this.$nextTick(() => {
|
|
|
|
|
this.measureWeekRowTops()
|
|
|
|
|
this.ensureMonthEventsButton()
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
// 计算某一天内单天事件的轨道数量,用于为跨天条预留垂直空间
|
|
|
|
|
getSingleDayLaneCount(date) {
|
|
|
|
|
@ -180,34 +272,8 @@ import { getToken } from '@/utils/auth';
|
|
|
|
|
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
|
|
|
|
|
// 当前需求:同一天事件无论是否交叉都逐行显示,因此预留高度直接按条数计算
|
|
|
|
|
return dayEvents.length
|
|
|
|
|
},
|
|
|
|
|
// 计算跨天切片跨度内最大单天轨道数
|
|
|
|
|
getMaxSingleDayLaneCountBetween(startISO, endISO) {
|
|
|
|
|
@ -259,140 +325,7 @@ import { getToken } from '@/utils/auth';
|
|
|
|
|
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 currentDate = new Date(d.getFullYear(), d.getMonth(), d.getDate())
|
|
|
|
|
const oneDayEvents = [];
|
|
|
|
|
|
|
|
|
|
(this.list || []).forEach(ev => {
|
|
|
|
|
const startDate = this.parseDateTime(ev.start_time)
|
|
|
|
|
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
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 为同一天所有事件分配垂直位置,避免重叠
|
|
|
|
|
return this.arrangeEventsVertically(oneDayEvents)
|
|
|
|
|
},
|
|
|
|
|
// 为同一天的事件分配垂直位置,避免重叠
|
|
|
|
|
arrangeEventsVertically(events) {
|
|
|
|
|
if (!events || events.length === 0) return events
|
|
|
|
|
|
|
|
|
|
// 按开始时间排序(片段无具体时间则按标题/ID兜底)
|
|
|
|
|
const sortedEvents = events.sort((a, b) => {
|
|
|
|
|
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))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 为每个事件分配垂直位置
|
|
|
|
|
const lanes = [] // 存储每个时间段的占用情况
|
|
|
|
|
const eventHeight = 16 // 事件高度
|
|
|
|
|
const eventSpacing = 2 // 事件间距
|
|
|
|
|
|
|
|
|
|
sortedEvents.forEach(event => {
|
|
|
|
|
const startTime = this.parseDateTime(event.start_time)
|
|
|
|
|
const endTime = event.end_time ? this.parseDateTime(event.end_time) : startTime
|
|
|
|
|
|
|
|
|
|
// 片段(跨天按全天)采用 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
|
|
|
|
|
let foundLane = false
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < lanes.length; i++) {
|
|
|
|
|
const lane = lanes[i]
|
|
|
|
|
// 检查当前时间段是否与已有事件冲突
|
|
|
|
|
const hasConflict = lane.some(occupied => {
|
|
|
|
|
return !(endHour <= occupied.start || startHour >= occupied.end)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (!hasConflict) {
|
|
|
|
|
laneIndex = i
|
|
|
|
|
foundLane = true
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 如果没有找到可用位置,创建新的位置
|
|
|
|
|
if (!foundLane) {
|
|
|
|
|
laneIndex = lanes.length
|
|
|
|
|
lanes.push([])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 记录当前事件占用的时间段
|
|
|
|
|
lanes[laneIndex].push({
|
|
|
|
|
start: startHour,
|
|
|
|
|
end: endHour
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 为事件添加垂直位置信息
|
|
|
|
|
event.verticalPosition = laneIndex
|
|
|
|
|
// 自上而下堆叠:从顶部开始计算偏移
|
|
|
|
|
event.topOffset = laneIndex * (eventHeight + eventSpacing)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return sortedEvents
|
|
|
|
|
},
|
|
|
|
|
// 获取同一天内所有事件(包括跨天事件)的垂直位置分配
|
|
|
|
|
getDayEventsWithPositions(date) {
|
|
|
|
|
const d = new Date(date)
|
|
|
|
|
const currentDateStr = d.toDateString()
|
|
|
|
|
|
|
|
|
|
// 获取单天事件
|
|
|
|
|
const singleDayEvents = this.list.filter(ev => {
|
|
|
|
|
const startDate = this.parseDateTime(ev.start_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 multiDayEvents = this.getContinuousEvents().filter(ev => {
|
|
|
|
|
const eventStart = new Date(ev.segStartISO)
|
|
|
|
|
const eventEnd = new Date(ev.segEndISO)
|
|
|
|
|
return eventStart <= d && d <= eventEnd
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 合并所有事件
|
|
|
|
|
const allEvents = [
|
|
|
|
|
...singleDayEvents.map(ev => ({ ...ev, isMultiDay: false })),
|
|
|
|
|
...multiDayEvents.map(ev => ({ ...ev, isMultiDay: true }))
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
// 为所有事件分配垂直位置
|
|
|
|
|
return this.arrangeEventsVertically(allEvents)
|
|
|
|
|
},
|
|
|
|
|
// 获取事件项的样式
|
|
|
|
|
getEventItemStyle(event) {
|
|
|
|
|
if (event.topOffset !== undefined) {
|
|
|
|
|
return {
|
|
|
|
|
position: 'relative',
|
|
|
|
|
top: `${Math.max(0, event.topOffset - 8)}px`,
|
|
|
|
|
zIndex: 70 + (event.verticalPosition || 0) // 单天事件保持在顶层
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return {}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
getEventClass(event, date) {
|
|
|
|
|
const startDate = new Date(event.start_time)
|
|
|
|
|
const currentDate = new Date(date)
|
|
|
|
|
@ -456,26 +389,10 @@ import { getToken } from '@/utils/auth';
|
|
|
|
|
return `${event.title}\n时间:${this.formatDateTime(event.start_time)} ~ ${this.formatDateTime(event.end_time)}`
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
getContinuousEvents() {
|
|
|
|
|
getCalendarSegments() {
|
|
|
|
|
const FIRST_DOW = 1 // 1=Monday to match :first-day-of-week="1"
|
|
|
|
|
const OFFSET_DAYS = 0 // 不做任何全局偏移
|
|
|
|
|
const continuousEvents = []
|
|
|
|
|
|
|
|
|
|
// 仅选择跨天事件
|
|
|
|
|
const multiDayEvents = this.list.filter(ev => {
|
|
|
|
|
if (!ev.end_time) return false
|
|
|
|
|
const s = this.parseDateTime(ev.start_time)
|
|
|
|
|
const e = this.parseDateTime(ev.end_time)
|
|
|
|
|
return s.toDateString() !== e.toDateString()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 获取所有单天事件,用于检查冲突
|
|
|
|
|
const singleDayEvents = this.list.filter(ev => {
|
|
|
|
|
if (!ev.end_time) return true
|
|
|
|
|
const s = this.parseDateTime(ev.start_time)
|
|
|
|
|
const e = this.parseDateTime(ev.end_time)
|
|
|
|
|
return s.toDateString() === e.toDateString()
|
|
|
|
|
})
|
|
|
|
|
const segments = []
|
|
|
|
|
|
|
|
|
|
const currentMonth = this.calendarDate.getMonth()
|
|
|
|
|
const currentYear = this.calendarDate.getFullYear()
|
|
|
|
|
@ -501,25 +418,19 @@ import { getToken } from '@/utils/auth';
|
|
|
|
|
return end
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
multiDayEvents.forEach(ev => {
|
|
|
|
|
// 统一处理所有事件:单天按 1 天跨度,跨天按区间跨度
|
|
|
|
|
;(this.list || []).forEach(ev => {
|
|
|
|
|
const eventStart = this.parseDateTime(ev.start_time)
|
|
|
|
|
const eventEndRaw = this.parseDateTime(ev.end_time)
|
|
|
|
|
const eventEndRaw = ev.end_time ? this.parseDateTime(ev.end_time) : eventStart
|
|
|
|
|
if (!eventStart || !eventEndRaw) return
|
|
|
|
|
const eventEnd = adjustEndForDisplay(eventEndRaw)
|
|
|
|
|
const normalizedEnd = eventEnd < eventStart ? eventStart : eventEnd
|
|
|
|
|
|
|
|
|
|
if (eventEnd < monthStart || eventStart > monthEnd) return
|
|
|
|
|
if (normalizedEnd < monthStart || eventStart > monthEnd) return
|
|
|
|
|
|
|
|
|
|
// Clamp to month range so we only render in current month viewport
|
|
|
|
|
const clampedStart = eventStart < monthStart ? monthStart : eventStart
|
|
|
|
|
const clampedEnd = eventEnd > monthEnd ? monthEnd : eventEnd
|
|
|
|
|
|
|
|
|
|
// 检查跨天事件是否与单天事件冲突
|
|
|
|
|
const hasSingleDayConflict = (date) => {
|
|
|
|
|
const dateStr = date.toDateString()
|
|
|
|
|
return singleDayEvents.some(singleEv => {
|
|
|
|
|
const singleStart = this.parseDateTime(singleEv.start_time)
|
|
|
|
|
return singleStart.toDateString() === dateStr
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
const clampedEnd = normalizedEnd > monthEnd ? monthEnd : normalizedEnd
|
|
|
|
|
|
|
|
|
|
let cursor = getWeekStart(clampedStart)
|
|
|
|
|
while (cursor <= clampedEnd) {
|
|
|
|
|
@ -542,7 +453,7 @@ import { getToken } from '@/utils/auth';
|
|
|
|
|
const displayStartCol = (adjStart.getDay() - FIRST_DOW + 7) % 7
|
|
|
|
|
const displayEndCol = displayStartCol + spanCols - 1
|
|
|
|
|
|
|
|
|
|
continuousEvents.push({
|
|
|
|
|
segments.push({
|
|
|
|
|
...ev,
|
|
|
|
|
weekStartISO: weekStart.toISOString(),
|
|
|
|
|
segStartISO: segStart.toISOString(),
|
|
|
|
|
@ -556,8 +467,7 @@ import { getToken } from '@/utils/auth';
|
|
|
|
|
displayStartCol,
|
|
|
|
|
displayEndCol,
|
|
|
|
|
laneIndex: 0,
|
|
|
|
|
// 添加冲突信息,用于后续处理
|
|
|
|
|
hasSingleDayConflict: hasSingleDayConflict
|
|
|
|
|
isSegment: true
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
cursor.setDate(cursor.getDate() + 7)
|
|
|
|
|
@ -566,7 +476,7 @@ import { getToken } from '@/utils/auth';
|
|
|
|
|
|
|
|
|
|
// 为同一周的分段分配“轨道”(lane),避免垂直重叠
|
|
|
|
|
const byWeek = {}
|
|
|
|
|
continuousEvents.forEach(seg => {
|
|
|
|
|
segments.forEach(seg => {
|
|
|
|
|
const key = seg.displayWeekStartISO || seg.weekStartISO
|
|
|
|
|
if (!byWeek[key]) byWeek[key] = []
|
|
|
|
|
byWeek[key].push(seg)
|
|
|
|
|
@ -598,7 +508,7 @@ import { getToken } from '@/utils/auth';
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return continuousEvents
|
|
|
|
|
return segments
|
|
|
|
|
},
|
|
|
|
|
getContinuousEventClass(event) {
|
|
|
|
|
const baseClass = 'continuous-event'
|
|
|
|
|
@ -632,8 +542,8 @@ import { getToken } from '@/utils/auth';
|
|
|
|
|
|
|
|
|
|
const cellWidth = 100 / 7
|
|
|
|
|
const cellHeight = 120 // 兜底高度
|
|
|
|
|
const overlayBaseTop = 50 // 与 CSS .continuous-events-overlay 保持一致
|
|
|
|
|
const dateNumberHeight = 8 // 进一步压缩日期占位
|
|
|
|
|
const overlayBaseTop = 0 // 事件条从日期格子顶部开始
|
|
|
|
|
const dateNumberHeight = 0
|
|
|
|
|
const eventHeight = 20
|
|
|
|
|
const eventSpacing = 3
|
|
|
|
|
|
|
|
|
|
@ -645,15 +555,12 @@ import { getToken } from '@/utils/auth';
|
|
|
|
|
? this.weekRowTops[weekRow]
|
|
|
|
|
: (weekRow * cellHeight)
|
|
|
|
|
|
|
|
|
|
// 为跨天条在该分段跨度内预留单天事件堆叠高度,避免遮挡
|
|
|
|
|
const singleDayLanes = this.getMaxSingleDayLaneCountBetween(event.segStartISO, event.segEndISO)
|
|
|
|
|
const singleDayStackPx = singleDayLanes * (16 + 2) // 与单天事件高度与间距保持一致
|
|
|
|
|
const safeGap = 0 // 去除额外间距,使横条进一步上移
|
|
|
|
|
const safeGap = 0
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
left: `calc(${startColAdjusted * cellWidth}% + 1px)`,
|
|
|
|
|
top: `${overlayBaseTop + measuredRowTop + dateNumberHeight + safeGap + singleDayStackPx + verticalOffset}px`, // 预留单天事件空间+安全间距
|
|
|
|
|
top: `${overlayBaseTop + measuredRowTop + dateNumberHeight + safeGap + verticalOffset}px`,
|
|
|
|
|
width: `calc(${event.spanCols * cellWidth}% - 2px)`,
|
|
|
|
|
height: `${eventHeight}px`,
|
|
|
|
|
zIndex: 500, // 置于表格之上,结合预留空隙不遮挡单天,确保可点
|
|
|
|
|
@ -920,6 +827,17 @@ import { getToken } from '@/utils/auth';
|
|
|
|
|
position: relative;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.calendar-panel ::v-deep .el-calendar__button-group .month-events-btn {
|
|
|
|
|
margin-left: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.month-events-summary {
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
color: #303133;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 不再需要覆盖层,跨天条直接渲染为 wrapper 的绝对定位子元素 */
|
|
|
|
|
|
|
|
|
|
.continuous-event {
|
|
|
|
|
@ -990,118 +908,13 @@ import { getToken } from '@/utils/auth';
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.event-list {
|
|
|
|
|
flex: 1;
|
|
|
|
|
overflow: visible;
|
|
|
|
|
position: relative;
|
|
|
|
|
z-index: 60; /* 提高单天事件的层级,确保在跨天事件之上 */
|
|
|
|
|
min-height: 60px; /* 确保有足够空间显示多个事件 */
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.event-item {
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
line-height: 14px;
|
|
|
|
|
padding: 1px 3px;
|
|
|
|
|
margin: 1px 0;
|
|
|
|
|
background: #409EFF;
|
|
|
|
|
color: white;
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
transition: background-color 0.2s;
|
|
|
|
|
position: relative;
|
|
|
|
|
height: 16px; /* 固定高度 */
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.event-title {
|
|
|
|
|
display: block;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.event-item:hover {
|
|
|
|
|
background: #337ecc;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 不同事件类型的颜色 */
|
|
|
|
|
.event-item.event-type-1 {
|
|
|
|
|
background: #67C23A; /* 课程 - 绿色 */
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.event-item.event-type-2 {
|
|
|
|
|
background: #409EFF; /* 会议 - 蓝色 */
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.event-item.event-type-3 {
|
|
|
|
|
background: #E6A23C; /* 自定义事件 - 橙色 */
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.event-item.event-type-4 {
|
|
|
|
|
background: #F56C6C; /* 资讯 - 红色 */
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.event-item.event-type-5 {
|
|
|
|
|
background: #909399; /* 其他 - 灰色 */
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.event-item.event-type-default {
|
|
|
|
|
background: #409EFF; /* 默认 - 蓝色 */
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 动态颜色支持 - 根据color字段设置背景色 */
|
|
|
|
|
.event-item[class*="event-color-"] {
|
|
|
|
|
/* 默认样式,会被具体的颜色类覆盖 */
|
|
|
|
|
}
|
|
|
|
|
.event-list { display: none; }
|
|
|
|
|
|
|
|
|
|
/* 连续事件的动态颜色支持 */
|
|
|
|
|
.continuous-event[class*="event-color-"] {
|
|
|
|
|
/* 默认样式,会被具体的颜色类覆盖 */
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 悬停效果 */
|
|
|
|
|
.event-item.event-type-1:hover {
|
|
|
|
|
background: #5CB85C;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.event-item.event-type-2:hover {
|
|
|
|
|
background: #337ecc;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.event-item.event-type-3:hover {
|
|
|
|
|
background: #D4952B;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.event-item.event-type-4:hover {
|
|
|
|
|
background: #E85555;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.event-item.event-type-5:hover {
|
|
|
|
|
background: #73767A;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.event-item.event-type-default:hover {
|
|
|
|
|
background: #337ecc;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 连续事件的特殊样式 */
|
|
|
|
|
.event-item[style*="position: absolute"] {
|
|
|
|
|
border-radius: 3px !important;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.event-item[style*="position: absolute"]:hover {
|
|
|
|
|
transform: translateY(-1px);
|
|
|
|
|
filter: brightness(1.1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.mt-4 {
|
|
|
|
|
margin-top: 24px;
|
|
|
|
|
}
|
|
|
|
|
|