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.

557 lines
16 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-page">
<!-- 顶部导航 -->
<!-- <view class="header">
<view class="logo-section">
<view class="logo"></view>
<view class="school-name">苏州科技商学院</view>
</view>
<view class="user-section">
<text>张同学</text>
<view class="vip-badge">VIP会员</view>
</view>
</view> -->
<!-- 自定义日历支持跨天横条显示 -->
<view class="calendar-container">
<CalendarGrid
:month="calendarDate"
:events="courses"
:rowHeightRpx="170"
:headerHeightRpx="72"
:weekHeaderHeightRpx="52"
:dateNumberHeightRpx="46"
@dayClick="onDateChange"
@monthChange="onMonthSwitch"
@edit="onEditEvent"
@eventClick="showCourseDetail"
/>
</view>
<!-- 课程类型筛选 -->
<!-- <view class="calendar-filters">
<view
v-for="item in filterTabs"
:key="item.value"
:class="['filter-badge', {active: filterType === item.value}]"
@tap="onFilterChange(item.value)"
>
{{ item.label }}
</view>
</view> -->
<!-- 当月日程列表 -->
<view class="month-events-container">
<view class="events-title">当月日程</view>
<view v-if="monthEvents.length" class="events-list no-scroll">
<view
v-for="ev in monthEvents"
:key="ev.id"
class="event-item"
@tap="showCourseDetail(ev)"
>
<view class="event-item-left">
<view :class="['type-dot', 'event-' + ev.type]"></view>
<view class="event-info">
<view class="event-title">{{ ev.title }}</view>
<view class="event-meta">
<!-- <text>{{ getCourseTypeName(ev.type) }}</text> -->
<text>{{ formatDateTimeRange(ev.start_time, ev.end_time) }}</text>
</view>
</view>
</view>
<view class="event-item-right">
<u-icon name="arrow-right" color="#999" size="28"></u-icon>
</view>
</view>
</view>
<view v-else class="empty-events">
<text>本月暂无日程</text>
</view>
</view>
<!-- 课程详情弹窗 -->
<u-popup v-model="showDetail" :width="'100vw'" :height="'100vh'" mode="center" :closeable="true" :mask-close-able="true">
<view class="course-modal">
<view class="modal-header">
<text class="modal-title">{{ detailData.title }}</text>
</view>
<view class="modal-body">
<view class="course-info">
<!-- <view class="info-item">
<text class="info-label">日程类型</text>
<text class="info-value">{{ getCourseTypeName(detailData.type) }}</text>
</view> -->
<view class="info-item">
<text class="info-label">开始时间</text>
<text class="info-value">{{ formatDateTime(detailData.start_time) }}</text>
</view>
<view class="info-item">
<text class="info-label">结束时间</text>
<text class="info-value">{{ formatDateTime(detailData.end_time) }}</text>
</view>
</view>
<view class="course-description" v-if="detailData.address">
<text class="desc-title">日程地点</text>
<text class="info-value">{{ detailData.address }}</text>
</view>
<view class="course-description">
<!-- <text class="desc-title">日程详情</text> -->
<!-- <u-parse :content="detailData.content || ''"></u-parse> -->
<view v-html="detailData.content || ''">
</view>
</view>
<view class="course-actions">
<!-- <u-button type="default" @tap="showDetail = false">关闭</u-button> -->
<!-- <u-button type="primary" @tap="enrollCourse"></u-button> -->
</view>
</view>
</view>
</u-popup>
</view>
</template>
<script>
import CalendarGrid from '@/components/calendar-grid/calendar-grid.vue'
export default {
components:{
CalendarGrid
},
data() {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
return {
filterTabs: [
{ label: '全部', value: '' },
{ label: '课程', value: 1 },
{ label: '自定义事件', value: 3 },
{ label: '资讯', value: 4 }
],
filterType: '',
calendarDate: `${year}-${month}`,
showDetail: false,
detailData: {},
courses: [] // 与 web 端一致的数据结构:[{ id, title, type, start_time, end_time, ... }]
}
},
computed: {
selectedDates() {
const dates = new Set()
this.courses.forEach(ev => {
if (!this.matchesFilter(ev.type)) return
const start = this.parseDateTime(ev.start_time)
const end = ev.end_time ? this.parseDateTime(ev.end_time) : this.parseDateTime(ev.start_time)
if (!start || !end) return
const cursor = new Date(start)
while (cursor <= end) {
dates.add(this.toDateString(cursor))
cursor.setDate(cursor.getDate() + 1)
}
})
return Array.from(dates).map(date => ({ date, info: '' }))
},
monthEvents() {
if (!this.calendarDate) return []
const [yearStr, monthStr] = (this.calendarDate || '').split('-')
const year = parseInt(yearStr, 10)
const month = parseInt(monthStr, 10)
if (!year || !month) return []
const monthStart = new Date(year, month - 1, 1)
const monthEnd = new Date(year, month, 0)
const filtered = this.courses.filter(ev => {
if (!this.matchesFilter(ev.type)) return false
const start = this.parseDateTime(ev.start_time)
const end = ev.end_time ? this.parseDateTime(ev.end_time) : this.parseDateTime(ev.start_time)
if (!start || !end) return false
// 只要与当月有交集就展示
return !(end < monthStart || start > monthEnd)
})
return filtered.sort((a, b) => this.parseDateTime(a.start_time) - this.parseDateTime(b.start_time))
}
},
created(){
this.getCalendar()
},
methods: {
matchesFilter(type) {
// 显示规则filterType 为空字符串时,显示全部;否则严格匹配类型
if (this.filterType === '' || this.filterType === null || this.filterType === undefined) return true
return type === this.filterType
},
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)
},
toDateString(d) {
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`
},
async getCalendar(){
try {
const res = await this.$u.api.calendarsGet({
month: this.calendarDate
})
const rows = (res && res.data) ? res.data : (Array.isArray(res) ? res : [])
this.courses = Array.isArray(rows) ? rows : []
} catch (error) {
console.error('getCalendar failed:', error)
}
},
onFilterChange(type) {
this.filterType = type;
},
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])
}
},
onMonthSwitch({ year, month }) {
this.calendarDate = `${year}-${String(month).padStart(2, '0')}`
this.getCalendar()
},
onEditEvent(id){
// 预留:可打开编辑弹窗
// console.log('edit', id)
},
getEventsForDate(dateStr) {
const targetDate = new Date(dateStr)
targetDate.setHours(0, 0, 0, 0)
return this.courses.filter(ev => {
if (this.filterType !== 'all' && ev.type !== this.filterType) return false
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) {
// 跳转逻辑:
// type=1 课程:直接跳转课程详情页面
// type=3 自定义事件:弹出详情,使用 v-html 渲染 content
// type=4 资讯:跳 webview
const type = ev.type
if (type === 1) {
if (ev.course_id) {
uni.navigateTo({ url: `/packages/course/detail?id=${ev.course_id}` })
return
}
// 没有 course_id 则弹窗兜底
this.detailData = ev
this.showDetail = true
return
}
if (type === 3) {
this.detailData = ev
this.showDetail = true
return
}
if (type === 4) {
if (ev.url) {
const encoded = ev.url
uni.navigateTo({ url: `/packages/webview/index?type=3&url=${encoded}` })
return
}
// 没有 url 则弹窗兜底
this.detailData = ev
this.showDetail = true
return
}
// 其他类型默认弹窗
this.detailData = ev
this.showDetail = true
},
enrollCourse() {
uni.showModal({
title: '提示',
content: `确定要报名"${this.detailData.title}"吗?`,
success: res => {
if (res.confirm) {
uni.showToast({ title: '报名成功', icon: 'success' });
this.showDetail = false;
}
}
});
},
checkEnrollmentDeadline(course) {
const deadline = new Date(course.enrollmentDeadline);
const now = new Date();
const daysUntilDeadline = Math.ceil((deadline - now) / (1000 * 60 * 60 * 24));
if (daysUntilDeadline <= 3 && daysUntilDeadline > 0) {
// 可用uni.showToast或其他提醒
uni.showToast({
title: `${course.title}报名即将截止`,
icon: 'none'
});
}
},
getCourseTypeName(type) {
const types = {
1: '课程',
2: '会议',
3: '自定义事件',
4: '资讯',
5: '其他'
}
return types[type] || '其他'
},
formatDateTime(dateStr) {
if (!dateStr) return ''
const d = this.parseDateTime(dateStr)
if (!d || isNaN(d.getTime())) return ''
return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')} ${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}`
},
formatDateTimeRange(startStr, endStr) {
const start = this.parseDateTime(startStr)
const end = endStr ? this.parseDateTime(endStr) : this.parseDateTime(startStr)
if (!start || !end) return ''
const startDay = `${String(start.getMonth() + 1).padStart(2,'0')}-${String(start.getDate()).padStart(2,'0')}`;
const startTime = `${String(start.getHours()).padStart(2,'0')}:${String(start.getMinutes()).padStart(2,'0')}`;
const endDay = `${String(end.getMonth() + 1).padStart(2,'0')}-${String(end.getDate()).padStart(2,'0')}`;
const endTime = `${String(end.getHours()).padStart(2,'0')}:${String(end.getMinutes()).padStart(2,'0')}`;
if (startDay === endDay) {
return `${startDay} ${startTime} - ${endTime}`;
} else {
return `${startDay} ${startTime} - ${endDay} ${endTime}`;
}
}
}
}
</script>
<style scoped>
.calendar-page {
min-height: 100vh;
background: #f8f9fa;
/* padding-bottom: 40rpx; */
}
.header {
background: #fff;
padding: 20rpx 30rpx 10rpx 30rpx;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 2px 4px rgba(0,0,0,0.08);
position: sticky;
top: 0;
z-index: 100;
}
.logo-section {
display: flex;
align-items: center;
gap: 15rpx;
}
.logo {
width: 40rpx;
height: 40rpx;
background: #3498db;
border-radius: 8rpx;
}
.school-name {
font-size: 32rpx;
font-weight: 600;
color: #2c3e50;
margin-left: 10rpx;
}
.user-section {
display: flex;
align-items: center;
gap: 10rpx;
font-size: 28rpx;
}
.vip-badge {
background: linear-gradient(45deg, #FFD700, #FFA500);
color: white;
padding: 4rpx 12rpx;
border-radius: 4rpx;
font-size: 22rpx;
font-weight: 500;
margin-left: 8rpx;
}
.calendar-filters {
display: flex;
gap: 18rpx;
align-items: center;
padding: 18rpx 20rpx 0 20rpx;
margin-bottom:20rpx;
}
.filter-badge {
padding: 8rpx 22rpx;
border-radius: 20rpx;
font-size: 26rpx;
font-weight: 500;
background: #f8f9fa;
color: #2c3e50;
transition: all 0.3s;
}
.filter-badge.active {
background: #3498db;
color: #fff;
}
.calendar-container {
background: #fff;
border-radius: 18rpx;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
/* margin: 20rpx;
padding: 10rpx 0 20rpx 0; */
}
.month-events-container {
margin-top: 20rpx;
padding: 20rpx;
background: #fff;
/* border-radius: 18rpx; */
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
}
.events-title {
font-size: 32rpx;
font-weight: 600;
color: #2c3e50;
margin-bottom: 20rpx;
}
.events-list {
max-height: none;
}
.events-list.no-scroll {
overflow: visible;
}
.event-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 0;
border-bottom: 1rpx solid #f0f0f0;
}
.event-item:last-child {
border-bottom: none;
}
.event-item-left {
display: flex;
align-items: center;
gap: 20rpx;
}
.type-dot {
width: 16rpx;
height: 16rpx;
border-radius: 50%;
flex-shrink: 0;
}
.type-dot.event-1 { background: #67C23A; }
.type-dot.event-2 { background: #409EFF; }
.type-dot.event-3 { background: #E6A23C; }
.type-dot.event-4 { background: #F56C6C; }
.type-dot.event-5 { background: #909399; }
.event-course { background: #1565c0; }
.event-activity { background: #6a1b9a; }
.event-workshop { background: #2e7d32; }
.event-info {
display: flex;
flex-direction: column;
}
.event-title {
font-size: 28rpx;
font-weight: 500;
color: #333;
margin-bottom: 8rpx;
}
.event-meta {
font-size: 24rpx;
color: #888;
}
.empty-events {
text-align: center;
color: #999;
padding: 40rpx 0;
font-size: 28rpx;
}
.course-modal {
background: #fff;
border-radius: 18rpx;
padding: 20rpx 20rpx 10rpx 20rpx;
width: 100vw;
height:100vh;
overflow: scroll;
}
.modal-header {
border-bottom: 1rpx solid #e9ecef;
padding-bottom: 10rpx;
margin-bottom: 10rpx;
}
.modal-title {
font-size: 34rpx;
font-weight: 700;
color: #1565c0;
}
.modal-body {
padding: 0;
}
.course-info {
display: grid;
grid-template-columns: 1fr;
gap: 10rpx;
margin-bottom: 18rpx;
}
.info-item {
background: #f8f9fa;
padding: 20rpx;
border-radius: 8rpx;
}
.info-label {
font-size: 28rpx;
color: #6c757d;
margin-right:20rpx;
}
.info-value {
font-size: 28rpx;
font-weight: 500;
color: #2c3e50;
}
.course-description {
background: #f8f9fa;
padding: 16rpx;
border-radius: 8rpx;
margin-bottom: 12rpx;
}
.desc-title {
color: #1565c0;
font-size: 28rpx;
font-weight: 600;
margin-bottom: 6rpx;
display: block;
}
.desc-content {
color: #333;
font-size: 28rpx;
margin-top: 4rpx;
}
.vip-content {
/* background: linear-gradient(90deg, #fffbe6 0%, #fff3cd 100%); */
/* border: 1rpx solid #ffe082; */
}
.course-actions {
display: flex;
gap: 18rpx;
justify-content: flex-end;
margin-top: 10rpx;
}
@media (max-width: 768px) {
.course-modal { min-width: 90vw; }
/* .calendar-container { margin: 4rpx; } */
.header { padding: 8rpx 4rpx; }
.school-name { font-size: 28rpx; }
}
</style>