Some checks failed
Uniapp 自动化打包 CI/CD / 打包 Uniapp 项目 (push) Has been cancelled
301 lines
8.3 KiB
Vue
301 lines
8.3 KiB
Vue
<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>
|