hookehuyr

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

添加 TaskCascaderFilter 组件用于教师端作业筛选,支持两级级联选择
在教师打卡页面集成该组件,根据筛选条件动态刷新打卡数据
移除调试用的 console.warn 日志
......@@ -39,6 +39,7 @@ declare module 'vue' {
SharePoster: typeof import('./components/ui/SharePoster.vue')['default']
SummerCampCard: typeof import('./components/ui/SummerCampCard.vue')['default']
TaskCalendar: typeof import('./components/ui/TaskCalendar.vue')['default']
TaskCascaderFilter: typeof import('./components/teacher/TaskCascaderFilter.vue')['default']
TaskFilter: typeof import('./components/teacher/TaskFilter.vue')['default']
TermsPopup: typeof import('./components/ui/TermsPopup.vue')['default']
UploadVideoPopup: typeof import('./components/ui/UploadVideoPopup.vue')['default']
......@@ -48,6 +49,7 @@ declare module 'vue' {
VanBadge: typeof import('vant/es')['Badge']
VanButton: typeof import('vant/es')['Button']
VanCalendar: typeof import('vant/es')['Calendar']
VanCascader: typeof import('vant/es')['Cascader']
VanCell: typeof import('vant/es')['Cell']
VanCellGroup: typeof import('vant/es')['CellGroup']
VanCheckbox: typeof import('vant/es')['Checkbox']
......
<template>
<div class="inline-block">
<div @click="show = true" class="text-sm text-green-600 flex items-center cursor-pointer ml-2">
<span class="mr-1">{{ selectedLabel }}</span>
<van-icon name="arrow-down" />
</div>
<van-popup v-model:show="show" round position="bottom">
<van-cascader
v-model="cascaderValue"
title="请选择作业"
:options="options"
:field-names="fieldNames"
active-color="#22c55e"
@close="show = false"
@finish="onFinish"
@change="onChange"
/>
</van-popup>
</div>
</template>
<script setup>
import { ref, watch, computed } from 'vue'
import { getTeacherTaskListAPI, getTeacherTaskDetailAPI } from '@/api/teacher'
const props = defineProps({
groupId: {
type: [String, Number],
default: ''
}
})
const emit = defineEmits(['change'])
const show = ref(false)
const cascaderValue = ref('')
const options = ref([])
const fieldNames = {
text: 'text',
value: 'value',
children: 'children',
color: '#374151'
}
const selectedTask = ref(null)
const selectedSubtask = ref(null)
const selectedLabel = computed(() => {
if (selectedSubtask.value) {
return selectedSubtask.value.text
}
if (selectedTask.value) {
return selectedTask.value.text
}
return '切换作业'
})
// 获取作业列表(第一级)
const fetchTaskList = async () => {
if (!props.groupId) {
options.value = []
return
}
try {
const res = await getTeacherTaskListAPI({
group_id: props.groupId,
limit: 1000,
page: 0
})
if (res.code === 1) {
// 构造第一级数据
// 注意:为了让级联选择器知道还有下一级,需要给children一个空数组
// 或者我们可以一次性加载?如果不确定数据量,建议动态加载
// 这里采用动态加载策略:给一个占位符,或者如果Vant Cascader支持,可以不给children但通过change事件动态添加
// Vant Cascader如果不给children,就认为是叶子节点。所以必须给children。
options.value = (res.data || []).map(item => ({
text: item.title,
value: item.id,
children: [], // 标记有子节点
color: '#374151'
}))
// 添加"全部作业"选项
options.value.unshift({
text: '全部作业',
value: '',
children: null, // 叶子节点
color: '#374151'
})
}
} catch (error) {
console.error('获取作业列表失败:', error)
}
}
// 获取子作业(第二级)
const fetchSubtaskList = async (taskId) => {
try {
const res = await getTeacherTaskDetailAPI({ id: taskId })
if (res.code === 1) {
const subtasks = res.data.subtask_list || []
return subtasks.map(item => ({
text: item.title,
value: item.id,
// 小作业没有下一级了
children: null,
color: '#374151'
}))
}
return []
} catch (error) {
console.error('获取作业详情失败:', error)
return []
}
}
// 监听 groupId 变化
watch(() => props.groupId, () => {
// 重置
cascaderValue.value = ''
selectedTask.value = null
selectedSubtask.value = null
fetchTaskList()
}, { immediate: true })
const onChange = async ({ value, selectedOptions, tabIndex }) => {
// 当选中第一级(大作业)时
if (tabIndex === 0) {
const targetOption = selectedOptions[0]
// 如果是"全部作业"(value为空字符串),它没有children,会直接触发finish(如果它是叶子节点)
// 但我们在构造时设置了children: null,所以它就是叶子节点
if (targetOption.value === '') {
return
}
// 如果还没有加载过子节点
if (targetOption.children && targetOption.children.length === 0) {
// 加载子作业
const subtasks = await fetchSubtaskList(targetOption.value)
// 添加"全部小作业"选项
const allSubtaskOption = {
text: '全部小作业',
value: '',
children: null,
color: '#374151'
}
targetOption.children = [allSubtaskOption, ...subtasks]
}
}
}
const onFinish = ({ selectedOptions }) => {
show.value = false
const taskOption = selectedOptions[0]
const subtaskOption = selectedOptions[1]
selectedTask.value = taskOption
selectedSubtask.value = subtaskOption
emit('change', {
task_id: taskOption ? taskOption.value : '',
subtask_id: subtaskOption ? subtaskOption.value : ''
})
}
</script>
<style scoped>
/* 样式调整 */
</style>
<!--
* @Date: 2025-05-29 15:34:17
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-12-18 10:01:20
* @LastEditTime: 2025-12-18 14:43:10
* @FilePath: /mlaj/src/views/checkin/IndexCheckInPage.vue
* @Description: 文件描述
-->
......@@ -364,7 +364,6 @@ const selectedSubtaskId = ref('')
const onSelectCourse = (course) => {
selectedSubtaskId.value = course;
console.warn('选中的作业:', course);
// 切换作业后, 刷新当前日期的打卡详情
getTaskDetail(dayjs(selectedDate.value).format('YYYY-MM'));
// 重置分页参数
......
<!--
* @Date: 2025-05-29 15:34:17
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-12-17 09:40:49
* @LastEditTime: 2025-12-18 15:08:53
* @FilePath: /mlaj/src/views/teacher/checkinPage.vue
* @Description: 文件描述
-->
......@@ -86,7 +86,10 @@
</div>-->
<div class="text-wrapper mt-4" style="padding: 0 1rem; color: #4caf50;">
<div class="text-header mb-4">学生动态</div>
<div class="text-header mb-4 flex items-center justify-between">
<span>学生动态</span>
<TaskCascaderFilter ref="taskCascaderFilter" v-if="selected_course_id" :group-id="selected_course_id" @change="onTaskFilterChange" />
</div>
<van-list
v-if="checkinDataList.length"
v-model:loading="loading"
......@@ -144,6 +147,7 @@ import AppLayout from "@/components/layout/AppLayout.vue";
import FrostedGlass from "@/components/ui/FrostedGlass.vue";
import CheckinCard from "@/components/checkin/CheckinCard.vue";
import CourseGroupCascader from '@/components/ui/CourseGroupCascader.vue'
import TaskCascaderFilter from '@/components/teacher/TaskCascaderFilter.vue'
import PostCountModel from '@/components/count/postCountModel.vue'
import { useTitle } from '@vueuse/core';
import dayjs from 'dayjs';
......@@ -241,6 +245,18 @@ const handleCourseChange = (val) => {
const selected_course_id = ref(null)
const selected_major_group_id = ref(null)
const selected_minor_group_id = ref(null)
const selected_task_id = ref(null)
const selected_subtask_id = ref(null)
/**
* 作业筛选变化
*/
const onTaskFilterChange = ({ task_id, subtask_id }) => {
selected_task_id.value = task_id
selected_subtask_id.value = subtask_id
getCheckedDates(currentMonth.value);
resetAndReload()
}
/**
* 级联筛选组件变化事件处理
......@@ -254,6 +270,9 @@ const on_cascade_change = (payload) => {
selected_course_id.value = value
selected_major_group_id.value = null
selected_minor_group_id.value = null
// 重置作业筛选
selected_task_id.value = null
selected_subtask_id.value = null
} else if (type === 'major_group') {
selected_major_group_id.value = value
selected_minor_group_id.value = null
......@@ -566,6 +585,8 @@ const getCheckedDates = async (month) => {
group_id: selected_course_id.value,
team_id: selected_major_group_id.value,
subteam_id: selected_minor_group_id.value,
task_id: selected_task_id.value,
subtask_id: selected_subtask_id.value,
month
});
if (checkedDatesResult.code) {
......@@ -596,6 +617,8 @@ const onLoad = async (date) => {
group_id: selected_course_id.value,
team_id: selected_major_group_id.value,
subteam_id: selected_minor_group_id.value,
task_id: selected_task_id.value,
subtask_id: selected_subtask_id.value,
});
if (res.code) {
// 整理数据结构
......