hookehuyr

feat(教师页面): 添加课程分组级联筛选组件并实现相关功能

refactor(签到页面): 使用课程分组级联筛选替换原有筛选逻辑

feat(签到模块): 增加补卡日期显示功能

docs(API): 更新接口参数和返回值的文档说明

style(组件): 优化筛选组件样式与交互
###
# @Date: 2025-03-20 23:40:15
# @LastEditors: hookehuyr hookehuyr@gmail.com
# @LastEditTime: 2025-09-03 12:37:12
# @LastEditTime: 2025-09-30 15:45:25
# @FilePath: /mlaj/.env.development
# @Description: 文件描述
###
......@@ -23,8 +23,8 @@ VITE_PIN =
# 反向代理服务器地址
# VITE_PROXY_TARGET = https://oa.anxinchashi.com/
# VITE_PROXY_TARGET = http://behalo.onwall.cn/
# VITE_PROXY_TARGET = http://oa-dev.onwall.cn/
VITE_PROXY_TARGET = https://oa.behalo.cc/
VITE_PROXY_TARGET = http://oa-dev.onwall.cn/
# VITE_PROXY_TARGET = https://oa.behalo.cc/
# VITE_PROXY_TARGET = https://www.wxgzjs.cn/
# PC端地址
......
/*
* @Date: 2025-06-06 09:26:16
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-10-27 15:51:02
* @LastEditTime: 2025-11-10 10:37:44
* @FilePath: /mlaj/src/api/checkin.js
* @Description: 签到模块相关接口
*/
......@@ -111,6 +111,8 @@ export const dislikeUploadTaskInfoAPI = (params) => fn(fetch.post(Api.TASK_UPLO
* @param grade_id 年级ID
* @param class_id 班级ID
* @param group_id 课程ID
* @param team_id 大分组ID
* @param subteam_id 小分组ID
* @param date 日期
* @param keyword 搜索
* @param order_by_time asc=正序,desc=倒序。默认为倒序
......
/*
* @Date: 2025-06-23 11:46:21
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-10-13 10:09:15
* @LastEditTime: 2025-11-07 17:16:04
* @FilePath: /mlaj/src/api/teacher.js
* @Description: 文件描述
*/
......@@ -23,9 +23,9 @@ const Api = {
/**
* 获取老师的年级、班级、课程列表信息
* @param {*} grade_id 年级ID 用来缩小班级、课程的筛选范围
* @param {*} class_id 班级ID 用来缩小课程的筛选范围
* @returns {Object} data { grade_list [{id, grade_name}], class_list [{id, class_name}], group_list [{id, title}] }
* @param {*} group_id 课程ID,用来缩小大分组的筛选范围
* @param {*} team_id 大分组ID,用来缩小大分组的筛选范围
* @returns {Object} data { grade_list [{id, grade_name}], class_list [{id, class_name}], group_list [{id, title}], team_list [{id, team_name}], subteam_list [{id, subteam_name}] }
*/
export const getTeacherGradeClassListAPI = (params) => fn(fetch.get(Api.TEACHER_GRADE_CLASS_LIST, params))
......
......@@ -16,6 +16,7 @@ declare module 'vue' {
CollapsibleCalendar: typeof import('./components/ui/CollapsibleCalendar.vue')['default']
ConfirmDialog: typeof import('./components/ui/ConfirmDialog.vue')['default']
CourseCard: typeof import('./components/ui/CourseCard.vue')['default']
CourseGroupCascader: typeof import('./components/ui/CourseGroupCascader.vue')['default']
CourseImageCard: typeof import('./components/ui/CourseImageCard.vue')['default']
CourseList: typeof import('./components/courses/CourseList.vue')['default']
FormPage: typeof import('./components/infoEntry/formPage.vue')['default']
......@@ -31,6 +32,7 @@ declare module 'vue' {
RouterView: typeof import('vue-router')['RouterView']
SearchBar: typeof import('./components/ui/SearchBar.vue')['default']
SummerCampCard: typeof import('./components/ui/SummerCampCard.vue')['default']
TeacherFilter: typeof import('./components/ui/TeacherFilter.vue')['default']
TermsPopup: typeof import('./components/ui/TermsPopup.vue')['default']
UploadVideoPopup: typeof import('./components/ui/UploadVideoPopup.vue')['default']
UserAgreement: typeof import('./components/ui/UserAgreement.vue')['default']
......
<!--
* @Author: hookehuyr hookehuyr@gmail.com
* @Date: 2025-11-07 11:00:00
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-11-07 17:46:23
* @FilePath: /mlaj/src/components/ui/CourseGroupCascader.vue
* @Description: 教师页面筛选组件(年级/班级/课程),内部管理v-model与options并对外emit change事件
-->
<template>
<!-- 课程 / 年级 / 班级 级联筛选 -->
<van-dropdown-menu active-color="#10b981" swipe-threshold="2">
<van-dropdown-item v-model="select_course_value" :options="course_option" @change="on_course_change" />
<van-dropdown-item v-model="select_major_group_value" :options="major_group_option"
@change="on_major_group_change" />
<van-dropdown-item v-model="select_minor_group_value" :options="minor_group_option"
@change="on_minor_group_change" />
</van-dropdown-menu>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { getTeacherGradeClassListAPI } from "@/api/teacher";
// 组件对外事件
const emit = defineEmits(['change'])
// 本组件内部管理的选择值(课程 / 年级 / 班级)
const select_course_value = ref(null)
const select_major_group_value = ref(null)
const select_minor_group_value = ref(null)
// 本组件内部管理的选项数据(课程 / 年级 / 班级)
const course_option = ref([])
const major_group_option = ref([])
const minor_group_option = ref([])
// 获取筛选选项列表
const getFilterList = async (group_id = null, team_id = null) => {
const { code, data } = await getTeacherGradeClassListAPI({ group_id, team_id });
if (code) {
// 处理数据
course_option.value = data.group_list?.map(item => {
return {
text: item.title,
value: item.id,
}
});
course_option.value.unshift({
text: '全部课程',
value: null,
});
major_group_option.value = data.team_list?.map(item => {
return {
text: item.team_name,
value: item.id,
}
});
major_group_option.value.unshift({
text: '全部年级',
value: null,
});
minor_group_option.value = data.subteam_list?.map(item => {
return {
text: item.subteam_name,
value: item.id,
}
});
minor_group_option.value.unshift({
text: '全部班级',
value: null,
});
}
}
/**
* 课程变化处理
* @param {number|null} val 选中的课程ID
* @returns {void}
*/
const on_course_change = async (val) => {
select_course_value.value = val
// 切换课程时重置分组选择
select_major_group_value.value = null
select_minor_group_value.value = null
// 重建年级选项
getFilterList(val)
// 对外发出change事件,标识来源与值
emit('change', { type: 'course', value: val })
}
/**
* 年级变化处理
* @param {number|null} val 选中的年级ID
* @returns {void}
*/
const on_major_group_change = async (val) => {
select_major_group_value.value = val
// 切换年级时重置班级选择并重建班级选项
select_minor_group_value.value = null
getFilterList(select_course_value.value, val)
// 对外发出change事件,标识来源与值
emit('change', { type: 'major_group', value: val })
}
/**
* 班级变化处理
* @param {number|null} val 选中的班级ID
* @returns {void}
*/
const on_minor_group_change = (val) => {
select_minor_group_value.value = val
// 对外发出change事件,标识来源与值
emit('change', { type: 'minor_group', value: val })
}
/**
* 组件挂载时初始化筛选选项
* @returns {void}
*/
onMounted(async () => {
getFilterList()
})
</script>
<style lang="less">
// 保持与页面一致的下拉菜单样式(如需可局部覆盖)
.van-dropdown-menu {
background-color: white;
border-radius: 0.75rem;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
}
.van-dropdown-item {
.van-cell__title {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
flex: 0 0 80%;
}
.van-cell__value {
flex: 0 0 20%;
}
}
</style>
<!--
* @Date: 2025-05-29 15:34:17
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-10-15 15:09:29
* @LastEditTime: 2025-11-10 10:38:27
* @FilePath: /mlaj/src/views/checkin/IndexCheckInPage.vue
* @Description: 文件描述
-->
......@@ -428,6 +428,7 @@ const formatter = (day) => {
const date = day.date.getDate();
let checkin_days = myCheckinDates.value;
let fill_checkin_days = myFillCheckinDates.value;
// 禁用未来日期
if (dayjs(day.date).isAfter(new Date(), 'day')) {
......@@ -456,6 +457,25 @@ const formatter = (day) => {
}
}
// 检查当前日期是否在补卡日期列表中
if (fill_checkin_days && fill_checkin_days.length > 0) {
// 格式化当前日期为YYYY-MM-DD格式,与fill_checkin_days中的格式匹配
const formattedDate = `${year}-${month.toString().padStart(2, '0')}-${date.toString().padStart(2, '0')}`;
// 检查是否已补卡
if (fill_checkin_days.includes(formattedDate)) {
// 如果是当前选中的已补卡日期,使用特殊样式
if (selectedDate.value === formattedDate) {
day.className = 'calendar-selected';
day.type = 'selected';
day.bottomInfo = '待补卡';
} else {
day.className = 'calendar-fill-checkin';
day.type = 'selected';
day.bottomInfo = '待补卡';
}
}
}
// 选中今天的日期
if (dayjs(day.date).isSame(new Date(), 'day')) {
day.className = 'calendar-today';
......@@ -651,6 +671,7 @@ const delCheckin = (post) => {
const taskDetail = ref({});
const myCheckinDates = ref([]);
const myFillCheckinDates = ref([]);
const checkinDataList = ref([]);
const showProgress = ref(true);
......@@ -666,6 +687,8 @@ const getTaskDetail = async (month) => {
teamAvatars.value = data.checkin_avatars;
// 获取当前用户的打卡日期
myCheckinDates.value = data.my_checkin_dates;
// 获取当前用户的补卡日期
myFillCheckinDates.value = data.makeup_checkin_dates;
// 把['2025-06-06'] 转化为 [6] 只取日期去掉0
// myCheckinDates.value = myCheckinDates.value.map(date => {
// return dayjs(date).date();
......@@ -812,6 +835,12 @@ const formatData = (data) => {
}
}
.calendar-fill-checkin {
.van-calendar__selected-day {
background: #a2d8a3 !important;
}
}
.calendar-today {
.van-calendar__selected-day {
background: #FAAB0C !important;
......
<!--
* @Date: 2025-05-29 15:34:17
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-10-27 16:34:26
* @LastEditTime: 2025-11-10 10:43:07
* @FilePath: /mlaj/src/views/teacher/checkinPage.vue
* @Description: 文件描述
-->
......@@ -9,11 +9,13 @@
<AppLayout :hasTitle="false">
<van-config-provider :theme-vars="themeVars">
<van-sticky>
<van-dropdown-menu active-color="#4caf50" swipe-threshold="2">
<!-- <van-dropdown-menu active-color="#4caf50" swipe-threshold="2">
<van-dropdown-item v-model="selectGradeValue" :options="gradeOption" @change="handleGradeChange" />
<van-dropdown-item v-model="selectClassValue" :options="classOption" @change="handleClassChange" />
<van-dropdown-item v-model="selectCourseValue" :options="courseOption" @change="handleCourseChange" />
</van-dropdown-menu>
</van-dropdown-menu> -->
<!-- 课程, 大分组, 小分组 筛选 -->
<CourseGroupCascader @change="on_cascade_change" />
</van-sticky>
<van-calendar ref="myRefCalendar" :show-title="false" :poppable="false" :show-confirm="false"
......@@ -22,6 +24,7 @@
@panel-change="onPanelChange"
@click-subtitle="onClickSubtitle">
</van-calendar>
<div style="padding: 0 1rem; margin-top: 1rem;">
<van-row gutter="15">
<van-col span="12">
......@@ -191,10 +194,11 @@ import AppLayout from "@/components/layout/AppLayout.vue";
import FrostedGlass from "@/components/ui/FrostedGlass.vue";
import VideoPlayer from "@/components/ui/VideoPlayer.vue";
import AudioPlayer from "@/components/ui/AudioPlayer.vue";
import CourseGroupCascader from '@/components/ui/CourseGroupCascader.vue'
import { useTitle } from '@vueuse/core';
import dayjs from 'dayjs';
import { getTaskDetailAPI, getCheckinTeacherListAPI, checkinTaskReviewAPI, likeUploadTaskInfoAPI, dislikeUploadTaskInfoAPI,getCheckinTeacherCheckedDatesAPI } from "@/api/checkin";
import { getTaskDetailAPI, getCheckinTeacherListAPI, checkinTaskReviewAPI, likeUploadTaskInfoAPI, dislikeUploadTaskInfoAPI, getCheckinTeacherCheckedDatesAPI } from "@/api/checkin";
import { getTeacherGradeClassListAPI } from "@/api/teacher";
const route = useRoute()
......@@ -214,6 +218,18 @@ const courseOption = ref([]);
const currentMonth = ref(dayjs().format('YYYY-MM'));
/**
* 重置分页参数并重新加载数据
*/
const resetAndReload = () => {
// 重置分页参数
page.value = 0
checkinDataList.value = []
finished.value = false
// 重新加载数据
onLoad()
}
const handleGradeChange = async (val) => {
console.log('val', val);
selectGradeValue.value = val;
......@@ -259,6 +275,37 @@ const handleCourseChange = (val) => {
onLoad()
}
// 级联筛选选中值
const selected_course_id = ref(null)
const selected_major_group_id = ref(null)
const selected_minor_group_id = ref(null)
/**
* 级联筛选组件变化事件处理
* @param {Object} payload - 事件数据,包含 { type, value }
* @returns {void}
*/
const on_cascade_change = (payload) => {
if (!payload) return
const { type, value } = payload
if (type === 'course') {
selected_course_id.value = value
selected_major_group_id.value = null
selected_minor_group_id.value = null
} else if (type === 'major_group') {
selected_major_group_id.value = value
selected_minor_group_id.value = null
} else if (type === 'minor_group') {
selected_minor_group_id.value = value
}
// 级联筛选变化
console.log('级联筛选变化: ', payload)
// 重新获取打卡日期
getCheckedDates(currentMonth.value);
// 重置分页参数并重新加载数据
resetAndReload();
}
const active = ref(0);
const myRefCalendar = ref(null);
......@@ -698,9 +745,12 @@ const getTaskDetail = async (month) => {
// 获取用户打卡日期的函数
const getCheckedDates = async (month) => {
const checkedDatesResult = await getCheckinTeacherCheckedDatesAPI({
grade_id: selectGradeValue.value,
class_id: selectClassValue.value,
group_id: selectCourseValue.value,
// grade_id: selectGradeValue.value,
// class_id: selectClassValue.value,
// group_id: selectCourseValue.value,
group_id: selected_course_id.value,
team_id: selected_major_group_id.value,
subteam_id: selected_minor_group_id.value,
month
});
if (checkedDatesResult.code) {
......@@ -725,9 +775,12 @@ const onLoad = async (date) => {
limit: limit.value,
page: nextPage,
date: current_date,
grade_id: selectGradeValue.value,
class_id: selectClassValue.value,
course_id: selectCourseValue.value,
// grade_id: selectGradeValue.value,
// class_id: selectClassValue.value,
// course_id: selectCourseValue.value,
group_id: selected_course_id.value,
team_id: selected_major_group_id.value,
subteam_id: selected_minor_group_id.value,
});
if (res.code) {
// 整理数据结构
......@@ -792,7 +845,7 @@ onMounted(async () => {
onLoad(dayjs().format('YYYY-MM-DD'));
}
// 获取老师的年级、班级、课程列表信息
getFilterList();
// getFilterList();
})
const formatData = (data) => {
......
......@@ -2,7 +2,7 @@
* @Author: hookehuyr hookehuyr@gmail.com
* @Date: 2025-01-20 10:00:00
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-09-26 11:31:09
* @LastEditTime: 2025-11-07 18:03:23
* @FilePath: /mlaj/src/views/teacher/myClassPage.vue
* @Description: 我的班级页面
-->
......@@ -28,7 +28,7 @@
</div>
<!-- 筛选器 -->
<div>
<!-- <div>
<van-sticky>
<van-dropdown-menu active-color="#10b981" swipe-threshold="2">
<van-dropdown-item v-model="selectGradeValue" :options="gradeOption" @change="handleGradeChange" />
......@@ -36,6 +36,12 @@
<van-dropdown-item v-model="selectCourseValue" :options="courseOption" @change="handleCourseChange" />
</van-dropdown-menu>
</van-sticky>
</div> -->
<!-- 课程, 大分组, 小分组 筛选 -->
<div>
<van-sticky>
<CourseGroupCascader @change="on_cascade_change" />
</van-sticky>
</div>
<div style="height: 0.5rem;"></div>
......@@ -159,6 +165,7 @@ import { useRouter } from 'vue-router'
import AppLayout from '@/layouts/AppLayout.vue'
import { useTitle } from '@vueuse/core';
import { useAuth } from '@/contexts/auth'
import CourseGroupCascader from '@/components/ui/CourseGroupCascader.vue'
import { getTeacherGradeClassListAPI, getStudentListAPI } from "@/api/teacher";
......@@ -277,6 +284,35 @@ const handleCourseChange = (val) => {
resetAndReload();
}
// 级联筛选选中值
const selected_course_id = ref(null)
const selected_major_group_id = ref(null)
const selected_minor_group_id = ref(null)
/**
* 级联筛选组件变化事件处理
* @param {Object} payload - 事件数据,包含 { type, value }
* @returns {void}
*/
const on_cascade_change = (payload) => {
if (!payload) return
const { type, value } = payload
if (type === 'course') {
selected_course_id.value = value
selected_major_group_id.value = null
selected_minor_group_id.value = null
} else if (type === 'major_group') {
selected_major_group_id.value = value
selected_minor_group_id.value = null
} else if (type === 'minor_group') {
selected_minor_group_id.value = value
}
// 级联筛选变化
console.log('级联筛选变化: ', payload)
// 重置分页参数并重新加载数据
resetAndReload();
}
/**
* 处理排序变化
* @param {string} value - 选中的排序方式
......@@ -368,9 +404,12 @@ const onLoad = async () => {
const res = await getStudentListAPI({
limit: limit.value,
page: nextPage,
grade_id: selectGradeValue.value,
class_id: selectClassValue.value,
course_id: selectCourseValue.value,
// grade_id: selectGradeValue.value,
// class_id: selectClassValue.value,
// course_id: selectCourseValue.value,
group_id: selected_course_id.value,
team_id: selected_major_group_id.value,
subteam_id: selected_minor_group_id.value,
keyword: searchKeyword.value,
});
......@@ -439,7 +478,7 @@ const getFilterList = async (grade_id=null, class_id=null) => {
*/
onMounted(async () => {
// 获取老师的年级、班级、课程列表信息
getFilterList();
// getFilterList();
})
/**
......