You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

271 lines
7.2 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<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>