AI生成的 vue3 日历组件,显示农历与节日,日期可选择,年月可切换

html 复制代码
<template>
  <div class="calendar-container">
    <!-- 头部控制区域 -->
    <div class="calendar-header">
      <button @click="previousYear" class="control-btn" title="上一年">
        &lt;&lt;
      </button>
      <button @click="previousMonth" class="control-btn" title="上一月">
        &lt;
      </button>
      
      <div class="date-display">
        <select v-model="currentYear" @change="handleYearChange" class="year-select">
          <option v-for="year in yearRange" :key="year" :value="year">
            {{ year }}年
          </option>
        </select>
        <select v-model="currentMonth" @change="handleMonthChange" class="month-select">
          <option v-for="month in 12" :key="month" :value="month - 1">
            {{ month }}月
          </option>
        </select>
      </div>
      
      <button @click="nextMonth" class="control-btn" title="下一月">
        &gt;
      </button>
      <button @click="nextYear" class="control-btn" title="下一年">
        &gt;&gt;
      </button>
      
      <button @click="goToday" class="today-btn">
        今天
      </button>
    </div>

    <!-- 星期标题 -->
    <div class="week-header">
      <div class="week-day" v-for="day in weekDays" :key="day">
        {{ day }}
      </div>
    </div>

    <!-- 日期网格 -->
    <div class="calendar-grid">
      <div 
        v-for="(day, index) in calendarDays" 
        :key="index"
        :class="[
          'calendar-day',
          {
            'current-month': day.isCurrentMonth,
            'today': isToday(day.date),
            'selected': isSelected(day.date),
            'weekend': isWeekend(day.date),
            'holiday': day.isHoliday
          }
        ]"
        @click="selectDate(day)"
      >
        <!-- 阳历日期 -->
        <div class="solar-date">
          {{ day.day }}
        </div>
        
        <!-- 农历和节日信息 -->
        <div class="lunar-info">
          <div v-if="day.festival" class="festival" :class="day.festivalType">
            {{ day.festival }}
          </div>
          <div v-else-if="day.isTerm" class="solar-term">
            {{ day.termName }}
          </div>
          <div v-else class="lunar-day">
            {{ day.lunarDayDisplay }}
          </div>
        </div>
        
        <!-- 农历月份(如果是初一) -->
        <div v-if="day.isFirstDayOfMonth" class="lunar-month">
          {{ day.lunarMonthDisplay }}月
        </div>
      </div>
    </div>

    <!-- 选中的日期信息 -->
    <div v-if="selectedDay" class="selected-info">
      <h3>选中日期信息</h3>
      <p>阳历: {{ formatDate(selectedDay.date) }}</p>
      <p>农历: {{ selectedDay.lunarDateDisplay }}</p>
      <p v-if="selectedDay.festival">节日: {{ selectedDay.festival }}</p>
      <p v-if="selectedDay.termName">节气: {{ selectedDay.termName }}</p>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'

// 简化农历数据 - 使用内置数据,不依赖外部库
const LUNAR_MONTHS = ['正月', '二月', '三月', '四月', '五月', '六月', 
                     '七月', '八月', '九月', '十月', '冬月', '腊月']
const LUNAR_DAYS = ['初一', '初二', '初三', '初四', '初五', '初六', '初七', '初八', '初九', '初十',
                   '十一', '十二', '十三', '十四', '十五', '十六', '十七', '十八', '十九', '二十',
                   '廿一', '廿二', '廿三', '廿四', '廿五', '廿六', '廿七', '廿八', '廿九', '三十']

// 节日数据
const FESTIVALS = {
  // 阳历节日
  solar: {
    '0101': '元旦',
    '0214': '情人节',
    '0308': '妇女节',
    '0312': '植树节',
    '0401': '愚人节',
    '0501': '劳动节',
    '0504': '青年节',
    '0601': '儿童节',
    '0701': '建党节',
    '0801': '建军节',
    '0910': '教师节',
    '1001': '国庆节',
    '1224': '平安夜',
    '1225': '圣诞节'
  },
  // 农历节日(需要根据实际日期计算)
  lunar: {
    '0101': '春节',
    '0115': '元宵',
    '0505': '端午',
    '0707': '七夕',
    '0815': '中秋',
    '0909': '重阳',
    '1208': '腊八'
  }
}

// 节气数据(简化版,每年日期有微调)
const SOLAR_TERMS = [
  { month: 2, day: 3, name: '立春' },
  { month: 2, day: 18, name: '雨水' },
  { month: 3, day: 5, name: '惊蛰' },
  { month: 3, day: 20, name: '春分' },
  { month: 4, day: 4, name: '清明' },
  { month: 4, day: 20, name: '谷雨' },
  { month: 5, day: 5, name: '立夏' },
  { month: 5, day: 21, name: '小满' },
  { month: 6, day: 5, name: '芒种' },
  { month: 6, day: 21, name: '夏至' },
  { month: 7, day: 7, name: '小暑' },
  { month: 7, day: 23, name: '大暑' },
  { month: 8, day: 7, name: '立秋' },
  { month: 8, day: 23, name: '处暑' },
  { month: 9, day: 7, name: '白露' },
  { month: 9, day: 23, name: '秋分' },
  { month: 10, day: 8, name: '寒露' },
  { month: 10, day: 23, name: '霜降' },
  { month: 11, day: 7, name: '立冬' },
  { month: 11, day: 22, name: '小雪' },
  { month: 12, day: 7, name: '大雪' },
  { month: 12, day: 21, name: '冬至' }
]

interface CalendarDay {
  date: Date
  day: number
  isCurrentMonth: boolean
  lunarMonth: number
  lunarDay: number
  lunarMonthDisplay: string
  lunarDayDisplay: string
  lunarDateDisplay: string
  isFirstDayOfMonth: boolean
  festival?: string
  festivalType?: 'traditional' | 'solar' | 'other'
  isTerm: boolean
  termName?: string
  isHoliday: boolean
}

// 响应式数据
const currentYear = ref(new Date().getFullYear())
const currentMonth = ref(new Date().getMonth())
const selectedDate = ref<Date | null>(new Date())
const selectedDay = ref<CalendarDay | null>(null)

// 星期标题
const weekDays = ['日', '一', '二', '三', '四', '五', '六']

// 年份范围
const yearRange = computed(() => {
  const current = new Date().getFullYear()
  return Array.from({ length: 11 }, (_, i) => current - 5 + i)
})

// 计算农历日期(简化版,使用固定算法)
function calculateLunarDate(solarDate: Date) {
  // 这是一个简化的农历计算,实际农历算法很复杂
  // 这里使用一个近似算法用于演示
  const year = solarDate.getFullYear()
  const month = solarDate.getMonth() + 1
  const day = solarDate.getDate()
  
  // 简化的农历计算(实际项目中应该使用完整算法)
  // 这里只做演示,返回固定的农历日期
  const baseDate = new Date(2024, 0, 1) // 2024年1月1日是农历冬月二十
  const diffDays = Math.floor((solarDate.getTime() - baseDate.getTime()) / (1000 * 60 * 60 * 24))
  
  let lunarMonth = 11 // 冬月
  let lunarDay = 20 + diffDays
  
  // 调整农历月份和日期
  while (lunarDay > 30) {
    lunarDay -= 30
    lunarMonth = (lunarMonth % 12) + 1
  }
  
  if (lunarDay < 1) {
    lunarMonth -= 1
    if (lunarMonth < 1) lunarMonth = 12
    lunarDay += 30
  }
  
  return {
    lunarMonth,
    lunarDay: Math.floor(lunarDay),
    lunarMonthDisplay: LUNAR_MONTHS[(lunarMonth - 1 + 12) % 12],
    lunarDayDisplay: LUNAR_DAYS[Math.floor(lunarDay) - 1] || LUNAR_DAYS[0]
  }
}

// 检查节气
function checkSolarTerm(date: Date) {
  const month = date.getMonth() + 1
  const day = date.getDate()
  
  const term = SOLAR_TERMS.find(t => t.month === month && Math.abs(t.day - day) <= 1)
  return term ? { isTerm: true, termName: term.name } : { isTerm: false }
}

// 检查节日
function checkFestival(date: Date, lunarMonth: number, lunarDay: number) {
  const month = (date.getMonth() + 1).toString().padStart(2, '0')
  const day = date.getDate().toString().padStart(2, '0')
  const solarKey = `${month}${day}`
  
  // 检查阳历节日
  if (FESTIVALS.solar[solarKey as keyof typeof FESTIVALS.solar]) {
    return {
      festival: FESTIVALS.solar[solarKey as keyof typeof FESTIVALS.solar],
      festivalType: 'solar' as const,
      isHoliday: true
    }
  }
  
  // 检查农历节日
  const lunarMonthStr = lunarMonth.toString().padStart(2, '0')
  const lunarDayStr = lunarDay.toString().padStart(2, '0')
  const lunarKey = `${lunarMonthStr}${lunarDayStr}`
  
  if (FESTIVALS.lunar[lunarKey as keyof typeof FESTIVALS.lunar]) {
    return {
      festival: FESTIVALS.lunar[lunarKey as keyof typeof FESTIVALS.lunar],
      festivalType: 'traditional' as const,
      isHoliday: true
    }
  }
  
  // 特殊判断:农历初一
  if (lunarDay === 1) {
    return { isHoliday: false }
  }
  
  return { isHoliday: false }
}

// 创建日历天对象
function createCalendarDay(date: Date, isCurrentMonth: boolean): CalendarDay {
  const day = date.getDate()
  
  // 计算农历
  const lunar = calculateLunarDate(date)
  
  // 检查节日
  const festivalInfo = checkFestival(date, lunar.lunarMonth, lunar.lunarDay)
  
  // 检查节气
  const termInfo = checkSolarTerm(date)
  
  const isFirstDayOfMonth = lunar.lunarDay === 1
  
  return {
    date,
    day,
    isCurrentMonth,
    lunarMonth: lunar.lunarMonth,
    lunarDay: lunar.lunarDay,
    lunarMonthDisplay: lunar.lunarMonthDisplay,
    lunarDayDisplay: lunar.lunarDayDisplay,
    lunarDateDisplay: `${lunar.lunarMonthDisplay}${lunar.lunarDayDisplay}`,
    isFirstDayOfMonth,
    isTerm: termInfo.isTerm,
    termName: termInfo.termName,
    isHoliday: festivalInfo.isHoliday || termInfo.isTerm,
    ...festivalInfo,
    ...(termInfo.isTerm ? { festivalType: 'other' as const } : {})
  }
}

// 计算日历天数
const calendarDays = computed(() => {
  const days: CalendarDay[] = []
  const firstDayOfMonth = new Date(currentYear.value, currentMonth.value, 1)
  const lastDayOfMonth = new Date(currentYear.value, currentMonth.value + 1, 0)
  
  // 计算上个月需要显示的天数
  const firstDayWeek = firstDayOfMonth.getDay()
  const lastDayOfPrevMonth = new Date(currentYear.value, currentMonth.value, 0).getDate()
  
  // 添加上个月末尾的几天
  for (let i = firstDayWeek - 1; i >= 0; i--) {
    const date = new Date(currentYear.value, currentMonth.value - 1, lastDayOfPrevMonth - i)
    days.push(createCalendarDay(date, false))
  }
  
  // 添加当前月的所有天
  const daysInMonth = lastDayOfMonth.getDate()
  for (let i = 1; i <= daysInMonth; i++) {
    const date = new Date(currentYear.value, currentMonth.value, i)
    days.push(createCalendarDay(date, true))
  }
  
  // 添加下个月开头的几天
  const totalCells = 42 // 6行 * 7列
  const remainingCells = totalCells - days.length
  for (let i = 1; i <= remainingCells; i++) {
    const date = new Date(currentYear.value, currentMonth.value + 1, i)
    days.push(createCalendarDay(date, false))
  }
  
  return days
})

// 检查是否是今天
function isToday(date: Date): boolean {
  const today = new Date()
  return date.toDateString() === today.toDateString()
}

// 检查是否是周末
function isWeekend(date: Date): boolean {
  const day = date.getDay()
  return day === 0 || day === 6
}

// 检查是否是选中日期
function isSelected(date: Date): boolean {
  if (!selectedDate.value) return false
  return date.toDateString() === selectedDate.value.toDateString()
}

// 选择日期
function selectDate(day: CalendarDay) {
  selectedDate.value = day.date
  selectedDay.value = day
  emit('select', {
    date: day.date,
    solar: `${currentYear.value}-${currentMonth.value + 1}-${day.day}`,
    lunar: day.lunarDateDisplay,
    festival: day.festival,
    solarTerm: day.termName
  })
}

// 导航控制
function previousMonth() {
  if (currentMonth.value === 0) {
    currentMonth.value = 11
    currentYear.value--
  } else {
    currentMonth.value--
  }
}

function nextMonth() {
  if (currentMonth.value === 11) {
    currentMonth.value = 0
    currentYear.value++
  } else {
    currentMonth.value++
  }
}

function previousYear() {
  currentYear.value--
}

function nextYear() {
  currentYear.value++
}

function handleYearChange() {
  // 年份变化处理
}

function handleMonthChange() {
  // 月份变化处理
}

function goToday() {
  const today = new Date()
  currentYear.value = today.getFullYear()
  currentMonth.value = today.getMonth()
  const day = createCalendarDay(today, true)
  selectDate(day)
}

function formatDate(date: Date): string {
  const weekdays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
  const year = date.getFullYear()
  const month = date.getMonth() + 1
  const day = date.getDate()
  const weekday = weekdays[date.getDay()]
  return `${year}年${month}月${day}日 ${weekday}`
}

// 定义emit
const emit = defineEmits<{
  select: [payload: {
    date: Date
    solar: string
    lunar: string
    festival?: string
    solarTerm?: string
  }]
}>()

// 初始化
onMounted(() => {
  goToday()
})

// 暴露方法
defineExpose({
  goToday,
  setDate: (date: Date) => {
    currentYear.value = date.getFullYear()
    currentMonth.value = date.getMonth()
    const day = createCalendarDay(date, true)
    selectDate(day)
  }
})
</script>

<style scoped>
/* 样式部分保持不变,和之前的组件一样 */
.calendar-container {
  font-family: 'Microsoft YaHei', sans-serif;
  max-width: 800px;
  margin: 0 auto;
  background: white;
  border-radius: 12px;
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
  overflow: hidden;
}

.calendar-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 20px;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  color: white;
}

.control-btn {
  background: rgba(255, 255, 255, 0.2);
  border: none;
  color: white;
  padding: 8px 12px;
  border-radius: 6px;
  cursor: pointer;
  font-size: 14px;
  transition: background 0.3s;
}

.control-btn:hover {
  background: rgba(255, 255, 255, 0.3);
}

.date-display {
  display: flex;
  align-items: center;
  gap: 10px;
}

.year-select,
.month-select {
  background: rgba(255, 255, 255, 0.9);
  border: none;
  padding: 8px 12px;
  border-radius: 6px;
  font-size: 16px;
  color: #333;
  cursor: pointer;
}

.today-btn {
  background: #4CAF50;
  color: white;
  border: none;
  padding: 8px 20px;
  border-radius: 6px;
  cursor: pointer;
  font-weight: bold;
  transition: background 0.3s;
}

.today-btn:hover {
  background: #45a049;
}

.week-header {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  background: #f8f9fa;
  padding: 12px 0;
  border-bottom: 1px solid #e9ecef;
}

.week-day {
  text-align: center;
  font-weight: bold;
  color: #666;
  font-size: 14px;
}

.calendar-grid {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 1px;
  background: #f0f0f0;
  padding: 1px;
}

.calendar-day {
  background: white;
  min-height: 100px;
  padding: 8px;
  cursor: pointer;
  transition: all 0.2s;
  border-radius: 4px;
  display: flex;
  flex-direction: column;
  position: relative;
}

.calendar-day:hover {
  background: #f8f9fa;
  transform: translateY(-2px);
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}

.calendar-day.current-month {
  background: white;
}

.calendar-day:not(.current-month) {
  background: #f8f9fa;
  color: #999;
}

.calendar-day.today {
  background: #e3f2fd;
  border: 2px solid #2196F3;
}

.calendar-day.selected {
  background: #e8f5e9;
  border: 2px solid #4CAF50;
}

.calendar-day.weekend .solar-date {
  color: #ff6b6b;
}

.calendar-day.holiday {
  background: #fff3e0;
}

.solar-date {
  font-size: 20px;
  font-weight: bold;
  color: #333;
  text-align: center;
}

.lunar-info {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-top: 4px;
}

.lunar-day,
.festival,
.solar-term {
  font-size: 12px;
  text-align: center;
  padding: 2px 4px;
  border-radius: 3px;
}

.festival.traditional {
  color: #d32f2f;
  background: #ffebee;
  font-weight: bold;
}

.festival.solar {
  color: #1976d2;
  background: #e3f2fd;
  font-weight: bold;
}

.festival.other {
  color: #388e3c;
  background: #e8f5e9;
}

.solar-term {
  color: #388e3c;
  background: #e8f5e9;
  font-weight: bold;
}

.lunar-month {
  position: absolute;
  top: 4px;
  left: 4px;
  font-size: 10px;
  color: #999;
}

.selected-info {
  margin-top: 20px;
  padding: 20px;
  background: #f8f9fa;
  border-radius: 8px;
  border-top: 1px solid #e9ecef;
}

.selected-info h3 {
  margin-top: 0;
  color: #333;
}

.selected-info p {
  margin: 8px 0;
  color: #666;
}
</style>

更新后:

html 复制代码
<template>
  <div class="calendar-container">
    <!-- 头部控制区域 -->
    <div class="calendar-header relative">
      <div class="header-left">
        <button @click="previousYear" class="control-btn" title="上一年">
          &lt;&lt;
        </button>
        <button @click="previousMonth" class="control-btn" title="上一月">
          &lt;
        </button>
      </div>
      
      <div class="date-display" @click="showYearMonthPicker = true">
        <text class="date-text">{{ currentYear }}年{{ currentMonth + 1 }}月</text>
      </div>
      
      <div class="header-right">
        <button @click="nextMonth" class="control-btn" title="下一月">
          &gt;
        </button>
        <button @click="nextYear" class="control-btn" title="下一年">
          &gt;&gt;
        </button>
      </div>
      <button @click="goToday" class="today-btn">
        今天
      </button>
    </div>

    <!-- uview-pro 年月选择器 -->
    <u-picker
      v-model="showYearMonthPicker"
      mode="multiSelector"
      :range="pickerRange"
      :default-selector="pickerDefaultSelector"
      range-key="label"
      @confirm="onPickerConfirm"
      @cancel="showYearMonthPicker = false"
    />

    <!-- 星期标题 -->
    <div class="week-header">
      <div class="week-day" v-for="day in weekDays" :key="day">
        {{ day }}
      </div>
    </div>

    <!-- 日期网格 -->
    <div class="calendar-grid">
      <div 
        v-for="(day, index) in calendarDays" 
        :key="index"
        :class="[
          'calendar-day',
          {
            'current-month': day.isCurrentMonth,
            'other-month': !day.isCurrentMonth,
            'today': isToday(day.date),
            'selected': isSelected(day.date),
            'weekend': isWeekend(day.date),
            'holiday': day.isHoliday
          }
        ]"
        @click="selectDate(day)"
      >
        <!-- 阳历日期 -->
        <div class="solar-date">
          {{ day.day }}
          <span v-if="isToday(day.date)" class="today-dot"></span>
        </div>
        
        <!-- 农历和节日信息 -->
        <div class="lunar-info">
          <div v-if="day.festival" class="festival" :class="day.festivalType">
            {{ mobileMode && day.festival.length > 3 ? day.festival.substring(0, 3) : day.festival }}
          </div>
          <div v-else-if="day.isTerm" class="solar-term">
            {{ mobileMode && day.termName && day.termName.length > 2 ? day.termName.substring(0, 2) : day.termName }}
          </div>
          <div v-else class="lunar-day">
            {{ day.lunarDayDisplay }}
          </div>
        </div>
        
        <!-- 农历月份(如果是初一) -->
        <div v-if="day.isFirstDayOfMonth && day.isCurrentMonth" class="lunar-month">
          {{ day.lunarMonthDisplay }}
        </div>
        
        <!-- 选中小圆点 -->
        <div v-if="isSelected(day.date)" class="selected-dot"></div>
      </div>
    </div>

    <!-- 选中的日期信息 -->
    <div v-if="selectedDay && !mobileMode" class="selected-info">
      <h3>选中日期信息</h3>
      <p>阳历: {{ formatDate(selectedDay.date) }}</p>
      <p>农历: {{ selectedDay.lunarDateDisplay }}</p>
      <p v-if="selectedDay.festival">节日: {{ selectedDay.festival }}</p>
      <p v-if="selectedDay.termName">节气: {{ selectedDay.termName }}</p>
    </div>
    
    <!-- 移动端选中信息 -->
    <div v-if="selectedDay && mobileMode" class="mobile-selected-info">
      <div class="mobile-selected-content">
        <div class="mobile-date-main">
          <div class="mobile-solar">{{ selectedDay.day }}</div>
          <div class="mobile-date-details">
            <div class="mobile-week">{{ getWeekday(selectedDay.date) }}</div>
            <div class="mobile-lunar">{{ selectedDay.lunarDateDisplay }}</div>
          </div>
        </div>
        <div v-if="selectedDay.festival || selectedDay.termName" class="mobile-festival">
          {{ selectedDay.festival || selectedDay.termName }}
        </div>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue'

// 简化农历数据
const LUNAR_MONTHS = ['正月', '二月', '三月', '四月', '五月', '六月', 
                     '七月', '八月', '九月', '十月', '冬月', '腊月']
const LUNAR_DAYS = ['初一', '初二', '初三', '初四', '初五', '初六', '初七', '初八', '初九', '初十',
                   '十一', '十二', '十三', '十四', '十五', '十六', '十七', '十八', '十九', '二十',
                   '廿一', '廿二', '廿三', '廿四', '廿五', '廿六', '廿七', '廿八', '廿九', '三十']

// 节日数据
const FESTIVALS = {
  solar: {
    '0101': '元旦',
    '0214': '情人节',
    '0308': '妇女节',
    '0312': '植树节',
    '0401': '愚人节',
    '0501': '劳动节',
    '0504': '青年节',
    '0601': '儿童节',
    '0701': '建党节',
    '0801': '建军节',
    '0910': '教师节',
    '1001': '国庆节',
    '1224': '平安夜',
    '1225': '圣诞节'
  },
  lunar: {
    '0101': '春节',
    '0115': '元宵',
    '0505': '端午',
    '0707': '七夕',
    '0815': '中秋',
    '0909': '重阳',
    '1208': '腊八'
  }
}

// 节气数据(每年日期有微调,这里是近似值)
// 2024年的节气日期作为基准
const SOLAR_TERMS_2024 = [
  { month: 2, day: 4, name: '立春' },
  { month: 2, day: 19, name: '雨水' },
  { month: 3, day: 5, name: '惊蛰' },
  { month: 3, day: 20, name: '春分' },
  { month: 4, day: 4, name: '清明' },
  { month: 4, day: 19, name: '谷雨' },
  { month: 5, day: 5, name: '立夏' },
  { month: 5, day: 20, name: '小满' },
  { month: 6, day: 5, name: '芒种' },
  { month: 6, day: 21, name: '夏至' },
  { month: 7, day: 6, name: '小暑' },
  { month: 7, day: 22, name: '大暑' },
  { month: 8, day: 7, name: '立秋' },
  { month: 8, day: 22, name: '处暑' },
  { month: 9, day: 7, name: '白露' },
  { month: 9, day: 22, name: '秋分' },
  { month: 10, day: 8, name: '寒露' },
  { month: 10, day: 23, name: '霜降' },
  { month: 11, day: 7, name: '立冬' },
  { month: 11, day: 22, name: '小雪' },
  { month: 12, day: 6, name: '大雪' },
  { month: 12, day: 21, name: '冬至' }
]

interface CalendarDay {
  date: Date
  day: number
  isCurrentMonth: boolean
  lunarMonth: number
  lunarDay: number
  lunarMonthDisplay: string
  lunarDayDisplay: string
  lunarDateDisplay: string
  isFirstDayOfMonth: boolean
  festival?: string
  festivalType?: 'traditional' | 'solar' | 'other'
  isTerm: boolean
  termName?: string
  isHoliday: boolean
}

// 响应式数据
const currentYear = ref(new Date().getFullYear())
const currentMonth = ref(new Date().getMonth())
const selectedDate = ref<Date | null>(new Date())
const selectedDay = ref<CalendarDay | null>(null)
const mobileMode = ref(false)
const showYearMonthPicker = ref(false)

// 监听窗口大小变化
const checkMobileMode = () => {
  mobileMode.value = window.innerWidth < 768
}

// 星期标题
const weekDays = ['日', '一', '二', '三', '四', '五', '六']

// 年份范围
const yearRange = computed(() => {
  const current = new Date().getFullYear()
  return Array.from({ length: 11 }, (_, i) => current - 5 + i)
})

// picker 数据 - 二维数组格式
const pickerRange = computed(() => {
  const current = new Date().getFullYear()
  const years = Array.from({ length: 21 }, (_, i) => ({
    label: `${current - 10 + i}年`,
    value: current - 10 + i
  }))
  const months = Array.from({ length: 12 }, (_, i) => ({
    label: `${i + 1}月`,
    value: i
  }))
  return [years, months]
})

// picker 默认选中索引 - 数组格式,表示各列选中的索引
const pickerDefaultSelector = computed(() => {
  const current = new Date().getFullYear()
  const yearIndex = currentYear.value - (current - 10)
  const monthIndex = currentMonth.value
  return [yearIndex, monthIndex]
})

// picker 确认事件 - 回调参数是索引数组
function onPickerConfirm(indexes: number[]) {
  // indexes 是数组,如 [10, 5] 表示第一列选中第10个,第二列选中第5个
  const [yearIndex, monthIndex] = indexes
  const yearItem = pickerRange.value[0][yearIndex]
  const monthItem = pickerRange.value[1][monthIndex]
  
  if (yearItem && monthItem) {
    currentYear.value = yearItem.value
    currentMonth.value = monthItem.value
  }
  showYearMonthPicker.value = false
}

// 计算农历日期(简化版)
function calculateLunarDate(solarDate: Date) {
  const year = solarDate.getFullYear()
  const month = solarDate.getMonth() + 1
  const day = solarDate.getDate()
  
  // 简化的农历计算
  const baseDate = new Date(2024, 0, 1)
  const diffDays = Math.floor((solarDate.getTime() - baseDate.getTime()) / (1000 * 60 * 60 * 24))
  
  let lunarMonth = 11
  let lunarDay = 20 + diffDays
  
  // 调整农历月份和日期
  while (lunarDay > 30) {
    lunarDay -= 30
    lunarMonth = (lunarMonth % 12) + 1
  }
  
  if (lunarDay < 1) {
    lunarMonth -= 1
    if (lunarMonth < 1) lunarMonth = 12
    lunarDay += 30
  }
  
  const actualLunarDay = Math.max(1, Math.min(30, Math.floor(lunarDay)))
  
  return {
    lunarMonth,
    lunarDay: actualLunarDay,
    lunarMonthDisplay: LUNAR_MONTHS[(lunarMonth - 1 + 12) % 12],
    lunarDayDisplay: LUNAR_DAYS[actualLunarDay - 1] || LUNAR_DAYS[0]
  }
}

// 检查节气 - 修复:精确匹配日期
function checkSolarTerm(date: Date) {
  const month = date.getMonth() + 1
  const day = date.getDate()
  
  // 根据年份调整节气日期(简化处理)
  const yearOffset = date.getFullYear() - 2024
  const terms = SOLAR_TERMS_2024.map(term => ({
    ...term,
    // 简单调整,实际节气日期变化不大
    day: term.day + Math.floor(yearOffset / 4)
  }))
  
  const term = terms.find(t => t.month === month && t.day === day)
  return term ? { isTerm: true, termName: term.name } : { isTerm: false }
}

// 检查节日
function checkFestival(date: Date, lunarMonth: number, lunarDay: number) {
  const month = (date.getMonth() + 1).toString().padStart(2, '0')
  const day = date.getDate().toString().padStart(2, '0')
  const solarKey = `${month}${day}`
  
  // 检查阳历节日
  if (FESTIVALS.solar[solarKey as keyof typeof FESTIVALS.solar]) {
    return {
      festival: FESTIVALS.solar[solarKey as keyof typeof FESTIVALS.solar],
      festivalType: 'solar' as const,
      isHoliday: true
    }
  }
  
  // 检查农历节日
  const lunarMonthStr = lunarMonth.toString().padStart(2, '0')
  const lunarDayStr = lunarDay.toString().padStart(2, '0')
  const lunarKey = `${lunarMonthStr}${lunarDayStr}`
  
  if (FESTIVALS.lunar[lunarKey as keyof typeof FESTIVALS.lunar]) {
    return {
      festival: FESTIVALS.lunar[lunarKey as keyof typeof FESTIVALS.lunar],
      festivalType: 'traditional' as const,
      isHoliday: true
    }
  }
  
  return { isHoliday: false }
}

// 创建日历天对象
function createCalendarDay(date: Date, isCurrentMonth: boolean): CalendarDay {
  const day = date.getDate()
  
  // 计算农历
  const lunar = calculateLunarDate(date)
  
  // 检查节日
  const festivalInfo = checkFestival(date, lunar.lunarMonth, lunar.lunarDay)
  
  // 检查节气
  const termInfo = checkSolarTerm(date)
  
  const isFirstDayOfMonth = lunar.lunarDay === 1
  
  return {
    date,
    day,
    isCurrentMonth,
    lunarMonth: lunar.lunarMonth,
    lunarDay: lunar.lunarDay,
    lunarMonthDisplay: lunar.lunarMonthDisplay,
    lunarDayDisplay: lunar.lunarDayDisplay,
    lunarDateDisplay: `${lunar.lunarMonthDisplay}${lunar.lunarDayDisplay}`,
    isFirstDayOfMonth,
    isTerm: termInfo.isTerm,
    termName: termInfo.termName,
    isHoliday: festivalInfo.isHoliday || termInfo.isTerm,
    ...festivalInfo,
    ...(termInfo.isTerm ? { festivalType: 'other' as const } : {})
  }
}

// 计算日历天数
const calendarDays = computed(() => {
  const days: CalendarDay[] = []
  const firstDayOfMonth = new Date(currentYear.value, currentMonth.value, 1)
  const lastDayOfMonth = new Date(currentYear.value, currentMonth.value + 1, 0)
  
  // 计算上个月需要显示的天数
  const firstDayWeek = firstDayOfMonth.getDay()
  const lastDayOfPrevMonth = new Date(currentYear.value, currentMonth.value, 0).getDate()
  
  // 添加上个月末尾的几天
  for (let i = firstDayWeek - 1; i >= 0; i--) {
    const date = new Date(currentYear.value, currentMonth.value - 1, lastDayOfPrevMonth - i)
    days.push(createCalendarDay(date, false))
  }
  
  // 添加当前月的所有天
  const daysInMonth = lastDayOfMonth.getDate()
  for (let i = 1; i <= daysInMonth; i++) {
    const date = new Date(currentYear.value, currentMonth.value, i)
    days.push(createCalendarDay(date, true))
  }
  
  // 添加下个月开头的几天
  const totalCells = 42 // 6行 * 7列
  const remainingCells = totalCells - days.length
  for (let i = 1; i <= remainingCells; i++) {
    const date = new Date(currentYear.value, currentMonth.value + 1, i)
    days.push(createCalendarDay(date, false))
  }
  
  return days
})

// 检查是否是今天
function isToday(date: Date): boolean {
  const today = new Date()
  return date.toDateString() === today.toDateString()
}

// 检查是否是周末
function isWeekend(date: Date): boolean {
  const day = date.getDay()
  return day === 0 || day === 6
}

// 检查是否是选中日期
function isSelected(date: Date): boolean {
  if (!selectedDate.value) return false
  return date.toDateString() === selectedDate.value.toDateString()
}

// 获取星期几
function getWeekday(date: Date): string {
  const weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
  return weekdays[date.getDay()]
}

// 选择日期
function selectDate(day: CalendarDay) {
  selectedDate.value = day.date
  selectedDay.value = day
  emit('select', {
    date: day.date,
    solar: `${currentYear.value}-${currentMonth.value + 1}-${day.day}`,
    lunar: day.lunarDateDisplay,
    festival: day.festival,
    solarTerm: day.termName
  })
}

// 导航控制
function previousMonth() {
  if (currentMonth.value === 0) {
    currentMonth.value = 11
    currentYear.value--
  } else {
    currentMonth.value--
  }
}

function nextMonth() {
  if (currentMonth.value === 11) {
    currentMonth.value = 0
    currentYear.value++
  } else {
    currentMonth.value++
  }
}

function previousYear() {
  currentYear.value--
}

function nextYear() {
  currentYear.value++
}


function goToday() {
  const today = new Date()
  currentYear.value = today.getFullYear()
  currentMonth.value = today.getMonth()
  const day = createCalendarDay(today, true)
  selectDate(day)
}

function formatDate(date: Date): string {
  const weekdays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
  const year = date.getFullYear()
  const month = date.getMonth() + 1
  const day = date.getDate()
  const weekday = weekdays[date.getDay()]
  return `${year}年${month}月${day}日 ${weekday}`
}

// 生命周期
onMounted(() => {
  checkMobileMode()
  window.addEventListener('resize', checkMobileMode)
  goToday()
})

onUnmounted(() => {
  window.removeEventListener('resize', checkMobileMode)
})

// 定义emit
const emit = defineEmits<{
  select: [payload: {
    date: Date
    solar: string
    lunar: string
    festival?: string
    solarTerm?: string
  }]
}>()

// 暴露方法
defineExpose({
  goToday,
  setDate: (date: Date) => {
    currentYear.value = date.getFullYear()
    currentMonth.value = date.getMonth()
    const day = createCalendarDay(date, true)
    selectDate(day)
  }
})
</script>

<style lang="scss" scoped>
.calendar-container {
  font-family: 'Microsoft YaHei', sans-serif;
  max-width: 800px;
  margin: 0 auto;
  background: white;
  border-radius: 12px;
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
  overflow: hidden;
}

/* 移动端样式 */
@media (max-width: 767px) {
  .calendar-container {
    border-radius: 8px;
    max-width: 100%;
    margin: 8px;
  }
  
  .calendar-header {
    padding: 12px;
    gap: 8px;
  }
  
  .date-display {
    justify-content: center;
    margin: 8px 0;
  }
  
  .year-select,
  .month-select {
    padding: 6px 10px;
    font-size: 14px;
  }
  
  .control-btn {
    padding: 6px 10px;
    font-size: 12px;
  }
  
  .today-btn {
    position: absolute;
    right: 0;
    float: right;
    padding: 6px 12px;
    font-size: 12px;
  }
  
  .week-header {
    padding: 8px 0;
  }
  
  .week-day {
    font-size: 12px;
  }
  
  .calendar-grid {
    gap: 1px;
  }
  
  .calendar-day {
    min-height: 70px;
    padding: 4px 2px;
  }
  
  .solar-date {
    font-size: 16px;
  }
  
  .lunar-day,
  .festival,
  .solar-term {
    font-size: 10px;
    padding: 1px 2px;
    line-height: 1.2;
  }
  
  .lunar-month {
    font-size: 8px;
    top: 2px;
    left: 2px;
  }
}

/* 桌面端样式 */
@media (min-width: 768px) {
  .calendar-header {
    padding: 20px;
  }
  
  .calendar-day {
    min-height: 100px;
    padding: 8px;
  }
  
  .solar-date {
    font-size: 20px;
  }
  
  .lunar-day,
  .festival,
  .solar-term {
    font-size: 12px;
  }
}

/* 公共样式 */
.calendar-header {
  display: flex;
  align-items: center;
  justify-content: center;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  color: white;
  position: relative;
}

.header-left {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-shrink: 0;
}

.header-right {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-shrink: 0;
}

.date-display {
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  padding: 8px;
  min-width: 0;
}

.date-text {
  font-size: 18px;
  font-weight: bold;
  white-space: nowrap;
}

.control-btn {
  background: rgba(255, 255, 255, 0.2);
  border: none;
  color: white;
  border-radius: 6px;
  cursor: pointer;
  transition: background 0.3s;
}

.control-btn:hover {
  background: rgba(255, 255, 255, 0.3);
}

.date-display {
  display: flex;
  align-items: center;
  gap: 10px;
}

.year-select,
.month-select {
  background: rgba(255, 255, 255, 0.9);
  border: none;
  border-radius: 6px;
  color: #333;
  cursor: pointer;
}

.today-btn {
  background: #4CAF50;
  color: white;
  border: none;
  border-radius: 6px;
  cursor: pointer;
  font-weight: bold;
  transition: background 0.3s;
}

.today-btn:hover {
  background: #45a049;
}

.week-header {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  background: #f8f9fa;
  border-bottom: 1px solid #e9ecef;
}

.week-day {
  text-align: center;
  font-weight: bold;
  color: #666;
}

.calendar-grid {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 1px;
  background: #f0f0f0;
  padding: 1px;
}

.calendar-day {
  background: white;
  cursor: pointer;
  transition: all 0.2s;
  border-radius: 4px;
  display: flex;
  flex-direction: column;
  position: relative;
}

.calendar-day:hover {
  background: #f8f9fa;
  transform: translateY(-2px);
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}

/* 修复:非当前月格子样式 */
.calendar-day.other-month {
  background: #fafafa;
  color: #aaa;
}

.calendar-day.other-month .solar-date {
  color: #bbb;
}

.calendar-day.other-month .lunar-day,
.calendar-day.other-month .lunar-month {
  color: #ccc;
}

.calendar-day.today {
  background: #e3f2fd;
}

.calendar-day.selected {
  background: #e8f5e9;
}

.calendar-day.weekend .solar-date {
  color: #ff6b6b;
}

.calendar-day.holiday {
  background: #fff3e0;
}

.solar-date {
  font-weight: bold;
  color: #333;
  text-align: center;
  position: relative;
}

.today-dot {
  position: absolute;
  top: -2px;
  right: -2px;
  width: 6px;
  height: 6px;
  background: #2196F3;
  border-radius: 50%;
}

.selected-dot {
  position: absolute;
  bottom: 4px;
  left: 50%;
  transform: translateX(-50%);
  width: 6px;
  height: 6px;
  background: #4CAF50;
  border-radius: 50%;
}

.lunar-info {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-top: 4px;
}

.lunar-day,
.festival,
.solar-term {
  text-align: center;
  border-radius: 3px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.festival.traditional {
  color: #d32f2f;
  background: #ffebee;
  font-weight: bold;
}

.festival.solar {
  color: #1976d2;
  background: #e3f2fd;
  font-weight: bold;
}

.festival.other {
  color: #388e3c;
  background: #e8f5e9;
}

.solar-term {
  color: #388e3c;
  background: #e8f5e9;
  font-weight: bold;
}

.lunar-month {
  position: absolute;
  top: 4px;
  left: 4px;
  color: #999;
}

.selected-info {
  margin-top: 20px;
  padding: 20px;
  background: #f8f9fa;
  border-radius: 8px;
  border-top: 1px solid #e9ecef;
}

.selected-info h3 {
  margin-top: 0;
  color: #333;
}

.selected-info p {
  margin: 8px 0;
  color: #666;
}

/* 移动端选中信息样式 */
.mobile-selected-info {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  background: white;
  padding: 12px;
  box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
  z-index: 100;
}

.mobile-selected-content {
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.mobile-date-main {
  display: flex;
  align-items: center;
  gap: 12px;
}

.mobile-solar {
  font-size: 24px;
  font-weight: bold;
  color: #333;
}

.mobile-date-details {
  display: flex;
  flex-direction: column;
}

.mobile-week {
  font-size: 14px;
  color: #666;
}

.mobile-lunar {
  font-size: 12px;
  color: #999;
}

.mobile-festival {
  font-size: 14px;
  color: #d32f2f;
  font-weight: bold;
  padding: 4px 8px;
  background: #ffebee;
  border-radius: 4px;
}
</style>
相关推荐
冲刺逆向2 小时前
【js逆向案例六】创宇盾(加速乐)通杀模版
java·前端·javascript
我穿棉裤了2 小时前
文字换行自动添加换行符“-”
前端·javascript·vue.js
six+seven2 小时前
Node.js内置模块fs
前端·node.js
少莫千华2 小时前
【HTML】CSS绘制奥运五环
前端·css·html
沛沛老爹2 小时前
Web开发者转型AI安全核心:Agent Skills沙盒环境与威胁缓解实战
java·前端·人工智能·安全·rag·web转型升级
仰泳之鹅2 小时前
【杂谈】C语言中的链接属性、声明周期以及static关键字
java·c语言·前端
2501_940315262 小时前
【无标题】(leetcode933)最近的请求次数
java·前端·javascript
每天吃饭的羊2 小时前
LeetCode 第一题
前端