hookehuyr

feat(teacher): 添加作业级联筛选组件并集成到打卡页面

添加 TaskCascaderFilter 组件用于教师端作业筛选,支持两级级联选择
在教师打卡页面集成该组件,根据筛选条件动态刷新打卡数据
移除调试用的 console.warn 日志
...@@ -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 // 整理数据结构
......