hookehuyr

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

- 新增 CollapsibleCalendar 组件,提供更好的日期选择体验
- 替换 IndexCheckInPage 中的 van-calendar 为新组件
- 优化日期选择逻辑和默认值处理
1 +<!--
2 + * @Date: 2025-01-25 15:34:17
3 + * @LastEditors: hookehuyr hookehuyr@gmail.com
4 + * @LastEditTime: 2025-09-24 22:41:07
5 + * @FilePath: /mlaj/src/components/ui/CollapsibleCalendar.vue
6 + * @Description: 可折叠日历组件
7 +-->
8 +<template>
9 + <div class="collapsible-calendar">
10 + <!-- 折叠状态显示 -->
11 + <div v-if="!isExpanded" class="calendar-collapsed" @click="expandCalendar">
12 + <div class="calendar-header">
13 + <div class="calendar-icon">
14 + <svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
15 + <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"/>
16 + <path d="M7 10H9V12H7V10ZM11 10H13V12H11V10ZM15 10H17V12H15V10Z" fill="#4caf50"/>
17 + </svg>
18 + </div>
19 + <div class="calendar-title-wrapper">
20 + <div class="calendar-title">{{ title }}</div>
21 + <div class="calendar-subtitle">点击选择日期</div>
22 + </div>
23 + </div>
24 + <div class="calendar-content">
25 + <div class="calendar-date-display">
26 + <div class="calendar-date-main">{{ formattedCurrentDate }}</div>
27 + <div class="calendar-weekday">{{ formattedWeekday }}</div>
28 + </div>
29 + <div class="calendar-action">
30 + <div class="calendar-action-text">指定日期</div>
31 + <div class="calendar-arrow">
32 + <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
33 + <path d="M7 10L12 15L17 10H7Z" fill="#4caf50"/>
34 + </svg>
35 + </div>
36 + </div>
37 + </div>
38 + </div>
39 +
40 + <!-- 展开状态的弹窗 -->
41 + <van-popup
42 + v-model:show="isExpanded"
43 + position="top"
44 + :style="{ height: '50%' }"
45 + @close="collapseCalendar"
46 + >
47 + <div class="calendar-popup-content">
48 + <van-calendar
49 + ref="calendarRef"
50 + :poppable="false"
51 + :show-confirm="false"
52 + switch-mode="year-month"
53 + color="#4caf50"
54 + :formatter="formatter"
55 + row-height="50"
56 + :show-mark="false"
57 + @select="onSelectDay"
58 + @click-subtitle="onClickSubtitle"
59 + >
60 + </van-calendar>
61 + </div>
62 + </van-popup>
63 + </div>
64 +</template>
65 +
66 +<script setup>
67 +import { ref, computed, watch, onMounted } from 'vue'
68 +import dayjs from 'dayjs'
69 +
70 +// Props定义
71 +const props = defineProps({
72 + title: {
73 + type: String,
74 + default: '选择日期'
75 + },
76 + formatter: {
77 + type: Function,
78 + required: true
79 + },
80 + modelValue: {
81 + type: [String, Date],
82 + default: null
83 + }
84 +})
85 +
86 +// Emits定义
87 +const emit = defineEmits(['update:modelValue', 'select', 'click-subtitle'])
88 +
89 +// 响应式数据
90 +const isExpanded = ref(false)
91 +const calendarRef = ref(null)
92 +const currentDate = ref(props.modelValue || new Date())
93 +
94 +// 计算属性:格式化当前日期显示
95 +const formattedCurrentDate = computed(() => {
96 + return dayjs(currentDate.value).format('YYYY年MM月DD日')
97 +})
98 +
99 +// 计算属性:格式化星期几显示
100 +const formattedWeekday = computed(() => {
101 + const weekdays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
102 + return weekdays[dayjs(currentDate.value).day()]
103 +})
104 +
105 +/**
106 + * 展开日历
107 + */
108 +const expandCalendar = () => {
109 + isExpanded.value = true
110 +}
111 +
112 +/**
113 + * 折叠日历
114 + */
115 +const collapseCalendar = () => {
116 + isExpanded.value = false
117 +}
118 +
119 +/**
120 + * 日期选择处理
121 + * @param {Date} day - 选中的日期
122 + */
123 +const onSelectDay = (day) => {
124 + currentDate.value = day
125 + emit('update:modelValue', day)
126 + emit('select', day)
127 +
128 + // 选择日期后自动关闭弹窗
129 + setTimeout(() => {
130 + collapseCalendar()
131 + }, 200)
132 +}
133 +
134 +/**
135 + * 点击副标题处理
136 + * @param {Event} evt - 点击事件
137 + */
138 +const onClickSubtitle = (evt) => {
139 + emit('click-subtitle', evt)
140 +}
141 +
142 +// 监听modelValue变化
143 +watch(() => props.modelValue, (newValue) => {
144 + if (newValue) {
145 + currentDate.value = newValue
146 + }
147 +})
148 +
149 +// 暴露方法给父组件
150 +defineExpose({
151 + expand: expandCalendar,
152 + collapse: collapseCalendar,
153 + reset: (date) => {
154 + currentDate.value = date || new Date()
155 + if (calendarRef.value && calendarRef.value.reset) {
156 + calendarRef.value.reset(date || new Date())
157 + }
158 + }
159 +})
160 +</script>
161 +
162 +<style lang="less" scoped>
163 +.collapsible-calendar {
164 + width: 100%;
165 +}
166 +
167 +.calendar-collapsed {
168 + background: linear-gradient(135deg, #ffffff 0%, #f8fffe 100%);
169 + border-radius: 16px;
170 + padding: 20px;
171 + box-shadow: 0 4px 20px rgba(76, 175, 80, 0.08), 0 2px 8px rgba(0, 0, 0, 0.06);
172 + cursor: pointer;
173 + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
174 + border: 1px solid rgba(76, 175, 80, 0.1);
175 + position: relative;
176 + overflow: hidden;
177 +
178 + &::before {
179 + content: '';
180 + position: absolute;
181 + top: 0;
182 + left: 0;
183 + right: 0;
184 + height: 3px;
185 + background: linear-gradient(90deg, #4caf50 0%, #66bb6a 100%);
186 + border-radius: 16px 16px 0 0;
187 + }
188 +
189 + &:hover {
190 + transform: translateY(-2px);
191 + box-shadow: 0 8px 30px rgba(76, 175, 80, 0.12), 0 4px 16px rgba(0, 0, 0, 0.08);
192 + border-color: rgba(76, 175, 80, 0.2);
193 + }
194 +
195 + &:active {
196 + transform: translateY(-1px);
197 + transition: all 0.1s ease;
198 + }
199 +
200 + .calendar-header {
201 + display: flex;
202 + align-items: center;
203 + margin-bottom: 16px;
204 + gap: 12px;
205 +
206 + .calendar-icon {
207 + display: flex;
208 + align-items: center;
209 + justify-content: center;
210 + width: 40px;
211 + height: 40px;
212 + background: linear-gradient(135deg, rgba(76, 175, 80, 0.1) 0%, rgba(76, 175, 80, 0.05) 100%);
213 + border-radius: 12px;
214 + flex-shrink: 0;
215 + }
216 +
217 + .calendar-title-wrapper {
218 + flex: 1;
219 + min-width: 0;
220 +
221 + .calendar-title {
222 + font-size: 18px;
223 + font-weight: 600;
224 + color: #2c3e50;
225 + margin-bottom: 2px;
226 + line-height: 1.3;
227 + overflow: hidden;
228 + text-overflow: ellipsis;
229 + white-space: nowrap;
230 + }
231 +
232 + .calendar-subtitle {
233 + font-size: 12px;
234 + color: #7f8c8d;
235 + font-weight: 400;
236 + }
237 + }
238 + }
239 +
240 + .calendar-content {
241 + display: flex;
242 + align-items: center;
243 + justify-content: space-between;
244 + gap: 16px;
245 +
246 + .calendar-date-display {
247 + flex: 1;
248 +
249 + .calendar-date-main {
250 + font-size: 16px;
251 + font-weight: 600;
252 + color: #34495e;
253 + margin-bottom: 4px;
254 + line-height: 1.2;
255 + }
256 +
257 + .calendar-weekday {
258 + font-size: 13px;
259 + color: #4caf50;
260 + font-weight: 500;
261 + }
262 + }
263 +
264 + .calendar-action {
265 + display: flex;
266 + align-items: center;
267 + gap: 8px;
268 + background: linear-gradient(135deg, rgba(76, 175, 80, 0.1) 0%, rgba(76, 175, 80, 0.05) 100%);
269 + padding: 8px 12px;
270 + border-radius: 20px;
271 + border: 1px solid rgba(76, 175, 80, 0.2);
272 + flex-shrink: 0;
273 +
274 + .calendar-action-text {
275 + font-size: 13px;
276 + color: #4caf50;
277 + font-weight: 600;
278 + white-space: nowrap;
279 + }
280 +
281 + .calendar-arrow {
282 + display: flex;
283 + align-items: center;
284 + transition: transform 0.2s ease;
285 + }
286 + }
287 + }
288 +
289 + &:hover .calendar-action .calendar-arrow {
290 + transform: translateY(2px);
291 + }
292 +}
293 +
294 +.calendar-popup-content {
295 + height: 100%;
296 + overflow: hidden;
297 +
298 + :deep(.van-calendar) {
299 + height: 100%;
300 +
301 + .van-calendar__header {
302 + box-shadow: none;
303 + border-bottom: 1px solid #eee;
304 + }
305 +
306 + .van-calendar__body {
307 + height: calc(100% - 44px);
308 + overflow-y: auto;
309 + }
310 + }
311 +}
312 +
313 +// 自定义日历样式
314 +:deep(.van-popup) {
315 + border-radius: 0 0 12px 12px;
316 +}
317 +</style>
1 <!-- 1 <!--
2 * @Date: 2025-05-29 15:34:17 2 * @Date: 2025-05-29 15:34:17
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2025-08-26 15:23:38 4 + * @LastEditTime: 2025-09-24 21:56:56
5 * @FilePath: /mlaj/src/views/checkin/IndexCheckInPage.vue 5 * @FilePath: /mlaj/src/views/checkin/IndexCheckInPage.vue
6 * @Description: 文件描述 6 * @Description: 文件描述
7 --> 7 -->
8 <template> 8 <template>
9 <AppLayout :hasTitle="false"> 9 <AppLayout :hasTitle="false">
10 <van-config-provider :theme-vars="themeVars"> 10 <van-config-provider :theme-vars="themeVars">
11 - <van-calendar ref="myRefCalendar" :title="taskDetail.title" :poppable="false" :show-confirm="false" 11 + <CollapsibleCalendar
12 - switch-mode="year-month" color="#4caf50" :formatter="formatter" row-height="50" :show-mark="false" 12 + ref="myRefCalendar"
13 + :title="taskDetail.title"
14 + :formatter="formatter"
15 + v-model="selectedDate"
13 @select="onSelectDay" 16 @select="onSelectDay"
14 - @click-subtitle="onClickSubtitle"> 17 + @click-subtitle="onClickSubtitle"
15 - </van-calendar> 18 + />
16 19
17 <div v-if="showProgress" class="text-wrapper"> 20 <div v-if="showProgress" class="text-wrapper">
18 <div class="text-header">目标进度</div> 21 <div class="text-header">目标进度</div>
...@@ -49,6 +52,10 @@ ...@@ -49,6 +52,10 @@
49 </div> 52 </div>
50 </div> 53 </div>
51 54
55 + <div class="text-wrapper">
56 + <div class="text-header">作业描述</div>
57 + </div>
58 +
52 <div v-if="!taskDetail.is_finish" class="text-wrapper"> 59 <div v-if="!taskDetail.is_finish" class="text-wrapper">
53 <div class="text-header">打卡类型</div> 60 <div class="text-header">打卡类型</div>
54 <div class="upload-wrapper"> 61 <div class="upload-wrapper">
...@@ -173,6 +180,7 @@ import AppLayout from "@/components/layout/AppLayout.vue"; ...@@ -173,6 +180,7 @@ import AppLayout from "@/components/layout/AppLayout.vue";
173 import FrostedGlass from "@/components/ui/FrostedGlass.vue"; 180 import FrostedGlass from "@/components/ui/FrostedGlass.vue";
174 import VideoPlayer from "@/components/ui/VideoPlayer.vue"; 181 import VideoPlayer from "@/components/ui/VideoPlayer.vue";
175 import AudioPlayer from "@/components/ui/AudioPlayer.vue"; 182 import AudioPlayer from "@/components/ui/AudioPlayer.vue";
183 +import CollapsibleCalendar from "@/components/ui/CollapsibleCalendar.vue";
176 import { useTitle } from '@vueuse/core'; 184 import { useTitle } from '@vueuse/core';
177 import dayjs from 'dayjs'; 185 import dayjs from 'dayjs';
178 186
...@@ -479,7 +487,7 @@ const formatter = (day) => { ...@@ -479,7 +487,7 @@ const formatter = (day) => {
479 } 487 }
480 488
481 // 添加一个响应式变量来存储当前选中的日期 489 // 添加一个响应式变量来存储当前选中的日期
482 -const selectedDate = ref(''); 490 +const selectedDate = ref(new Date());
483 491
484 const onSelectDay = (day) => { 492 const onSelectDay = (day) => {
485 getTaskDetail(dayjs(day).format('YYYY-MM')); 493 getTaskDetail(dayjs(day).format('YYYY-MM'));
...@@ -659,10 +667,12 @@ const onLoad = async (date) => { ...@@ -659,10 +667,12 @@ const onLoad = async (date) => {
659 onMounted(async () => { 667 onMounted(async () => {
660 const current_date = route.query.date; 668 const current_date = route.query.date;
661 if (current_date) { 669 if (current_date) {
670 + selectedDate.value = new Date(current_date);
662 getTaskDetail(dayjs(current_date).format('YYYY-MM')); 671 getTaskDetail(dayjs(current_date).format('YYYY-MM'));
663 myRefCalendar.value?.reset(new Date(current_date)); 672 myRefCalendar.value?.reset(new Date(current_date));
664 onLoad(current_date); 673 onLoad(current_date);
665 } else { 674 } else {
675 + selectedDate.value = new Date();
666 getTaskDetail(dayjs().format('YYYY-MM')); 676 getTaskDetail(dayjs().format('YYYY-MM'));
667 onLoad(dayjs().format('YYYY-MM-DD')); 677 onLoad(dayjs().format('YYYY-MM-DD'));
668 } 678 }
......