hookehuyr

feat(教师端): 重构学生详情页签到和反馈功能

更新API路径和字段命名以匹配后端接口
添加状态格式化函数统一显示文本
实现分页加载和重置功能
完善反馈表单的提交和显示逻辑
/*
* @Date: 2025-06-23 11:46:21
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-06-26 16:15:15
* @LastEditTime: 2025-06-27 09:55:12
* @FilePath: /mlaj/src/api/teacher.js
* @Description: 文件描述
*/
......@@ -15,9 +15,9 @@ const Api = {
STUDENT_DETAIL: '/srv/?a=user&t=student_detail',
STUDENT_CHECKIN_LIST: '/srv/?a=checkin&t=student_checkin_list',
STUDENT_UPLOAD_LIST: '/srv/?a=checkin&t=student_upload_list',
STUDENT_CHECKIN_FEEDBACK_LIST: '/srv/?a=checkin_feedback&t=list',
ADD_CHECKIN_FEEDBACK: '/srv/?a=checkin_feedback&t=add',
DEL_CHECKIN_FEEDBACK: '/srv/?a=checkin_feedback&t=del',
STUDENT_CHECKIN_FEEDBACK_LIST: '/srv/?a=feedback&t=list',
ADD_CHECKIN_FEEDBACK: '/srv/?a=feedback&t=add',
DEL_CHECKIN_FEEDBACK: '/srv/?a=feedback&t=del',
}
/**
......
......@@ -2,7 +2,7 @@
* @Author: hookehuyr hookehuyr@gmail.com
* @Date: 2025-06-19 17:12:19
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-06-26 16:36:01
* @LastEditTime: 2025-06-27 10:49:01
* @FilePath: /mlaj/src/views/teacher/studentPage.vue
* @Description: 学生详情页面
-->
......@@ -94,7 +94,7 @@
<!-- 状态筛选 -->
<div class="flex items-center justify-end mb-4">
<div @click="showStatusPopup = true" class="flex items-center text-sm text-gray-600 cursor-pointer">
<span>{{ statusFilter }}</span>
<span>{{ formatStatus(statusFilter) }}</span>
<van-icon name="arrow-down" size="14" class="ml-1" />
</div>
</div>
......@@ -132,12 +132,13 @@
<!-- 右侧:状态 -->
<div class="flex items-center">
<span v-if="record.status === '正常'" class="text-green-600 text-sm mr-2">{{ record.status }}</span>
<span v-else-if="record.status === '迟到'" class="text-orange-500 text-sm mr-2">{{ record.status }}</span>
<span v-else class="text-red-500 text-sm mr-2">{{ record.status }}</span>
<span v-if="record.status === 'checked'" class="text-green-600 text-sm mr-2">{{ formatStatus(record.status) }}</span>
<span v-else-if="record.status === 'absence'" class="text-orange-500 text-sm mr-2">{{ formatStatus(record.status) }}</span>
<span v-else class="text-red-500 text-sm mr-2">{{ formatStatus(record.status) }}</span>
<van-icon v-if="record.status === '正常'" name="passed" color="#10b981" size="16" />
<van-icon v-else-if="record.status === '迟到'" name="warning-o" color="#f59e0b" size="16" />
<van-icon v-if="record.status === 'checked'" name="passed" color="#10b981" size="16" />
<van-icon v-else-if="record.status === 'absence'" name="warning-o" color="#f59e0b" size="16" />
<van-icon v-else-if="record.status === 'late'" name="warning-o" color="#f59e0b" size="16" />
<van-icon v-else name="close" color="#ef4444" size="16" />
</div>
</div>
......@@ -153,7 +154,7 @@
<div class="flex items-center justify-between mb-3">
<div class="flex items-center">
<van-icon name="calendar-o" size="16" color="#10b981" class="mr-2" />
<span class="text-sm text-gray-600">{{ evaluation.date }}</span>
<span class="text-sm text-gray-600">{{ dayjs(evaluation.created_time).format('YYYY-MM-DD HH:mm:ss') }}</span>
</div>
<van-icon
name="delete-o"
......@@ -168,7 +169,7 @@
<div class="flex items-start mb-3">
<van-icon name="chat-o" size="16" color="#3b82f6" class="mr-2 mt-0.5" />
<div class="flex-1">
<p class="text-gray-800 text-sm leading-relaxed">{{ evaluation.content }}</p>
<p class="text-gray-800 text-sm leading-relaxed">{{ evaluation.note }}</p>
</div>
</div>
......@@ -268,6 +269,7 @@
</div>
</div>
</van-list>
<van-empty v-show="activeTab === 'homework' && !checkinDataList.length" description="暂无数据" />
<van-back-top right="5vw" bottom="10vh" />
<div style="height: 5rem;"></div>
......@@ -298,14 +300,14 @@
<!-- 评分 -->
<div class="mb-4">
<div class="text-sm text-gray-600 mb-2">评分</div>
<van-rate v-model="commentForm.rating" :size="24" color="#ffd21e" void-color="#eee" />
<van-rate v-model="commentForm.score" :size="24" color="#ffd21e" void-color="#eee" :readonly="currentCommentPost.is_feedback" />
</div>
<!-- 点评内容 -->
<div class="mb-6">
<div class="text-sm text-gray-600 mb-2">点评内容</div>
<van-field
v-model="commentForm.content"
v-model="commentForm.note"
type="textarea"
placeholder="请输入点评内容..."
rows="4"
......@@ -313,11 +315,12 @@
show-word-limit
:border="false"
class="bg-gray-50 rounded-lg"
:readonly="currentCommentPost.is_feedback"
/>
</div>
<!-- 操作按钮 -->
<div class="flex gap-3">
<div v-if="!currentCommentPost.is_feedback" class="flex gap-3">
<van-button
block
type="default"
......@@ -331,11 +334,20 @@
type="primary"
@click="submitComment"
class="flex-1"
:disabled="!commentForm.content.trim()"
>
提交
</van-button>
</div>
<div v-else class="flex gap-3">
<van-button
block
type="default"
@click="closeCommentPopup"
class="flex-1"
>
关闭
</van-button>
</div>
</div>
</van-popup>
......@@ -404,8 +416,9 @@ const showStatusPopup = ref(false)
const showCommentPopup = ref(false)
const currentCommentPost = ref(null)
const commentForm = ref({
rating: 0,
content: ''
checkin_id: '',
score: 0,
note: ''
})
/**
......@@ -413,15 +426,17 @@ const commentForm = ref({
* @param {Object} post - 作业帖子对象
*/
const openCommentPopup = (post) => {
console.warn(post);
currentCommentPost.value = post
commentForm.value.checkin_id = post.id
// 如果已有点评,填充表单
if (post.comment) {
commentForm.value.rating = post.comment.rating || 0
commentForm.value.content = post.comment.content || ''
if (post.feedback_id) {
commentForm.value.score = post.feedback_score || 0
commentForm.value.note = post.feedback || ''
} else {
// 重置表单
commentForm.value.rating = 0
commentForm.value.content = ''
commentForm.value.score = 0
commentForm.value.note = ''
}
showCommentPopup.value = true
}
......@@ -430,22 +445,28 @@ const openCommentPopup = (post) => {
* 关闭点评弹窗
*/
const closeCommentPopup = () => {
// 如果已点评,关闭弹窗
if (currentCommentPost.value && currentCommentPost.value.is_feedback) {
showCommentPopup.value = false
return
}
// 未点评的关闭重置显示
showCommentPopup.value = false
currentCommentPost.value = null
commentForm.value.rating = 0
commentForm.value.content = ''
commentForm.value.score = 0
commentForm.value.note = ''
}
/**
* 提交点评
*/
const submitComment = async () => {
if (!commentForm.value.content.trim()) {
if (!commentForm.value.note.trim()) {
showFailToast('请输入点评内容')
return
}
if (commentForm.value.rating === 0) {
if (commentForm.value.score === 0) {
showFailToast('请选择评分')
return
}
......@@ -454,23 +475,23 @@ const submitComment = async () => {
showLoadingToast('提交中...')
// 这里应该调用API提交点评
// await submitCommentAPI(currentCommentPost.value.id, commentForm.value)
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 1000))
// 更新本地数据
if (currentCommentPost.value) {
const { code, data } = await addCheckinFeedbackAPI(commentForm.value)
if (code) {
commentForm.value.feedback_id = data.id
// 更新本地数据
currentCommentPost.value.is_feedback = true
currentCommentPost.value.comment = {
rating: commentForm.value.rating,
content: commentForm.value.content,
created_at: new Date().toISOString()
}
checkinDataList.value.forEach(item => {
if (item.id === currentCommentPost.value.id) {
item.feedback_id = commentForm.value.feedback_id
item.feedback_score = commentForm.value.score
item.feedback = commentForm.value.note
}
})
showSuccessToast('点评提交成功')
closeCommentPopup()
}
showSuccessToast('点评提交成功')
closeCommentPopup()
} catch (error) {
console.error('提交点评失败:', error)
showFailToast('提交失败,请重试')
......@@ -492,101 +513,48 @@ const toggleCourseSelection = (course) => {
// 可以在这里添加其他业务逻辑,比如筛选相关数据
console.log('当前选中的课程:', selectedCourses.value)
resetAndReload()
resetAndReloadRecords()
resetAndReloadEvaluations()
}
// 状态选项
const statusOptions = ref([
{ text: '按状态', value: '按状态' },
{ text: '正常', value: '正常' },
{ text: '迟到', value: '迟到' },
{ text: '缺勤', value: '缺勤' }
])
{ text: '正常', value: 'checked' },
{ text: '缺勤', value: 'absence' }
]);
const formatStatus = (status) => {
const statusMap = {
checked: '正常',
absence: '缺勤'
}
return statusMap[status] || status
}
// 记录列表
const records = ref([
{
id: 1,
date: '2023-11-20',
time: '07:45',
status: '正常'
},
{
id: 2,
date: '2023-11-19',
time: '07:50',
status: '正常'
},
{
id: 3,
date: '2023-11-18',
time: '07:42',
status: '正常'
},
{
id: 4,
date: '2023-11-17',
time: '08:10',
status: '迟到'
},
{
id: 5,
date: '2023-11-16',
time: '07:48',
status: '正常'
},
{
id: 6,
date: '2023-11-15',
time: '00:00',
status: '缺勤'
}
])
const records = ref([])
// 列表加载状态
const recordLoading = ref(false)
const recordFinished = ref(false)
const recordLimit = ref(100)
const recordPage = ref(0)
// 删除对话框相关状态
const showDeleteDialog = ref(false)
const currentDeleteId = ref(null)
// 班主任点评列表数据
const evaluationList = ref([
{
id: 1,
date: '2023-11-20 14:30',
content: '李华同学在本周的学习中表现优秀,课堂参与度很高,作业完成质量也很好。希望继续保持这种学习态度,在数学方面可以多加练习。',
score: 4
},
{
id: 2,
date: '2023-11-15 16:45',
content: '本周李华在语文课上积极发言,写作能力有明显提升。但需要注意课堂纪律,不要和同桌说话。总体表现良好。',
score: 4
},
{
id: 3,
date: '2023-11-10 15:20',
content: '李华同学这周在英语学习上进步很大,单词记忆和口语表达都有提升。建议多读英语文章,培养语感。',
score: 5
},
{
id: 4,
date: '2023-11-05 17:10',
content: '本周表现一般,上课有时会走神,作业完成情况还可以。希望能够更加专注学习,提高课堂效率。',
score: 3
},
{
id: 5,
date: '2023-10-30 14:50',
content: '李华同学在体育课上表现积极,团队合作意识强。学习方面需要加强时间管理,合理安排各科学习时间。',
score: 4
}
])
const evaluationList = ref([])
// 班主任点评列表加载状态
const evaluationLoading = ref(false)
const evaluationFinished = ref(false)
const evaluationLimit = ref(100)
const evaluationPage = ref(0)
/**
* 过滤后的记录列表
......@@ -624,21 +592,43 @@ const onStatusSelect = (option) => {
/**
* 加载更多记录数据
*/
const onRecordLoad = () => {
setTimeout(() => {
recordLoading.value = false
recordFinished.value = true
}, 1000)
const onRecordLoad = async () => {
const nextPage = recordPage.value;
//
const res = await getStudentCheckinListAPI({
limit: recordLimit.value,
page: nextPage,
user_id: route.params.id,
group_id: selectedCourses.value.length ? selectedCourses.value[0]['id'] : '',
});
if (res.code) {
// 整理数据结构
records.value = [...records.value, ...res.data];
recordFinished.value = res.data.length < recordLimit.value;
recordPage.value = nextPage + 1;
}
recordLoading.value = false;
}
/**
* 加载更多班主任点评数据
*/
const onEvaluationLoad = () => {
setTimeout(() => {
evaluationLoading.value = false
evaluationFinished.value = true
}, 1000)
const onEvaluationLoad = async () => {
const nextPage = evaluationPage.value;
//
const res = await getCheckinFeedbackListAPI({
limit: evaluationLimit.value,
page: nextPage,
user_id: route.params.id,
group_id: selectedCourses.value.length ? selectedCourses.value[0]['id'] : '',
});
if (res.code) {
// 整理数据结构
evaluationList.value = [...evaluationList.value, ...res.data];
evaluationFinished.value = res.data.length < evaluationLimit.value;
evaluationPage.value = nextPage + 1;
}
evaluationLoading.value = false;
}
/**
......@@ -653,12 +643,17 @@ const deleteEvaluation = (evaluationId) => {
/**
* 确认删除操作
*/
const confirmDelete = () => {
const confirmDelete = async () => {
// 这里应该调用API删除点评,暂时模拟删除操作
const index = evaluationList.value.findIndex(item => item.id === currentDeleteId.value)
if (index !== -1) {
evaluationList.value.splice(index, 1)
showSuccessToast('删除成功')
const { code } = await delCheckinFeedbackAPI({
i: currentDeleteId.value
})
if (code) {
const index = evaluationList.value.findIndex(item => item.id === currentDeleteId.value)
if (index !== -1) {
evaluationList.value.splice(index, 1)
showSuccessToast('删除成功')
}
}
showDeleteDialog.value = false
currentDeleteId.value = null
......@@ -667,7 +662,7 @@ const confirmDelete = () => {
/**
* 取消删除操作
*/
const cancelDelete = () => {
const cancelDelete = async () => {
showDeleteDialog.value = false
currentDeleteId.value = null
console.log('用户取消删除操作')
......@@ -680,17 +675,18 @@ const checkinDataList = ref([]);
onMounted(async () => {
// 从路由参数获取学生ID
const studentId = route.params.id
console.log('学生详情页面已加载,学生ID:', studentId)
// 这里可以根据studentId调用API获取学生详细信息
loadStudentData(studentId)
await loadStudentData(studentId)
const current_date = route.query.date;
if (current_date) {
onLoad(current_date);
} else {
onLoad(dayjs().format('YYYY-MM-DD'));
}
// 加载作业记录
onLoad()
// 加载签到记录
onRecordLoad()
// 加载班主任点评
onEvaluationLoad()
})
/**
......@@ -701,6 +697,7 @@ const loadStudentData = async (studentId) => {
const { code, data } = await getStudentDetailAPI({ i: studentId })
if (code) {
studentInfo.value = data;
studentInfo.value?.lesson_list.sort((a, b) => a.title.length - b.title.length)
selectedCourses.value = [studentInfo.value.lesson_list[0] || '']
}
}
......@@ -722,6 +719,14 @@ const handleTabChange = (name) => {
stopAllAudio();
})
if (name === 'homework') {
resetAndReload()
} else if (name === 'evaluation') {
resetAndReloadEvaluations()
} else if (name === 'statistics') {
resetAndReloadRecords()
}
};
// 存储所有视频播放器的引用
......@@ -912,23 +917,22 @@ const handLike = async (post) => {
const loading = ref(false)
const finished = ref(false)
const limit = ref(3)
const limit = ref(10)
const page = ref(0)
const onLoad = async (date) => {
const nextPage = page.value;
const current_date = date || route.query.date || dayjs().format('YYYY-MM-DD');
//
const res = await getCheckinTeacherListAPI({
const res = await getStudentUploadListAPI({
limit: limit.value,
page: nextPage,
task_id: route.query.id,
date: current_date
user_id: route.params.id,
group_id: selectedCourses.value.length ? selectedCourses.value[0]['id'] : '',
});
if (res.code) {
// 整理数据结构
checkinDataList.value = [...checkinDataList.value, ...formatData(res.data)];
finished.value = res.data.checkin_list.length < limit.value;
finished.value = res.data.length < limit.value;
page.value = nextPage + 1;
}
loading.value = false;
......@@ -936,7 +940,7 @@ const onLoad = async (date) => {
const formatData = (data) => {
let formattedData = [];
formattedData = data?.checkin_list.map((item, index) => {
formattedData = data?.map((item, index) => {
let images = [];
let audio = [];
let videoList = [];
......@@ -980,12 +984,48 @@ const formatData = (data) => {
is_liked: item.is_like,
is_my: item.is_my,
file_type: item.file_type,
is_feedback: item.is_feedback || false, // 是否已点评
is_feedback: item.feedback_id || false, // 是否已点评
feedback : item.feedback || '',
feedback_id : item.feedback_id || '',
feedback_score : item.feedback_score || 0,
comment: item.comment || null, // 点评内容
}
})
return formattedData;
}
/**
* 重置分页参数并重新加载数据
*/
const resetAndReload = () => {
page.value = 0;
checkinDataList.value = [];
finished.value = false;
loading.value = true;
onLoad();
}
/**
* 重置分页参数并重新加载数据
*/
const resetAndReloadRecords = () => {
recordPage.value = 0;
records.value = [];
recordFinished.value = false;
recordLoading.value = true;
onRecordLoad();
}
/**
* 重置分页参数并重新加载数据
*/
const resetAndReloadEvaluations = () => {
evaluationPage.value = 0;
evaluationList.value = [];
evaluationFinished.value = false;
evaluationLoading.value = true;
onEvaluationLoad();
}
</script>
<style lang="less">
......