|
|
|
|
@ -1,67 +1,44 @@
|
|
|
|
|
<template>
|
|
|
|
|
<div class="admin-calendar">
|
|
|
|
|
<!-- 顶部操作区 -->
|
|
|
|
|
<div class="admin-header">
|
|
|
|
|
<i class="el-icon-date"></i> 课程日历管理
|
|
|
|
|
<el-button type="success" icon="el-icon-plus" @click="openCreateModal('add')">新建日历事件</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
<!-- 日历预览区 -->
|
|
|
|
|
<div class="admin-main">
|
|
|
|
|
<!-- 数据拉取区 -->
|
|
|
|
|
<div class="admin-panel" style="max-width: 350px;">
|
|
|
|
|
<!-- <div class="admin-panel-title"><i class="el-icon-download"></i> 数据拉取区</div>
|
|
|
|
|
<el-form label-width="90px" size="small">
|
|
|
|
|
<el-form-item label="数据类型">
|
|
|
|
|
<el-select v-model="dataType" @change="onTypeChange" placeholder="请选择类型">
|
|
|
|
|
<el-option label="课程" value="course" />
|
|
|
|
|
<el-option label="活动" value="activity" />
|
|
|
|
|
<el-option label="移动课堂" value="workshop" />
|
|
|
|
|
</el-select>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item label="已有数据">
|
|
|
|
|
<el-select v-model="dataId" @change="onDataChange" placeholder="请选择数据">
|
|
|
|
|
<el-option v-for="item in dataList" :key="item.id" :label="item.title" :value="item.id" />
|
|
|
|
|
</el-select>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<div class="text-secondary" style="font-size:13px;min-height:20px;">{{ dataDateInfo }}</div>
|
|
|
|
|
<el-button type="primary" icon="el-icon-link" style="width:100%;margin-top:10px;"
|
|
|
|
|
@click="fetchData">拉取并关联</el-button>
|
|
|
|
|
</el-form> -->
|
|
|
|
|
<el-button type="success" icon="el-icon-plus" style="width:100%;margin-top:10px;"
|
|
|
|
|
@click="openCreateModal('add')">新建日历事件</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
<!-- 日历预览区 -->
|
|
|
|
|
<div class="admin-panel" style="min-width:0;flex:2;">
|
|
|
|
|
<div class="admin-panel-title"><i class="el-icon-view"></i> 日历预览区</div>
|
|
|
|
|
<el-calendar v-model="calendarDate">
|
|
|
|
|
<div class="calendar-panel">
|
|
|
|
|
<div class="calendar-wrapper">
|
|
|
|
|
<el-calendar v-model="calendarDate" :first-day-of-week="7">
|
|
|
|
|
<template slot="dateCell" slot-scope="{date}">
|
|
|
|
|
<div class="cell-content">
|
|
|
|
|
<span>{{ date.getDate() }}</span>
|
|
|
|
|
<div v-for="ev in eventsForDate(date)" :key="ev._id" class="event-dot" :title="ev.title"
|
|
|
|
|
@click.stop="showEvent(ev)"></div>
|
|
|
|
|
<div v-for="ev in eventsForDate(date)" :key="ev._id" style="">
|
|
|
|
|
{{ev.title}}
|
|
|
|
|
<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)]"
|
|
|
|
|
:title="getEventTooltip(ev, date)"
|
|
|
|
|
@click.stop="openCreateModal('editor', ev.id)"
|
|
|
|
|
>
|
|
|
|
|
<span class="event-title">{{ ev.title }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
</el-calendar>
|
|
|
|
|
<!-- 连续事件覆盖层 -->
|
|
|
|
|
<div class="continuous-events-overlay">
|
|
|
|
|
<div
|
|
|
|
|
v-for="event in getContinuousEvents()"
|
|
|
|
|
:key="`continuous-${event.id}-${event.weekStart}`"
|
|
|
|
|
:class="['continuous-event', `event-type-${event.type || 'default'}`]"
|
|
|
|
|
:style="getContinuousEventStyle(event)"
|
|
|
|
|
:title="getEventTooltip(event, new Date(event.weekStart))"
|
|
|
|
|
@click.stop="openCreateModal('editor', event.id)"
|
|
|
|
|
>
|
|
|
|
|
{{ event.title }}
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
</el-calendar>
|
|
|
|
|
<div class="mt-4">
|
|
|
|
|
<h3 class="mb-2"><i class="el-icon-menu"></i> 当月事件列表</h3>
|
|
|
|
|
<el-empty v-if="list.length === 0" description="本月暂无事件" />
|
|
|
|
|
<el-timeline v-else>
|
|
|
|
|
<el-timeline-item v-for="(ev, idx) in list" :key="ev._id" :timestamp="ev.start | formatDateTime"
|
|
|
|
|
:color="isExpired(ev) ? '#F56C6C' : '#409EFF'">
|
|
|
|
|
<div>
|
|
|
|
|
<b>{{ ev.title }}</b>
|
|
|
|
|
<el-tag size="mini" style="margin-left:8px;">{{ typeText(ev.type) }}</el-tag>
|
|
|
|
|
<span style="font-size:13px;"> {{ ev.start_time | formatDateTime }} ~ {{ ev.end_time | formatDateTime }}</span>
|
|
|
|
|
<!-- <span v-if="ev.location" style="margin-left:8px;"><i class="el-icon-location-outline"></i>
|
|
|
|
|
{{ ev.location }}</span> -->
|
|
|
|
|
<!-- <el-tag v-if="ev.expire && ev.expire !== 'none'" size="mini" type="warning"
|
|
|
|
|
style="margin-left:8px;">到期后:{{ expireText(ev.expire) }}</el-tag> -->
|
|
|
|
|
</div>
|
|
|
|
|
<div style="margin-top:4px;">
|
|
|
|
|
<el-button size="mini" type="primary" icon="el-icon-edit" @click="openCreateModal('editor',ev.id)">编辑</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
</el-timeline-item>
|
|
|
|
|
</el-timeline>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
@ -70,293 +47,642 @@
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
let idSeed = 1000;
|
|
|
|
|
const mockData = {
|
|
|
|
|
course: [{
|
|
|
|
|
id: 101,
|
|
|
|
|
title: '2025产业加速营',
|
|
|
|
|
start: '2025-06-04T09:00',
|
|
|
|
|
end: '2025-06-04T17:00',
|
|
|
|
|
location: 'A101',
|
|
|
|
|
teacher: '王教授',
|
|
|
|
|
description: '智能制造专题'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 102,
|
|
|
|
|
title: '2025人工智能专题课程',
|
|
|
|
|
start: '2025-06-17T09:00',
|
|
|
|
|
end: '2025-06-19T17:00',
|
|
|
|
|
location: 'A201',
|
|
|
|
|
teacher: 'AI专家组',
|
|
|
|
|
description: 'AI理论与实操'
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
activity: [{
|
|
|
|
|
id: 201,
|
|
|
|
|
title: '校友企业参访',
|
|
|
|
|
start: '2025-06-10T13:30',
|
|
|
|
|
end: '2025-06-10T17:00',
|
|
|
|
|
location: '高新区',
|
|
|
|
|
teacher: '李总监',
|
|
|
|
|
description: '新能源企业参访'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 202,
|
|
|
|
|
title: '校友论坛',
|
|
|
|
|
start: '2025-06-20T14:00',
|
|
|
|
|
end: '2025-06-20T17:30',
|
|
|
|
|
location: '报告厅',
|
|
|
|
|
teacher: '特邀嘉宾',
|
|
|
|
|
description: '数字经济新机遇'
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
workshop: [{
|
|
|
|
|
id: 301,
|
|
|
|
|
title: '创新创业移动课堂',
|
|
|
|
|
start: '2025-06-08T09:00',
|
|
|
|
|
end: '2025-06-10T17:00',
|
|
|
|
|
location: 'C301',
|
|
|
|
|
teacher: '创业导师团',
|
|
|
|
|
description: '创新创业实战训练'
|
|
|
|
|
}]
|
|
|
|
|
};
|
|
|
|
|
import addCalendar from "./components/addCalendar.vue"
|
|
|
|
|
|
|
|
|
|
import addCalendar from './components/addCalendar.vue'
|
|
|
|
|
import {
|
|
|
|
|
index
|
|
|
|
|
} from "@/api/calendars/index.js"
|
|
|
|
|
} from '@/api/calendars/index.js'
|
|
|
|
|
export default {
|
|
|
|
|
components:{
|
|
|
|
|
components: {
|
|
|
|
|
addCalendar
|
|
|
|
|
},
|
|
|
|
|
data() {
|
|
|
|
|
return {
|
|
|
|
|
list:[],
|
|
|
|
|
dataType: 'course',
|
|
|
|
|
dataId: '',
|
|
|
|
|
dataList: [],
|
|
|
|
|
dataDateInfo: '',
|
|
|
|
|
calendarDate: new Date(),
|
|
|
|
|
events: [],
|
|
|
|
|
modalVisible: false,
|
|
|
|
|
modalTitle: '新建事件',
|
|
|
|
|
modalForm: {
|
|
|
|
|
type: 'course',
|
|
|
|
|
title: '',
|
|
|
|
|
start: '',
|
|
|
|
|
end: '',
|
|
|
|
|
location: '',
|
|
|
|
|
teacher: '',
|
|
|
|
|
description: '',
|
|
|
|
|
expire: 'none'
|
|
|
|
|
},
|
|
|
|
|
editingIndex: null,
|
|
|
|
|
eventDetail: null,
|
|
|
|
|
eventDetailVisible: false
|
|
|
|
|
list: [],
|
|
|
|
|
calendarDate: new Date()
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
computed: {
|
|
|
|
|
monthEvents() {
|
|
|
|
|
const now = this.calendarDate instanceof Date ? this.calendarDate : new Date(this.calendarDate);
|
|
|
|
|
const month = now.getMonth();
|
|
|
|
|
const year = now.getFullYear();
|
|
|
|
|
return this.events.filter(ev => {
|
|
|
|
|
const evDate = new Date(ev.start);
|
|
|
|
|
return evDate.getMonth() === month && evDate.getFullYear() === year;
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
selectMonth(){
|
|
|
|
|
console.log(this.calendarDate)
|
|
|
|
|
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;
|
|
|
|
|
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: {
|
|
|
|
|
dataType: 'onTypeChange'
|
|
|
|
|
calendarDate: {
|
|
|
|
|
handler() {
|
|
|
|
|
this.getList()
|
|
|
|
|
},
|
|
|
|
|
deep: true
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
created() {
|
|
|
|
|
this.onTypeChange();
|
|
|
|
|
this.getList()
|
|
|
|
|
},
|
|
|
|
|
methods: {
|
|
|
|
|
async getList(){
|
|
|
|
|
async getList() {
|
|
|
|
|
const res = await index({
|
|
|
|
|
month:this.selectMonth
|
|
|
|
|
month: this.selectMonth
|
|
|
|
|
})
|
|
|
|
|
this.list = res
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
onTypeChange() {
|
|
|
|
|
this.dataList = mockData[this.dataType] || [];
|
|
|
|
|
this.dataId = '';
|
|
|
|
|
this.dataDateInfo = '';
|
|
|
|
|
},
|
|
|
|
|
onDataChange() {
|
|
|
|
|
const item = (this.dataList || []).find(d => d.id === this.dataId);
|
|
|
|
|
if (item) {
|
|
|
|
|
this.dataDateInfo = `时间:${item.start.replace('T',' ')} ~ ${item.end.replace('T',' ')}`;
|
|
|
|
|
} else {
|
|
|
|
|
this.dataDateInfo = '';
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
fetchData() {
|
|
|
|
|
if (!this.dataId) return this.$message.warning('请选择要拉取的数据');
|
|
|
|
|
const item = (this.dataList || []).find(d => d.id === this.dataId);
|
|
|
|
|
if (item) {
|
|
|
|
|
this.events.push({
|
|
|
|
|
_id: ++idSeed,
|
|
|
|
|
title: item.title,
|
|
|
|
|
start: item.start,
|
|
|
|
|
end: item.end,
|
|
|
|
|
location: item.location,
|
|
|
|
|
teacher: item.teacher,
|
|
|
|
|
description: item.description,
|
|
|
|
|
className: `event-${this.dataType}`,
|
|
|
|
|
expire: 'none'
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
openCreateModal(type,id) {
|
|
|
|
|
console.log("type",type,id)
|
|
|
|
|
if(type == 'editor'){
|
|
|
|
|
openCreateModal(type, id) {
|
|
|
|
|
if (type === 'editor') {
|
|
|
|
|
this.$refs.addCalendar.id = id
|
|
|
|
|
}
|
|
|
|
|
this.$refs.addCalendar.type = type
|
|
|
|
|
this.$refs.addCalendar.isShow = true
|
|
|
|
|
},
|
|
|
|
|
saveEvent() {
|
|
|
|
|
if (!this.modalForm.title || !this.modalForm.start || !this.modalForm.end) {
|
|
|
|
|
this.$message.error('请填写完整信息');
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
eventsForDate(date) {
|
|
|
|
|
const d = new Date(date)
|
|
|
|
|
return this.list.filter(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 eventObj = {
|
|
|
|
|
_id: this.editingIndex !== null ? this.events[this.editingIndex]._id : ++idSeed,
|
|
|
|
|
title: this.modalForm.title,
|
|
|
|
|
start: this.modalForm.start,
|
|
|
|
|
end: this.modalForm.end,
|
|
|
|
|
location: this.modalForm.location,
|
|
|
|
|
teacher: this.modalForm.teacher,
|
|
|
|
|
description: this.modalForm.description,
|
|
|
|
|
className: `event-${this.modalForm.type}`,
|
|
|
|
|
expire: this.modalForm.expire
|
|
|
|
|
};
|
|
|
|
|
if (this.editingIndex !== null) {
|
|
|
|
|
this.$set(this.events, this.editingIndex, eventObj);
|
|
|
|
|
|
|
|
|
|
// 单天事件正常显示
|
|
|
|
|
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()
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
getEventClass(event, date) {
|
|
|
|
|
const startDate = new Date(event.start_time)
|
|
|
|
|
const currentDate = new Date(date)
|
|
|
|
|
|
|
|
|
|
// 如果没有end_time,直接返回单天事件样式
|
|
|
|
|
if (!event.end_time) {
|
|
|
|
|
return `single-day event-type-${event.type || 'default'}`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const endDate = new Date(event.end_time)
|
|
|
|
|
|
|
|
|
|
// 判断是否是跨天事件
|
|
|
|
|
const isMultiDay = startDate.getDate() !== endDate.getDate() ||
|
|
|
|
|
startDate.getMonth() !== endDate.getMonth() ||
|
|
|
|
|
startDate.getFullYear() !== endDate.getFullYear()
|
|
|
|
|
|
|
|
|
|
if (!isMultiDay) {
|
|
|
|
|
return `single-day event-type-${event.type || 'default'}`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 判断当前日期在跨天事件中的位置
|
|
|
|
|
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()) {
|
|
|
|
|
return `multi-day-start event-type-${event.type || 'default'}`
|
|
|
|
|
} else if (currentDateOnly.getTime() === eventEndDate.getTime()) {
|
|
|
|
|
return `multi-day-end event-type-${event.type || 'default'}`
|
|
|
|
|
} else {
|
|
|
|
|
return `multi-day-middle 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 {
|
|
|
|
|
this.events.push(eventObj);
|
|
|
|
|
return `${event.title}\n时间:${this.formatDateTime(event.start_time)} ~ ${this.formatDateTime(event.end_time)}`
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
getContinuousEvents() {
|
|
|
|
|
const FIRST_DOW = 0 // 0=Sunday to match :first-day-of-week="7" on el-calendar
|
|
|
|
|
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 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)
|
|
|
|
|
return d
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
multiDayEvents.forEach(ev => {
|
|
|
|
|
const eventStart = this.parseDateTime(ev.start_time)
|
|
|
|
|
const eventEnd = this.parseDateTime(ev.end_time)
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
continuousEvents.push({
|
|
|
|
|
...ev,
|
|
|
|
|
weekStartISO: weekStart.toISOString(),
|
|
|
|
|
segStartISO: segStart.toISOString(),
|
|
|
|
|
segEndISO: segEnd.toISOString(),
|
|
|
|
|
startCol,
|
|
|
|
|
spanCols,
|
|
|
|
|
endCol
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
cursor.setDate(cursor.getDate() + 7)
|
|
|
|
|
}
|
|
|
|
|
this.modalVisible = false;
|
|
|
|
|
},
|
|
|
|
|
editEvent(idx) {
|
|
|
|
|
const ev = this.monthEvents[idx];
|
|
|
|
|
this.modalTitle = '编辑事件';
|
|
|
|
|
this.modalForm = {
|
|
|
|
|
type: ev.className.replace('event-', ''),
|
|
|
|
|
title: ev.title,
|
|
|
|
|
start: ev.start,
|
|
|
|
|
end: ev.end,
|
|
|
|
|
location: ev.location,
|
|
|
|
|
teacher: ev.teacher,
|
|
|
|
|
description: ev.description,
|
|
|
|
|
expire: ev.expire || 'none'
|
|
|
|
|
};
|
|
|
|
|
this.editingIndex = this.events.findIndex(e => e._id === ev._id);
|
|
|
|
|
this.modalVisible = true;
|
|
|
|
|
},
|
|
|
|
|
eventsForDate(date) {
|
|
|
|
|
const d = new Date(date);
|
|
|
|
|
return this.list.filter(ev => {
|
|
|
|
|
const evDate = new Date(ev.start_time);
|
|
|
|
|
return evDate.getFullYear() === d.getFullYear() && evDate.getMonth() === d.getMonth() && evDate
|
|
|
|
|
.getDate() === d.getDate();
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
showEvent(ev) {
|
|
|
|
|
this.eventDetail = ev;
|
|
|
|
|
this.eventDetailVisible = true;
|
|
|
|
|
},
|
|
|
|
|
isExpired(ev) {
|
|
|
|
|
return new Date(ev.end) < new Date() && ev.expire === 'mark';
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// 为同一周的分段分配“轨道”(lane),避免垂直重叠
|
|
|
|
|
const byWeek = {}
|
|
|
|
|
continuousEvents.forEach(seg => {
|
|
|
|
|
const key = seg.weekStartISO
|
|
|
|
|
if (!byWeek[key]) byWeek[key] = []
|
|
|
|
|
byWeek[key].push(seg)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
Object.values(byWeek).forEach(segs => {
|
|
|
|
|
// 按开始列排序,开始相同则优先更长的,减少冲突
|
|
|
|
|
segs.sort((a, b) => (a.startCol - b.startCol) || (b.spanCols - a.spanCols))
|
|
|
|
|
const laneEndCols = [] // 每个轨道当前占据的最后列
|
|
|
|
|
segs.forEach(seg => {
|
|
|
|
|
let placed = false
|
|
|
|
|
for (let i = 0; i < laneEndCols.length; i += 1) {
|
|
|
|
|
if (laneEndCols[i] < seg.startCol) {
|
|
|
|
|
seg.laneIndex = i
|
|
|
|
|
laneEndCols[i] = seg.endCol
|
|
|
|
|
placed = true
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (!placed) {
|
|
|
|
|
seg.laneIndex = laneEndCols.length
|
|
|
|
|
laneEndCols.push(seg.endCol)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return continuousEvents
|
|
|
|
|
},
|
|
|
|
|
getContinuousEventStyle(event) {
|
|
|
|
|
const FIRST_DOW = 0
|
|
|
|
|
const currentMonth = this.calendarDate.getMonth()
|
|
|
|
|
const currentYear = this.calendarDate.getFullYear()
|
|
|
|
|
const firstDay = new Date(currentYear, currentMonth, 1)
|
|
|
|
|
|
|
|
|
|
// 可控的视觉偏移:将分段起始向后顺延 7 天
|
|
|
|
|
const adjStart = new Date(event.segStartISO)
|
|
|
|
|
adjStart.setDate(adjStart.getDate() + 7)
|
|
|
|
|
|
|
|
|
|
const msPerDay = 1000 * 60 * 60 * 24
|
|
|
|
|
const daysFromFirstOfMonth = Math.floor((adjStart - firstDay) / msPerDay)
|
|
|
|
|
const firstDayOffset = (firstDay.getDay() - FIRST_DOW + 7) % 7
|
|
|
|
|
const totalDaysFromCalendarStart = daysFromFirstOfMonth + firstDayOffset
|
|
|
|
|
const weekRow = Math.floor(totalDaysFromCalendarStart / 7)
|
|
|
|
|
|
|
|
|
|
// 起始列使用偏移后的日期重新计算
|
|
|
|
|
const startColAdjusted = (adjStart.getDay() - FIRST_DOW + 7) % 7
|
|
|
|
|
|
|
|
|
|
const cellWidth = 100 / 7
|
|
|
|
|
const cellHeight = 100
|
|
|
|
|
const headerHeight = 50
|
|
|
|
|
const dateNumberHeight = 25
|
|
|
|
|
const eventHeight = 16
|
|
|
|
|
const eventSpacing = 2
|
|
|
|
|
const verticalOffset = (event.laneIndex || 0) * (eventHeight + eventSpacing)
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
left: `calc(${startColAdjusted * cellWidth}% + 2px)`,
|
|
|
|
|
top: `${headerHeight + weekRow * cellHeight + dateNumberHeight + verticalOffset}px`,
|
|
|
|
|
width: `calc(${event.spanCols * cellWidth}% - 4px)`,
|
|
|
|
|
height: `${eventHeight}px`,
|
|
|
|
|
zIndex: 1000,
|
|
|
|
|
background: `linear-gradient(90deg, ${this.getEventTypeColor(event.type)} 0%, ${this.darkenColor(this.getEventTypeColor(event.type))} 100%)`,
|
|
|
|
|
borderRadius: '3px',
|
|
|
|
|
fontSize: '11px',
|
|
|
|
|
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: '1px',
|
|
|
|
|
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 t;
|
|
|
|
|
},
|
|
|
|
|
expireText(val) {
|
|
|
|
|
if (val === 'mark') return '标记为已过期';
|
|
|
|
|
if (val === 'delete') return '自动删除';
|
|
|
|
|
if (val === 'remind') return '发送提醒';
|
|
|
|
|
return '无';
|
|
|
|
|
if (!className) return ''
|
|
|
|
|
if (className === 1) return '课程'
|
|
|
|
|
if (className === 3) return '自定义事件'
|
|
|
|
|
if (className === 4) return '资讯'
|
|
|
|
|
return ''
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
filters: {
|
|
|
|
|
formatDateTime(val) {
|
|
|
|
|
if (!val) return '';
|
|
|
|
|
return val.replace('T', ' ');
|
|
|
|
|
if (!val) return ''
|
|
|
|
|
return val.replace('T', ' ')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
<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: #fff;
|
|
|
|
|
background: transparent;
|
|
|
|
|
padding: 20px 30px 10px 30px;
|
|
|
|
|
border-bottom: 1px solid #e9ecef;
|
|
|
|
|
font-size: 22px;
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
color: #2c3e50;
|
|
|
|
|
letter-spacing: 2px;
|
|
|
|
|
border-bottom: none;
|
|
|
|
|
box-shadow: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.admin-main {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 24px;
|
|
|
|
|
padding: 30px 30px 0 30px;
|
|
|
|
|
min-height: 90vh;
|
|
|
|
|
flex: 1;
|
|
|
|
|
padding: 20px 30px;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.admin-panel {
|
|
|
|
|
.calendar-panel {
|
|
|
|
|
background: #fff;
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
|
|
|
|
padding: 24px 18px;
|
|
|
|
|
flex: 1 1 0;
|
|
|
|
|
min-width: 320px;
|
|
|
|
|
padding: 24px;
|
|
|
|
|
height: 100%;
|
|
|
|
|
overflow: auto;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.calendar-wrapper {
|
|
|
|
|
position: relative;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.continuous-events-overlay {
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 0;
|
|
|
|
|
left: 0;
|
|
|
|
|
right: 0;
|
|
|
|
|
bottom: 0;
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
z-index: 100;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.continuous-event {
|
|
|
|
|
pointer-events: auto;
|
|
|
|
|
transition: all 0.2s ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.continuous-event:hover {
|
|
|
|
|
transform: translateY(-1px);
|
|
|
|
|
filter: brightness(1.1);
|
|
|
|
|
z-index: 101;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 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 {
|
|
|
|
|
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;
|
|
|
|
|
gap: 18px;
|
|
|
|
|
padding: 2px;
|
|
|
|
|
overflow: visible;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.admin-panel-title {
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
color: #1565c0;
|
|
|
|
|
.date-number {
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
color: #333;
|
|
|
|
|
margin-bottom: 2px;
|
|
|
|
|
position: relative;
|
|
|
|
|
z-index: 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.cell-content {
|
|
|
|
|
.event-list {
|
|
|
|
|
flex: 1;
|
|
|
|
|
overflow: visible;
|
|
|
|
|
position: relative;
|
|
|
|
|
min-height: 24px;
|
|
|
|
|
z-index: 5;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.event-dot {
|
|
|
|
|
width: 8px;
|
|
|
|
|
height: 8px;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
.event-item {
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
line-height: 14px;
|
|
|
|
|
padding: 1px 3px;
|
|
|
|
|
margin: 1px 0;
|
|
|
|
|
background: #409EFF;
|
|
|
|
|
display: inline-block;
|
|
|
|
|
margin-right: 2px;
|
|
|
|
|
color: white;
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
transition: background-color 0.2s;
|
|
|
|
|
position: relative;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.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; /* 默认 - 蓝色 */
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 悬停效果 */
|
|
|
|
|
.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 {
|
|
|
|
|
@ -367,15 +693,17 @@
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.text-secondary {
|
|
|
|
|
color: #888;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@media (max-width: 1200px) {
|
|
|
|
|
.admin-header {
|
|
|
|
|
padding: 15px 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.admin-main {
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 18px;
|
|
|
|
|
padding: 18px 6px 0 6px;
|
|
|
|
|
padding: 15px 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.calendar-panel {
|
|
|
|
|
padding: 16px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
|