|
|
<template>
|
|
|
<view class="calendar-widget">
|
|
|
<!-- 月份切换加载指示器 -->
|
|
|
<!-- <view v-if="switchingMonth" class="month-loading">
|
|
|
<u-loading mode="circle" size="24"></u-loading>
|
|
|
<text class="loading-text">加载中...</text>
|
|
|
</view> -->
|
|
|
|
|
|
<!-- 自定义日历(支持跨天横条显示) -->
|
|
|
<view class="calendar-title">学院日历</view>
|
|
|
<view class="calendar-container" :class="{ 'loading-opacity': switchingMonth }">
|
|
|
<CalendarGrid
|
|
|
:month="calendarDate"
|
|
|
:events="courses"
|
|
|
:rowHeightRpx="146"
|
|
|
:headerHeightRpx="60"
|
|
|
:weekHeaderHeightRpx="40"
|
|
|
:dateNumberHeightRpx="36"
|
|
|
@dayClick="onDateChange"
|
|
|
@monthChange="onMonthSwitch"
|
|
|
@edit="onEditEvent"
|
|
|
@eventClick="showCourseDetail"
|
|
|
/>
|
|
|
</view>
|
|
|
</view>
|
|
|
</template>
|
|
|
|
|
|
<script>
|
|
|
import CalendarGrid from '@/components/calendar-grid/calendar-grid.vue'
|
|
|
|
|
|
export default {
|
|
|
name: 'CalendarWidget',
|
|
|
components: {
|
|
|
CalendarGrid
|
|
|
},
|
|
|
data() {
|
|
|
const now = new Date();
|
|
|
const year = now.getFullYear();
|
|
|
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
|
return {
|
|
|
calendarDate: `${year}-${month}`,
|
|
|
courses: [],
|
|
|
monthEvents: [],
|
|
|
loading: false,
|
|
|
switchingMonth: false
|
|
|
}
|
|
|
},
|
|
|
mounted() {
|
|
|
this.loadCourses()
|
|
|
},
|
|
|
methods: {
|
|
|
// 解析日期时间字符串
|
|
|
parseDateTime(dateTimeStr) {
|
|
|
if (!dateTimeStr) return null
|
|
|
const [datePart, timePart = '00:00:00'] = dateTimeStr.trim().split(/[T\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)
|
|
|
},
|
|
|
|
|
|
// 加载课程数据(初始加载)
|
|
|
async loadCourses() {
|
|
|
if (this.loading) return
|
|
|
|
|
|
try {
|
|
|
this.loading = true
|
|
|
await this.loadCoursesForMonth(this.calendarDate)
|
|
|
} catch (error) {
|
|
|
console.error('加载课程数据失败:', error)
|
|
|
uni.showToast({
|
|
|
title: '加载日历数据失败',
|
|
|
icon: 'none'
|
|
|
})
|
|
|
} finally {
|
|
|
this.loading = false
|
|
|
}
|
|
|
},
|
|
|
|
|
|
// 加载指定月份的课程数据
|
|
|
async loadCoursesForMonth(monthDate) {
|
|
|
const res = await this.$u.api.calendarsGet({
|
|
|
month: monthDate
|
|
|
})
|
|
|
|
|
|
const rows = (res && res.data) ? res.data : (Array.isArray(res) ? res : [])
|
|
|
this.courses = Array.isArray(rows) ? rows : []
|
|
|
|
|
|
console.log('日历数据加载成功:', this.courses)
|
|
|
},
|
|
|
|
|
|
// 日期变化
|
|
|
onDateChange({ fulldate }) {
|
|
|
const evs = this.getEventsForDate(fulldate)
|
|
|
if (evs.length) {
|
|
|
// 简单按开始时间排序
|
|
|
evs.sort((a,b) => this.parseDateTime(a.start_time) - this.parseDateTime(b.start_time))
|
|
|
this.showCourseDetail(evs[0])
|
|
|
}
|
|
|
},
|
|
|
|
|
|
// 月份切换
|
|
|
async onMonthSwitch({ year, month }) {
|
|
|
const newDate = `${year}-${String(month).padStart(2, '0')}`
|
|
|
|
|
|
// 如果已经是当前月份,不需要重新加载
|
|
|
if (newDate === this.calendarDate) return
|
|
|
|
|
|
// 设置切换状态
|
|
|
this.switchingMonth = true
|
|
|
|
|
|
try {
|
|
|
// 更新日历日期
|
|
|
this.calendarDate = newDate
|
|
|
|
|
|
// 加载新月份的数据
|
|
|
await this.loadCoursesForMonth(newDate)
|
|
|
} catch (error) {
|
|
|
console.error('月份切换失败:', error)
|
|
|
uni.showToast({
|
|
|
title: '切换月份失败',
|
|
|
icon: 'none'
|
|
|
})
|
|
|
} finally {
|
|
|
// 延迟一点时间让用户看到切换效果
|
|
|
setTimeout(() => {
|
|
|
this.switchingMonth = false
|
|
|
}, 300)
|
|
|
}
|
|
|
},
|
|
|
|
|
|
// 编辑事件
|
|
|
onEditEvent(event) {
|
|
|
console.log('编辑事件:', event)
|
|
|
},
|
|
|
|
|
|
// 获取指定日期的事件
|
|
|
getEventsForDate(dateStr) {
|
|
|
const targetDate = new Date(dateStr)
|
|
|
targetDate.setHours(0, 0, 0, 0)
|
|
|
|
|
|
return this.courses.filter(ev => {
|
|
|
const startDate = this.parseDateTime(ev.start_time)
|
|
|
const endDate = ev.end_time ? this.parseDateTime(ev.end_time) : this.parseDateTime(ev.start_time)
|
|
|
if (!startDate || !endDate) return false
|
|
|
startDate.setHours(0,0,0,0)
|
|
|
endDate.setHours(0,0,0,0)
|
|
|
return targetDate >= startDate && targetDate <= endDate
|
|
|
})
|
|
|
},
|
|
|
|
|
|
// 显示课程详情
|
|
|
showCourseDetail(ev) {
|
|
|
// 交互规则:
|
|
|
// - 仅当存在“真实可展示内容”时才弹窗;仅有标题不弹窗
|
|
|
// - 可跳转时仍按原逻辑跳转
|
|
|
const type = ev.type
|
|
|
|
|
|
// 课程
|
|
|
if (type === 1) {
|
|
|
if (ev.course_id) {
|
|
|
uni.navigateTo({ url: `/packages/course/detail?id=${ev.course_id}&newsUrl=${ev.url}` })
|
|
|
return
|
|
|
}
|
|
|
// 无 course_id → 需要有开始时间或地点才弹窗
|
|
|
const hasDetail = !!(ev.start_time || ev.location)
|
|
|
if (!hasDetail) return
|
|
|
uni.showModal({
|
|
|
title: ev.title || '课程详情',
|
|
|
content: `时间:${ev.start_time || '待定'}\n地点:${ev.location || '待定'}`,
|
|
|
showCancel: false
|
|
|
})
|
|
|
return
|
|
|
}
|
|
|
|
|
|
// 自定义事件:需要 content 才弹
|
|
|
if (type === 3) {
|
|
|
const hasDetail = !!(ev.content && String(ev.content).trim())
|
|
|
if (!hasDetail) return
|
|
|
uni.showModal({
|
|
|
title: ev.title || '事件详情',
|
|
|
content: ev.content,
|
|
|
showCancel: false
|
|
|
})
|
|
|
return
|
|
|
}
|
|
|
|
|
|
// 资讯:有 url 跳转;否则需要 content 才弹
|
|
|
if (type === 4) {
|
|
|
if (ev.url) {
|
|
|
const encoded = ev.url
|
|
|
uni.navigateTo({ url: `/packages/webview/index?type=3&url=${encoded}` })
|
|
|
return
|
|
|
}
|
|
|
const hasDetail = !!(ev.content && String(ev.content).trim())
|
|
|
if (!hasDetail) return
|
|
|
uni.showModal({
|
|
|
title: ev.title || '资讯详情',
|
|
|
content: ev.content,
|
|
|
showCancel: false
|
|
|
})
|
|
|
return
|
|
|
}
|
|
|
|
|
|
// 其他类型:仅当有 content 才弹
|
|
|
const hasDetail = !!(ev.content && String(ev.content).trim())
|
|
|
if (!hasDetail) return
|
|
|
uni.showModal({
|
|
|
title: ev.title || '详情',
|
|
|
content: ev.content,
|
|
|
showCancel: false
|
|
|
})
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
</script>
|
|
|
|
|
|
<style scoped>
|
|
|
.calendar-widget {
|
|
|
padding: 25rpx;
|
|
|
}
|
|
|
|
|
|
.month-loading {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
padding: 12rpx 20rpx;
|
|
|
background: rgba(255, 255, 255, 0.95);
|
|
|
border-radius: 20rpx;
|
|
|
margin-bottom: 12rpx;
|
|
|
position: relative;
|
|
|
z-index: 10;
|
|
|
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
|
|
animation: slideDown 0.3s ease-out;
|
|
|
}
|
|
|
|
|
|
@keyframes slideDown {
|
|
|
from {
|
|
|
opacity: 0;
|
|
|
transform: translateY(-10rpx);
|
|
|
}
|
|
|
to {
|
|
|
opacity: 1;
|
|
|
transform: translateY(0);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.loading-text {
|
|
|
margin-left: 12rpx;
|
|
|
font-size: 22rpx;
|
|
|
color: #666;
|
|
|
}
|
|
|
|
|
|
.loading-opacity {
|
|
|
opacity: 0.7;
|
|
|
transition: opacity 0.2s ease;
|
|
|
pointer-events: none;
|
|
|
}
|
|
|
|
|
|
.calendar-container {
|
|
|
margin-bottom: 0;
|
|
|
}
|
|
|
.calendar-title{
|
|
|
font-size: 34rpx;
|
|
|
color: #fff;
|
|
|
border-radius: 20rpx 20rpx 0 0;
|
|
|
text-align: center;
|
|
|
background:linear-gradient(90deg, #ddba99, #b18d6d);
|
|
|
padding:20rpx;
|
|
|
}
|
|
|
</style>
|