feat(打卡): 新增文本打卡功能并优化打卡类型选择界面
- 添加文本打卡路由和页面组件 - 重构打卡类型选择为动态配置,从后端获取类型数据 - 优化打卡类型选择按钮的样式和交互 - 更新教师表单页面的作业类型显示逻辑
Showing
4 changed files
with
251 additions
and
21 deletions
| 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) { |
| ... | @@ -663,6 +715,19 @@ onMounted(async () => { | ... | @@ -663,6 +715,19 @@ onMounted(async () => { |
| 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 | } | ... | ... |
src/views/checkin/upload/text.vue
0 → 100644
| 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 | --> |
| ... | @@ -881,7 +881,7 @@ onMounted(async () => { | ... | @@ -881,7 +881,7 @@ onMounted(async () => { |
| 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, // 提交给后台的值 | ... | ... |
-
Please register or login to post a comment