hookehuyr

feat(教师端): 实现作业管理和学生记录功能

- 在教师端添加作业管理页面,支持分页加载作业列表
- 实现作业详情页面,展示作业信息和学生完成情况
- 修改学生记录页面,从URL获取用户ID和日期参数
- 添加相关API接口用于获取作业列表和详情
- 移除Mock数据,使用真实API获取数据
###
# @Date: 2025-03-20 23:40:15
# @LastEditors: hookehuyr hookehuyr@gmail.com
# @LastEditTime: 2025-09-30 15:45:25
# @LastEditTime: 2025-12-02 18:34:41
# @FilePath: /mlaj/.env.development
# @Description: 文件描述
###
......
/*
* @Date: 2025-06-06 09:26:16
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-11-17 14:33:59
* @LastEditTime: 2025-12-01 14:06:29
* @FilePath: /mlaj/src/api/checkin.js
* @Description: 签到模块相关接口
*/
......@@ -113,6 +113,7 @@ export const dislikeUploadTaskInfoAPI = (params) => fn(fetch.post(Api.TASK_UPLO
* @param group_id 课程ID
* @param team_id 大分组ID
* @param subteam_id 小分组ID
* @param created_by 学员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-11-10 10:56:17
* @LastEditTime: 2025-12-01 14:13:18
* @FilePath: /mlaj/src/api/teacher.js
* @Description: 文件描述
*/
......@@ -11,6 +11,8 @@ const Api = {
TEACHER_GRADE_CLASS_LIST: '/srv/?a=user&t=teacher_grade_class_group_list',
TEACHER_FIND_SETTINGS: '/srv/?a=task&t=teacher_find_settings',
TEACHER_ADD_TASK: '/srv/?a=task&t=teacher_add',
TEACHER_LIST_TASK: '/srv/?a=task&t=teacher_list',
TEACHER_DETAIL_TASK: '/srv/?a=task&t=teacher_detail',
STUDENT_LIST: '/srv/?a=user&t=student_list',
STUDENT_DETAIL: '/srv/?a=user&t=student_detail',
STUDENT_STAT: '/srv/?a=user&t=student_stat',
......@@ -56,6 +58,47 @@ export const getTeacherFindSettingsAPI = (params) => fn(fetch.get(Api.TEACHER_FI
export const setTeacherTaskAPI = (params) => fn(fetch.post(Api.TEACHER_ADD_TASK, params))
/**
* 获取老师的作业列表
* @param {*} group_id 课程ID
* @param {*} team_id 大分组ID
* @param {*} subteam_id 小分组ID
* @param {*} limit 条数 100
* @param {*} page 页码 0
* @returns {Object} data {
* id integer 作业id
* title string 作业名称
* cycle string 作业周期 0=本周期 30=每月 7=每周 1=每日
* frequency integer 每周期交作业的次数
* attachment_type array[string] 提交类型 text=文本 image=图片 video=视频 audio=音频
* begin_date string 开始时间
* end_date number 结束时间
* task_type string 任务类型 checkin=签到 file=上传附件
* }
*/
export const getTeacherTaskListAPI = (params) => fn(fetch.get(Api.TEACHER_LIST_TASK, params))
/**
* 获取老师的作业详情
* @param {*} id 作业ID
* @param {*} date 日期。默认是今天 示例值: [""]
* @returns {Object} data {
* id integer 作业id
* begin_date string 开始时间
* end_date string 结束时间
* task_type string 任务类型 checkin=签到 file=上传附件
* title string 作业名称
* cycle string 作业周期 0=本周期 30=每月 7=每周 1=每日
* frequency integer 每周期交作业的次数
* attachment_type array[string] 提交类型 text=文本 image=图片 video=视频 audio=音频
* note string 作业描述
* need_commit_count integer 需要提交次数
* real_commit_count integer 已提交次数
* user_list [{id 用户ID, name 姓名, avatar 头像, is_commit 是否提交了作业}]
* }
*/
export const getTeacherTaskDetailAPI = (params) => fn(fetch.get(Api.TEACHER_DETAIL_TASK, params))
/**
* 获取学员列表
* @param {*} grade_id 年级ID
* @param {*} class_id 班级ID
......
<!--
* @Date: 2025-11-19 22:05:00
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-11-19 22:12:24
* @LastEditTime: 2025-12-02 18:44:51
* @FilePath: /mlaj/src/views/teacher/studentRecordPage.vue
* @Description: 学生作业记录页面(仅作业记录与点评功能),固定 user_id 与 group_id
-->
......@@ -120,16 +120,16 @@ import { useTitle } from '@vueuse/core'
import { showSuccessToast, showFailToast, showLoadingToast } from 'vant'
import VideoPlayer from '@/components/ui/VideoPlayer.vue'
import AudioPlayer from '@/components/ui/AudioPlayer.vue'
import { getStudentUploadListAPI, addCheckinFeedbackAPI } from '@/api/teacher'
import { likeUploadTaskInfoAPI, dislikeUploadTaskInfoAPI } from '@/api/checkin'
import { addCheckinFeedbackAPI } from '@/api/teacher'
import { likeUploadTaskInfoAPI, dislikeUploadTaskInfoAPI, getCheckinTeacherListAPI } from '@/api/checkin'
const $route = useRoute()
const $router = useRouter()
useTitle($route.meta.title)
// 固定的用户与班级ID
const fixedUserId = 817017
const fixedGroupId = 816653
// 从url获取固定的用户与班级ID
const fixedUserId = Number($route.query.created_by)
const fixedDate = $route.query.date
// 列表相关状态
const checkinDataList = ref([])
......@@ -401,15 +401,15 @@ function stopAllVideos() {
*/
async function onLoad() {
const nextPage = page.value
const res = await getStudentUploadListAPI({
const res = await getCheckinTeacherListAPI({
limit: limit.value,
page: nextPage,
user_id: fixedUserId,
group_id: fixedGroupId,
created_by: fixedUserId,
date: fixedDate,
})
if (res.code) {
checkinDataList.value = [...checkinDataList.value, ...formatData(res.data)]
finished.value = res.data.length < limit.value
checkinDataList.value = [...checkinDataList.value, ...formatData(res.data.checkin_list)]
finished.value = res.data.checkin_list.length < limit.value
page.value = nextPage + 1
}
loading.value = false
......
<!--
* @Date: 2025-11-19 21:00:00
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-11-21 16:19:23
* @LastEditTime: 2025-12-02 18:48:32
* @FilePath: /mlaj/src/views/teacher/taskHomePage.vue
* @Description: 教师端作业主页(头部介绍、统计、日历与学生完成情况;数据Mock)
-->
......@@ -10,7 +10,7 @@
<!-- 头部卡片:名称、介绍、细项(参考图片1结构) -->
<div class="headCard bg-white rounded-lg shadow px-4 py-4">
<div class="title text-2xl font-bold text-gray-900 mb-2">{{ task_title }}</div>
<div class="intro text-base text-gray-700 leading-relaxed mb-3">{{ task_intro }}</div>
<div class="intro text-base text-gray-700 leading-relaxed mb-3" v-html="task_intro"></div>
<!-- 三图展示(可选),使用CDN示例地址并加压缩参数 -->
<!-- <div class="images grid grid-cols-3 gap-3 mb-3">
<img v-for="(img, idx) in task_images" :key="idx" :src="img"
......@@ -28,7 +28,7 @@
<div class="statsCard bg-white rounded-lg shadow px-4 py-4 mt-4">
<van-row gutter="16">
<!-- 出勤率 -->
<van-col span="12">
<!-- <van-col span="12">
<div class="text-center">
<div class="relative w-16 h-16 mx-auto mb-2">
<van-circle v-model:current-rate="checkin_count" :rate="checkin_count" :text="checkin_text"
......@@ -36,7 +36,7 @@
</div>
<div class="text-sm text-gray-600">出勤率</div>
</div>
</van-col>
</van-col> -->
<!-- 任务完成 -->
<van-col span="12">
<div class="text-center">
......@@ -84,7 +84,7 @@
<!-- 学生完成情况(参考图片2样式) -->
<div class="studentsCard bg-white rounded-lg shadow px-4 py-4 mt-4">
<div class="flex items-center justify-between mb-3">
<div class="text-base font-semibold text-gray-800">完成情况({{ completed_count }}/{{ students.length }})
<div class="text-base font-semibold text-gray-800">完成情况({{ completed_count }}/{{ user_list.length }})
</div>
<!-- <div class="text-xs text-gray-500">当前日期:{{ current_date_text }}</div> -->
<div class="text-xs text-gray-500">点击查看作业记录</div>
......@@ -104,10 +104,11 @@
</template>
<script setup>
import { ref, computed } from 'vue'
import { ref, computed, onMounted, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useTitle } from '@vueuse/core'
import checkCorner from '@/assets/images/dui.png'
import { getTeacherTaskDetailAPI } from '@/api/teacher'
const $route = useRoute()
const $router = useRouter()
......@@ -157,63 +158,106 @@ function open_specific_date_picker() {
}
//
// Mock:作业基础信息
// API 数据状态
//
const task_id = $route.params.id || '0'
const task_title = ref('组长每日共修打卡内容')
const task_intro = ref('组长与副组长以上每日语音打卡内容:《义工宣言》《万事如意祈请文》《吉祥圆满感恩文》')
const task_title = ref('')
const task_intro = ref('')
// 图片暂时保留 Mock 或从 API 扩展字段获取(当前 API 未返回图片列表,暂时保留为空或 Mock)
const task_images = ref([
'https://cdn.ipadbiz.cn/mlaj/demo-task-1.png?imageMogr2/thumbnail/200x/strip/quality/70',
'https://cdn.ipadbiz.cn/mlaj/demo-task-2.png?imageMogr2/thumbnail/200x/strip/quality/70',
'https://cdn.ipadbiz.cn/mlaj/demo-task-3.png?imageMogr2/thumbnail/200x/strip/quality/70'
])
const task_details = ref({
cycle: '每天',
frequency: '每日一次',
time_range: '00:00 ~ 23:59',
attachment_type: '语音/文本'
cycle: '',
frequency: '',
time_range: '',
attachment_type: ''
})
//
// Mock:统计数据
// 统计数据
//
const checkin_count = ref(56)
const upload_count = ref(62)
const checkin_count = ref(0)
const upload_count = ref(0)
const checkin_text = computed(() => `${checkin_count.value}%`)
const upload_text = computed(() => `${upload_count.value}%`)
//
// Mock:学生与完成记录
// 注:每个学生给出若干已完成的日期字符串(YYYY-MM-DD)
// 学生与完成记录
//
const today = new Date()
const selected_date = ref(format_date(today))
const students = ref([
{ id: '1', name: '王菲', completed_dates: ['2025-11-18', selected_date.value] },
{ id: '2', name: '朱明献', completed_dates: ['2025-11-18'] },
{ id: '3', name: '陈小云', completed_dates: [] },
{ id: '4', name: '冯新虎', completed_dates: [selected_date.value] },
{ id: '5', name: '罗睿', completed_dates: ['2025-11-17', '2025-11-18'] },
{ id: '6', name: '吴绍婷', completed_dates: [] },
{ id: '7', name: '焦淑敏', completed_dates: [selected_date.value] },
{ id: '8', name: '李言斐', completed_dates: [] },
{ id: '9', name: '陈正统', completed_dates: [] },
{ id: '10', name: '杨子娟', completed_dates: [] },
{ id: '11', name: '方萍', completed_dates: [selected_date.value] },
{ id: '12', name: '冯静', completed_dates: [selected_date.value] },
{ id: '13', name: '尤瑞', completed_dates: [] },
{ id: '14', name: '鲁镇伟', completed_dates: [] },
{ id: '15', name: '黄润', completed_dates: [selected_date.value] },
{ id: '16', name: '王亚琼', completed_dates: [selected_date.value] },
{ id: '17', name: '高晓云', completed_dates: [selected_date.value] },
{ id: '18', name: '张朗', completed_dates: [] },
{ id: '19', name: '姚娟', completed_dates: [selected_date.value] },
{ id: '20', name: '李凯', completed_dates: [selected_date.value] },
{ id: '21', name: '李鑫', completed_dates: [] },
{ id: '22', name: '礼忠斌', completed_dates: [] },
{ id: '23', name: '谭小梅', completed_dates: [selected_date.value] },
{ id: '24', name: '赵红梅', completed_dates: [] }
])
const user_list = ref([])
/**
* 获取作业详情和学生完成情况
*/
async function fetchData() {
try {
const res = await getTeacherTaskDetailAPI({
id: task_id,
date: selected_date.value
})
if (res.code) {
task_title.value = res.data.title
task_intro.value = res.data.note
// 格式化周期显示
const cycleMap = {
'0': '本周期',
'30': '每月',
'7': '每周',
'1': '每日' // 假设 1 代表每日
}
// 如果后端返回的是数字字符串,尝试映射,否则直接显示
const cycleText = cycleMap[res.data.cycle] || res.data.cycle || '每日'
// 格式化附件类型
let attachmentText = '文本'
if (Array.isArray(res.data.attachment_type)) {
const typeMap = {
'text': '文本',
'image': '图片',
'video': '视频',
'audio': '音频'
}
attachmentText = res.data.attachment_type.map(t => typeMap[t] || t).join('/')
} else if (res.data.attachment_type) {
attachmentText = res.data.attachment_type
}
task_details.value = {
cycle: cycleText,
frequency: `每周期${res.data.frequency || 1}次`,
time_range: `${res.data.begin_date || '00:00'} ~ ${res.data.end_date || '23:59'}`, // 注意:API返回的begin_date/end_date可能是日期也可能是时间,这里暂且直接展示
attachment_type: attachmentText
}
user_list.value = res.data.user_list || []
// 计算完成率
if (res.data.need_commit_count > 0) {
upload_count.value = Math.round((res.data.real_commit_count / res.data.need_commit_count) * 100)
} else {
upload_count.value = 0
}
}
} catch (error) {
console.error('Failed to fetch task details:', error)
}
}
// 监听日期变化,重新获取数据
watch(selected_date, () => {
fetchData()
})
onMounted(() => {
fetchData()
})
/**
* 将日期对象格式化为 YYYY-MM-DD
......@@ -266,13 +310,13 @@ function on_date_select(val) {
/**
* 计算某日期下学生完成情况列表
* @returns {{id:string,name:string,completed:boolean}[]} 学生状态列表
* 注释:根据 selected_date 在每个学生的 completed_dates 中判断是否完成。
*/
const students_status = computed(() => {
return students.value.map(stu => ({
return user_list.value.map(stu => ({
id: stu.id,
name: stu.name,
completed: stu.completed_dates.includes(selected_date.value)
completed: !!stu.is_commit,
avatar: stu.avatar // 保留头像字段以便将来使用
}))
})
......@@ -295,7 +339,7 @@ const current_date_text = computed(() => selected_date.value)
*/
function go_student_record(stu) {
// 跳转到固定ID的作业记录页面,当前版本不使用传入ID
$router.push({ name: 'StudentRecord' })
$router.push({ name: 'StudentRecord', query: { created_by: stu.id, date: selected_date.value } })
}
</script>
......
<!--
* @Date: 2025-11-19 10:18:00
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-11-19 20:25:40
* @LastEditTime: 2025-12-01 14:30:47
* @FilePath: /mlaj/src/views/teacher/taskManagePage.vue
* @Description: 教师端作业管理页面(列表展示作业名称与开始/截止时间,数据先Mock)
* @Description: 教师端作业管理页面
-->
<template>
<div class="TaskManagePage">
<!-- 页面头部 -->
<!-- <div class="pageHeader bg-white rounded-lg shadow px-4 py-3">
<div class="flex items-center justify-between">
<div class="title text-base font-semibold text-gray-800">作业管理</div>
</div>
</div> -->
<!-- 作业列表 -->
<van-list
v-model:loading="loading"
:finished="finished"
finished-text="没有更多了"
@load="onLoad"
>
<div class="listWrapper mt-4">
<div v-for="task in task_list" :key="task.id" class="taskItem bg-white rounded-lg shadow px-4 py-3 mb-3">
<div class="flex items-center justify-between">
......@@ -34,32 +33,61 @@
</div>
</div>
</div>
</van-list>
</div>
<!--
页面说明:
- 列表项显示作业名称与开始/截止时间,样式参考项目统一风格(白卡片+阴影+圆角)。
- 数据暂用Mock,后续可替换为真实API。
-->
</template>
<script setup>
import { ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useTitle } from '@vueuse/core'
import { getTeacherTaskListAPI } from '@/api/teacher'
const $route = useRoute()
const $router = useRouter()
useTitle($route.meta.title)
//
// Mock数据:作业列表
//
const task_list = ref([
{ id: '101', title: '英语口语练习', begin_date: '2025-11-01', end_date: '2025-11-30' },
{ id: '102', title: '语文古诗背诵', begin_date: '2025-11-05', end_date: '2025-11-25' },
{ id: '103', title: '数学口算巩固', begin_date: '2025-11-10', end_date: '2025-12-10' }
])
// 分页状态
const loading = ref(false)
const finished = ref(false)
const page = ref(0)
const limit = 10
// 作业列表数据
const task_list = ref([])
/**
* 加载列表数据
*/
const onLoad = async () => {
try {
const params = {
page: page.value,
limit: limit
}
const res = await getTeacherTaskListAPI(params)
// 假设接口返回的数据是数组,如果是 { data: [] } 结构请调整
const list = Array.isArray(res) ? res.data : (res.data || [])
if (list.length > 0) {
task_list.value.push(...list)
page.value++
}
// 加载状态结束
loading.value = false
// 数据全部加载完成
if (list.length < limit) {
finished.value = true
}
} catch (error) {
console.error('获取作业列表失败', error)
loading.value = false
finished.value = true
}
}
/**
* 格式化日期范围
......