feat(日历组件): 增加日期选择快捷方式和重置功能
添加今日/昨日快捷选择功能,并支持特定日期选择弹窗 为 TaskCalendar 组件添加 noDefaultSelect 属性和 reset_selection 方法 优化日期解析逻辑和面板年月初始化方式
Showing
2 changed files
with
161 additions
and
12 deletions
| ... | @@ -57,19 +57,45 @@ import { ref, computed, watch } from 'vue' | ... | @@ -57,19 +57,45 @@ import { ref, computed, watch } from 'vue' |
| 57 | * 组件对外暴露的 v-model 值:选中日期(YYYY-MM-DD) | 57 | * 组件对外暴露的 v-model 值:选中日期(YYYY-MM-DD) |
| 58 | */ | 58 | */ |
| 59 | const props = defineProps({ | 59 | const props = defineProps({ |
| 60 | - modelValue: { type: String, default: '' } | 60 | + modelValue: { type: String, default: '' }, |
| 61 | + // 是否不在初始化时默认选中“今日”,用于弹窗场景 | ||
| 62 | + noDefaultSelect: { type: Boolean, default: false } | ||
| 61 | }) | 63 | }) |
| 62 | const emit = defineEmits(['update:modelValue', 'select']) | 64 | const emit = defineEmits(['update:modelValue', 'select']) |
| 63 | 65 | ||
| 64 | // | 66 | // |
| 65 | // 状态:当前面板年月与选中日期 | 67 | // 状态:当前面板年月与选中日期 |
| 66 | // | 68 | // |
| 67 | -const selected_date = ref(props.modelValue || format_date(new Date())) | 69 | +/** |
| 68 | -const panel_year = ref(Number(selected_date.value.slice(0, 4))) | 70 | + * 解析 YYYY-MM-DD 字符串为日期对象 |
| 69 | -const panel_month = ref(Number(selected_date.value.slice(5, 7))) | 71 | + * @param {string} str - 日期字符串 |
| 72 | + * @returns {Date} 日期对象 | ||
| 73 | + */ | ||
| 74 | +function parse_date_str(str) { | ||
| 75 | + if (!str) return new Date() | ||
| 76 | + const parts = String(str).split('-') | ||
| 77 | + const y = Number(parts[0]) || new Date().getFullYear() | ||
| 78 | + const m = Number(parts[1]) || (new Date().getMonth() + 1) | ||
| 79 | + const d = Number(parts[2]) || 1 | ||
| 80 | + return new Date(y, m - 1, d) | ||
| 81 | +} | ||
| 82 | + | ||
| 83 | +// 选中日期:根据 noDefaultSelect 决定是否默认高亮“今日” | ||
| 84 | +const selected_date = ref( | ||
| 85 | + props.noDefaultSelect ? (props.modelValue || '') : (props.modelValue || format_date(new Date())) | ||
| 86 | +) | ||
| 87 | +// 面板年月:优先使用传入值,否则使用当前日期 | ||
| 88 | +const base_d = parse_date_str(props.modelValue) | ||
| 89 | +const panel_year = ref(base_d.getFullYear()) | ||
| 90 | +const panel_month = ref(base_d.getMonth() + 1) | ||
| 70 | 91 | ||
| 71 | watch(() => props.modelValue, (val) => { | 92 | watch(() => props.modelValue, (val) => { |
| 72 | - if (val) selected_date.value = val | 93 | + if (val) { |
| 94 | + selected_date.value = val | ||
| 95 | + const d = parse_date_str(val) | ||
| 96 | + panel_year.value = d.getFullYear() | ||
| 97 | + panel_month.value = d.getMonth() + 1 | ||
| 98 | + } | ||
| 73 | }) | 99 | }) |
| 74 | 100 | ||
| 75 | /** | 101 | /** |
| ... | @@ -201,6 +227,19 @@ function on_confirm_year_month() { | ... | @@ -201,6 +227,19 @@ function on_confirm_year_month() { |
| 201 | panel_month.value = Number(m) | 227 | panel_month.value = Number(m) |
| 202 | show_date_picker.value = false | 228 | show_date_picker.value = false |
| 203 | } | 229 | } |
| 230 | + | ||
| 231 | +/** | ||
| 232 | + * 重置选中状态:清空选中日期,用于父组件“今日/昨日”切换后取消高亮 | ||
| 233 | + * @returns {void} | ||
| 234 | + */ | ||
| 235 | +function reset_selection() { | ||
| 236 | + selected_date.value = '' | ||
| 237 | +} | ||
| 238 | + | ||
| 239 | +// 暴露方法给父组件 | ||
| 240 | +defineExpose({ | ||
| 241 | + reset_selection | ||
| 242 | +}) | ||
| 204 | </script> | 243 | </script> |
| 205 | 244 | ||
| 206 | <style lang="less" scoped> | 245 | <style lang="less" scoped> | ... | ... |
| 1 | <!-- | 1 | <!-- |
| 2 | * @Date: 2025-11-19 21:00:00 | 2 | * @Date: 2025-11-19 21:00:00 |
| 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com | 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com |
| 4 | - * @LastEditTime: 2025-11-20 11:31:17 | 4 | + * @LastEditTime: 2025-11-20 21:30:48 |
| 5 | * @FilePath: /mlaj/src/views/teacher/taskHomePage.vue | 5 | * @FilePath: /mlaj/src/views/teacher/taskHomePage.vue |
| 6 | * @Description: 教师端作业主页(头部介绍、统计、日历与学生完成情况;数据Mock) | 6 | * @Description: 教师端作业主页(头部介绍、统计、日历与学生完成情况;数据Mock) |
| 7 | --> | 7 | --> |
| ... | @@ -52,8 +52,33 @@ | ... | @@ -52,8 +52,33 @@ |
| 52 | 52 | ||
| 53 | <!-- 日历:选择日期后展示该日期的学生完成情况 --> | 53 | <!-- 日历:选择日期后展示该日期的学生完成情况 --> |
| 54 | <div class="calendarCard bg-white rounded-lg shadow px-4 py-4 mt-4"> | 54 | <div class="calendarCard bg-white rounded-lg shadow px-4 py-4 mt-4"> |
| 55 | - <div class="text-base font-semibold text-gray-800 mb-2">选择日期查看完成情况</div> | 55 | + <div class="text-base font-semibold text-gray-800 mb-2">选择日期</div> |
| 56 | - <TaskCalendar v-model="selected_date" @select="on_date_select" /> | 56 | + <!-- 快捷方式:今日 / 昨日 / 某个日期(弹出日历) --> |
| 57 | + <div class="QuickDateRow flex items-center gap-3"> | ||
| 58 | + <div class="quickChip inline-flex items-center rounded-full px-3 py-1 text-sm font-medium cursor-pointer" | ||
| 59 | + :class="selected_mode === 'today' ? 'bg-green-500 text-white border border-green-500 hover:bg-green-600' : 'bg-white text-gray-700 border border-gray-300 hover:bg-gray-50'" | ||
| 60 | + @click="select_today"> | ||
| 61 | + <!-- <van-icon v-if="selected_mode === 'today'" name="success" size="14" class="mr-1" /> --> | ||
| 62 | + 今日 | ||
| 63 | + </div> | ||
| 64 | + <div class="quickChip inline-flex items-center rounded-full px-3 py-1 text-sm font-medium cursor-pointer" | ||
| 65 | + :class="selected_mode === 'yesterday' ? 'bg-green-500 text-white border border-green-500 hover:bg-green-600' : 'bg-white text-gray-700 border border-gray-300 hover:bg-gray-50'" | ||
| 66 | + @click="select_yesterday"> | ||
| 67 | + <!-- <van-icon v-if="selected_mode === 'yesterday'" name="success" size="14" class="mr-1" /> --> | ||
| 68 | + 昨日 | ||
| 69 | + </div> | ||
| 70 | + <div class="quickChip inline-flex items-center rounded-full px-3 py-1 text-sm font-medium cursor-pointer" | ||
| 71 | + :class="selected_mode === 'specific' ? 'bg-green-500 text-white border border-green-500 hover:bg-green-600' : 'bg-white text-gray-700 border border-gray-300 hover:bg-gray-50'" | ||
| 72 | + @click="open_specific_date_picker"> | ||
| 73 | + <!-- <van-icon v-if="selected_mode === 'specific'" name="success" size="14" class="mr-1" /> --> | ||
| 74 | + {{ specific_label }} | ||
| 75 | + </div> | ||
| 76 | + </div> | ||
| 77 | + <!-- 选中日期展示 --> | ||
| 78 | + <!-- <div class="mt-3 text-sm text-gray-600">当前选择:{{ current_date_text }}</div> --> | ||
| 79 | + <!-- 日历弹窗:点击“某个日期”弹出 --> | ||
| 80 | + <van-calendar v-model:show="show_calendar_popup" :default-date="calendar_default_date" color="#10b981" | ||
| 81 | + :show-confirm="true" switch-mode="month" @confirm="on_date_select" /> | ||
| 57 | </div> | 82 | </div> |
| 58 | 83 | ||
| 59 | <!-- 学生完成情况(参考图片2样式) --> | 84 | <!-- 学生完成情况(参考图片2样式) --> |
| ... | @@ -61,7 +86,7 @@ | ... | @@ -61,7 +86,7 @@ |
| 61 | <div class="flex items-center justify-between mb-3"> | 86 | <div class="flex items-center justify-between mb-3"> |
| 62 | <div class="text-base font-semibold text-gray-800">完成情况({{ completed_count }}/{{ students.length }}) | 87 | <div class="text-base font-semibold text-gray-800">完成情况({{ completed_count }}/{{ students.length }}) |
| 63 | </div> | 88 | </div> |
| 64 | - <div class="text-xs text-gray-500">当前日期:{{ current_date_text }}</div> | 89 | + <!-- <div class="text-xs text-gray-500">当前日期:{{ current_date_text }}</div> --> |
| 65 | </div> | 90 | </div> |
| 66 | <div class="grid grid-cols-5 gap-3 StudentsGrid"> | 91 | <div class="grid grid-cols-5 gap-3 StudentsGrid"> |
| 67 | <div v-for="(stu, idx) in students_status" :key="stu.id" | 92 | <div v-for="(stu, idx) in students_status" :key="stu.id" |
| ... | @@ -81,13 +106,55 @@ | ... | @@ -81,13 +106,55 @@ |
| 81 | import { ref, computed } from 'vue' | 106 | import { ref, computed } from 'vue' |
| 82 | import { useRoute, useRouter } from 'vue-router' | 107 | import { useRoute, useRouter } from 'vue-router' |
| 83 | import { useTitle } from '@vueuse/core' | 108 | import { useTitle } from '@vueuse/core' |
| 84 | -import TaskCalendar from '@/components/ui/TaskCalendar.vue' | ||
| 85 | import checkCorner from '@/assets/images/dui.png' | 109 | import checkCorner from '@/assets/images/dui.png' |
| 86 | 110 | ||
| 87 | const $route = useRoute() | 111 | const $route = useRoute() |
| 88 | const $router = useRouter() | 112 | const $router = useRouter() |
| 89 | useTitle('作业主页') | 113 | useTitle('作业主页') |
| 90 | 114 | ||
| 115 | +// 弹窗显示状态:是否展示“某个日期”选择日历 | ||
| 116 | +const show_calendar_popup = ref(false) | ||
| 117 | +// Calendar 默认选中日期(为 null 时不预选) | ||
| 118 | +const calendar_default_date = ref(null) | ||
| 119 | +// 快捷项当前选中模式:today | yesterday | specific | ||
| 120 | +const selected_mode = ref('today') | ||
| 121 | +// 特定日期按钮的文字展示,选择后替换为实际日期 | ||
| 122 | +const specific_label = ref('特定日期') | ||
| 123 | + | ||
| 124 | +/** | ||
| 125 | + * 选择“今日”,设置为今日日期,并触发查询逻辑 | ||
| 126 | + * @returns {void} | ||
| 127 | + */ | ||
| 128 | +function select_today() { | ||
| 129 | + const now = new Date() | ||
| 130 | + selected_date.value = format_date(now) | ||
| 131 | + selected_mode.value = 'today' | ||
| 132 | + specific_label.value = '特定日期' | ||
| 133 | + // 清空弹窗内 Calendar 的默认选中状态 | ||
| 134 | + calendar_default_date.value = null | ||
| 135 | +} | ||
| 136 | + | ||
| 137 | +/** | ||
| 138 | + * 选择“昨日”,设置为昨日日期,并触发查询逻辑 | ||
| 139 | + * @returns {void} | ||
| 140 | + */ | ||
| 141 | +function select_yesterday() { | ||
| 142 | + const y = new Date(Date.now() - 24 * 60 * 60 * 1000) | ||
| 143 | + selected_date.value = format_date(y) | ||
| 144 | + selected_mode.value = 'yesterday' | ||
| 145 | + specific_label.value = '特定日期' | ||
| 146 | + // 清空弹窗内 Calendar 的默认选中状态 | ||
| 147 | + calendar_default_date.value = null | ||
| 148 | +} | ||
| 149 | + | ||
| 150 | +/** | ||
| 151 | + * 打开“某个日期”选择器弹窗 | ||
| 152 | + * @returns {void} | ||
| 153 | + */ | ||
| 154 | +function open_specific_date_picker() { | ||
| 155 | + show_calendar_popup.value = true | ||
| 156 | +} | ||
| 157 | + | ||
| 91 | // | 158 | // |
| 92 | // Mock:作业基础信息 | 159 | // Mock:作业基础信息 |
| 93 | // | 160 | // |
| ... | @@ -166,9 +233,33 @@ function format_date(d) { | ... | @@ -166,9 +233,33 @@ function format_date(d) { |
| 166 | * @returns {void} | 233 | * @returns {void} |
| 167 | * 注释:更新选中日期,并联动学生完成情况。 | 234 | * 注释:更新选中日期,并联动学生完成情况。 |
| 168 | */ | 235 | */ |
| 236 | +/** | ||
| 237 | + * 字符串转日期对象(YYYY-MM-DD -> Date) | ||
| 238 | + * @param {string} s - 日期字符串 | ||
| 239 | + * @returns {Date} 日期对象 | ||
| 240 | + */ | ||
| 241 | +function parse_date_text(s) { | ||
| 242 | + const parts = String(s).split('-') | ||
| 243 | + const y = Number(parts[0]) || new Date().getFullYear() | ||
| 244 | + const m = Number(parts[1]) || (new Date().getMonth() + 1) | ||
| 245 | + const d = Number(parts[2]) || new Date().getDate() | ||
| 246 | + return new Date(y, m - 1, d) | ||
| 247 | +} | ||
| 248 | + | ||
| 249 | +/** | ||
| 250 | + * 处理日历选择事件(兼容字符串与Date对象) | ||
| 251 | + * @param {string|Date} val - 选中的日期 | ||
| 252 | + * @returns {void} | ||
| 253 | + */ | ||
| 169 | function on_date_select(val) { | 254 | function on_date_select(val) { |
| 170 | - // 自定义日历组件返回 YYYY-MM-DD 字符串 | 255 | + const is_date_obj = val instanceof Date |
| 171 | - selected_date.value = val | 256 | + const text = is_date_obj ? format_date(val) : String(val) |
| 257 | + selected_date.value = text | ||
| 258 | + show_calendar_popup.value = false | ||
| 259 | + selected_mode.value = 'specific' | ||
| 260 | + specific_label.value = text | ||
| 261 | + // 将所选日期作为下次弹窗的默认选中 | ||
| 262 | + calendar_default_date.value = is_date_obj ? val : parse_date_text(text) | ||
| 172 | } | 263 | } |
| 173 | 264 | ||
| 174 | /** | 265 | /** |
| ... | @@ -226,6 +317,7 @@ function go_student_record(stu) { | ... | @@ -226,6 +317,7 @@ function go_student_record(stu) { |
| 226 | .details { | 317 | .details { |
| 227 | border-left: 0.15rem solid #10b981; | 318 | border-left: 0.15rem solid #10b981; |
| 228 | padding-left: 0.75rem; | 319 | padding-left: 0.75rem; |
| 320 | + | ||
| 229 | .detailItem { | 321 | .detailItem { |
| 230 | margin-bottom: 0.25rem; | 322 | margin-bottom: 0.25rem; |
| 231 | } | 323 | } |
| ... | @@ -233,20 +325,24 @@ function go_student_record(stu) { | ... | @@ -233,20 +325,24 @@ function go_student_record(stu) { |
| 233 | } | 325 | } |
| 234 | 326 | ||
| 235 | .studentsCard { | 327 | .studentsCard { |
| 328 | + | ||
| 236 | // 兜底:强制学生列表为5列栅格,避免一行仅1个的问题 | 329 | // 兜底:强制学生列表为5列栅格,避免一行仅1个的问题 |
| 237 | .StudentsGrid { | 330 | .StudentsGrid { |
| 238 | display: grid; | 331 | display: grid; |
| 239 | grid-template-columns: repeat(5, 1fr); | 332 | grid-template-columns: repeat(5, 1fr); |
| 240 | gap: 0.75rem; // 等同于 tailwind 的 gap-3 | 333 | gap: 0.75rem; // 等同于 tailwind 的 gap-3 |
| 241 | } | 334 | } |
| 335 | + | ||
| 242 | .grid>div { | 336 | .grid>div { |
| 243 | transition: all 0.2s ease-in-out; | 337 | transition: all 0.2s ease-in-out; |
| 244 | } | 338 | } |
| 339 | + | ||
| 245 | .studentItem { | 340 | .studentItem { |
| 246 | min-height: 4rem; | 341 | min-height: 4rem; |
| 247 | // 卡片圆角裁切,配合右上角图片效果 | 342 | // 卡片圆角裁切,配合右上角图片效果 |
| 248 | overflow: hidden; | 343 | overflow: hidden; |
| 249 | } | 344 | } |
| 345 | + | ||
| 250 | .cornerIcon { | 346 | .cornerIcon { |
| 251 | // 右上角图片样式(对勾角标),贴边显示 | 347 | // 右上角图片样式(对勾角标),贴边显示 |
| 252 | position: absolute; | 348 | position: absolute; |
| ... | @@ -256,5 +352,19 @@ function go_student_record(stu) { | ... | @@ -256,5 +352,19 @@ function go_student_record(stu) { |
| 256 | height: 18px; | 352 | height: 18px; |
| 257 | } | 353 | } |
| 258 | } | 354 | } |
| 355 | + | ||
| 356 | + .calendarCard { | ||
| 357 | + .QuickDateRow { | ||
| 358 | + margin-top: 0.25rem; | ||
| 359 | + | ||
| 360 | + .quickChip { | ||
| 361 | + transition: all 0.2s ease-in-out; | ||
| 362 | + | ||
| 363 | + &:active { | ||
| 364 | + opacity: 0.85; | ||
| 365 | + } | ||
| 366 | + } | ||
| 367 | + } | ||
| 368 | + } | ||
| 259 | } | 369 | } |
| 260 | </style> | 370 | </style> | ... | ... |
-
Please register or login to post a comment