|
|
<template>
|
|
|
<div class="admin-calendar">
|
|
|
<!-- 顶部操作区 -->
|
|
|
<div class="admin-header" style="display: flex; align-items: center; gap: 20px;">
|
|
|
<el-button type="success" icon="el-icon-plus" @click="openCreateModal('add')">新建日历事件</el-button>
|
|
|
<el-button type="primary" icon="el-icon-plus" @click="exportCalendar">导出日历事件</el-button>
|
|
|
<div style="color: #333; font-size: 16px; font-weight: 500;">
|
|
|
本月开课天数:{{ monthDayCalendar }}天 {{ calendarDate.getFullYear() }}年累计开课天数:{{ yearDayCalendar }}天
|
|
|
</div>
|
|
|
</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" @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>
|
|
|
</template>
|
|
|
</el-calendar>
|
|
|
<!-- 跨天事件条:直接作为 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>
|
|
|
<addCalendar ref="addCalendar" @refresh="getList"></addCalendar>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script>
|
|
|
|
|
|
import addCalendar from './components/addCalendar.vue'
|
|
|
import {
|
|
|
index
|
|
|
} from '@/api/calendars/index.js'
|
|
|
import { getToken } from '@/utils/auth';
|
|
|
export default {
|
|
|
components: {
|
|
|
addCalendar
|
|
|
},
|
|
|
data() {
|
|
|
return {
|
|
|
list: [],
|
|
|
calendarDate: new Date(),
|
|
|
// 记录每一周行在容器内的实际像素 top,解决不等高行导致定位偏差
|
|
|
weekRowTops: [],
|
|
|
monthDayCalendar: 0,
|
|
|
yearDayCalendar: 0
|
|
|
}
|
|
|
},
|
|
|
computed: {
|
|
|
selectMonth() {
|
|
|
const now = this.calendarDate instanceof Date ? this.calendarDate : new Date(this.calendarDate)
|
|
|
const month = now.getMonth() + 1 < 10 ? '0' + (now.getMonth() + 1) : now.getMonth() + 1
|
|
|
const year = now.getFullYear()
|
|
|
return year + '-' + month
|
|
|
}
|
|
|
},
|
|
|
watch: {
|
|
|
calendarDate: {
|
|
|
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() {
|
|
|
try {
|
|
|
await this.$confirm('请选择导出范围', '导出日历事件', {
|
|
|
confirmButtonText: '导出本月',
|
|
|
cancelButtonText: '导出全部',
|
|
|
distinguishCancelAndClose: true,
|
|
|
type: 'info'
|
|
|
})
|
|
|
this.triggerExport(this.selectMonth)
|
|
|
} catch (action) {
|
|
|
if (action === 'cancel') {
|
|
|
this.triggerExport('')
|
|
|
}
|
|
|
// 关闭弹框不做任何操作
|
|
|
}
|
|
|
},
|
|
|
triggerExport(month) {
|
|
|
const monthQuery = month ? `month=${month}` : 'month='
|
|
|
const url =
|
|
|
process.env.VUE_APP_BASE_API +
|
|
|
`/api/admin/calendars/index?${monthQuery}` +
|
|
|
'&is_export=1&export_fields[is_publish_text]=是否对外展示' +
|
|
|
'&export_fields[is_count_days_text]=是否统计天数' +
|
|
|
'&export_fields[is_count_people_text]=是否统计人数' +
|
|
|
'&export_fields[days]=天数' +
|
|
|
'&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[history_courses]=课程人数' +
|
|
|
'&export_fields[content]=内容' +
|
|
|
'&token=' + getToken()
|
|
|
window.open(url, '_blank')
|
|
|
},
|
|
|
onDateCellClick() {
|
|
|
// 阻止 el-calendar 默认点击日期触发的月份切换
|
|
|
return false
|
|
|
},
|
|
|
async getList() {
|
|
|
const res = await index({
|
|
|
month: this.selectMonth
|
|
|
})
|
|
|
// 统一规范化 id,避免 _id 与 id 不一致导致点击与定位异常
|
|
|
this.list = (res.list || res || []).map(e => ({ ...e, id: e.id || e._id }))
|
|
|
this.monthDayCalendar = res.monthDayCalendar || 0
|
|
|
this.yearDayCalendar = res.yearDayCalendar || 0
|
|
|
// 重新生成动态样式
|
|
|
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') {
|
|
|
addRef.id = finalId
|
|
|
}
|
|
|
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 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)
|
|
|
|
|
|
// 基础样式类
|
|
|
let baseClass = ''
|
|
|
|
|
|
// 如果没有end_time,直接返回单天事件样式
|
|
|
if (!event.end_time) {
|
|
|
baseClass = 'single-day'
|
|
|
} else {
|
|
|
const endDate = new Date(event.end_time)
|
|
|
|
|
|
// 判断是否是跨天事件
|
|
|
const isMultiDay = startDate.getDate() !== endDate.getDate() ||
|
|
|
startDate.getMonth() !== endDate.getMonth() ||
|
|
|
startDate.getFullYear() !== endDate.getFullYear()
|
|
|
|
|
|
if (!isMultiDay) {
|
|
|
baseClass = 'single-day'
|
|
|
} else {
|
|
|
// 判断当前日期在跨天事件中的位置
|
|
|
const eventStartDate = new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate())
|
|
|
const eventEndDate = new Date(endDate.getFullYear(), endDate.getMonth(), endDate.getDate())
|
|
|
const currentDateOnly = new Date(currentDate.getFullYear(), currentDate.getMonth(), currentDate.getDate())
|
|
|
|
|
|
if (currentDateOnly.getTime() === eventStartDate.getTime()) {
|
|
|
baseClass = 'multi-day-start'
|
|
|
} else if (currentDateOnly.getTime() === eventEndDate.getTime()) {
|
|
|
baseClass = 'multi-day-end'
|
|
|
} else {
|
|
|
baseClass = 'multi-day-middle'
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 如果有自定义颜色,添加color类;否则使用默认的type类
|
|
|
if (event.color) {
|
|
|
return `${baseClass} event-color-${event.color.replace('#', '')}`
|
|
|
} else {
|
|
|
return `${baseClass} event-type-${event.type || 'default'}`
|
|
|
}
|
|
|
},
|
|
|
getEventTooltip(event, date) {
|
|
|
const startDate = new Date(event.start_time)
|
|
|
|
|
|
// 如果没有end_time,只显示事件标题和开始时间
|
|
|
if (!event.end_time) {
|
|
|
return `${event.title}\n时间:${this.formatDateTime(event.start_time)}`
|
|
|
}
|
|
|
|
|
|
const endDate = new Date(event.end_time)
|
|
|
|
|
|
const isMultiDay = startDate.getDate() !== endDate.getDate() ||
|
|
|
startDate.getMonth() !== endDate.getMonth() ||
|
|
|
startDate.getFullYear() !== endDate.getFullYear()
|
|
|
|
|
|
if (isMultiDay) {
|
|
|
return `${event.title}\n时间:${this.formatDateTime(event.start_time)} ~ ${this.formatDateTime(event.end_time)}`
|
|
|
} else {
|
|
|
return `${event.title}\n时间:${this.formatDateTime(event.start_time)} ~ ${this.formatDateTime(event.end_time)}`
|
|
|
}
|
|
|
},
|
|
|
getContinuousEvents() {
|
|
|
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 currentMonth = this.calendarDate.getMonth()
|
|
|
const currentYear = this.calendarDate.getFullYear()
|
|
|
const monthStart = new Date(currentYear, currentMonth, 1)
|
|
|
const monthEnd = new Date(currentYear, currentMonth + 1, 0)
|
|
|
|
|
|
function getWeekStart(date) {
|
|
|
const d = new Date(date)
|
|
|
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
|
|
|
}
|
|
|
|
|
|
function adjustEndForDisplay(d) {
|
|
|
// 若结束时间恰好为 00:00:00,则视为不包含该天(显示到前一日),避免与下一事件在同日重叠
|
|
|
const end = new Date(d)
|
|
|
if (end.getHours() === 0 && end.getMinutes() === 0 && end.getSeconds() === 0) {
|
|
|
end.setSeconds(end.getSeconds() - 1)
|
|
|
}
|
|
|
return end
|
|
|
}
|
|
|
|
|
|
multiDayEvents.forEach(ev => {
|
|
|
const eventStart = this.parseDateTime(ev.start_time)
|
|
|
const eventEndRaw = this.parseDateTime(ev.end_time)
|
|
|
const eventEnd = adjustEndForDisplay(eventEndRaw)
|
|
|
|
|
|
if (eventEnd < 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
|
|
|
})
|
|
|
}
|
|
|
|
|
|
let cursor = getWeekStart(clampedStart)
|
|
|
while (cursor <= clampedEnd) {
|
|
|
const weekStart = new Date(cursor)
|
|
|
const weekEnd = new Date(cursor)
|
|
|
weekEnd.setDate(weekEnd.getDate() + 6)
|
|
|
|
|
|
// Segment inside this week
|
|
|
const segStart = clampedStart > weekStart ? clampedStart : weekStart
|
|
|
const segEnd = clampedEnd < weekEnd ? clampedEnd : weekEnd
|
|
|
if (segStart <= segEnd) {
|
|
|
const startCol = (segStart.getDay() - FIRST_DOW + 7) % 7
|
|
|
const endCol = (segEnd.getDay() - FIRST_DOW + 7) % 7
|
|
|
const spanCols = endCol - startCol + 1
|
|
|
|
|
|
// 计算用于显示的偏移后起点与周键
|
|
|
const adjStart = new Date(segStart)
|
|
|
adjStart.setDate(adjStart.getDate() + OFFSET_DAYS)
|
|
|
const displayWeekStart = getWeekStart(adjStart)
|
|
|
const displayStartCol = (adjStart.getDay() - FIRST_DOW + 7) % 7
|
|
|
const displayEndCol = displayStartCol + spanCols - 1
|
|
|
|
|
|
continuousEvents.push({
|
|
|
...ev,
|
|
|
weekStartISO: weekStart.toISOString(),
|
|
|
segStartISO: segStart.toISOString(),
|
|
|
segEndISO: segEnd.toISOString(),
|
|
|
startCol,
|
|
|
spanCols,
|
|
|
endCol,
|
|
|
// 显示用字段(含偏移)
|
|
|
adjSegStartISO: adjStart.toISOString(),
|
|
|
displayWeekStartISO: displayWeekStart.toISOString(),
|
|
|
displayStartCol,
|
|
|
displayEndCol,
|
|
|
laneIndex: 0,
|
|
|
// 添加冲突信息,用于后续处理
|
|
|
hasSingleDayConflict: hasSingleDayConflict
|
|
|
})
|
|
|
}
|
|
|
cursor.setDate(cursor.getDate() + 7)
|
|
|
}
|
|
|
})
|
|
|
|
|
|
// 为同一周的分段分配“轨道”(lane),避免垂直重叠
|
|
|
const byWeek = {}
|
|
|
continuousEvents.forEach(seg => {
|
|
|
const key = seg.displayWeekStartISO || seg.weekStartISO
|
|
|
if (!byWeek[key]) byWeek[key] = []
|
|
|
byWeek[key].push(seg)
|
|
|
})
|
|
|
|
|
|
Object.values(byWeek).forEach(segs => {
|
|
|
// 排序:开始列优先,其次跨度更长,最后按 id 稳定
|
|
|
segs.sort((a, b) => (a.displayStartCol - b.displayStartCol) || (b.spanCols - a.spanCols) || String(a.id).localeCompare(String(b.id)))
|
|
|
// 使用严格区间冲突检测的多区间轨道
|
|
|
const lanes = [] // 每个元素是该轨道内的区间数组 [{start,end}]
|
|
|
segs.forEach(seg => {
|
|
|
let laneIndex = 0
|
|
|
let placed = false
|
|
|
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) {
|
|
|
laneIndex = lanes.length
|
|
|
lanes.push([{ start: seg.displayStartCol, end: seg.displayEndCol }])
|
|
|
}
|
|
|
seg.laneIndex = laneIndex
|
|
|
})
|
|
|
})
|
|
|
|
|
|
return continuousEvents
|
|
|
},
|
|
|
getContinuousEventClass(event) {
|
|
|
const baseClass = 'continuous-event'
|
|
|
if (event.color) {
|
|
|
return `${baseClass} event-color-${event.color.replace('#', '')}`
|
|
|
} else {
|
|
|
return `${baseClass} event-type-${event.type || 'default'}`
|
|
|
}
|
|
|
},
|
|
|
getContinuousEventStyle(event) {
|
|
|
const FIRST_DOW = 1
|
|
|
const currentMonth = this.calendarDate.getMonth()
|
|
|
const currentYear = this.calendarDate.getFullYear()
|
|
|
const firstDay = new Date(currentYear, currentMonth, 1)
|
|
|
|
|
|
// 可控的视觉偏移:使用预计算的偏移起点
|
|
|
const adjStart = new Date(event.adjSegStartISO || event.segStartISO)
|
|
|
|
|
|
const msPerDay = 1000 * 60 * 60 * 24
|
|
|
const firstDayOffset = (firstDay.getDay() - FIRST_DOW + 7) % 7
|
|
|
// 使用显示用周起点来确定行,确保与分组一致
|
|
|
const displayWeekStart = new Date(event.displayWeekStartISO || adjStart)
|
|
|
const daysFromFirstOfMonth = Math.floor((displayWeekStart - firstDay) / msPerDay)
|
|
|
const totalDaysFromCalendarStart = daysFromFirstOfMonth + firstDayOffset
|
|
|
const weekRow = Math.floor(totalDaysFromCalendarStart / 7)
|
|
|
|
|
|
// 起始列使用偏移后的日期重新计算(若已携带显示列则直接用)
|
|
|
const startColAdjusted = (event.displayStartCol != null)
|
|
|
? event.displayStartCol
|
|
|
: (adjStart.getDay() - FIRST_DOW + 7) % 7
|
|
|
|
|
|
const cellWidth = 100 / 7
|
|
|
const cellHeight = 120 // 兜底高度
|
|
|
const overlayBaseTop = 50 // 与 CSS .continuous-events-overlay 保持一致
|
|
|
const dateNumberHeight = 8 // 进一步压缩日期占位
|
|
|
const eventHeight = 20
|
|
|
const eventSpacing = 3
|
|
|
|
|
|
// 使用跨天分段在本周内的 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}% + 1px)`,
|
|
|
top: `${overlayBaseTop + measuredRowTop + dateNumberHeight + safeGap + singleDayStackPx + verticalOffset}px`, // 预留单天事件空间+安全间距
|
|
|
width: `calc(${event.spanCols * cellWidth}% - 2px)`,
|
|
|
height: `${eventHeight}px`,
|
|
|
zIndex: 500, // 置于表格之上,结合预留空隙不遮挡单天,确保可点
|
|
|
background: `linear-gradient(90deg, ${this.getEventTypeColor(event.type)} 0%, ${this.darkenColor(this.getEventTypeColor(event.type))} 100%)`,
|
|
|
borderRadius: '3px',
|
|
|
fontSize: '12px',
|
|
|
lineHeight: `${eventHeight}px`,
|
|
|
color: 'white',
|
|
|
padding: '0 4px',
|
|
|
whiteSpace: 'nowrap',
|
|
|
overflow: 'hidden',
|
|
|
textOverflow: 'ellipsis',
|
|
|
cursor: 'pointer',
|
|
|
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.3)'
|
|
|
}
|
|
|
},
|
|
|
getEventStyle(event, date) {
|
|
|
const startDate = new Date(event.start_time)
|
|
|
const currentDate = new Date(date)
|
|
|
|
|
|
// 如果没有end_time,使用默认样式
|
|
|
if (!event.end_time) {
|
|
|
return {}
|
|
|
}
|
|
|
|
|
|
const endDate = new Date(event.end_time)
|
|
|
|
|
|
// 判断是否是跨天事件
|
|
|
const isMultiDay = startDate.getDate() !== endDate.getDate() ||
|
|
|
startDate.getMonth() !== endDate.getMonth() ||
|
|
|
startDate.getFullYear() !== endDate.getFullYear()
|
|
|
|
|
|
if (!isMultiDay) {
|
|
|
return {}
|
|
|
}
|
|
|
|
|
|
// 获取当前周的开始日期(周日)
|
|
|
const currentWeekStart = new Date(currentDate)
|
|
|
const dayOfWeek = currentDate.getDay()
|
|
|
currentWeekStart.setDate(currentDate.getDate() - dayOfWeek)
|
|
|
|
|
|
// 计算事件在当前周的开始和结束位置
|
|
|
const eventStartDate = new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate())
|
|
|
const eventEndDate = new Date(endDate.getFullYear(), endDate.getMonth(), endDate.getDate())
|
|
|
const currentDateOnly = new Date(currentDate.getFullYear(), currentDate.getMonth(), currentDate.getDate())
|
|
|
|
|
|
// 计算当前周内事件的开始和结束天数
|
|
|
const weekStart = new Date(currentWeekStart)
|
|
|
const weekEnd = new Date(currentWeekStart)
|
|
|
weekEnd.setDate(weekEnd.getDate() + 6)
|
|
|
|
|
|
// 事件在当前周的实际开始和结束日期
|
|
|
const eventWeekStart = eventStartDate < weekStart ? weekStart : eventStartDate
|
|
|
const eventWeekEnd = eventEndDate > weekEnd ? weekEnd : eventEndDate
|
|
|
|
|
|
// 如果当前日期不在事件范围内,不应用特殊样式
|
|
|
if (currentDateOnly < eventWeekStart || currentDateOnly > eventWeekEnd) {
|
|
|
return {}
|
|
|
}
|
|
|
|
|
|
// 如果是事件在当前周的第一天,显示标题并延伸到周末尾或事件结束
|
|
|
if (currentDateOnly.getTime() === eventWeekStart.getTime()) {
|
|
|
const startDayOfWeek = eventWeekStart.getDay()
|
|
|
const endDayOfWeek = eventWeekEnd.getDay()
|
|
|
const spanDays = endDayOfWeek - startDayOfWeek + 1
|
|
|
|
|
|
// 根据事件类型设置背景色
|
|
|
const bgColor = this.getEventTypeColor(event.type)
|
|
|
|
|
|
return {
|
|
|
position: 'absolute',
|
|
|
left: '0',
|
|
|
top: '105px', // 使用固定值:headerHeight(50) + dateNumberHeight(40) + 15px = 105px,确保完全在日期数字下方
|
|
|
width: `calc(${spanDays * 100}% - 2px)`,
|
|
|
zIndex: 10,
|
|
|
background: `linear-gradient(90deg, ${bgColor} 0%, ${this.darkenColor(bgColor)} 100%)`,
|
|
|
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.2)',
|
|
|
border: '1px solid rgba(255, 255, 255, 0.3)',
|
|
|
borderRadius: '3px'
|
|
|
}
|
|
|
} else if (currentDateOnly > eventWeekStart && currentDateOnly <= eventWeekEnd) {
|
|
|
// 其他天完全隐藏内容,保留占位
|
|
|
return {
|
|
|
opacity: '0',
|
|
|
pointerEvents: 'none'
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return {}
|
|
|
},
|
|
|
getEventDisplayTitle(event, date) {
|
|
|
const startDate = new Date(event.start_time)
|
|
|
const currentDate = new Date(date)
|
|
|
|
|
|
// 如果没有end_time,显示完整标题
|
|
|
if (!event.end_time) {
|
|
|
return event.title
|
|
|
}
|
|
|
|
|
|
const endDate = new Date(event.end_time)
|
|
|
|
|
|
// 判断是否是跨天事件
|
|
|
const isMultiDay = startDate.getDate() !== endDate.getDate() ||
|
|
|
startDate.getMonth() !== endDate.getMonth() ||
|
|
|
startDate.getFullYear() !== endDate.getFullYear()
|
|
|
|
|
|
if (!isMultiDay) {
|
|
|
return event.title
|
|
|
}
|
|
|
|
|
|
// 对于跨天事件,只在开始日期显示标题
|
|
|
const eventStartDate = new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate())
|
|
|
const currentDateOnly = new Date(currentDate.getFullYear(), currentDate.getMonth(), currentDate.getDate())
|
|
|
|
|
|
// 获取当前周的开始日期
|
|
|
const currentWeekStart = new Date(currentDate)
|
|
|
const dayOfWeek = currentDate.getDay()
|
|
|
currentWeekStart.setDate(currentDate.getDate() - dayOfWeek)
|
|
|
|
|
|
const weekStart = new Date(currentWeekStart)
|
|
|
const eventWeekStart = eventStartDate < weekStart ? weekStart : eventStartDate
|
|
|
|
|
|
if (currentDateOnly.getTime() === eventWeekStart.getTime()) {
|
|
|
return event.title
|
|
|
}
|
|
|
|
|
|
return '' // 其他日期不显示标题
|
|
|
},
|
|
|
getEventTypeColor(type) {
|
|
|
// 根据事件类型返回不同颜色
|
|
|
const colorMap = {
|
|
|
1: '#67C23A', // 课程 - 绿色
|
|
|
2: '#409EFF', // 会议 - 蓝色
|
|
|
3: '#E6A23C', // 自定义事件 - 橙色
|
|
|
4: '#F56C6C', // 资讯 - 红色
|
|
|
5: '#909399', // 其他 - 灰色
|
|
|
default: '#409EFF' // 默认蓝色
|
|
|
}
|
|
|
return colorMap[type] || colorMap.default
|
|
|
},
|
|
|
// 可靠的日期解析:避免 Safari/时区导致的偏移
|
|
|
parseDateTime(dateTimeStr) {
|
|
|
if (!dateTimeStr) return null
|
|
|
// 支持 "YYYY-MM-DD HH:mm:ss" 或 "YYYY-MM-DD" 形式
|
|
|
const [datePart, timePart = '00:00:00'] = dateTimeStr.trim().split(/\s+/)
|
|
|
const [y, m, d] = datePart.split('-').map(n => parseInt(n, 10))
|
|
|
const [hh = 0, mm = 0, ss = 0] = timePart.split(':').map(n => parseInt(n, 10))
|
|
|
return new Date(y, (m || 1) - 1, d || 1, hh || 0, mm || 0, ss || 0)
|
|
|
},
|
|
|
darkenColor(color) {
|
|
|
// 将颜色变暗,用于渐变效果
|
|
|
const colorMap = {
|
|
|
'#67C23A': '#5CB85C', // 绿色变暗
|
|
|
'#409EFF': '#337ecc', // 蓝色变暗
|
|
|
'#E6A23C': '#D4952B', // 橙色变暗
|
|
|
'#F56C6C': '#E85555', // 红色变暗
|
|
|
'#909399': '#73767A' // 灰色变暗
|
|
|
}
|
|
|
return colorMap[color] || '#337ecc'
|
|
|
},
|
|
|
formatDateTime(val) {
|
|
|
if (!val) return ''
|
|
|
return val.replace('T', ' ')
|
|
|
},
|
|
|
typeText(className) {
|
|
|
if (!className) return ''
|
|
|
if (className === 1) return '课程'
|
|
|
if (className === 3) return '自定义事件'
|
|
|
if (className === 4) return '资讯'
|
|
|
return ''
|
|
|
},
|
|
|
// 生成动态样式
|
|
|
generateDynamicStyles() {
|
|
|
// 移除已存在的动态样式
|
|
|
const existingStyle = document.getElementById('dynamic-calendar-styles')
|
|
|
if (existingStyle) {
|
|
|
existingStyle.remove()
|
|
|
}
|
|
|
|
|
|
// 创建新的样式元素
|
|
|
const style = document.createElement('style')
|
|
|
style.id = 'dynamic-calendar-styles'
|
|
|
|
|
|
// 生成CSS规则
|
|
|
let cssRules = ''
|
|
|
|
|
|
// 为每个事件生成颜色样式
|
|
|
this.list.forEach(event => {
|
|
|
if (event.color) {
|
|
|
const colorClass = event.color.replace('#', '')
|
|
|
cssRules += `
|
|
|
.admin-calendar .event-item.event-color-${colorClass},
|
|
|
.admin-calendar .continuous-event.event-color-${colorClass} {
|
|
|
background: ${event.color} !important;
|
|
|
}
|
|
|
.admin-calendar .event-item.event-color-${colorClass}:hover,
|
|
|
.admin-calendar .continuous-event.event-color-${colorClass}:hover {
|
|
|
background: ${this.adjustBrightness(event.color, -20)} !important;
|
|
|
}
|
|
|
`
|
|
|
}
|
|
|
})
|
|
|
|
|
|
style.textContent = cssRules
|
|
|
document.head.appendChild(style)
|
|
|
},
|
|
|
|
|
|
// 调整颜色亮度
|
|
|
adjustBrightness(hex, percent) {
|
|
|
const num = parseInt(hex.replace('#', ''), 16)
|
|
|
const amt = Math.round(2.55 * percent)
|
|
|
const R = (num >> 16) + amt
|
|
|
const G = (num >> 8 & 0x00FF) + amt
|
|
|
const B = (num & 0x0000FF) + amt
|
|
|
return '#' + (0x1000000 + (R < 255 ? R < 1 ? 0 : R : 255) * 0x10000 +
|
|
|
(G < 255 ? G < 1 ? 0 : G : 255) * 0x100 +
|
|
|
(B < 255 ? B < 1 ? 0 : B : 255)).toString(16).slice(1)
|
|
|
}
|
|
|
},
|
|
|
filters: {
|
|
|
formatDateTime(val) {
|
|
|
if (!val) return ''
|
|
|
return val.replace('T', ' ')
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
</script>
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
::v-deep .el-calendar__body{
|
|
|
padding-left:0;
|
|
|
padding-right:0;
|
|
|
}
|
|
|
.admin-calendar {
|
|
|
background: #f4f6fa;
|
|
|
min-height: 100vh;
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
}
|
|
|
|
|
|
.admin-header {
|
|
|
background: transparent;
|
|
|
padding: 20px 30px 10px 30px;
|
|
|
border-bottom: none;
|
|
|
box-shadow: none;
|
|
|
}
|
|
|
|
|
|
.admin-main {
|
|
|
flex: 1;
|
|
|
padding: 20px 30px;
|
|
|
overflow: hidden;
|
|
|
}
|
|
|
|
|
|
.calendar-panel {
|
|
|
background: #fff;
|
|
|
border-radius: 12px;
|
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
|
|
padding: 24px;
|
|
|
height: 100%;
|
|
|
overflow: auto;
|
|
|
}
|
|
|
|
|
|
.calendar-wrapper {
|
|
|
position: relative;
|
|
|
}
|
|
|
|
|
|
/* 不再需要覆盖层,跨天条直接渲染为 wrapper 的绝对定位子元素 */
|
|
|
|
|
|
.continuous-event {
|
|
|
pointer-events: auto; /* 条目本身可点击(覆盖父层 none) */
|
|
|
transition: all 0.2s ease;
|
|
|
}
|
|
|
|
|
|
.continuous-event:hover {
|
|
|
transform: translateY(-1px);
|
|
|
filter: brightness(1.1);
|
|
|
z-index: 501; /* 悬停时略高于默认 */
|
|
|
}
|
|
|
|
|
|
/* Element UI 日历样式覆盖 */
|
|
|
.calendar-panel ::v-deep .el-calendar-table {
|
|
|
overflow: visible;
|
|
|
}
|
|
|
|
|
|
.calendar-panel ::v-deep .el-calendar-table .el-calendar-day {
|
|
|
position: relative;
|
|
|
overflow: visible;
|
|
|
height: auto;
|
|
|
min-height: 100px;
|
|
|
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;
|
|
|
border: 1px solid #ebeef5;
|
|
|
}
|
|
|
|
|
|
.calendar-panel ::v-deep .el-calendar-table tbody tr {
|
|
|
overflow: visible;
|
|
|
}
|
|
|
|
|
|
.calendar-panel ::v-deep .el-calendar-table tbody {
|
|
|
overflow: visible;
|
|
|
}
|
|
|
|
|
|
.cell-content {
|
|
|
position: relative;
|
|
|
min-height: 100px;
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
padding: 2px;
|
|
|
overflow: visible;
|
|
|
}
|
|
|
|
|
|
.date-number {
|
|
|
font-weight: bold;
|
|
|
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 {
|
|
|
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-"] {
|
|
|
/* 默认样式,会被具体的颜色类覆盖 */
|
|
|
}
|
|
|
|
|
|
/* 连续事件的动态颜色支持 */
|
|
|
.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;
|
|
|
}
|
|
|
|
|
|
.mb-2 {
|
|
|
margin-bottom: 8px;
|
|
|
}
|
|
|
|
|
|
@media (max-width: 1200px) {
|
|
|
.admin-header {
|
|
|
padding: 15px 20px;
|
|
|
}
|
|
|
|
|
|
.admin-main {
|
|
|
padding: 15px 20px;
|
|
|
}
|
|
|
|
|
|
.calendar-panel {
|
|
|
padding: 16px;
|
|
|
}
|
|
|
}
|
|
|
</style>
|