html
<template>
<div class="calendar-container">
<!-- 头部控制区域 -->
<div class="calendar-header">
<button @click="previousYear" class="control-btn" title="上一年">
<<
</button>
<button @click="previousMonth" class="control-btn" title="上一月">
<
</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="下一月">
>
</button>
<button @click="nextYear" class="control-btn" title="下一年">
>>
</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="上一年">
<<
</button>
<button @click="previousMonth" class="control-btn" title="上一月">
<
</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="下一月">
>
</button>
<button @click="nextYear" class="control-btn" title="下一年">
>>
</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>