Files
SmartParks_uniapp/components/CommonCalendar.vue

301 lines
8.3 KiB
Vue
Raw Normal View History

2025-09-05 16:54:53 +08:00
<template>
<view class="calendar-container">
<!-- 展开/收起按钮可以外部隐藏 -->
<view class="calendar-header">
<button size="mini" @click="toggleMode">
{{ mode === 'month' ? '收起为周' : '展开为月' }}
</button>
<text>{{ displayTitle }}</text>
</view>
<view
class="calendar-content"
@touchstart="onTouchStart"
@touchmove="onTouchMove"
@touchend="onTouchEnd"
:style="{ height: mode === 'month' ? '450rpx' : '120rpx', transition: 'height 0.3s' }"
>
<!-- 星期栏 -->
<view class="calendar-week">
<text v-for="(w, i) in weeks" :key="i">{{ w }}</text>
</view>
<!-- 日期 -->
<view class="calendar-days">
<view
v-for="(item, i) in renderDays"
:key="i"
:class="{
'calendar-day': true,
'today': item.isToday,
'selected': isSelected(item),
'other-month': item.otherMonth
}"
@click="selectDate(item)"
>
{{ item.day || '' }}
</view>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'CustomCalendar',
props: {
initialMode: { type: String, default: 'month' }, // 'month' or 'week'
initialDate: { type: String, default: '' },
allowWeekSwitch: { type: Boolean, default: true } // 控制是否允许上下滑切换
},
data() {
const today = new Date()
return {
mode: this.initialMode,
selected: this.initialDate || this.formatDate(today),
weeks: ['日', '一', '二', '三', '四', '五', '六'],
renderDays: [],
touchStartX: 0,
touchStartY: 0,
touchMoveX: 0,
touchMoveY: 0,
anchorDate: new Date() // 当前视图基准日期
}
},
computed: {
displayTitle() {
return `${this.anchorDate.getFullYear()}${this.anchorDate.getMonth() + 1}`
}
},
watch: {
mode() {
this.generateRenderDays()
},
selected() {
this.generateRenderDays()
}
},
mounted() {
this.anchorDate = this.safeDate(this.selected)
this.generateRenderDays()
},
methods: {
// =================== 日期工具 ===================
formatDate(date) {
const y = date.getFullYear()
const m = date.getMonth() + 1
const d = date.getDate()
return `${y}-${m < 10 ? '0' + m : m}-${d < 10 ? '0' + d : d}`
},
safeDate(date) {
return new Date(date)
},
startOfMonth(date) {
const d = this.safeDate(date)
d.setDate(1)
return d
},
startOfWeek(date) {
const d = this.safeDate(date)
const day = d.getDay()
d.setDate(d.getDate() - day)
return d
},
pad(num) {
return num < 10 ? '0' + num : '' + num
},
// =================== 渲染日历 ===================
generateRenderDays() {
const days = []
if (this.mode === 'month') {
const year = this.anchorDate.getFullYear()
const month = this.anchorDate.getMonth() + 1
const firstDayWeek = new Date(year, month - 1, 1).getDay()
const monthDays = new Date(year, month, 0).getDate()
// 上月补位
const lastMonth = month === 1 ? 12 : month - 1
const lastMonthYear = month === 1 ? year - 1 : year
const lastMonthDays = new Date(lastMonthYear, lastMonth, 0).getDate()
for (let i = 0; i < firstDayWeek; i++) {
days.push({
day: lastMonthDays - firstDayWeek + 1 + i,
date: this.formatDate(new Date(lastMonthYear, lastMonth - 1, lastMonthDays - firstDayWeek + 1 + i)),
otherMonth: true,
isToday: false
})
}
// 本月
const todayStr = this.formatDate(new Date())
for (let i = 1; i <= monthDays; i++) {
const dateStr = `${year}-${this.pad(month)}-${this.pad(i)}`
days.push({
day: i,
date: dateStr,
otherMonth: false,
isToday: dateStr === todayStr
})
}
// 下月补位
while (days.length % 7 !== 0) {
const nextDay = days.length - (firstDayWeek + monthDays) + 1
days.push({
day: nextDay,
date: this.formatDate(new Date(month === 12 ? year + 1 : year, month % 12, nextDay)),
otherMonth: true,
isToday: false
})
}
} else {
// 周视图
const base = this.safeDate(this.selected)
const weekDay = base.getDay()
for (let i = 0; i < 7; i++) {
const d = new Date(base)
d.setDate(base.getDate() - weekDay + i)
days.push({
day: d.getDate(),
date: this.formatDate(d),
otherMonth: d.getMonth() !== base.getMonth(),
isToday: this.formatDate(d) === this.formatDate(new Date())
})
}
}
this.renderDays = days
},
// =================== 事件 ===================
toggleMode() {
if (!this.allowWeekSwitch) return
this.mode = this.mode === 'month' ? 'week' : 'month'
this.anchorDate = this.mode === 'month' ? this.startOfMonth(this.safeDate(this.selected)) : this.startOfWeek(this.safeDate(this.selected))
this.generateRenderDays()
},
selectDate(item) {
if (!item.day) return
this.selected = item.date
this.$emit('dateChange', item.date)
},
isSelected(item) {
return item.date === this.selected && !item.otherMonth
},
// =================== 滑动 ===================
onTouchStart(e) {
const t = e.changedTouches[0]
this.touchStartX = t.clientX
this.touchStartY = t.clientY
},
onTouchMove(e) {
const t = e.changedTouches[0]
this.touchMoveX = t.clientX
this.touchMoveY = t.clientY
},
onTouchEnd(e) {
const t = e.changedTouches[0]
const endX = t.clientX
const endY = t.clientY
const deltaX = endX - this.touchStartX
const deltaY = endY - this.touchStartY
const threshold = 40
// 垂直滑动切换周/月
if (Math.abs(deltaY) > Math.abs(deltaX) && Math.abs(deltaY) > threshold) {
if (!this.allowWeekSwitch) return
if (deltaY < 0 && this.mode !== 'week') {
this.mode = 'week'
this.anchorDate = this.startOfWeek(this.safeDate(this.selected))
this.generateRenderDays()
} else if (deltaY > 0 && this.mode !== 'month') {
this.mode = 'month'
this.anchorDate = this.startOfMonth(this.safeDate(this.selected))
this.generateRenderDays()
}
return
}
// 水平滑动翻页
if (Math.abs(deltaX) > threshold && Math.abs(deltaX) >= Math.abs(deltaY)) {
if (deltaX < 0) this.slide(1)
else this.slide(-1)
}
},
slide(direction) {
if (this.mode === 'month') {
const y = this.anchorDate.getFullYear()
let m = this.anchorDate.getMonth() + 1 + direction
let year = y
if (m > 12) { m = 1; year++ }
if (m < 1) { m = 12; year-- }
this.anchorDate = new Date(year, m - 1, 1)
} else {
const d = this.safeDate(this.selected)
d.setDate(d.getDate() + direction * 7)
this.selected = this.formatDate(d)
this.anchorDate = this.startOfWeek(d)
}
this.generateRenderDays()
}
}
}
</script>
<style scoped>
.calendar-container {
width: 100%;
background: #fff;
border-radius: 12rpx;
box-shadow: 0 2rpx 16rpx #eee;
padding: 12rpx;
}
.calendar-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8rpx;
}
.calendar-content {
overflow: hidden;
}
.calendar-week {
display: grid;
grid-template-columns: repeat(7, 1fr);
color: #999;
font-size: 24rpx;
padding: 8rpx 0;
text-align: center;
}
.calendar-days {
display: grid;
grid-template-columns: repeat(7, 1fr);
grid-auto-rows: 80rpx; /* 每个格子固定高度 */
}
.calendar-day {
display: flex;
align-items: center;
justify-content: center;
width: 60rpx; /* 固定宽度 */
height: 60rpx; /* 固定高度,和宽度一致 */
margin: auto; /* 居中对齐,避免被挤压 */
font-size: 30rpx;
border-radius: 50%;
transition: background 0.2s;
}
.today {
color: #007aff;
font-weight: bold;
border: 1rpx solid #007aff;
}
.selected {
background: #007aff;
color: #fff;
}
.other-month {
color: #ccc;
}
</style>