首次提交:初始化项目代码
This commit is contained in:
@@ -0,0 +1,466 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user