hookehuyr

feat(日历组件): 添加可折叠日历组件并替换原有日历

- 新增 CollapsibleCalendar 组件,提供更好的日期选择体验
- 替换 IndexCheckInPage 中的 van-calendar 为新组件
- 优化日期选择逻辑和默认值处理
<!--
* @Date: 2025-01-25 15:34:17
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-09-24 22:41:07
* @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: '50%' }"
@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"
>
</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)
}
// 监听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>
<!--
* @Date: 2025-05-29 15:34:17
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-08-26 15:23:38
* @LastEditTime: 2025-09-24 21:56:56
* @FilePath: /mlaj/src/views/checkin/IndexCheckInPage.vue
* @Description: 文件描述
-->
<template>
<AppLayout :hasTitle="false">
<van-config-provider :theme-vars="themeVars">
<van-calendar ref="myRefCalendar" :title="taskDetail.title" :poppable="false" :show-confirm="false"
switch-mode="year-month" color="#4caf50" :formatter="formatter" row-height="50" :show-mark="false"
<CollapsibleCalendar
ref="myRefCalendar"
:title="taskDetail.title"
:formatter="formatter"
v-model="selectedDate"
@select="onSelectDay"
@click-subtitle="onClickSubtitle">
</van-calendar>
@click-subtitle="onClickSubtitle"
/>
<div v-if="showProgress" class="text-wrapper">
<div class="text-header">目标进度</div>
......@@ -49,6 +52,10 @@
</div>
</div>
<div class="text-wrapper">
<div class="text-header">作业描述</div>
</div>
<div v-if="!taskDetail.is_finish" class="text-wrapper">
<div class="text-header">打卡类型</div>
<div class="upload-wrapper">
......@@ -173,6 +180,7 @@ import AppLayout from "@/components/layout/AppLayout.vue";
import FrostedGlass from "@/components/ui/FrostedGlass.vue";
import VideoPlayer from "@/components/ui/VideoPlayer.vue";
import AudioPlayer from "@/components/ui/AudioPlayer.vue";
import CollapsibleCalendar from "@/components/ui/CollapsibleCalendar.vue";
import { useTitle } from '@vueuse/core';
import dayjs from 'dayjs';
......@@ -479,7 +487,7 @@ const formatter = (day) => {
}
// 添加一个响应式变量来存储当前选中的日期
const selectedDate = ref('');
const selectedDate = ref(new Date());
const onSelectDay = (day) => {
getTaskDetail(dayjs(day).format('YYYY-MM'));
......@@ -659,10 +667,12 @@ const onLoad = async (date) => {
onMounted(async () => {
const current_date = route.query.date;
if (current_date) {
selectedDate.value = new Date(current_date);
getTaskDetail(dayjs(current_date).format('YYYY-MM'));
myRefCalendar.value?.reset(new Date(current_date));
onLoad(current_date);
} else {
selectedDate.value = new Date();
getTaskDetail(dayjs().format('YYYY-MM'));
onLoad(dayjs().format('YYYY-MM-DD'));
}
......