Files
SmartParks_uniapp/components/CommonCalendar.vue
liyuanchao 57fe929080
Some checks failed
Uniapp 自动化打包 CI/CD / 打包 Uniapp 项目 (push) Has been cancelled
请假 巡检
2025-09-12 09:24:59 +08:00

315 lines
8.8 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">
<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: false } // 控制是否允许上下滑切换
},
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(newMode, oldMode) {
this.generateRenderDays()
// 通知父组件mode已改变
this.$emit('modeChange', newMode);
},
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-- }
// 切换后的基准日期设为该月1号
this.anchorDate = new Date(year, m - 1, 1)
this.selected = this.formatDate(this.anchorDate) // ✅ 选中当月1号
this.$emit('dateChange', this.selected)
} else {
// 以当前 anchorDate 为基准移动周
const d = this.safeDate(this.anchorDate)
d.setDate(d.getDate() + direction * 7)
// 更新基准周
this.anchorDate = this.startOfWeek(d)
// ✅ 选中该周周日startOfWeek返回的就是周日
this.selected = this.formatDate(this.anchorDate)
this.$emit('dateChange', this.selected)
}
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>