peixue-dev/peidu/uniapp/components/calendar/calendar.vue

617 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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