This commit is contained in:
300
components/CommonCalendar.vue
Normal file
300
components/CommonCalendar.vue
Normal file
@@ -0,0 +1,300 @@
|
||||
<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>
|
Reference in New Issue
Block a user