617 lines
14 KiB
Vue
617 lines
14 KiB
Vue
<template>
|
||
<view class="calendar-container">
|
||
<!-- 月份选择器 -->
|
||
<view class="calendar-header">
|
||
<view class="month-selector">
|
||
<text class="btn-prev" @click="prevMonth">◀</text>
|
||
<text class="current-month">{{ currentYear }}年{{ currentMonth }}月</text>
|
||
<text class="btn-next" @click="nextMonth">▶</text>
|
||
</view>
|
||
<text class="btn-today" @click="goToday">今天</text>
|
||
</view>
|
||
|
||
<!-- 星期标题 -->
|
||
<view class="calendar-weekdays">
|
||
<text class="weekday" v-for="day in weekdays" :key="day">{{ day }}</text>
|
||
</view>
|
||
|
||
<!-- 日期网格 -->
|
||
<view class="calendar-grid">
|
||
<view
|
||
class="calendar-day"
|
||
v-for="(day, index) in calendarDays"
|
||
:key="index"
|
||
:class="{
|
||
'other-month': !day.isCurrentMonth,
|
||
'today': day.isToday,
|
||
'selected': day.isSelected,
|
||
'has-schedule': day.hasSchedule,
|
||
'disabled': day.disabled
|
||
}"
|
||
@click="selectDay(day)"
|
||
>
|
||
<view class="day-number">{{ day.day }}</view>
|
||
<view class="day-indicator" v-if="day.hasSchedule">
|
||
<view class="dot" :class="day.scheduleType"></view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 日程列表 -->
|
||
<view class="schedule-list" v-if="selectedDate && schedules.length > 0">
|
||
<view class="schedule-header">
|
||
<text class="schedule-title">{{ selectedDateText }} 的工单</text>
|
||
<text class="schedule-count">共{{ schedules.length }}个</text>
|
||
</view>
|
||
<view class="schedule-items">
|
||
<view
|
||
class="schedule-item"
|
||
v-for="schedule in schedules"
|
||
:key="schedule.id"
|
||
@click="handleScheduleClick(schedule)"
|
||
>
|
||
<!-- 时间段 -->
|
||
<view class="schedule-time">
|
||
<text class="time-icon">🕐</text>
|
||
<text class="time-text">{{ schedule.startTime }} - {{ schedule.endTime }}</text>
|
||
</view>
|
||
|
||
<!-- 工单内容 -->
|
||
<view class="schedule-content">
|
||
<!-- 学生信息 -->
|
||
<view class="info-row">
|
||
<text class="info-label">学生:</text>
|
||
<text class="info-value">{{ schedule.studentName || '未分配' }}</text>
|
||
</view>
|
||
|
||
<!-- 服务地址 -->
|
||
<view class="info-row">
|
||
<text class="info-label">地址:</text>
|
||
<text class="info-value">{{ schedule.address || '未填写' }}</text>
|
||
</view>
|
||
|
||
<!-- 时段 -->
|
||
<view class="info-row">
|
||
<text class="info-label">时段:</text>
|
||
<text class="info-value">{{ schedule.startTime }} - {{ schedule.endTime }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 状态标签 -->
|
||
<view class="schedule-status" :class="'status-' + schedule.status">
|
||
{{ getStatusText(schedule.status) }}
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 空状态 -->
|
||
<view class="empty-schedule" v-if="selectedDate && schedules.length === 0">
|
||
<text class="empty-icon">📅</text>
|
||
<text class="empty-text">{{ selectedDateText }} 暂无日程</text>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
export default {
|
||
name: 'Calendar',
|
||
props: {
|
||
// 日程数据
|
||
scheduleData: {
|
||
type: Array,
|
||
default: () => []
|
||
},
|
||
// 角色类型:parent-家长, teacher-陪伴员, manager-管理师
|
||
role: {
|
||
type: String,
|
||
default: 'parent'
|
||
}
|
||
},
|
||
|
||
data() {
|
||
return {
|
||
currentYear: new Date().getFullYear(),
|
||
currentMonth: new Date().getMonth() + 1,
|
||
selectedDate: null,
|
||
weekdays: ['日', '一', '二', '三', '四', '五', '六'],
|
||
calendarDays: [],
|
||
schedules: []
|
||
}
|
||
},
|
||
|
||
computed: {
|
||
selectedDateText() {
|
||
if (!this.selectedDate) return ''
|
||
const date = new Date(this.selectedDate)
|
||
return `${date.getMonth() + 1}月${date.getDate()}日`
|
||
}
|
||
},
|
||
|
||
watch: {
|
||
scheduleData: {
|
||
handler() {
|
||
this.generateCalendar()
|
||
},
|
||
deep: true
|
||
}
|
||
},
|
||
|
||
mounted() {
|
||
this.generateCalendar()
|
||
this.selectToday()
|
||
},
|
||
|
||
methods: {
|
||
// 生成日历
|
||
generateCalendar() {
|
||
const year = this.currentYear
|
||
const month = this.currentMonth
|
||
|
||
// 当月第一天
|
||
const firstDay = new Date(year, month - 1, 1)
|
||
// 当月最后一天
|
||
const lastDay = new Date(year, month, 0)
|
||
// 当月天数
|
||
const daysInMonth = lastDay.getDate()
|
||
// 第一天是星期几
|
||
const firstDayWeek = firstDay.getDay()
|
||
|
||
// 上个月需要显示的天数
|
||
const prevMonthDays = firstDayWeek
|
||
const prevMonth = month === 1 ? 12 : month - 1
|
||
const prevYear = month === 1 ? year - 1 : year
|
||
const prevMonthLastDay = new Date(prevYear, prevMonth, 0).getDate()
|
||
|
||
const days = []
|
||
const today = new Date()
|
||
const todayStr = this.formatDate(today)
|
||
|
||
// 上个月的日期
|
||
for (let i = prevMonthDays - 1; i >= 0; i--) {
|
||
const day = prevMonthLastDay - i
|
||
const dateStr = this.formatDate(new Date(prevYear, prevMonth - 1, day))
|
||
days.push({
|
||
day,
|
||
date: dateStr,
|
||
isCurrentMonth: false,
|
||
isToday: false,
|
||
isSelected: false,
|
||
hasSchedule: this.hasScheduleOnDate(dateStr),
|
||
scheduleType: this.getScheduleType(dateStr),
|
||
disabled: true
|
||
})
|
||
}
|
||
|
||
// 当月的日期
|
||
for (let i = 1; i <= daysInMonth; i++) {
|
||
const dateStr = this.formatDate(new Date(year, month - 1, i))
|
||
days.push({
|
||
day: i,
|
||
date: dateStr,
|
||
isCurrentMonth: true,
|
||
isToday: dateStr === todayStr,
|
||
isSelected: dateStr === this.selectedDate,
|
||
hasSchedule: this.hasScheduleOnDate(dateStr),
|
||
scheduleType: this.getScheduleType(dateStr),
|
||
disabled: false
|
||
})
|
||
}
|
||
|
||
// 下个月的日期(补齐6行)
|
||
const remainingDays = 42 - days.length
|
||
const nextMonth = month === 12 ? 1 : month + 1
|
||
const nextYear = month === 12 ? year + 1 : year
|
||
|
||
for (let i = 1; i <= remainingDays; i++) {
|
||
const dateStr = this.formatDate(new Date(nextYear, nextMonth - 1, i))
|
||
days.push({
|
||
day: i,
|
||
date: dateStr,
|
||
isCurrentMonth: false,
|
||
isToday: false,
|
||
isSelected: false,
|
||
hasSchedule: this.hasScheduleOnDate(dateStr),
|
||
scheduleType: this.getScheduleType(dateStr),
|
||
disabled: true
|
||
})
|
||
}
|
||
|
||
this.calendarDays = days
|
||
},
|
||
|
||
// 格式化日期
|
||
formatDate(date) {
|
||
const year = date.getFullYear()
|
||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||
const day = String(date.getDate()).padStart(2, '0')
|
||
return `${year}-${month}-${day}`
|
||
},
|
||
|
||
// 检查日期是否有日程
|
||
hasScheduleOnDate(dateStr) {
|
||
return this.scheduleData.some(schedule => schedule.date === dateStr)
|
||
},
|
||
|
||
// 获取日程类型
|
||
getScheduleType(dateStr) {
|
||
const schedules = this.scheduleData.filter(schedule => schedule.date === dateStr)
|
||
if (schedules.length === 0) return ''
|
||
|
||
// 根据状态返回不同类型
|
||
if (schedules.some(s => s.status === 'pending')) return 'pending'
|
||
if (schedules.some(s => s.status === 'confirmed')) return 'confirmed'
|
||
if (schedules.some(s => s.status === 'completed')) return 'completed'
|
||
return 'default'
|
||
},
|
||
|
||
// 选择日期
|
||
selectDay(day) {
|
||
if (day.disabled) return
|
||
|
||
this.selectedDate = day.date
|
||
this.loadSchedules(day.date)
|
||
|
||
// 更新选中状态
|
||
this.calendarDays.forEach(d => {
|
||
d.isSelected = d.date === day.date
|
||
})
|
||
|
||
this.$emit('date-select', day.date)
|
||
},
|
||
|
||
// 加载日程
|
||
loadSchedules(dateStr) {
|
||
this.schedules = this.scheduleData.filter(schedule => schedule.date === dateStr)
|
||
},
|
||
|
||
// 上一月
|
||
prevMonth() {
|
||
if (this.currentMonth === 1) {
|
||
this.currentYear--
|
||
this.currentMonth = 12
|
||
} else {
|
||
this.currentMonth--
|
||
}
|
||
this.generateCalendar()
|
||
this.$emit('month-change', { year: this.currentYear, month: this.currentMonth })
|
||
},
|
||
|
||
// 下一月
|
||
nextMonth() {
|
||
if (this.currentMonth === 12) {
|
||
this.currentYear++
|
||
this.currentMonth = 1
|
||
} else {
|
||
this.currentMonth++
|
||
}
|
||
this.generateCalendar()
|
||
this.$emit('month-change', { year: this.currentYear, month: this.currentMonth })
|
||
},
|
||
|
||
// 回到今天
|
||
goToday() {
|
||
const today = new Date()
|
||
this.currentYear = today.getFullYear()
|
||
this.currentMonth = today.getMonth() + 1
|
||
this.generateCalendar()
|
||
this.selectToday()
|
||
},
|
||
|
||
// 选择今天
|
||
selectToday() {
|
||
const today = new Date()
|
||
const todayStr = this.formatDate(today)
|
||
const todayItem = this.calendarDays.find(d => d.date === todayStr)
|
||
if (todayItem) {
|
||
this.selectDay(todayItem)
|
||
}
|
||
},
|
||
|
||
// 获取状态文本
|
||
getStatusText(status) {
|
||
const statusMap = {
|
||
pending: '待服务',
|
||
confirmed: '已确认',
|
||
inProgress: '进行中',
|
||
completed: '已完成',
|
||
cancelled: '已取消'
|
||
}
|
||
return statusMap[status] || '未知'
|
||
},
|
||
|
||
// 点击日程
|
||
handleScheduleClick(schedule) {
|
||
this.$emit('schedule-click', schedule)
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.calendar-container {
|
||
background: #fff;
|
||
border-radius: 20rpx;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.calendar-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 30rpx;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
}
|
||
|
||
.month-selector {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 30rpx;
|
||
}
|
||
|
||
.btn-prev, .btn-next {
|
||
font-size: 32rpx;
|
||
color: #fff;
|
||
padding: 10rpx;
|
||
}
|
||
|
||
.current-month {
|
||
font-size: 32rpx;
|
||
font-weight: bold;
|
||
color: #fff;
|
||
min-width: 200rpx;
|
||
text-align: center;
|
||
}
|
||
|
||
.btn-today {
|
||
font-size: 26rpx;
|
||
color: #fff;
|
||
padding: 10rpx 20rpx;
|
||
background: rgba(255, 255, 255, 0.2);
|
||
border-radius: 20rpx;
|
||
}
|
||
|
||
.calendar-weekdays {
|
||
display: grid;
|
||
grid-template-columns: repeat(7, 1fr);
|
||
padding: 20rpx 0;
|
||
background: #f5f7fa;
|
||
}
|
||
|
||
.weekday {
|
||
text-align: center;
|
||
font-size: 26rpx;
|
||
color: #666;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.calendar-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(7, 1fr);
|
||
gap: 2rpx;
|
||
padding: 10rpx;
|
||
}
|
||
|
||
.calendar-day {
|
||
aspect-ratio: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
position: relative;
|
||
border-radius: 12rpx;
|
||
transition: all 0.3s;
|
||
|
||
&.other-month {
|
||
opacity: 0.3;
|
||
}
|
||
|
||
&.today {
|
||
background: #e3f2fd;
|
||
|
||
.day-number {
|
||
color: #2196f3;
|
||
font-weight: bold;
|
||
}
|
||
}
|
||
|
||
&.selected {
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
|
||
.day-number {
|
||
color: #fff;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.dot {
|
||
background: #fff !important;
|
||
}
|
||
}
|
||
|
||
&.has-schedule {
|
||
background: #f0f9ff;
|
||
}
|
||
|
||
&.disabled {
|
||
pointer-events: none;
|
||
}
|
||
}
|
||
|
||
.day-number {
|
||
font-size: 28rpx;
|
||
color: #333;
|
||
}
|
||
|
||
.day-indicator {
|
||
position: absolute;
|
||
bottom: 8rpx;
|
||
display: flex;
|
||
gap: 4rpx;
|
||
}
|
||
|
||
.dot {
|
||
width: 8rpx;
|
||
height: 8rpx;
|
||
border-radius: 50%;
|
||
|
||
&.pending {
|
||
background: #ff9800;
|
||
}
|
||
|
||
&.confirmed {
|
||
background: #2196f3;
|
||
}
|
||
|
||
&.completed {
|
||
background: #4caf50;
|
||
}
|
||
|
||
&.default {
|
||
background: #999;
|
||
}
|
||
}
|
||
|
||
.schedule-list {
|
||
padding: 30rpx;
|
||
background: #f5f7fa;
|
||
}
|
||
|
||
.schedule-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 20rpx;
|
||
}
|
||
|
||
.schedule-title {
|
||
font-size: 32rpx;
|
||
font-weight: bold;
|
||
color: #333;
|
||
}
|
||
|
||
.schedule-count {
|
||
font-size: 24rpx;
|
||
color: #999;
|
||
}
|
||
|
||
.schedule-items {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 20rpx;
|
||
}
|
||
|
||
.schedule-item {
|
||
background: #fff;
|
||
border-radius: 16rpx;
|
||
padding: 24rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 16rpx;
|
||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
|
||
}
|
||
|
||
.schedule-time {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10rpx;
|
||
}
|
||
|
||
.time-icon {
|
||
font-size: 28rpx;
|
||
}
|
||
|
||
.time-text {
|
||
font-size: 26rpx;
|
||
color: #666;
|
||
}
|
||
|
||
.schedule-content {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12rpx;
|
||
}
|
||
|
||
.schedule-name {
|
||
font-size: 30rpx;
|
||
font-weight: bold;
|
||
color: #333;
|
||
margin-bottom: 8rpx;
|
||
}
|
||
|
||
.info-row {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
font-size: 26rpx;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.info-label {
|
||
color: #999;
|
||
flex-shrink: 0;
|
||
min-width: 80rpx;
|
||
}
|
||
|
||
.info-value {
|
||
color: #666;
|
||
flex: 1;
|
||
word-break: break-all;
|
||
}
|
||
|
||
.schedule-desc {
|
||
font-size: 24rpx;
|
||
color: #999;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.schedule-status {
|
||
align-self: flex-start;
|
||
padding: 6rpx 16rpx;
|
||
border-radius: 20rpx;
|
||
font-size: 22rpx;
|
||
|
||
&.status-pending {
|
||
background: #fff3e0;
|
||
color: #ff9800;
|
||
}
|
||
|
||
&.status-confirmed {
|
||
background: #e3f2fd;
|
||
color: #2196f3;
|
||
}
|
||
|
||
&.status-inProgress {
|
||
background: #e8f5e9;
|
||
color: #4caf50;
|
||
}
|
||
|
||
&.status-completed {
|
||
background: #f3e5f5;
|
||
color: #9c27b0;
|
||
}
|
||
|
||
&.status-cancelled {
|
||
background: #ffebee;
|
||
color: #f44336;
|
||
}
|
||
}
|
||
|
||
.empty-schedule {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 100rpx 0;
|
||
background: #f5f7fa;
|
||
}
|
||
|
||
.empty-icon {
|
||
font-size: 100rpx;
|
||
margin-bottom: 20rpx;
|
||
}
|
||
|
||
.empty-text {
|
||
font-size: 28rpx;
|
||
color: #999;
|
||
}
|
||
</style>
|