feat(teacher): 添加作业级联筛选组件并集成到打卡页面
添加 TaskCascaderFilter 组件用于教师端作业筛选,支持两级级联选择 在教师打卡页面集成该组件,根据筛选条件动态刷新打卡数据 移除调试用的 console.warn 日志
Showing
4 changed files
with
204 additions
and
4 deletions
| ... | @@ -39,6 +39,7 @@ declare module 'vue' { | ... | @@ -39,6 +39,7 @@ declare module 'vue' { |
| 39 | SharePoster: typeof import('./components/ui/SharePoster.vue')['default'] | 39 | SharePoster: typeof import('./components/ui/SharePoster.vue')['default'] |
| 40 | SummerCampCard: typeof import('./components/ui/SummerCampCard.vue')['default'] | 40 | SummerCampCard: typeof import('./components/ui/SummerCampCard.vue')['default'] |
| 41 | TaskCalendar: typeof import('./components/ui/TaskCalendar.vue')['default'] | 41 | TaskCalendar: typeof import('./components/ui/TaskCalendar.vue')['default'] |
| 42 | + TaskCascaderFilter: typeof import('./components/teacher/TaskCascaderFilter.vue')['default'] | ||
| 42 | TaskFilter: typeof import('./components/teacher/TaskFilter.vue')['default'] | 43 | TaskFilter: typeof import('./components/teacher/TaskFilter.vue')['default'] |
| 43 | TermsPopup: typeof import('./components/ui/TermsPopup.vue')['default'] | 44 | TermsPopup: typeof import('./components/ui/TermsPopup.vue')['default'] |
| 44 | UploadVideoPopup: typeof import('./components/ui/UploadVideoPopup.vue')['default'] | 45 | UploadVideoPopup: typeof import('./components/ui/UploadVideoPopup.vue')['default'] |
| ... | @@ -48,6 +49,7 @@ declare module 'vue' { | ... | @@ -48,6 +49,7 @@ declare module 'vue' { |
| 48 | VanBadge: typeof import('vant/es')['Badge'] | 49 | VanBadge: typeof import('vant/es')['Badge'] |
| 49 | VanButton: typeof import('vant/es')['Button'] | 50 | VanButton: typeof import('vant/es')['Button'] |
| 50 | VanCalendar: typeof import('vant/es')['Calendar'] | 51 | VanCalendar: typeof import('vant/es')['Calendar'] |
| 52 | + VanCascader: typeof import('vant/es')['Cascader'] | ||
| 51 | VanCell: typeof import('vant/es')['Cell'] | 53 | VanCell: typeof import('vant/es')['Cell'] |
| 52 | VanCellGroup: typeof import('vant/es')['CellGroup'] | 54 | VanCellGroup: typeof import('vant/es')['CellGroup'] |
| 53 | VanCheckbox: typeof import('vant/es')['Checkbox'] | 55 | VanCheckbox: typeof import('vant/es')['Checkbox'] | ... | ... |
| 1 | +<template> | ||
| 2 | + <div class="inline-block"> | ||
| 3 | + <div @click="show = true" class="text-sm text-green-600 flex items-center cursor-pointer ml-2"> | ||
| 4 | + <span class="mr-1">{{ selectedLabel }}</span> | ||
| 5 | + <van-icon name="arrow-down" /> | ||
| 6 | + </div> | ||
| 7 | + | ||
| 8 | + <van-popup v-model:show="show" round position="bottom"> | ||
| 9 | + <van-cascader | ||
| 10 | + v-model="cascaderValue" | ||
| 11 | + title="请选择作业" | ||
| 12 | + :options="options" | ||
| 13 | + :field-names="fieldNames" | ||
| 14 | + active-color="#22c55e" | ||
| 15 | + @close="show = false" | ||
| 16 | + @finish="onFinish" | ||
| 17 | + @change="onChange" | ||
| 18 | + /> | ||
| 19 | + </van-popup> | ||
| 20 | + </div> | ||
| 21 | +</template> | ||
| 22 | + | ||
| 23 | +<script setup> | ||
| 24 | +import { ref, watch, computed } from 'vue' | ||
| 25 | +import { getTeacherTaskListAPI, getTeacherTaskDetailAPI } from '@/api/teacher' | ||
| 26 | + | ||
| 27 | +const props = defineProps({ | ||
| 28 | + groupId: { | ||
| 29 | + type: [String, Number], | ||
| 30 | + default: '' | ||
| 31 | + } | ||
| 32 | +}) | ||
| 33 | + | ||
| 34 | +const emit = defineEmits(['change']) | ||
| 35 | + | ||
| 36 | +const show = ref(false) | ||
| 37 | +const cascaderValue = ref('') | ||
| 38 | +const options = ref([]) | ||
| 39 | +const fieldNames = { | ||
| 40 | + text: 'text', | ||
| 41 | + value: 'value', | ||
| 42 | + children: 'children', | ||
| 43 | + color: '#374151' | ||
| 44 | +} | ||
| 45 | + | ||
| 46 | +const selectedTask = ref(null) | ||
| 47 | +const selectedSubtask = ref(null) | ||
| 48 | + | ||
| 49 | +const selectedLabel = computed(() => { | ||
| 50 | + if (selectedSubtask.value) { | ||
| 51 | + return selectedSubtask.value.text | ||
| 52 | + } | ||
| 53 | + if (selectedTask.value) { | ||
| 54 | + return selectedTask.value.text | ||
| 55 | + } | ||
| 56 | + return '切换作业' | ||
| 57 | +}) | ||
| 58 | + | ||
| 59 | +// 获取作业列表(第一级) | ||
| 60 | +const fetchTaskList = async () => { | ||
| 61 | + if (!props.groupId) { | ||
| 62 | + options.value = [] | ||
| 63 | + return | ||
| 64 | + } | ||
| 65 | + | ||
| 66 | + try { | ||
| 67 | + const res = await getTeacherTaskListAPI({ | ||
| 68 | + group_id: props.groupId, | ||
| 69 | + limit: 1000, | ||
| 70 | + page: 0 | ||
| 71 | + }) | ||
| 72 | + | ||
| 73 | + if (res.code === 1) { | ||
| 74 | + // 构造第一级数据 | ||
| 75 | + // 注意:为了让级联选择器知道还有下一级,需要给children一个空数组 | ||
| 76 | + // 或者我们可以一次性加载?如果不确定数据量,建议动态加载 | ||
| 77 | + // 这里采用动态加载策略:给一个占位符,或者如果Vant Cascader支持,可以不给children但通过change事件动态添加 | ||
| 78 | + // Vant Cascader如果不给children,就认为是叶子节点。所以必须给children。 | ||
| 79 | + options.value = (res.data || []).map(item => ({ | ||
| 80 | + text: item.title, | ||
| 81 | + value: item.id, | ||
| 82 | + children: [], // 标记有子节点 | ||
| 83 | + color: '#374151' | ||
| 84 | + })) | ||
| 85 | + | ||
| 86 | + // 添加"全部作业"选项 | ||
| 87 | + options.value.unshift({ | ||
| 88 | + text: '全部作业', | ||
| 89 | + value: '', | ||
| 90 | + children: null, // 叶子节点 | ||
| 91 | + color: '#374151' | ||
| 92 | + }) | ||
| 93 | + } | ||
| 94 | + } catch (error) { | ||
| 95 | + console.error('获取作业列表失败:', error) | ||
| 96 | + } | ||
| 97 | +} | ||
| 98 | + | ||
| 99 | +// 获取子作业(第二级) | ||
| 100 | +const fetchSubtaskList = async (taskId) => { | ||
| 101 | + try { | ||
| 102 | + const res = await getTeacherTaskDetailAPI({ id: taskId }) | ||
| 103 | + if (res.code === 1) { | ||
| 104 | + const subtasks = res.data.subtask_list || [] | ||
| 105 | + return subtasks.map(item => ({ | ||
| 106 | + text: item.title, | ||
| 107 | + value: item.id, | ||
| 108 | + // 小作业没有下一级了 | ||
| 109 | + children: null, | ||
| 110 | + color: '#374151' | ||
| 111 | + })) | ||
| 112 | + } | ||
| 113 | + return [] | ||
| 114 | + } catch (error) { | ||
| 115 | + console.error('获取作业详情失败:', error) | ||
| 116 | + return [] | ||
| 117 | + } | ||
| 118 | +} | ||
| 119 | + | ||
| 120 | +// 监听 groupId 变化 | ||
| 121 | +watch(() => props.groupId, () => { | ||
| 122 | + // 重置 | ||
| 123 | + cascaderValue.value = '' | ||
| 124 | + selectedTask.value = null | ||
| 125 | + selectedSubtask.value = null | ||
| 126 | + fetchTaskList() | ||
| 127 | +}, { immediate: true }) | ||
| 128 | + | ||
| 129 | +const onChange = async ({ value, selectedOptions, tabIndex }) => { | ||
| 130 | + // 当选中第一级(大作业)时 | ||
| 131 | + if (tabIndex === 0) { | ||
| 132 | + const targetOption = selectedOptions[0] | ||
| 133 | + | ||
| 134 | + // 如果是"全部作业"(value为空字符串),它没有children,会直接触发finish(如果它是叶子节点) | ||
| 135 | + // 但我们在构造时设置了children: null,所以它就是叶子节点 | ||
| 136 | + if (targetOption.value === '') { | ||
| 137 | + return | ||
| 138 | + } | ||
| 139 | + | ||
| 140 | + // 如果还没有加载过子节点 | ||
| 141 | + if (targetOption.children && targetOption.children.length === 0) { | ||
| 142 | + // 加载子作业 | ||
| 143 | + const subtasks = await fetchSubtaskList(targetOption.value) | ||
| 144 | + | ||
| 145 | + // 添加"全部小作业"选项 | ||
| 146 | + const allSubtaskOption = { | ||
| 147 | + text: '全部小作业', | ||
| 148 | + value: '', | ||
| 149 | + children: null, | ||
| 150 | + color: '#374151' | ||
| 151 | + } | ||
| 152 | + | ||
| 153 | + targetOption.children = [allSubtaskOption, ...subtasks] | ||
| 154 | + } | ||
| 155 | + } | ||
| 156 | +} | ||
| 157 | + | ||
| 158 | +const onFinish = ({ selectedOptions }) => { | ||
| 159 | + show.value = false | ||
| 160 | + | ||
| 161 | + const taskOption = selectedOptions[0] | ||
| 162 | + const subtaskOption = selectedOptions[1] | ||
| 163 | + | ||
| 164 | + selectedTask.value = taskOption | ||
| 165 | + selectedSubtask.value = subtaskOption | ||
| 166 | + | ||
| 167 | + emit('change', { | ||
| 168 | + task_id: taskOption ? taskOption.value : '', | ||
| 169 | + subtask_id: subtaskOption ? subtaskOption.value : '' | ||
| 170 | + }) | ||
| 171 | +} | ||
| 172 | +</script> | ||
| 173 | + | ||
| 174 | +<style scoped> | ||
| 175 | +/* 样式调整 */ | ||
| 176 | +</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-12-18 10:01:20 | 4 | + * @LastEditTime: 2025-12-18 14:43:10 |
| 5 | * @FilePath: /mlaj/src/views/checkin/IndexCheckInPage.vue | 5 | * @FilePath: /mlaj/src/views/checkin/IndexCheckInPage.vue |
| 6 | * @Description: 文件描述 | 6 | * @Description: 文件描述 |
| 7 | --> | 7 | --> |
| ... | @@ -364,7 +364,6 @@ const selectedSubtaskId = ref('') | ... | @@ -364,7 +364,6 @@ const selectedSubtaskId = ref('') |
| 364 | 364 | ||
| 365 | const onSelectCourse = (course) => { | 365 | const onSelectCourse = (course) => { |
| 366 | selectedSubtaskId.value = course; | 366 | selectedSubtaskId.value = course; |
| 367 | - console.warn('选中的作业:', course); | ||
| 368 | // 切换作业后, 刷新当前日期的打卡详情 | 367 | // 切换作业后, 刷新当前日期的打卡详情 |
| 369 | getTaskDetail(dayjs(selectedDate.value).format('YYYY-MM')); | 368 | getTaskDetail(dayjs(selectedDate.value).format('YYYY-MM')); |
| 370 | // 重置分页参数 | 369 | // 重置分页参数 | ... | ... |
| 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-12-17 09:40:49 | 4 | + * @LastEditTime: 2025-12-18 15:08:53 |
| 5 | * @FilePath: /mlaj/src/views/teacher/checkinPage.vue | 5 | * @FilePath: /mlaj/src/views/teacher/checkinPage.vue |
| 6 | * @Description: 文件描述 | 6 | * @Description: 文件描述 |
| 7 | --> | 7 | --> |
| ... | @@ -86,7 +86,10 @@ | ... | @@ -86,7 +86,10 @@ |
| 86 | </div>--> | 86 | </div>--> |
| 87 | 87 | ||
| 88 | <div class="text-wrapper mt-4" style="padding: 0 1rem; color: #4caf50;"> | 88 | <div class="text-wrapper mt-4" style="padding: 0 1rem; color: #4caf50;"> |
| 89 | - <div class="text-header mb-4">学生动态</div> | 89 | + <div class="text-header mb-4 flex items-center justify-between"> |
| 90 | + <span>学生动态</span> | ||
| 91 | + <TaskCascaderFilter ref="taskCascaderFilter" v-if="selected_course_id" :group-id="selected_course_id" @change="onTaskFilterChange" /> | ||
| 92 | + </div> | ||
| 90 | <van-list | 93 | <van-list |
| 91 | v-if="checkinDataList.length" | 94 | v-if="checkinDataList.length" |
| 92 | v-model:loading="loading" | 95 | v-model:loading="loading" |
| ... | @@ -144,6 +147,7 @@ import AppLayout from "@/components/layout/AppLayout.vue"; | ... | @@ -144,6 +147,7 @@ import AppLayout from "@/components/layout/AppLayout.vue"; |
| 144 | import FrostedGlass from "@/components/ui/FrostedGlass.vue"; | 147 | import FrostedGlass from "@/components/ui/FrostedGlass.vue"; |
| 145 | import CheckinCard from "@/components/checkin/CheckinCard.vue"; | 148 | import CheckinCard from "@/components/checkin/CheckinCard.vue"; |
| 146 | import CourseGroupCascader from '@/components/ui/CourseGroupCascader.vue' | 149 | import CourseGroupCascader from '@/components/ui/CourseGroupCascader.vue' |
| 150 | +import TaskCascaderFilter from '@/components/teacher/TaskCascaderFilter.vue' | ||
| 147 | import PostCountModel from '@/components/count/postCountModel.vue' | 151 | import PostCountModel from '@/components/count/postCountModel.vue' |
| 148 | import { useTitle } from '@vueuse/core'; | 152 | import { useTitle } from '@vueuse/core'; |
| 149 | import dayjs from 'dayjs'; | 153 | import dayjs from 'dayjs'; |
| ... | @@ -241,6 +245,18 @@ const handleCourseChange = (val) => { | ... | @@ -241,6 +245,18 @@ const handleCourseChange = (val) => { |
| 241 | const selected_course_id = ref(null) | 245 | const selected_course_id = ref(null) |
| 242 | const selected_major_group_id = ref(null) | 246 | const selected_major_group_id = ref(null) |
| 243 | const selected_minor_group_id = ref(null) | 247 | const selected_minor_group_id = ref(null) |
| 248 | +const selected_task_id = ref(null) | ||
| 249 | +const selected_subtask_id = ref(null) | ||
| 250 | + | ||
| 251 | +/** | ||
| 252 | + * 作业筛选变化 | ||
| 253 | + */ | ||
| 254 | +const onTaskFilterChange = ({ task_id, subtask_id }) => { | ||
| 255 | + selected_task_id.value = task_id | ||
| 256 | + selected_subtask_id.value = subtask_id | ||
| 257 | + getCheckedDates(currentMonth.value); | ||
| 258 | + resetAndReload() | ||
| 259 | +} | ||
| 244 | 260 | ||
| 245 | /** | 261 | /** |
| 246 | * 级联筛选组件变化事件处理 | 262 | * 级联筛选组件变化事件处理 |
| ... | @@ -254,6 +270,9 @@ const on_cascade_change = (payload) => { | ... | @@ -254,6 +270,9 @@ const on_cascade_change = (payload) => { |
| 254 | selected_course_id.value = value | 270 | selected_course_id.value = value |
| 255 | selected_major_group_id.value = null | 271 | selected_major_group_id.value = null |
| 256 | selected_minor_group_id.value = null | 272 | selected_minor_group_id.value = null |
| 273 | + // 重置作业筛选 | ||
| 274 | + selected_task_id.value = null | ||
| 275 | + selected_subtask_id.value = null | ||
| 257 | } else if (type === 'major_group') { | 276 | } else if (type === 'major_group') { |
| 258 | selected_major_group_id.value = value | 277 | selected_major_group_id.value = value |
| 259 | selected_minor_group_id.value = null | 278 | selected_minor_group_id.value = null |
| ... | @@ -566,6 +585,8 @@ const getCheckedDates = async (month) => { | ... | @@ -566,6 +585,8 @@ const getCheckedDates = async (month) => { |
| 566 | group_id: selected_course_id.value, | 585 | group_id: selected_course_id.value, |
| 567 | team_id: selected_major_group_id.value, | 586 | team_id: selected_major_group_id.value, |
| 568 | subteam_id: selected_minor_group_id.value, | 587 | subteam_id: selected_minor_group_id.value, |
| 588 | + task_id: selected_task_id.value, | ||
| 589 | + subtask_id: selected_subtask_id.value, | ||
| 569 | month | 590 | month |
| 570 | }); | 591 | }); |
| 571 | if (checkedDatesResult.code) { | 592 | if (checkedDatesResult.code) { |
| ... | @@ -596,6 +617,8 @@ const onLoad = async (date) => { | ... | @@ -596,6 +617,8 @@ const onLoad = async (date) => { |
| 596 | group_id: selected_course_id.value, | 617 | group_id: selected_course_id.value, |
| 597 | team_id: selected_major_group_id.value, | 618 | team_id: selected_major_group_id.value, |
| 598 | subteam_id: selected_minor_group_id.value, | 619 | subteam_id: selected_minor_group_id.value, |
| 620 | + task_id: selected_task_id.value, | ||
| 621 | + subtask_id: selected_subtask_id.value, | ||
| 599 | }); | 622 | }); |
| 600 | if (res.code) { | 623 | if (res.code) { |
| 601 | // 整理数据结构 | 624 | // 整理数据结构 | ... | ... |
-
Please register or login to post a comment