hookehuyr

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

- 添加文本打卡路由和页面组件
- 重构打卡类型选择为动态配置,从后端获取类型数据
- 优化打卡类型选择按钮的样式和交互
- 更新教师表单页面的作业类型显示逻辑
1 /* 1 /*
2 * @Date: 2025-03-21 13:28:30 2 * @Date: 2025-03-21 13:28:30
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2025-06-06 15:45:36 4 + * @LastEditTime: 2025-09-30 16:34:59
5 * @FilePath: /mlaj/src/router/checkin.js 5 * @FilePath: /mlaj/src/router/checkin.js
6 * @Description: 文件描述 6 * @Description: 文件描述
7 */ 7 */
...@@ -78,4 +78,13 @@ export default [ ...@@ -78,4 +78,13 @@ export default [
78 requiresAuth: true 78 requiresAuth: true
79 } 79 }
80 }, 80 },
81 + {
82 + path: '/checkin/text',
83 + name: 'TextCheckIn',
84 + component: () => import('@root/src/views/checkin/upload/text.vue'),
85 + meta: {
86 + title: '打卡文本',
87 + requiresAuth: true
88 + }
89 + },
81 ] 90 ]
......
...@@ -65,17 +65,17 @@ ...@@ -65,17 +65,17 @@
65 <div v-if="!taskDetail.is_finish" class="text-wrapper"> 65 <div v-if="!taskDetail.is_finish" class="text-wrapper">
66 <div class="text-header">打卡类型</div> 66 <div class="text-header">打卡类型</div>
67 <div class="upload-wrapper"> 67 <div class="upload-wrapper">
68 - <div @click="goToCheckinImagePage" class="upload-boxer"> 68 + <div
69 - <div><van-icon name="photo" size="2.5rem" /></div> 69 + v-for="option in attachmentTypeOptions"
70 - <div style="font-size: 0.85rem;">图文打卡</div> 70 + :key="option.key"
71 - </div> 71 + @click="handleCheckinTypeClick(option.key)"
72 - <div @click="goToCheckinVideoPage()" class="upload-boxer"> 72 + class="upload-button"
73 - <div><van-icon name="video" size="2.5rem" /></div> 73 + >
74 - <div style="font-size: 0.85rem;">视频打卡</div> 74 + <van-icon
75 - </div> 75 + :name="getIconName(option.key)"
76 - <div @click="goToCheckinAudioPage()" class="upload-boxer"> 76 + size="1.2rem"
77 - <div><van-icon name="music" size="2.5rem" /></div> 77 + />
78 - <div style="font-size: 0.85rem;">音频打卡</div> 78 + <span class="button-text">{{ option.value }}</span>
79 </div> 79 </div>
80 </div> 80 </div>
81 </div> 81 </div>
...@@ -192,6 +192,7 @@ import { useTitle } from '@vueuse/core'; ...@@ -192,6 +192,7 @@ import { useTitle } from '@vueuse/core';
192 import dayjs from 'dayjs'; 192 import dayjs from 'dayjs';
193 193
194 import { getTaskDetailAPI, getUploadTaskListAPI, delUploadTaskInfoAPI, likeUploadTaskInfoAPI, dislikeUploadTaskInfoAPI } from "@/api/checkin"; 194 import { getTaskDetailAPI, getUploadTaskListAPI, delUploadTaskInfoAPI, likeUploadTaskInfoAPI, dislikeUploadTaskInfoAPI } from "@/api/checkin";
195 +import { getTeacherFindSettingsAPI } from "@/api/teacher";
195 196
196 const route = useRoute() 197 const route = useRoute()
197 const router = useRouter() 198 const router = useRouter()
...@@ -311,7 +312,7 @@ const handleVideoPause = (post) => { ...@@ -311,7 +312,7 @@ const handleVideoPause = (post) => {
311 312
312 /** 313 /**
313 * 停止除当前播放器外的所有其他视频 314 * 停止除当前播放器外的所有其他视频
314 - * @param {Object} currentPlayer - 当前播放的视频播放器实例 315 + * @param {Object} currentPlayer - 当前播放的视频播放器实例
315 * @param {Object} currentPost - 当前播放的帖子对象 316 * @param {Object} currentPost - 当前播放的帖子对象
316 */ 317 */
317 const stopOtherVideos = (currentPlayer, currentPost) => { 318 const stopOtherVideos = (currentPlayer, currentPost) => {
...@@ -504,6 +505,54 @@ const onClickSubtitle = (evt) => { ...@@ -504,6 +505,54 @@ const onClickSubtitle = (evt) => {
504 console.warn('点击了日期标题'); 505 console.warn('点击了日期标题');
505 } 506 }
506 507
508 +/**
509 + * 根据打卡类型获取对应的图标名称
510 + * @param {string} type - 打卡类型
511 + * @returns {string} 图标名称
512 + */
513 +const getIconName = (type) => {
514 + const iconMap = {
515 + 'text': 'edit',
516 + 'image': 'photo',
517 + 'video': 'video',
518 + 'audio': 'music'
519 + };
520 + return iconMap[type] || 'edit';
521 +};
522 +
523 +/**
524 + * 处理打卡类型点击事件
525 + * @param {string} type - 打卡类型
526 + */
527 +const handleCheckinTypeClick = (type) => {
528 + switch (type) {
529 + case 'text':
530 + goToCheckinTextPage();
531 + break;
532 + case 'image':
533 + goToCheckinImagePage();
534 + break;
535 + case 'video':
536 + goToCheckinVideoPage();
537 + break;
538 + case 'audio':
539 + goToCheckinAudioPage();
540 + break;
541 + default:
542 + console.warn('未知的打卡类型:', type);
543 + }
544 +};
545 +
546 +const goToCheckinTextPage = () => {
547 + router.push({
548 + path: '/checkin/text',
549 + query: {
550 + id: route.query.id,
551 + type: 'text'
552 + }
553 + })
554 +}
555 +
507 const goToCheckinImagePage = () => { 556 const goToCheckinImagePage = () => {
508 router.push({ 557 router.push({
509 path: '/checkin/image', 558 path: '/checkin/image',
...@@ -611,6 +660,9 @@ const myCheckinDates = ref([]); ...@@ -611,6 +660,9 @@ const myCheckinDates = ref([]);
611 const checkinDataList = ref([]); 660 const checkinDataList = ref([]);
612 const showProgress = ref(true); 661 const showProgress = ref(true);
613 662
663 +// 作品类型选项
664 +const attachmentTypeOptions = ref([]);
665 +
614 const getTaskDetail = async (month) => { 666 const getTaskDetail = async (month) => {
615 const { code, data } = await getTaskDetailAPI({ i: route.query.id, month }); 667 const { code, data } = await getTaskDetailAPI({ i: route.query.id, month });
616 if (code) { 668 if (code) {
...@@ -652,17 +704,30 @@ const onLoad = async (date) => { ...@@ -652,17 +704,30 @@ const onLoad = async (date) => {
652 }; 704 };
653 705
654 onMounted(async () => { 706 onMounted(async () => {
655 - const current_date = route.query.date; 707 + const current_date = route.query.date;
656 - if (current_date) { 708 + if (current_date) {
657 - selectedDate.value = new Date(current_date); 709 + selectedDate.value = new Date(current_date);
658 - getTaskDetail(dayjs(current_date).format('YYYY-MM')); 710 + getTaskDetail(dayjs(current_date).format('YYYY-MM'));
659 - myRefCalendar.value?.reset(new Date(current_date)); 711 + myRefCalendar.value?.reset(new Date(current_date));
660 - onLoad(current_date); 712 + onLoad(current_date);
661 - } else { 713 + } else {
662 - selectedDate.value = new Date(); 714 + selectedDate.value = new Date();
663 - getTaskDetail(dayjs().format('YYYY-MM')); 715 + getTaskDetail(dayjs().format('YYYY-MM'));
664 - onLoad(dayjs().format('YYYY-MM-DD')); 716 + onLoad(dayjs().format('YYYY-MM-DD'));
665 - } 717 + }
718 +
719 + // 获取作品类型数据
720 + try {
721 + const { code, data } = await getTeacherFindSettingsAPI();
722 + if (code && data.task_attachment_type) {
723 + attachmentTypeOptions.value = Object.entries(data.task_attachment_type).map(([key, value]) => ({
724 + key,
725 + value
726 + }));
727 + }
728 + } catch (error) {
729 + console.error('获取作品类型数据失败:', error);
730 + }
666 }) 731 })
667 732
668 const formatData = (data) => { 733 const formatData = (data) => {
...@@ -778,14 +843,36 @@ const formatData = (data) => { ...@@ -778,14 +843,36 @@ const formatData = (data) => {
778 .upload-wrapper { 843 .upload-wrapper {
779 display: flex; 844 display: flex;
780 margin: 1rem 0; 845 margin: 1rem 0;
781 - gap: 1rem; 846 + gap: 0.5rem;
782 - .upload-boxer { 847 + flex-wrap: wrap;
783 - text-align: center; 848 +
849 + .upload-button {
850 + display: flex;
851 + align-items: center;
852 + justify-content: center;
853 + gap: 0.5rem;
784 border: 1px solid #a2d8a3; 854 border: 1px solid #a2d8a3;
785 - border-radius: 5px; 855 + border-radius: 20px;
786 - padding: 1rem 0; 856 + padding: 0.5rem 1rem;
787 - flex: 1;
788 background-color: #fff; 857 background-color: #fff;
858 + cursor: pointer;
859 + transition: all 0.2s ease;
860 + min-width: 80px;
861 +
862 + &:hover {
863 + background-color: #f0fdf4;
864 + border-color: #4caf50;
865 + }
866 +
867 + &:active {
868 + transform: scale(0.98);
869 + }
870 +
871 + .button-text {
872 + font-size: 0.8rem;
873 + color: #4caf50;
874 + font-weight: 500;
875 + }
789 } 876 }
790 } 877 }
791 } 878 }
......
1 +<template>
2 + <div class="checkin-upload-text p-4">
3 + <div class="title text-center pb-4 font-bold">{{ route.meta.title }}</div>
4 +
5 + <!-- 文字输入区域 -->
6 + <div class="mb-4 border">
7 + <van-field
8 + v-model="message"
9 + rows="8"
10 + autosize
11 + type="textarea"
12 + placeholder="请输入打卡内容, 至少需要10个字符"
13 + maxlength="500"
14 + show-word-limit
15 + />
16 + </div>
17 +
18 + <!-- 提交按钮 -->
19 + <div class="fixed bottom-0 left-0 right-0 p-4 bg-white">
20 + <van-button
21 + type="primary"
22 + block
23 + :loading="uploading"
24 + :disabled="!canSubmit"
25 + @click="onSubmit"
26 + >
27 + 提交
28 + </van-button>
29 + </div>
30 + </div>
31 +</template>
32 +
33 +<script setup>
34 +import { ref, computed, onMounted } from 'vue'
35 +import { useRoute, useRouter } from 'vue-router'
36 +import { showToast, showLoadingToast } from 'vant'
37 +import { addUploadTaskAPI, getUploadTaskInfoAPI, editUploadTaskInfoAPI } from "@/api/checkin";
38 +import { useTitle } from '@vueuse/core';
39 +
40 +const route = useRoute()
41 +const router = useRouter()
42 +useTitle(route.meta.title);
43 +
44 +// 留言内容
45 +const message = ref('')
46 +// 上传状态
47 +const uploading = ref(false)
48 +
49 +/**
50 + * 是否可以提交
51 + */
52 +const canSubmit = computed(() => {
53 + return message.value.trim() !== '' && message.value.trim().length >= 10
54 +})
55 +
56 +/**
57 + * 提交表单
58 + */
59 +const onSubmit = async () => {
60 + if (uploading.value) return
61 +
62 + if (message.value.trim().length < 10) {
63 + showToast('打卡内容至少需要10个字符')
64 + return
65 + }
66 +
67 + uploading.value = true
68 + const toast = showLoadingToast({
69 + message: '提交中...',
70 + forbidClick: true,
71 + })
72 +
73 + try {
74 + if (route.query.status === 'edit') {
75 + // 编辑打卡接口
76 + const { code, data } = await editUploadTaskInfoAPI({
77 + i: route.query.post_id,
78 + note: message.value,
79 + meta_id: [], // 文本类型不需要文件
80 + file_type: route.query.type,
81 + });
82 + if (code) {
83 + showToast('提交成功')
84 + router.back()
85 + }
86 + } else {
87 + // 新增打卡接口
88 + const { code, data } = await addUploadTaskAPI({
89 + task_id: route.query.id,
90 + note: message.value,
91 + meta_id: [], // 文本类型不需要文件
92 + file_type: route.query.type,
93 + });
94 + if (code) {
95 + showToast('提交成功')
96 + router.back()
97 + }
98 + }
99 + } catch (error) {
100 + showToast('提交失败,请重试')
101 + } finally {
102 + uploading.value = false
103 + }
104 +}
105 +
106 +/**
107 + * 页面挂载时的初始化逻辑
108 + */
109 +onMounted(async () => {
110 + if (route.query.status === 'edit') {
111 + const { code, data } = await getUploadTaskInfoAPI({ i: route.query.post_id });
112 + if (code) {
113 + message.value = data.note
114 + }
115 + }
116 +})
117 +</script>
118 +
119 +<style lang="less" scoped>
120 +.checkin-upload-text {
121 + min-height: 100vh;
122 + padding-bottom: 80px;
123 +}
124 +
125 +.van-field {
126 + border-radius: 8px;
127 + background-color: #f8f9fa;
128 +}
129 +
130 +.van-field__control {
131 + font-size: 16px;
132 + line-height: 1.5;
133 +}
134 +</style>
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
2 * @Author: hookehuyr hookehuyr@gmail.com 2 * @Author: hookehuyr hookehuyr@gmail.com
3 * @Date: 2025-01-20 10:00:00 3 * @Date: 2025-01-20 10:00:00
4 * @LastEditors: hookehuyr hookehuyr@gmail.com 4 * @LastEditors: hookehuyr hookehuyr@gmail.com
5 - * @LastEditTime: 2025-09-30 15:46:22 5 + * @LastEditTime: 2025-09-30 16:22:57
6 * @FilePath: /mlaj/src/views/teacher/formPage.vue 6 * @FilePath: /mlaj/src/views/teacher/formPage.vue
7 * @Description: 教师作业新增表单页面 7 * @Description: 教师作业新增表单页面
8 --> 8 -->
...@@ -46,17 +46,17 @@ ...@@ -46,17 +46,17 @@
46 <div v-if="$route.query.type === 'homework'" class="mb-4"> 46 <div v-if="$route.query.type === 'homework'" class="mb-4">
47 <label class="setting-label">作业类型</label> 47 <label class="setting-label">作业类型</label>
48 <van-checkbox-group v-model="formData.attachment_type" direction="horizontal" checked-color="#4caf50" icon-size="15px"> 48 <van-checkbox-group v-model="formData.attachment_type" direction="horizontal" checked-color="#4caf50" icon-size="15px">
49 - <van-checkbox 49 + <van-checkbox
50 - v-for="option in attachmentTypeOptions" 50 + v-for="option in attachmentTypeOptions"
51 - :key="option.name" 51 + :key="option.name"
52 - :name="option.name" 52 + :name="option.name"
53 shape="square" 53 shape="square"
54 > 54 >
55 <span class="text-sm">{{ option.text }}</span> 55 <span class="text-sm">{{ option.text }}</span>
56 </van-checkbox> 56 </van-checkbox>
57 </van-checkbox-group> 57 </van-checkbox-group>
58 </div> 58 </div>
59 - 59 +
60 <!-- 作业周期 --> 60 <!-- 作业周期 -->
61 <van-row gutter="10" class="mb-4"> 61 <van-row gutter="10" class="mb-4">
62 <van-col span="24"> 62 <van-col span="24">
...@@ -73,7 +73,7 @@ ...@@ -73,7 +73,7 @@
73 /> 73 />
74 </van-col> 74 </van-col>
75 </van-row> 75 </van-row>
76 - 76 +
77 <!-- 每周期提交数量和目标总数 --> 77 <!-- 每周期提交数量和目标总数 -->
78 <van-row gutter="10"> 78 <van-row gutter="10">
79 <van-col span="12"> 79 <van-col span="12">
...@@ -880,15 +880,15 @@ onMounted(async () => { ...@@ -880,15 +880,15 @@ onMounted(async () => {
880 text, 880 text,
881 value: String(value) // 确保值为字符串类型 881 value: String(value) // 确保值为字符串类型
882 })); 882 }));
883 - 883 +
884 - // 处理作类型数据 884 + // 处理作类型数据
885 if (data.task_attachment_type) { 885 if (data.task_attachment_type) {
886 attachmentTypeOptions.value = Object.entries(data.task_attachment_type).map(([key, value]) => ({ 886 attachmentTypeOptions.value = Object.entries(data.task_attachment_type).map(([key, value]) => ({
887 name: key, // 提交给后台的值 887 name: key, // 提交给后台的值
888 text: value // 显示的文本 888 text: value // 显示的文本
889 })); 889 }));
890 } 890 }
891 - 891 +
892 grades.value = data.grade_list.map(grade => ({ 892 grades.value = data.grade_list.map(grade => ({
893 name: grade.grade_name, 893 name: grade.grade_name,
894 id: String(grade.id) 894 id: String(grade.id)
......