Files

466 lines
11 KiB
Vue
Raw Permalink 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="inline-calendar" :style="calendarStyle">
<view class="calendar-header" v-if="showMonthNav">
<view class="nav-btn" @click="prevMonth">
<text class="nav-icon"></text>
</view>
<text class="month-title">{{ currentYear }}{{ currentMonth }}</text>
<view class="nav-btn" @click="nextMonth">
<text class="nav-icon"></text>
</view>
</view>
<view class="weekday-header">
<view class="weekday-cell" v-for="(day, i) in weekdayLabels" :key="'wd-' + i">
<text class="weekday-text" :class="{ 'is-weekend': i === 5 || i === 6 }">{{ day }}</text>
</view>
</view>
<view class="days-grid">
<view
class="day-row"
v-for="(week, wi) in weeks"
:key="wi"
>
<view
class="day-item"
:class="getDayClass(date)"
:style="getDayStyle(date)"
v-for="(date, di) in week"
:key="di"
@click="onDateClick(date)"
>
<text class="day-number">{{ date.getDate() }}</text>
<view
class="mark-dot"
v-if="isMarked(date) && markType === 'dot'"
></view>
</view>
</view>
</view>
</view>
</template>
<script>
import themeMixin from '@/mixins/themeMixin.js'
import { THEME_CHANGE_EVENT } from '@/utils/theme'
// 周一到周日的完整中文名(展示在日历数字上方)
const WEEKDAY_LABELS = ['一', '二', '三', '四', '五', '六', '日']
export default {
name: 'InlineCalendar',
mixins: [themeMixin],
props: {
value: {
type: String,
default: ''
},
markDates: {
type: Array,
default: () => []
},
markType: {
type: String,
default: 'dot'
},
rangeSelect: {
type: Boolean,
default: false
},
startDate: {
type: String,
default: ''
},
endDate: {
type: String,
default: ''
},
showMonthNav: {
type: Boolean,
default: true
},
minDate: {
type: String,
default: ''
},
maxDate: {
type: String,
default: ''
}
},
data() {
return {
currentDate: new Date(),
innerStartDate: '',
innerEndDate: ''
}
},
computed: {
calendarStyle() {
return {
backgroundColor: this.themeConfig.cardBgColor,
borderRadius: '20rpx'
}
},
weekdayLabels() {
return WEEKDAY_LABELS
},
currentYear() {
return this.currentDate.getFullYear()
},
currentMonth() {
return this.currentDate.getMonth() + 1
},
todayStr() {
return this.formatDate(new Date())
},
weeks() {
const year = this.currentDate.getFullYear()
const month = this.currentDate.getMonth()
const firstDay = new Date(year, month, 1)
const dayOfWeek = firstDay.getDay()
const daysSinceMonday = dayOfWeek === 0 ? 6 : dayOfWeek - 1
const gridStart = new Date(year, month, 1 - daysSinceMonday)
const result = []
for (let w = 0; w < 6; w++) {
const week = []
for (let d = 0; d < 7; d++) {
const date = new Date(gridStart)
date.setDate(gridStart.getDate() + w * 7 + d)
week.push(date)
}
result.push(week)
}
return result
},
effectiveStart() {
return this.rangeSelect ? (this.startDate || this.innerStartDate) : ''
},
effectiveEnd() {
return this.rangeSelect ? (this.endDate || this.innerEndDate) : ''
}
},
watch: {
value: {
immediate: true,
handler(val) {
if (val) {
const d = this.parseDate(val)
if (d) {
this.currentDate = new Date(d.getFullYear(), d.getMonth(), 1)
}
}
}
},
startDate(val) {
if (val) this.innerStartDate = val
},
endDate(val) {
if (val) this.innerEndDate = val
}
},
created() {
this.syncThemeConfig()
this.watchThemeChange()
if (this.value) {
const d = this.parseDate(this.value)
if (d) {
this.currentDate = new Date(d.getFullYear(), d.getMonth(), 1)
}
}
if (this.startDate) this.innerStartDate = this.startDate
if (this.endDate) this.innerEndDate = this.endDate
},
beforeUnmount() {
if (this.themeChangeHandler) {
uni.$off(THEME_CHANGE_EVENT, this.themeChangeHandler)
}
},
methods: {
formatDate(date) {
const y = date.getFullYear()
const m = String(date.getMonth() + 1).padStart(2, '0')
const d = String(date.getDate()).padStart(2, '0')
return `${y}-${m}-${d}`
},
parseDate(str) {
if (!str) return null
const parts = str.split('-')
if (parts.length !== 3) return null
return new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2]))
},
isSameDay(d1, d2) {
return d1.getFullYear() === d2.getFullYear() &&
d1.getMonth() === d2.getMonth() &&
d1.getDate() === d2.getDate()
},
isToday(date) {
return this.isSameDay(date, new Date())
},
isCurrentMonth(date) {
return date.getFullYear() === this.currentYear &&
date.getMonth() === this.currentMonth - 1
},
isMarked(date) {
const str = this.formatDate(date)
return this.markDates.indexOf(str) !== -1
},
isDisabled(date) {
const str = this.formatDate(date)
if (this.minDate && str < this.minDate) return true
if (this.maxDate && str > this.maxDate) return true
return false
},
getDayClass(date) {
const str = this.formatDate(date)
const classes = {}
if (!this.isCurrentMonth(date)) classes['day-item--other-month'] = true
if (this.isToday(date)) classes['day-item--today'] = true
if (this.isDisabled(date)) classes['day-item--disabled'] = true
if (this.rangeSelect) {
if (this.effectiveStart && this.effectiveEnd) {
if (str === this.effectiveStart || str === this.effectiveEnd) {
classes['day-item--selected'] = true
} else if (str > this.effectiveStart && str < this.effectiveEnd) {
classes['day-item--in-range'] = true
}
} else if (this.effectiveStart && str === this.effectiveStart) {
classes['day-item--selected'] = true
}
} else {
if (this.value && str === this.value) {
classes['day-item--selected'] = true
}
}
if (this.isMarked(date) && this.markType === 'bg') {
classes['day-item--mark-bg'] = true
}
return classes
},
getDayStyle(date) {
const styles = {}
if (this.isToday(date)) {
styles.borderColor = this.themeConfig.primaryColor
}
if (this.isMarked(date) && this.markType === 'bg') {
styles.backgroundColor = this.themeConfig.primaryColor + '26'
}
if (!this.isCurrentMonth(date)) {
styles.color = this.themeConfig.textDisabled
}
return styles
},
onDateClick(date) {
if (this.isDisabled(date)) return
const str = this.formatDate(date)
if (this.rangeSelect) {
if (!this.innerStartDate || (this.innerStartDate && this.innerEndDate)) {
this.innerStartDate = str
this.innerEndDate = ''
} else {
if (str <= this.innerStartDate) {
this.innerEndDate = this.innerStartDate
this.innerStartDate = str
} else {
this.innerEndDate = str
}
this.$emit('rangeChange', {
start: this.innerStartDate,
end: this.innerEndDate
})
}
return
}
this.$emit('change', str)
this.$emit('input', str)
},
prevMonth() {
let y = this.currentYear
let m = this.currentMonth - 2
if (m < 0) {
m = 11
y--
}
this.currentDate = new Date(y, m, 1)
},
nextMonth() {
let y = this.currentYear
let m = this.currentMonth
if (m > 11) {
m = 0
y++
}
this.currentDate = new Date(y, m, 1)
}
}
}
</script>
<style lang="scss" scoped>
.inline-calendar {
padding: 24rpx;
user-select: none;
.calendar-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 24rpx;
}
.nav-btn {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 14rpx;
transition: background-color 0.2s;
}
.nav-btn:active {
background-color: rgba(0, 0, 0, 0.05);
}
.nav-icon {
font-size: 40rpx;
color: v-bind('themeConfig.textSecondary');
font-weight: 300;
}
.month-title {
font-size: 32rpx;
font-weight: 600;
color: v-bind('themeConfig.textPrimary');
}
.weekday-header {
display: flex;
gap: 8rpx;
margin: 0 auto 16rpx;
width: 552rpx; /* 与 .day-row 7*72 + 6*8 保持一致 */
}
.weekday-cell {
width: 72rpx;
flex-shrink: 0;
display: flex;
justify-content: center;
align-items: center;
height: 36rpx;
border-radius: 8rpx;
background-color: v-bind('themeConfig.bgColor');
}
.weekday-text {
font-size: 24rpx;
font-weight: 600;
color: v-bind('themeConfig.textSecondary');
letter-spacing: 1rpx;
}
.weekday-text.is-weekend {
color: v-bind('themeConfig.primaryColor');
}
.days-grid {
display: table;
flex-direction: column;
gap: 8rpx;
margin: 0 auto;
}
.day-row {
display: flex;
gap: 8rpx;
}
.day-item {
width: 72rpx;
height: 72rpx;
border-radius: 10rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
background-color: transparent;
transition: all 0.2s ease;
flex-shrink: 0;
}
.day-number {
font-size: 28rpx;
color: v-bind('themeConfig.textPrimary');
line-height: 1;
}
.day-item--other-month {
opacity: 0.25;
}
.day-item--other-month .day-number {
color: v-bind('themeConfig.textDisabled');
}
.day-item--today {
border: 2rpx solid transparent;
box-sizing: border-box;
}
.day-item--today .day-number {
font-weight: 600;
}
.day-item--selected {
background: v-bind('themeConfig.gradientSoft');
}
.day-item--selected .day-number {
color: #fff;
font-weight: 600;
}
.day-item--in-range {
background-color: v-bind('themeConfig.primaryColor + "1A"');
border-radius: 0;
}
.day-item--in-range .day-number {
color: v-bind('themeConfig.primaryColor');
}
.day-item--disabled {
opacity: 0.3;
pointer-events: none;
}
.day-item--mark-bg {
background-color: v-bind('themeConfig.primaryColor + "26"');
}
.mark-dot {
position: absolute;
bottom: 8rpx;
width: 8rpx;
height: 8rpx;
border-radius: 50%;
background-color: v-bind('themeConfig.primaryColor');
}
.day-item--selected .mark-dot {
background-color: #fff;
}
.day-item--in-range .mark-dot {
background-color: v-bind('themeConfig.primaryColor');
}
}
</style>