<script setup> import { ref, computed, watch, onMounted } from 'vue' // Props 定义 const props = defineProps({ lang: { type: String, default: 'zh' }, type: { type: String, default: 'calendar' }, checkDate: { type: Boolean, default: false }, bgweek: { type: String, default: '#FF8F22' }, bgday: { type: String, default: '#FF8F22' }, signin_but_bg: { type: String, default: '#909399' }, supplementary: { type: Boolean, default: true }, already: { type: Array, default: () => [] }, checkinDays: { type: [Number, String], default: 0 }, integral: { type: [Number, String], default: 0 }, isIntegral: { type: Boolean, default: false } }) // Emits const emit = defineEmits(['shift', 'change']) // 响应式数据 const weeked = ref('') const dayArr = ref([]) const localDate = ref('') const currentDate = new Date() const year = ref(currentDate.getFullYear()) const month = ref(currentDate.getMonth() + 1) const day = ref(currentDate.getDate()) const aheadDay = ref(0) const prv = ref(true) const next = ref(true) const is_day_signin = ref(false) // 计算属性 const weekArr = computed(() => props.lang === 'zh' ? ['日', '一', '二', '三', '四', '五', '六'] : ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] ) const thisMonth = computed(() => currentDate.getMonth() + 1) // 格式化数字 const formatNum = (num) => num < 10 ? `0${num}` : num // 初始化日期 const initDate = () => { dayArr.value = [] const totalDay = new Date(year.value, month.value, 0).getDate() for (let i = 1; i <= totalDay; i++) { const value = new Date(year.value, month.value - 1, i).getDay() if (i === 1 && value !== 0) { addBefore(value) aheadDay.value = value } const dateObj = { date: `${year.value}-${formatNum(month.value)}-${formatNum(i)}`, day: i, flag: false } dayArr.value.push(dateObj) if (i === totalDay && value !== 6) { addAfter(value) } } } // 补充前空白日期 const addBefore = (value) => { const totalDay = new Date(year.value, month.value - 1, 0).getDate() for (let i = 0; i < value; i++) { dayArr.value.push({ date: '', day: totalDay - (value - i) + 1 }) } } // 补充后空白日期 const addAfter = (value) => { for (let i = 0; i < (6 - value); i++) { dayArr.value.push({ date: '', day: i + 1 }) } } // 签到处理 const daySign = (obj) => { const index = aheadDay.value + day.value - 1 if (dayArr.value[index].flag) return false dayArr.value[index].flag = true emit('change', obj.date) is_day_signin.value = true showSuccessToast('已签到') } // 补签处理 const signToday = (obj, index) => { if (props.type === 'calendar') return if (currentDate.getMonth() + 1 !== parseInt(obj.date.split('-')[1])) return if (obj.date && obj.day < day.value) { if (dayArr.value[index].flag) { showFailToast('已签到') } else { if (day.value > obj.day && !props.supplementary) return showSuccessToast(day.value > obj.day ? '补签成功' : '签到成功') dayArr.value[index].flag = true emit('change', obj.date) } } } // 月份切换 const changeMonth = (direction) => { if (direction === 'prev') { if (month.value === 1) { year.value-- month.value = 12 } else { month.value-- } } else { if (month.value === 12) { year.value++ month.value = 1 } else { month.value++ } } initDate() updateNavigationButtons() } // 更新导航按钮状态 const updateNavigationButtons = () => { prv.value = year.value >= currentDate.getFullYear() || month.value > currentDate.getMonth() + 2 next.value = year.value <= currentDate.getFullYear() || month.value < currentDate.getMonth() } // 监听已签到数据变化 watch(() => props.already, (newVal) => { dayArr.value.forEach((day, index) => { const timestamp = new Date(day.date).getTime() day.flag = newVal.includes(timestamp) if (day.flag && day.date === localDate.value) { is_day_signin.value = true } }) }, { deep: true }) // 初始化 onMounted(() => { initDate() localDate.value = `${year.value}-${formatNum(month.value)}-${formatNum(day.value)}` weeked.value = weekArr.value[currentDate.getDay()] if (props.type !== 'calendar') { dayArr.value.forEach(day => day.flag = false) } }) </script> <template> <div class="calendar-container"> <!-- 头部信息 --> <div class="header"> <div class="checkin-info"> <h4>已连续签到 <span>{{ checkinDays }}</span> 天</h4> <p v-if="isIntegral">今日获得+{{ integral }}信用分</p> </div> <!-- <div class="actions"> <span v-if="supplementary" class="makeup-btn" @click="$emit('shift')"> 补签 </span> </div> --> </div> <!-- 日历主体 --> <div class="calendar-body"> <!-- 月份导航 --> <div class="month-nav"> <div class="nav-btn" @click="changeMonth('prev')"> <span v-show="prv">上月</span> </div> <div class="current-month">{{ year }}年{{ month }}月</div> <div class="nav-btn" @click="changeMonth('next')"> <span v-show="next">下月</span> </div> </div> <!-- 星期栏 --> <div class="week-row"> <div v-for="week in weekArr" :key="week" class="week-cell" :style="{ color: week === weeked ? bgweek : '' }"> {{ week }} </div> </div> <!-- 日期格子 --> <div class="date-grid"> <div v-for="(date, index) in dayArr" :key="index" class="date-cell" :class="{ 'empty': date.date === '', 'selected': date.date === localDate || date.flag }" :style="{ background: (date.date === localDate || date.flag) ? bgday : '' }" @click="signToday(date, index)"> {{ date.day }} <div :class="{ 'dot': date.flag, 'makeup': date.day < day, 'today': date.day === day }" /> </div> </div> </div> <!-- 签到按钮 --> <div class="sign-button"> <button :disabled="thisMonth !== month" :style="{ background: is_day_signin ? signin_but_bg : (thisMonth === month ? bgday : signin_but_bg) }" @click="daySign(dayArr[aheadDay + day - 1])"> 签到 </button> </div> </div> </template> <style lang="scss" scoped> .calendar-container { width: 100%; display: flex; flex-direction: column; .header { display: flex; justify-content: space-between; align-items: center; padding: 15px; background: #fff; border-radius: 10px; margin-bottom: 8px; .checkin-info { h4 { font-weight: 600; font-size: 18px; line-height: 25px; span { color: #FF871E; margin: 0 5px; font-size: 16px; } } p { font-size: 14px; line-height: 20px; color: #FF871E; } } .makeup-btn { font-size: 12px; color: #FF871E; border: 1px solid #FF871E; padding: 5px 10px; border-radius: 16px; &:active { opacity: 0.8; } } } .calendar-body { padding: 10px 20px; background: #fff; border-radius: 12px; .month-nav { display: flex; justify-content: space-between; align-items: center; margin: 15px 0; .nav-btn { min-width: 35px; cursor: pointer; &:active { opacity: 0.8; } } } .week-row { display: flex; justify-content: space-between; .week-cell { width: 35px; height: 35px; display: flex; align-items: center; justify-content: center; } } .date-grid { display: flex; flex-wrap: wrap; .date-cell { width: 35px; height: 35px; margin: 5px; display: flex; align-items: center; justify-content: center; position: relative; &.empty { color: #999; } &.selected { color: #fff; border-radius: 50%; } .dot { width: 5px; height: 5px; border-radius: 50%; position: absolute; bottom: 5%; left: 50%; transform: translateX(-50%); background: #fff; } } } } .sign-button { display: flex; justify-content: center; margin-top: 40px; button { width: 325px; height: 40px; border-radius: 15px; border: none; outline: none; color: #fff; font-size: 16px; &:active { opacity: 0.9; } &:disabled { opacity: 0.6; } } } } </style>