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

5 months ago
<template>
<view class="calendar-page">
<!-- 顶部导航 -->
4 months ago
<!-- <view class="header">
5 months ago
<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>
4 months ago
</view> -->
5 months ago
4 months ago
<!-- 自定义日历支持跨天横条显示 -->
<view class="calendar-container">
<CalendarGrid
:month="calendarDate"
:events="courses"
3 months ago
:rowHeightRpx="170"
4 months ago
:headerHeightRpx="72"
:weekHeaderHeightRpx="52"
:dateNumberHeightRpx="46"
@dayClick="onDateChange"
@monthChange="onMonthSwitch"
@edit="onEditEvent"
3 months ago
@eventClick="showCourseDetail"
4 months ago
/>
</view>
5 months ago
<!-- 课程类型筛选 -->
4 months ago
<!-- <view class="calendar-filters">
5 months ago
<view
v-for="item in filterTabs"
:key="item.value"
:class="['filter-badge', {active: filterType === item.value}]"
@tap="onFilterChange(item.value)"
>
{{ item.label }}
</view>
4 months ago
</view> -->
5 months ago
<!-- 当月日程列表 -->
<view class="month-events-container">
3 months ago
<view class="events-title">当月日程</view>
4 months ago
<view v-if="monthEvents.length" class="events-list no-scroll">
5 months ago
<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">
4 months ago
<!-- <text>{{ getCourseTypeName(ev.type) }}</text> -->
<text>{{ formatDateTimeRange(ev.start_time, ev.end_time) }}</text>
5 months ago
</view>
</view>
</view>
<view class="event-item-right">
<u-icon name="arrow-right" color="#999" size="28"></u-icon>
</view>
</view>
4 months ago
</view>
5 months ago
<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">
4 months ago
<!-- <view class="info-item">
4 months ago
<text class="info-label">日程类型</text>
5 months ago
<text class="info-value">{{ getCourseTypeName(detailData.type) }}</text>
4 months ago
</view> -->
5 months ago
<view class="info-item">
4 months ago
<text class="info-label">开始时间</text>
<text class="info-value">{{ formatDateTime(detailData.start_time) }}</text>
5 months ago
</view>
<view class="info-item">
4 months ago
<text class="info-label">结束时间</text>
<text class="info-value">{{ formatDateTime(detailData.end_time) }}</text>
5 months ago
</view>
</view>
3 months ago
<view class="course-description" v-if="detailData.address">
<text class="desc-title">日程地点</text>
<text class="info-value">{{ detailData.address }}</text>
</view>
5 months ago
<view class="course-description">
3 months ago
<!-- <text class="desc-title">日程详情</text> -->
4 months ago
<!-- <u-parse :content="detailData.content || ''"></u-parse> -->
<view v-html="detailData.content || ''">
5 months ago
</view>
</view>
<view class="course-actions">
5 months ago
<!-- <u-button type="default" @tap="showDetail = false">关闭</u-button> -->
<!-- <u-button type="primary" @tap="enrollCourse"></u-button> -->
5 months ago
</view>
</view>
</view>
</u-popup>
</view>
</template>
<script>
4 months ago
import CalendarGrid from '@/components/calendar-grid/calendar-grid.vue'
5 months ago
export default {
components:{
4 months ago
CalendarGrid
5 months ago
},
data() {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
return {
filterTabs: [
4 months ago
{ label: '全部', value: '' },
{ label: '课程', value: 1 },
{ label: '自定义事件', value: 3 },
{ label: '资讯', value: 4 }
5 months ago
],
4 months ago
filterType: '',
5 months ago
calendarDate: `${year}-${month}`,
showDetail: false,
detailData: {},
4 months ago
courses: [] // 与 web 端一致的数据结构:[{ id, title, type, start_time, end_time, ... }]
5 months ago
}
},
computed: {
selectedDates() {
4 months ago
const dates = new Set()
5 months ago
this.courses.forEach(ev => {
4 months ago
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)
5 months ago
}
4 months ago
})
return Array.from(dates).map(date => ({ date, info: '' }))
5 months ago
},
monthEvents() {
4 months ago
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)
5 months ago
const filtered = this.courses.filter(ev => {
4 months ago
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)
})
5 months ago
4 months ago
return filtered.sort((a, b) => this.parseDateTime(a.start_time) - this.parseDateTime(b.start_time))
5 months ago
}
},
4 months ago
created(){
this.getCalendar()
},
5 months ago
methods: {
4 months ago
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)
}
},
5 months ago
onFilterChange(type) {
this.filterType = type;
},
onDateChange({ fulldate }) {
4 months ago
const evs = this.getEventsForDate(fulldate)
5 months ago
if (evs.length) {
4 months ago
// 简单按开始时间排序
evs.sort((a,b) => this.parseDateTime(a.start_time) - this.parseDateTime(b.start_time))
this.showCourseDetail(evs[0])
5 months ago
}
},
onMonthSwitch({ year, month }) {
4 months ago
this.calendarDate = `${year}-${String(month).padStart(2, '0')}`
this.getCalendar()
},
onEditEvent(id){
// 预留:可打开编辑弹窗
// console.log('edit', id)
5 months ago
},
getEventsForDate(dateStr) {
4 months ago
const targetDate = new Date(dateStr)
targetDate.setHours(0, 0, 0, 0)
5 months ago
return this.courses.filter(ev => {
4 months ago
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
})
5 months ago
},
showCourseDetail(ev) {
4 months ago
// 跳转逻辑:
4 months ago
// type=1 课程:直接跳转课程详情页面
4 months ago
// 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
}
4 months ago
// 没有 course_id 则弹窗兜底
4 months ago
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
5 months ago
},
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 = {
4 months ago
1: '课程',
2: '会议',
3: '自定义事件',
4: '资讯',
5: '其他'
}
return types[type] || '其他'
5 months ago
},
formatDateTime(dateStr) {
4 months ago
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')}`
5 months ago
},
formatDateTimeRange(startStr, endStr) {
4 months ago
const start = this.parseDateTime(startStr)
const end = endStr ? this.parseDateTime(endStr) : this.parseDateTime(startStr)
if (!start || !end) return ''
5 months ago
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;
3 months ago
/* padding-bottom: 40rpx; */
5 months ago
}
.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);
3 months ago
/* margin: 20rpx;
padding: 10rpx 0 20rpx 0; */
5 months ago
}
.month-events-container {
3 months ago
margin-top: 20rpx;
5 months ago
padding: 20rpx;
background: #fff;
3 months ago
/* border-radius: 18rpx; */
5 months ago
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 {
4 months ago
max-height: none;
}
.events-list.no-scroll {
overflow: visible;
5 months ago
}
.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;
}
3 months ago
.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; }
5 months ago
.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;
4 months ago
grid-template-columns: 1fr;
5 months ago
gap: 10rpx;
margin-bottom: 18rpx;
}
.info-item {
background: #f8f9fa;
4 months ago
padding: 20rpx;
5 months ago
border-radius: 8rpx;
}
.info-label {
5 months ago
font-size: 28rpx;
5 months ago
color: #6c757d;
4 months ago
margin-right:20rpx;
5 months ago
}
.info-value {
5 months ago
font-size: 28rpx;
5 months ago
font-weight: 500;
color: #2c3e50;
}
.course-description {
background: #f8f9fa;
padding: 16rpx;
border-radius: 8rpx;
margin-bottom: 12rpx;
}
.desc-title {
color: #1565c0;
5 months ago
font-size: 28rpx;
5 months ago
font-weight: 600;
margin-bottom: 6rpx;
display: block;
}
.desc-content {
color: #333;
5 months ago
font-size: 28rpx;
5 months ago
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; }
3 months ago
/* .calendar-container { margin: 4rpx; } */
5 months ago
.header { padding: 8rpx 4rpx; }
5 months ago
.school-name { font-size: 28rpx; }
5 months ago
}
</style>