CollapsibleCalendar.vue 8.7 KB
<!--
 * @Date: 2025-01-25 15:34:17
 * @LastEditors: hookehuyr hookehuyr@gmail.com
 * @LastEditTime: 2025-09-25 11:16:01
 * @FilePath: /mlaj/src/components/ui/CollapsibleCalendar.vue
 * @Description: 可折叠日历组件
-->
<template>
    <div class="collapsible-calendar ">
        <!-- 折叠状态显示 -->
        <div v-if="!isExpanded" class="calendar-collapsed" @click="expandCalendar">
            <div class="calendar-header">
                <div class="calendar-icon">
                    <svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                        <path d="M19 3H18V1H16V3H8V1H6V3H5C3.89 3 3.01 3.9 3.01 5L3 19C3 20.1 3.89 21 5 21H19C20.1 21 21 20.1 21 19V5C21 3.9 20.1 3 19 3ZM19 19H5V8H19V19Z" fill="#4caf50"/>
                        <path d="M7 10H9V12H7V10ZM11 10H13V12H11V10ZM15 10H17V12H15V10Z" fill="#4caf50"/>
                    </svg>
                </div>
                <div class="calendar-title-wrapper">
                    <div class="calendar-title">{{ title }}</div>
                    <div class="calendar-subtitle">点击选择日期</div>
                </div>
            </div>
            <div class="calendar-content">
                <div class="calendar-date-display">
                    <div class="calendar-date-main">{{ formattedCurrentDate }}</div>
                    <div class="calendar-weekday">{{ formattedWeekday }}</div>
                </div>
                <div class="calendar-action">
                    <div class="calendar-action-text">指定日期</div>
                    <div class="calendar-arrow">
                        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                            <path d="M7 10L12 15L17 10H7Z" fill="#4caf50"/>
                        </svg>
                    </div>
                </div>
            </div>
        </div>

        <!-- 展开状态的弹窗 -->
        <van-popup
            v-model:show="isExpanded"
            position="top"
            :style="{ height: '60%' }"
            @close="collapseCalendar"
        >
            <div class="calendar-popup-content">
                <van-calendar
                    ref="calendarRef"
                    :poppable="false"
                    :show-confirm="false"
                    switch-mode="year-month"
                    color="#4caf50"
                    :formatter="formatter"
                    row-height="50"
                    :show-mark="false"
                    @select="onSelectDay"
                    @click-subtitle="onClickSubtitle"
                    @panel-change="onPanelChange"
                >
                </van-calendar>
            </div>
        </van-popup>
    </div>
</template>

<script setup>
import { ref, computed, watch, onMounted } from 'vue'
import dayjs from 'dayjs'

// Props定义
const props = defineProps({
    title: {
        type: String,
        default: '选择日期'
    },
    formatter: {
        type: Function,
        required: true
    },
    modelValue: {
        type: [String, Date],
        default: null
    }
})

// Emits定义
const emit = defineEmits(['update:modelValue', 'select', 'click-subtitle'])

// 响应式数据
const isExpanded = ref(false)
const calendarRef = ref(null)
const currentDate = ref(props.modelValue || new Date())

// 计算属性:格式化当前日期显示
const formattedCurrentDate = computed(() => {
    return dayjs(currentDate.value).format('YYYY年MM月DD日')
})

// 计算属性:格式化星期几显示
const formattedWeekday = computed(() => {
    const weekdays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
    return weekdays[dayjs(currentDate.value).day()]
})

/**
 * 展开日历
 */
const expandCalendar = () => {
    isExpanded.value = true
}

/**
 * 折叠日历
 */
const collapseCalendar = () => {
    isExpanded.value = false
}

/**
 * 日期选择处理
 * @param {Date} day - 选中的日期
 */
const onSelectDay = (day) => {
    currentDate.value = day
    emit('update:modelValue', day)
    emit('select', day)

    // 选择日期后自动关闭弹窗
    setTimeout(() => {
        collapseCalendar()
    }, 200)
}

/**
 * 点击副标题处理
 * @param {Event} evt - 点击事件
 */
const onClickSubtitle = (evt) => {
    emit('click-subtitle', evt)
}

/**
 * 面板切换处理
 * @param {Object} panel - 切换后的面板信息
 */
const onPanelChange = (panel) => {
    emit('panel-change', panel)
}

// 监听modelValue变化
watch(() => props.modelValue, (newValue) => {
    if (newValue) {
        currentDate.value = newValue
    }
})

// 暴露方法给父组件
defineExpose({
    expand: expandCalendar,
    collapse: collapseCalendar,
    reset: (date) => {
        currentDate.value = date || new Date()
        if (calendarRef.value && calendarRef.value.reset) {
            calendarRef.value.reset(date || new Date())
        }
    }
})
</script>

<style lang="less" scoped>
.collapsible-calendar {
    width: 100%;
}

.calendar-collapsed {
    // background: linear-gradient(135deg, #ffffff 0%, #f8fffe 100%);
    // border-radius: 16px;
    padding: 20px;
    box-shadow: 0 4px 20px rgba(76, 175, 80, 0.08), 0 2px 8px rgba(0, 0, 0, 0.06);
    cursor: pointer;
    transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
    border: 1px solid rgba(76, 175, 80, 0.1);
    position: relative;
    overflow: hidden;

    &::before {
        content: '';
        position: absolute;
        top: 0;
        left: 0;
        right: 0;
        height: 3px;
        background: linear-gradient(90deg, #4caf50 0%, #66bb6a 100%);
        border-radius: 16px 16px 0 0;
    }

    &:hover {
        transform: translateY(-2px);
        box-shadow: 0 8px 30px rgba(76, 175, 80, 0.12), 0 4px 16px rgba(0, 0, 0, 0.08);
        border-color: rgba(76, 175, 80, 0.2);
    }

    &:active {
        transform: translateY(-1px);
        transition: all 0.1s ease;
    }

    .calendar-header {
        display: flex;
        align-items: center;
        margin-bottom: 16px;
        gap: 12px;

        .calendar-icon {
            display: flex;
            align-items: center;
            justify-content: center;
            width: 40px;
            height: 40px;
            background: linear-gradient(135deg, rgba(76, 175, 80, 0.1) 0%, rgba(76, 175, 80, 0.05) 100%);
            border-radius: 12px;
            flex-shrink: 0;
        }

        .calendar-title-wrapper {
            flex: 1;
            min-width: 0;

            .calendar-title {
                font-size: 18px;
                font-weight: 600;
                color: #2c3e50;
                margin-bottom: 2px;
                line-height: 1.3;
                overflow: hidden;
                text-overflow: ellipsis;
                white-space: nowrap;
            }

            .calendar-subtitle {
                font-size: 12px;
                color: #7f8c8d;
                font-weight: 400;
            }
        }
    }

    .calendar-content {
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: 16px;

        .calendar-date-display {
            flex: 1;

            .calendar-date-main {
                font-size: 16px;
                font-weight: 600;
                color: #34495e;
                margin-bottom: 4px;
                line-height: 1.2;
            }

            .calendar-weekday {
                font-size: 13px;
                color: #4caf50;
                font-weight: 500;
            }
        }

        .calendar-action {
            display: flex;
            align-items: center;
            gap: 8px;
            background: linear-gradient(135deg, rgba(76, 175, 80, 0.1) 0%, rgba(76, 175, 80, 0.05) 100%);
            padding: 8px 12px;
            border-radius: 20px;
            border: 1px solid rgba(76, 175, 80, 0.2);
            flex-shrink: 0;

            .calendar-action-text {
                font-size: 13px;
                color: #4caf50;
                font-weight: 600;
                white-space: nowrap;
            }

            .calendar-arrow {
                display: flex;
                align-items: center;
                transition: transform 0.2s ease;
            }
        }
    }

    &:hover .calendar-action .calendar-arrow {
        transform: translateY(2px);
    }
}

.calendar-popup-content {
    height: 100%;
    overflow: hidden;

    :deep(.van-calendar) {
        height: 100%;

        .van-calendar__header {
            box-shadow: none;
            border-bottom: 1px solid #eee;
        }

        .van-calendar__body {
            height: calc(100% - 44px);
            overflow-y: auto;
        }
    }
}

// 自定义日历样式
:deep(.van-popup) {
    border-radius: 0 0 12px 12px;
}
</style>