Files
SmartParks_uniapp/components/CommonCalendar.vue
liyuanchao c7ff9a5234
Some checks failed
Uniapp 自动化打包 CI/CD / 打包 Uniapp 项目 (push) Has been cancelled
1.
2025-09-05 16:54:53 +08:00

301 lines
8.3 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: 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>