hookehuyr

feat(打卡): 新增文本打卡功能并优化打卡类型选择界面

- 添加文本打卡路由和页面组件
- 重构打卡类型选择为动态配置,从后端获取类型数据
- 优化打卡类型选择按钮的样式和交互
- 更新教师表单页面的作业类型显示逻辑
/*
* @Date: 2025-03-21 13:28:30
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-06-06 15:45:36
* @LastEditTime: 2025-09-30 16:34:59
* @FilePath: /mlaj/src/router/checkin.js
* @Description: 文件描述
*/
......@@ -78,4 +78,13 @@ export default [
requiresAuth: true
}
},
{
path: '/checkin/text',
name: 'TextCheckIn',
component: () => import('@root/src/views/checkin/upload/text.vue'),
meta: {
title: '打卡文本',
requiresAuth: true
}
},
]
......
......@@ -65,17 +65,17 @@
<div v-if="!taskDetail.is_finish" class="text-wrapper">
<div class="text-header">打卡类型</div>
<div class="upload-wrapper">
<div @click="goToCheckinImagePage" class="upload-boxer">
<div><van-icon name="photo" size="2.5rem" /></div>
<div style="font-size: 0.85rem;">图文打卡</div>
</div>
<div @click="goToCheckinVideoPage()" class="upload-boxer">
<div><van-icon name="video" size="2.5rem" /></div>
<div style="font-size: 0.85rem;">视频打卡</div>
</div>
<div @click="goToCheckinAudioPage()" class="upload-boxer">
<div><van-icon name="music" size="2.5rem" /></div>
<div style="font-size: 0.85rem;">音频打卡</div>
<div
v-for="option in attachmentTypeOptions"
:key="option.key"
@click="handleCheckinTypeClick(option.key)"
class="upload-button"
>
<van-icon
:name="getIconName(option.key)"
size="1.2rem"
/>
<span class="button-text">{{ option.value }}</span>
</div>
</div>
</div>
......@@ -192,6 +192,7 @@ import { useTitle } from '@vueuse/core';
import dayjs from 'dayjs';
import { getTaskDetailAPI, getUploadTaskListAPI, delUploadTaskInfoAPI, likeUploadTaskInfoAPI, dislikeUploadTaskInfoAPI } from "@/api/checkin";
import { getTeacherFindSettingsAPI } from "@/api/teacher";
const route = useRoute()
const router = useRouter()
......@@ -311,7 +312,7 @@ const handleVideoPause = (post) => {
/**
* 停止除当前播放器外的所有其他视频
* @param {Object} currentPlayer - 当前播放的视频播放器实例
* @param {Object} currentPlayer - 当前播放的视频播放器实例
* @param {Object} currentPost - 当前播放的帖子对象
*/
const stopOtherVideos = (currentPlayer, currentPost) => {
......@@ -504,6 +505,54 @@ const onClickSubtitle = (evt) => {
console.warn('点击了日期标题');
}
/**
* 根据打卡类型获取对应的图标名称
* @param {string} type - 打卡类型
* @returns {string} 图标名称
*/
const getIconName = (type) => {
const iconMap = {
'text': 'edit',
'image': 'photo',
'video': 'video',
'audio': 'music'
};
return iconMap[type] || 'edit';
};
/**
* 处理打卡类型点击事件
* @param {string} type - 打卡类型
*/
const handleCheckinTypeClick = (type) => {
switch (type) {
case 'text':
goToCheckinTextPage();
break;
case 'image':
goToCheckinImagePage();
break;
case 'video':
goToCheckinVideoPage();
break;
case 'audio':
goToCheckinAudioPage();
break;
default:
console.warn('未知的打卡类型:', type);
}
};
const goToCheckinTextPage = () => {
router.push({
path: '/checkin/text',
query: {
id: route.query.id,
type: 'text'
}
})
}
const goToCheckinImagePage = () => {
router.push({
path: '/checkin/image',
......@@ -611,6 +660,9 @@ const myCheckinDates = ref([]);
const checkinDataList = ref([]);
const showProgress = ref(true);
// 作品类型选项
const attachmentTypeOptions = ref([]);
const getTaskDetail = async (month) => {
const { code, data } = await getTaskDetailAPI({ i: route.query.id, month });
if (code) {
......@@ -663,6 +715,19 @@ onMounted(async () => {
getTaskDetail(dayjs().format('YYYY-MM'));
onLoad(dayjs().format('YYYY-MM-DD'));
}
// 获取作品类型数据
try {
const { code, data } = await getTeacherFindSettingsAPI();
if (code && data.task_attachment_type) {
attachmentTypeOptions.value = Object.entries(data.task_attachment_type).map(([key, value]) => ({
key,
value
}));
}
} catch (error) {
console.error('获取作品类型数据失败:', error);
}
})
const formatData = (data) => {
......@@ -778,14 +843,36 @@ const formatData = (data) => {
.upload-wrapper {
display: flex;
margin: 1rem 0;
gap: 1rem;
.upload-boxer {
text-align: center;
gap: 0.5rem;
flex-wrap: wrap;
.upload-button {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
border: 1px solid #a2d8a3;
border-radius: 5px;
padding: 1rem 0;
flex: 1;
border-radius: 20px;
padding: 0.5rem 1rem;
background-color: #fff;
cursor: pointer;
transition: all 0.2s ease;
min-width: 80px;
&:hover {
background-color: #f0fdf4;
border-color: #4caf50;
}
&:active {
transform: scale(0.98);
}
.button-text {
font-size: 0.8rem;
color: #4caf50;
font-weight: 500;
}
}
}
}
......
<template>
<div class="checkin-upload-text p-4">
<div class="title text-center pb-4 font-bold">{{ route.meta.title }}</div>
<!-- 文字输入区域 -->
<div class="mb-4 border">
<van-field
v-model="message"
rows="8"
autosize
type="textarea"
placeholder="请输入打卡内容, 至少需要10个字符"
maxlength="500"
show-word-limit
/>
</div>
<!-- 提交按钮 -->
<div class="fixed bottom-0 left-0 right-0 p-4 bg-white">
<van-button
type="primary"
block
:loading="uploading"
:disabled="!canSubmit"
@click="onSubmit"
>
提交
</van-button>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { showToast, showLoadingToast } from 'vant'
import { addUploadTaskAPI, getUploadTaskInfoAPI, editUploadTaskInfoAPI } from "@/api/checkin";
import { useTitle } from '@vueuse/core';
const route = useRoute()
const router = useRouter()
useTitle(route.meta.title);
// 留言内容
const message = ref('')
// 上传状态
const uploading = ref(false)
/**
* 是否可以提交
*/
const canSubmit = computed(() => {
return message.value.trim() !== '' && message.value.trim().length >= 10
})
/**
* 提交表单
*/
const onSubmit = async () => {
if (uploading.value) return
if (message.value.trim().length < 10) {
showToast('打卡内容至少需要10个字符')
return
}
uploading.value = true
const toast = showLoadingToast({
message: '提交中...',
forbidClick: true,
})
try {
if (route.query.status === 'edit') {
// 编辑打卡接口
const { code, data } = await editUploadTaskInfoAPI({
i: route.query.post_id,
note: message.value,
meta_id: [], // 文本类型不需要文件
file_type: route.query.type,
});
if (code) {
showToast('提交成功')
router.back()
}
} else {
// 新增打卡接口
const { code, data } = await addUploadTaskAPI({
task_id: route.query.id,
note: message.value,
meta_id: [], // 文本类型不需要文件
file_type: route.query.type,
});
if (code) {
showToast('提交成功')
router.back()
}
}
} catch (error) {
showToast('提交失败,请重试')
} finally {
uploading.value = false
}
}
/**
* 页面挂载时的初始化逻辑
*/
onMounted(async () => {
if (route.query.status === 'edit') {
const { code, data } = await getUploadTaskInfoAPI({ i: route.query.post_id });
if (code) {
message.value = data.note
}
}
})
</script>
<style lang="less" scoped>
.checkin-upload-text {
min-height: 100vh;
padding-bottom: 80px;
}
.van-field {
border-radius: 8px;
background-color: #f8f9fa;
}
.van-field__control {
font-size: 16px;
line-height: 1.5;
}
</style>
......@@ -2,7 +2,7 @@
* @Author: hookehuyr hookehuyr@gmail.com
* @Date: 2025-01-20 10:00:00
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-09-30 15:46:22
* @LastEditTime: 2025-09-30 16:22:57
* @FilePath: /mlaj/src/views/teacher/formPage.vue
* @Description: 教师作业新增表单页面
-->
......@@ -881,7 +881,7 @@ onMounted(async () => {
value: String(value) // 确保值为字符串类型
}));
// 处理作类型数据
// 处理作类型数据
if (data.task_attachment_type) {
attachmentTypeOptions.value = Object.entries(data.task_attachment_type).map(([key, value]) => ({
name: key, // 提交给后台的值
......