首次提交:初始化项目代码

This commit is contained in:
sunct
2026-07-31 14:42:05 +08:00
commit ecca407485
84 changed files with 24158 additions and 0 deletions
+466
View File
@@ -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>