refactor(打卡弹窗): 将打卡弹窗统一为通用组件 CheckInDialog
重构 CourseDetailPage、StudyCoursePage 和 StudyDetailPage 中的打卡弹窗逻辑,提取为通用组件 CheckInDialog 组件支持通过 props 传入今日和历史打卡任务列表,并统一处理弹窗显隐和交互逻辑 移除各页面中冗余的打卡相关状态和方法,简化代码结构
Showing
5 changed files
with
102 additions
and
302 deletions
| ... | @@ -75,3 +75,16 @@ https://oa-dev.onwall.cn/f/mlaj | ... | @@ -75,3 +75,16 @@ https://oa-dev.onwall.cn/f/mlaj |
| 75 | - 依赖:`pnpm add qrcode`(在 Canvas 内本地生成二维码,避免跨域图片导致画布污染)。 | 75 | - 依赖:`pnpm add qrcode`(在 Canvas 内本地生成二维码,避免跨域图片导致画布污染)。 |
| 76 | - 跨域:通过 `crossorigin="anonymous"` 加载封面,并追加时间戳防缓存;若封面跨域不允许,则显示降级卡片,仍可长按截图保存。 | 76 | - 跨域:通过 `crossorigin="anonymous"` 加载封面,并追加时间戳防缓存;若封面跨域不允许,则显示降级卡片,仍可长按截图保存。 |
| 77 | - 文案:使用中文字体并自动换行限制行数,末行超出追加省略号。 | 77 | - 文案:使用中文字体并自动换行限制行数,末行超出追加省略号。 |
| 78 | + | ||
| 79 | + - 打卡弹窗统一为通用组件 CheckInDialog | ||
| 80 | + - 目的:统一 CourseDetailPage、StudyCoursePage、StudyDetailPage 三处页面的打卡弹窗与交互,避免重复逻辑。 | ||
| 81 | + - 组件:`/src/components/ui/CheckInDialog.vue`,`v-model:show` 控制显隐;支持外部传入任务列表。 | ||
| 82 | + - Props: | ||
| 83 | + - `items_today`:今日打卡任务数组(外部传入)。 | ||
| 84 | + - `items_history`:历史打卡任务数组(外部传入)。 | ||
| 85 | + - 数据结构:每项需包含 `id`、`title(name)`、`task_type`(`checkin`/`upload`)、`is_gray`。 | ||
| 86 | + - 使用位置: | ||
| 87 | + - `/src/views/courses/CourseDetailPage.vue` | ||
| 88 | + - `/src/views/profile/StudyCoursePage.vue` | ||
| 89 | + - `/src/views/study/StudyDetailPage.vue` | ||
| 90 | + - 清理:上述页面已移除旧弹窗的冗余状态与方法(如 `default_list`、`showTaskList`、`showTimeoutTaskList`、`selectedCheckIn` 等),统一由组件内部处理。 | ... | ... |
| ... | @@ -8,7 +8,10 @@ | ... | @@ -8,7 +8,10 @@ |
| 8 | > | 8 | > |
| 9 | <div class="p-4"> | 9 | <div class="p-4"> |
| 10 | <div class="flex justify-between items-center mb-3"> | 10 | <div class="flex justify-between items-center mb-3"> |
| 11 | - <h3 class="font-medium">今日打卡</h3> | 11 | + <h3 class="font-medium"> |
| 12 | + <span :class="{ 'text-green-500' : active_tab === 'today' }" @click="active_tab = 'today'">今日打卡</span> | ||
| 13 | + <span v-if="has_history" :class="{ 'text-green-500' : active_tab === 'history' }" @click="active_tab = 'history'">历史打卡</span> | ||
| 14 | + </h3> | ||
| 12 | <van-icon name="cross" @click="handleClose" /> | 15 | <van-icon name="cross" @click="handleClose" /> |
| 13 | </div> | 16 | </div> |
| 14 | 17 | ||
| ... | @@ -22,7 +25,7 @@ | ... | @@ -22,7 +25,7 @@ |
| 22 | <template v-else> | 25 | <template v-else> |
| 23 | <div class="grid grid-cols-2 gap-4 py-2"> <!-- grid-cols-2 强制每行2列,gap控制间距 --> | 26 | <div class="grid grid-cols-2 gap-4 py-2"> <!-- grid-cols-2 强制每行2列,gap控制间距 --> |
| 24 | <button | 27 | <button |
| 25 | - v-for="checkInType in checkInTypes" | 28 | + v-for="checkInType in active_list" |
| 26 | :key="checkInType.id" | 29 | :key="checkInType.id" |
| 27 | class="flex flex-col items-center p-2 rounded-lg border transition-colors | 30 | class="flex flex-col items-center p-2 rounded-lg border transition-colors |
| 28 | bg-white/70 border-gray-100 hover:bg-white" | 31 | bg-white/70 border-gray-100 hover:bg-white" |
| ... | @@ -68,7 +71,7 @@ | ... | @@ -68,7 +71,7 @@ |
| 68 | </template> | 71 | </template> |
| 69 | 72 | ||
| 70 | <script setup> | 73 | <script setup> |
| 71 | -import { ref } from 'vue' | 74 | +import { ref, computed, onMounted } from 'vue' |
| 72 | import { showToast } from 'vant' | 75 | import { showToast } from 'vant' |
| 73 | import { useRoute, useRouter } from 'vue-router' | 76 | import { useRoute, useRouter } from 'vue-router' |
| 74 | import { getTaskListAPI, checkinTaskAPI } from "@/api/checkin"; | 77 | import { getTaskListAPI, checkinTaskAPI } from "@/api/checkin"; |
| ... | @@ -80,11 +83,12 @@ const route = useRoute() | ... | @@ -80,11 +83,12 @@ const route = useRoute() |
| 80 | const router = useRouter() | 83 | const router = useRouter() |
| 81 | 84 | ||
| 82 | const props = defineProps({ | 85 | const props = defineProps({ |
| 83 | - show: { | 86 | + /** 弹窗显隐 */ |
| 84 | - type: Boolean, | 87 | + show: { type: Boolean, required: true, default: false }, |
| 85 | - required: true, | 88 | + /** 今日打卡任务(外部传入,可选) */ |
| 86 | - default: false | 89 | + items_today: { type: Array, default: () => [] }, |
| 87 | - } | 90 | + /** 历史打卡任务(外部传入,可选) */ |
| 91 | + items_history: { type: Array, default: () => [] } | ||
| 88 | }) | 92 | }) |
| 89 | 93 | ||
| 90 | const emit = defineEmits(['update:show', 'check-in-success', 'check-in-data']) | 94 | const emit = defineEmits(['update:show', 'check-in-success', 'check-in-data']) |
| ... | @@ -94,6 +98,36 @@ const checkInContent = ref('') | ... | @@ -94,6 +98,36 @@ const checkInContent = ref('') |
| 94 | const isCheckingIn = ref(false) | 98 | const isCheckingIn = ref(false) |
| 95 | const checkInSuccess = ref(false) | 99 | const checkInSuccess = ref(false) |
| 96 | 100 | ||
| 101 | +/** | ||
| 102 | + * @var {import('vue').Ref<'today'|'history'>} active_tab | ||
| 103 | + * @description 当前选中的任务标签页:今日或历史。 | ||
| 104 | + */ | ||
| 105 | +const active_tab = ref('today') | ||
| 106 | + | ||
| 107 | +/** | ||
| 108 | + * @function has_history | ||
| 109 | + * @description 是否存在历史任务(用于显示“历史打卡”标签)。 | ||
| 110 | + * @returns {boolean} | ||
| 111 | + */ | ||
| 112 | +const has_history = computed(() => { | ||
| 113 | + const list = Array.isArray(props.items_history) ? props.items_history : [] | ||
| 114 | + return list.length > 0 | ||
| 115 | +}) | ||
| 116 | + | ||
| 117 | +/** | ||
| 118 | + * @function active_list | ||
| 119 | + * @description 当前展示的任务列表:优先使用外部传入,未传时回退为组件内部获取的列表。 | ||
| 120 | + * @returns {Array} | ||
| 121 | + */ | ||
| 122 | +const active_list = computed(() => { | ||
| 123 | + const today = Array.isArray(props.items_today) ? props.items_today : [] | ||
| 124 | + const history = Array.isArray(props.items_history) ? props.items_history : [] | ||
| 125 | + if (active_tab.value === 'today') { | ||
| 126 | + return today.length ? today : checkInTypes.value | ||
| 127 | + } | ||
| 128 | + return history | ||
| 129 | +}) | ||
| 130 | + | ||
| 97 | const handleCheckInSelect = (type) => { | 131 | const handleCheckInSelect = (type) => { |
| 98 | if (type.is_gray && type.task_type === 'checkin') { | 132 | if (type.is_gray && type.task_type === 'checkin') { |
| 99 | showToast('您已经完成了今天的打卡') | 133 | showToast('您已经完成了今天的打卡') |
| ... | @@ -151,18 +185,20 @@ const handleClose = () => { | ... | @@ -151,18 +185,20 @@ const handleClose = () => { |
| 151 | } | 185 | } |
| 152 | 186 | ||
| 153 | onMounted(async () => { | 187 | onMounted(async () => { |
| 154 | - // 获取签到列表 | 188 | + // 当未从外部传入“今日任务”时,回退为组件内部获取的通用任务列表 |
| 155 | - const task = await getTaskListAPI() | 189 | + if (!Array.isArray(props.items_today) || props.items_today.length === 0) { |
| 156 | - if (task.code) { | 190 | + const task = await getTaskListAPI() |
| 157 | - emit('check-in-data', task.data) | 191 | + if (task.code) { |
| 158 | - task.data.forEach(item => { | 192 | + emit('check-in-data', task.data) |
| 159 | - checkInTypes.value.push({ | 193 | + task.data.forEach(item => { |
| 160 | - id: item.id, | 194 | + checkInTypes.value.push({ |
| 161 | - name: item.title, | 195 | + id: item.id, |
| 162 | - task_type: item.task_type, | 196 | + name: item.title, |
| 163 | - is_gray: item.is_gray | 197 | + task_type: item.task_type, |
| 164 | - }) | 198 | + is_gray: item.is_gray |
| 165 | - }) | 199 | + }) |
| 166 | - } | 200 | + }) |
| 201 | + } | ||
| 202 | + } | ||
| 167 | }) | 203 | }) |
| 168 | </script> | 204 | </script> | ... | ... |
| ... | @@ -264,70 +264,13 @@ | ... | @@ -264,70 +264,13 @@ |
| 264 | <!-- Review Popup --> | 264 | <!-- Review Popup --> |
| 265 | <ReviewPopup v-model:show="showReviewPopup" title="立即评价" @submit="handleReviewSubmit" /> | 265 | <ReviewPopup v-model:show="showReviewPopup" title="立即评价" @submit="handleReviewSubmit" /> |
| 266 | 266 | ||
| 267 | - <!-- 打卡弹窗 --> | 267 | + <!-- 打卡弹窗(统一组件) --> |
| 268 | - <van-popup | 268 | + <CheckInDialog |
| 269 | - v-model:show="showCheckInDialog" | 269 | + v-model:show="showCheckInDialog" |
| 270 | - round | 270 | + :items_today="task_list" |
| 271 | - position="bottom" | 271 | + :items_history="timeout_task_list" |
| 272 | - @close="closeCheckInDialog" | 272 | + @check-in-success="handleCheckInSuccess" |
| 273 | - :style="{ minHeight: '30%', maxHeight: '80%', width: '100%' }" | 273 | + /> |
| 274 | - > | ||
| 275 | - <div class="p-4"> | ||
| 276 | - <div class="flex justify-between items-center mb-3"> | ||
| 277 | - <h3 class="font-medium"> | ||
| 278 | - <span :class="{ 'text-green-500' : showTaskList }" @click="toggleTask('today')">今日打卡</span> | ||
| 279 | - <span :class="{ 'text-green-500' : showTimeoutTaskList }" @click="toggleTask('timeout')">历史打卡</span> | ||
| 280 | - </h3> | ||
| 281 | - <van-icon name="cross" @click="showCheckInDialog = false" /> | ||
| 282 | - </div> | ||
| 283 | - | ||
| 284 | - <div v-if="checkInSuccess" class="bg-green-50 border border-green-200 rounded-lg p-4 text-center"> | ||
| 285 | - <svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10 text-green-500 mx-auto mb-2" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | ||
| 286 | - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /> | ||
| 287 | - </svg> | ||
| 288 | - <h4 class="text-green-700 font-medium mb-1">打卡成功!</h4> | ||
| 289 | - </div> | ||
| 290 | - <template v-else> | ||
| 291 | - <div class="grid grid-cols-2 gap-4 py-2"> | ||
| 292 | - <button | ||
| 293 | - v-for="checkInType in default_list" | ||
| 294 | - :key="checkInType.id" | ||
| 295 | - class="flex flex-col items-center p-2 rounded-lg border transition-colors | ||
| 296 | - bg-white/70 border-gray-100 hover:bg-white" | ||
| 297 | - :class="{ | ||
| 298 | - 'bg-green-100 border-green-200': selectedCheckIn?.id === checkInType.id | ||
| 299 | - }" | ||
| 300 | - @click="handleCheckInSelect(checkInType)" | ||
| 301 | - > | ||
| 302 | - <div class="w-12 h-12 rounded-full flex items-center justify-center mb-1 transition-colors | ||
| 303 | - bg-gray-100 text-gray-500" | ||
| 304 | - :class="{ | ||
| 305 | - 'bg-green-500 text-white': selectedCheckIn?.id === checkInType.id | ||
| 306 | - }" | ||
| 307 | - > | ||
| 308 | - <van-icon v-if="checkInType.task_type === 'checkin'" name="edit" size="1.5rem" :color="checkInType.is_gray ? 'gray' : ''" /> | ||
| 309 | - <van-icon v-if="checkInType.task_type === 'upload'" name="tosend" size="1.5rem" :color="checkInType.is_gray ? 'gray' : ''" /> | ||
| 310 | - </div> | ||
| 311 | - <span :class="['text-xs', checkInType.is_gray ? 'text-gray-500' : '']">{{ checkInType.name }}</span> | ||
| 312 | - </button> | ||
| 313 | - </div> | ||
| 314 | - | ||
| 315 | - <div v-if="selectedCheckIn" class="mt-3"> | ||
| 316 | - <button | ||
| 317 | - class="mt-2 w-full bg-gradient-to-r from-green-500 to-green-600 text-white py-2 rounded-lg flex items-center justify-center" | ||
| 318 | - @click="handleCheckInSubmit" | ||
| 319 | - :disabled="isCheckingIn" | ||
| 320 | - > | ||
| 321 | - <template v-if="isCheckingIn"> | ||
| 322 | - <div class="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin mr-2"></div> | ||
| 323 | - 提交中... | ||
| 324 | - </template> | ||
| 325 | - <template v-else>提交打卡</template> | ||
| 326 | - </button> | ||
| 327 | - </div> | ||
| 328 | - </template> | ||
| 329 | - </div> | ||
| 330 | - </van-popup> | ||
| 331 | 274 | ||
| 332 | <!-- 咨询弹窗:底部只有关闭按钮,内容支持富文本 --> | 275 | <!-- 咨询弹窗:底部只有关闭按钮,内容支持富文本 --> |
| 333 | <van-popup | 276 | <van-popup |
| ... | @@ -397,11 +340,12 @@ import { sharePage } from '@/composables/useShare.js' | ... | @@ -397,11 +340,12 @@ import { sharePage } from '@/composables/useShare.js' |
| 397 | import AppLayout from '@/components/layout/AppLayout.vue' | 340 | import AppLayout from '@/components/layout/AppLayout.vue' |
| 398 | import FrostedGlass from '@/components/ui/FrostedGlass.vue' | 341 | import FrostedGlass from '@/components/ui/FrostedGlass.vue' |
| 399 | import SharePoster from '@/components/ui/SharePoster.vue' | 342 | import SharePoster from '@/components/ui/SharePoster.vue' |
| 343 | +import CheckInDialog from '@/components/ui/CheckInDialog.vue' | ||
| 400 | 344 | ||
| 401 | // 导入接口 | 345 | // 导入接口 |
| 402 | import { getCourseDetailAPI, getGroupCommentListAPI, addGroupCommentAPI } from "@/api/course"; | 346 | import { getCourseDetailAPI, getGroupCommentListAPI, addGroupCommentAPI } from "@/api/course"; |
| 403 | import { addFavoriteAPI, cancelFavoriteAPI } from "@/api/favorite"; | 347 | import { addFavoriteAPI, cancelFavoriteAPI } from "@/api/favorite"; |
| 404 | -import { checkinTaskAPI } from '@/api/checkin'; | 348 | +// 已统一使用通用打卡弹窗,移除未使用的打卡提交接口 |
| 405 | 349 | ||
| 406 | // | 350 | // |
| 407 | // Open Graph 元标签:进入课程详情页时动态插入,离开页面时移除 | 351 | // Open Graph 元标签:进入课程详情页时动态插入,离开页面时移除 |
| ... | @@ -568,13 +512,7 @@ const handleIntroduceClick = (event) => { | ... | @@ -568,13 +512,7 @@ const handleIntroduceClick = (event) => { |
| 568 | // 打卡相关状态 | 512 | // 打卡相关状态 |
| 569 | const task_list = ref([]) | 513 | const task_list = ref([]) |
| 570 | const timeout_task_list = ref([]) | 514 | const timeout_task_list = ref([]) |
| 571 | -const default_list = ref([]) | ||
| 572 | -const showTaskList = ref(true) | ||
| 573 | -const showTimeoutTaskList = ref(false) | ||
| 574 | const showCheckInDialog = ref(false) | 515 | const showCheckInDialog = ref(false) |
| 575 | -const selectedCheckIn = ref(null) | ||
| 576 | -const isCheckingIn = ref(false) | ||
| 577 | -const checkInSuccess = ref(false) | ||
| 578 | 516 | ||
| 579 | // 咨询弹窗相关状态 | 517 | // 咨询弹窗相关状态 |
| 580 | /** | 518 | /** |
| ... | @@ -860,7 +798,7 @@ onMounted(async () => { | ... | @@ -860,7 +798,7 @@ onMounted(async () => { |
| 860 | }); | 798 | }); |
| 861 | } | 799 | } |
| 862 | 800 | ||
| 863 | - default_list.value = task_list.value; | 801 | + // 统一弹窗组件后不再维护 default_list |
| 864 | 802 | ||
| 865 | // 进入详情页时写入 Open Graph 元标签,提升分享预览效果 | 803 | // 进入详情页时写入 Open Graph 元标签,提升分享预览效果 |
| 866 | set_og_meta({ | 804 | set_og_meta({ |
| ... | @@ -979,54 +917,6 @@ const goToStudyDetail = (item) => { | ... | @@ -979,54 +917,6 @@ const goToStudyDetail = (item) => { |
| 979 | * 处理打卡选择 | 917 | * 处理打卡选择 |
| 980 | * @param {Object} type - 打卡类型对象 | 918 | * @param {Object} type - 打卡类型对象 |
| 981 | */ | 919 | */ |
| 982 | -const handleCheckInSelect = (type) => { | ||
| 983 | - if (type.is_gray && type.task_type === 'checkin') { | ||
| 984 | - showToast('您已经完成了今天的打卡'); | ||
| 985 | - return; | ||
| 986 | - } | ||
| 987 | - if (type.task_type === 'upload') { | ||
| 988 | - router.push({ | ||
| 989 | - path: '/checkin/index', | ||
| 990 | - query: { | ||
| 991 | - id: type.id | ||
| 992 | - } | ||
| 993 | - }); | ||
| 994 | - showCheckInDialog.value = false; | ||
| 995 | - return; | ||
| 996 | - } else { | ||
| 997 | - selectedCheckIn.value = type; | ||
| 998 | - } | ||
| 999 | -}; | ||
| 1000 | - | ||
| 1001 | -/** | ||
| 1002 | - * 处理打卡提交 | ||
| 1003 | - */ | ||
| 1004 | -const handleCheckInSubmit = async () => { | ||
| 1005 | - if (!selectedCheckIn.value) { | ||
| 1006 | - showToast('请选择打卡项目'); | ||
| 1007 | - return; | ||
| 1008 | - } | ||
| 1009 | - | ||
| 1010 | - isCheckingIn.value = true; | ||
| 1011 | - try { | ||
| 1012 | - const { code } = await checkinTaskAPI({ task_id: selectedCheckIn.value.id }); | ||
| 1013 | - if (code) { | ||
| 1014 | - checkInSuccess.value = true; | ||
| 1015 | - // 重置表单 | ||
| 1016 | - setTimeout(() => { | ||
| 1017 | - checkInSuccess.value = false; | ||
| 1018 | - selectedCheckIn.value = null; | ||
| 1019 | - showCheckInDialog.value = false; | ||
| 1020 | - }, 1500); | ||
| 1021 | - } | ||
| 1022 | - } catch (error) { | ||
| 1023 | - console.error('打卡失败:', error); | ||
| 1024 | - showToast('打卡失败,请重试'); | ||
| 1025 | - } finally { | ||
| 1026 | - isCheckingIn.value = false; | ||
| 1027 | - } | ||
| 1028 | -}; | ||
| 1029 | - | ||
| 1030 | /** | 920 | /** |
| 1031 | * 打开打卡弹窗 | 921 | * 打开打卡弹窗 |
| 1032 | */ | 922 | */ |
| ... | @@ -1041,36 +931,13 @@ const goToCheckin = () => { | ... | @@ -1041,36 +931,13 @@ const goToCheckin = () => { |
| 1041 | }) | 931 | }) |
| 1042 | return; | 932 | return; |
| 1043 | } | 933 | } |
| 1044 | - if(!default_list.value.length) { | 934 | + if(!(task_list.value.length || timeout_task_list.value.length)) { |
| 1045 | showToast('暂无打卡任务'); | 935 | showToast('暂无打卡任务'); |
| 1046 | return; | 936 | return; |
| 1047 | } | 937 | } |
| 1048 | showCheckInDialog.value = true; | 938 | showCheckInDialog.value = true; |
| 1049 | }; | 939 | }; |
| 1050 | 940 | ||
| 1051 | -/** | ||
| 1052 | - * 切换打卡任务类型 | ||
| 1053 | - * @param {string} type - 任务类型 ('today' | 'timeout') | ||
| 1054 | - */ | ||
| 1055 | -const toggleTask = (type) => { | ||
| 1056 | - if(type === 'today') { | ||
| 1057 | - showTaskList.value = true; | ||
| 1058 | - showTimeoutTaskList.value = false; | ||
| 1059 | - default_list.value = task_list.value; | ||
| 1060 | - } else { | ||
| 1061 | - showTaskList.value = false; | ||
| 1062 | - showTimeoutTaskList.value = true; | ||
| 1063 | - default_list.value = timeout_task_list.value; | ||
| 1064 | - } | ||
| 1065 | -} | ||
| 1066 | - | ||
| 1067 | -/** | ||
| 1068 | - * 关闭打卡弹窗 | ||
| 1069 | - */ | ||
| 1070 | -const closeCheckInDialog = () => { | ||
| 1071 | - showCheckInDialog.value = false; | ||
| 1072 | -} | ||
| 1073 | - | ||
| 1074 | setTimeout(() => { | 941 | setTimeout(() => { |
| 1075 | // TAG:微信分享 | 942 | // TAG:微信分享 |
| 1076 | // 自定义分享内容 | 943 | // 自定义分享内容 |
| ... | @@ -1108,3 +975,10 @@ setTimeout(() => { | ... | @@ -1108,3 +975,10 @@ setTimeout(() => { |
| 1108 | background-color: #4caf50; | 975 | background-color: #4caf50; |
| 1109 | } | 976 | } |
| 1110 | </style> | 977 | </style> |
| 978 | +/** | ||
| 979 | + * 处理打卡成功 | ||
| 980 | + * 注释:统一弹窗触发成功事件后,页面提示成功。 | ||
| 981 | + */ | ||
| 982 | +const handleCheckInSuccess = () => { | ||
| 983 | + showToast('打卡成功'); | ||
| 984 | +} | ... | ... |
| ... | @@ -118,70 +118,13 @@ | ... | @@ -118,70 +118,13 @@ |
| 118 | </div> | 118 | </div> |
| 119 | </div> | 119 | </div> |
| 120 | 120 | ||
| 121 | - <!-- 打卡弹窗 --> | 121 | + <!-- 打卡弹窗:统一使用 CheckInDialog 组件 --> |
| 122 | - <van-popup | 122 | + <CheckInDialog |
| 123 | v-model:show="showCheckInDialog" | 123 | v-model:show="showCheckInDialog" |
| 124 | - round | 124 | + :items_today="task_list" |
| 125 | - position="bottom" | 125 | + :items_history="timeout_task_list" |
| 126 | - @close="closeCheckInDialog" | 126 | + @check-in-success="handleCheckInSuccess" |
| 127 | - :style="{ minHeight: '30%', maxHeight: '80%', width: '100%' }" | 127 | + /> |
| 128 | - > | ||
| 129 | - <div class="p-4"> | ||
| 130 | - <div class="flex justify-between items-center mb-3"> | ||
| 131 | - <h3 class="font-medium"> | ||
| 132 | - <span :class="{ 'text-green-500' : showTaskList }" @click="toggleTask('today')">今日打卡</span> | ||
| 133 | - <span :class="{ 'text-green-500' : showTimeoutTaskList }" @click="toggleTask('timeout')">历史打卡</span> | ||
| 134 | - </h3> | ||
| 135 | - <van-icon name="cross" @click="showCheckInDialog = false" /> | ||
| 136 | - </div> | ||
| 137 | - | ||
| 138 | - <div v-if="checkInSuccess" class="bg-green-50 border border-green-200 rounded-lg p-4 text-center"> | ||
| 139 | - <svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10 text-green-500 mx-auto mb-2" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | ||
| 140 | - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /> | ||
| 141 | - </svg> | ||
| 142 | - <h4 class="text-green-700 font-medium mb-1">打卡成功!</h4> | ||
| 143 | - </div> | ||
| 144 | - <template v-else> | ||
| 145 | - <div class="grid grid-cols-2 gap-4 py-2"> | ||
| 146 | - <button | ||
| 147 | - v-for="checkInType in default_list" | ||
| 148 | - :key="checkInType.id" | ||
| 149 | - class="flex flex-col items-center p-2 rounded-lg border transition-colors | ||
| 150 | - bg-white/70 border-gray-100 hover:bg-white" | ||
| 151 | - :class="{ | ||
| 152 | - 'bg-green-100 border-green-200': selectedCheckIn?.id === checkInType.id | ||
| 153 | - }" | ||
| 154 | - @click="handleCheckInSelect(checkInType)" | ||
| 155 | - > | ||
| 156 | - <div class="w-12 h-12 rounded-full flex items-center justify-center mb-1 transition-colors | ||
| 157 | - bg-gray-100 text-gray-500" | ||
| 158 | - :class="{ | ||
| 159 | - 'bg-green-500 text-white': selectedCheckIn?.id === checkInType.id | ||
| 160 | - }" | ||
| 161 | - > | ||
| 162 | - <van-icon v-if="checkInType.task_type === 'checkin'" name="edit" size="1.5rem" :color="checkInType.is_gray ? 'gray' : ''" /> | ||
| 163 | - <van-icon v-if="checkInType.task_type === 'upload'" name="tosend" size="1.5rem" :color="checkInType.is_gray ? 'gray' : ''" /> | ||
| 164 | - </div> | ||
| 165 | - <span :class="['text-xs', checkInType.is_gray ? 'text-gray-500' : '']">{{ checkInType.name }}</span> | ||
| 166 | - </button> | ||
| 167 | - </div> | ||
| 168 | - | ||
| 169 | - <div v-if="selectedCheckIn" class="mt-3"> | ||
| 170 | - <button | ||
| 171 | - class="mt-2 w-full bg-gradient-to-r from-green-500 to-green-600 text-white py-2 rounded-lg flex items-center justify-center" | ||
| 172 | - @click="handleCheckInSubmit" | ||
| 173 | - :disabled="isCheckingIn" | ||
| 174 | - > | ||
| 175 | - <template v-if="isCheckingIn"> | ||
| 176 | - <div class="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin mr-2"></div> | ||
| 177 | - 提交中... | ||
| 178 | - </template> | ||
| 179 | - <template v-else>提交打卡</template> | ||
| 180 | - </button> | ||
| 181 | - </div> | ||
| 182 | - </template> | ||
| 183 | - </div> | ||
| 184 | - </van-popup> | ||
| 185 | </div> | 128 | </div> |
| 186 | </AppLayout> | 129 | </AppLayout> |
| 187 | </template> | 130 | </template> |
| ... | @@ -193,10 +136,10 @@ import { useRouter } from "vue-router"; | ... | @@ -193,10 +136,10 @@ import { useRouter } from "vue-router"; |
| 193 | import dayjs from 'dayjs'; | 136 | import dayjs from 'dayjs'; |
| 194 | import { showToast } from 'vant'; | 137 | import { showToast } from 'vant'; |
| 195 | import AppLayout from '@/components/layout/AppLayout.vue'; | 138 | import AppLayout from '@/components/layout/AppLayout.vue'; |
| 139 | +import CheckInDialog from '@/components/ui/CheckInDialog.vue'; | ||
| 196 | 140 | ||
| 197 | // 导入接口 | 141 | // 导入接口 |
| 198 | import { getCourseDetailAPI } from '@/api/course'; | 142 | import { getCourseDetailAPI } from '@/api/course'; |
| 199 | -import { checkinTaskAPI } from '@/api/checkin'; | ||
| 200 | 143 | ||
| 201 | const router = useRouter(); | 144 | const router = useRouter(); |
| 202 | 145 | ||
| ... | @@ -334,9 +277,6 @@ const course_type_maps = ref({ | ... | @@ -334,9 +277,6 @@ const course_type_maps = ref({ |
| 334 | 277 | ||
| 335 | const task_list = ref([]); | 278 | const task_list = ref([]); |
| 336 | const timeout_task_list = ref([]); | 279 | const timeout_task_list = ref([]); |
| 337 | -const default_list = ref([]); | ||
| 338 | -const showTaskList = ref(true); | ||
| 339 | -const showTimeoutTaskList = ref(false); | ||
| 340 | 280 | ||
| 341 | onMounted(async () => { | 281 | onMounted(async () => { |
| 342 | /** | 282 | /** |
| ... | @@ -376,8 +316,6 @@ onMounted(async () => { | ... | @@ -376,8 +316,6 @@ onMounted(async () => { |
| 376 | } | 316 | } |
| 377 | 317 | ||
| 378 | course_lessons.value = data.schedule || []; | 318 | course_lessons.value = data.schedule || []; |
| 379 | - default_list.value = task_list.value; | ||
| 380 | - showTaskList.value = true; | ||
| 381 | } | 319 | } |
| 382 | else { | 320 | else { |
| 383 | // 课程不存在,跳转到课程主页面 | 321 | // 课程不存在,跳转到课程主页面 |
| ... | @@ -515,83 +453,22 @@ const goToStudyDetail = (lessonId) => { | ... | @@ -515,83 +453,22 @@ const goToStudyDetail = (lessonId) => { |
| 515 | 453 | ||
| 516 | // 打卡相关状态 | 454 | // 打卡相关状态 |
| 517 | const showCheckInDialog = ref(false); | 455 | const showCheckInDialog = ref(false); |
| 518 | -const selectedCheckIn = ref(null); | ||
| 519 | -const isCheckingIn = ref(false); | ||
| 520 | -const checkInSuccess = ref(false); | ||
| 521 | 456 | ||
| 522 | // 处理打卡选择 | 457 | // 处理打卡选择 |
| 523 | -const handleCheckInSelect = (type) => { | ||
| 524 | - if (type.is_gray && type.task_type === 'checkin') { | ||
| 525 | - showToast('您已经完成了今天的打卡'); | ||
| 526 | - return; | ||
| 527 | - } | ||
| 528 | - if (type.task_type === 'upload') { | ||
| 529 | - router.push({ | ||
| 530 | - path: '/checkin/index', | ||
| 531 | - query: { | ||
| 532 | - id: type.id | ||
| 533 | - } | ||
| 534 | - }); | ||
| 535 | - showCheckInDialog.value = false; | ||
| 536 | - return; | ||
| 537 | - } else { | ||
| 538 | - selectedCheckIn.value = type; | ||
| 539 | - } | ||
| 540 | -}; | ||
| 541 | - | ||
| 542 | -// 处理打卡提交 | ||
| 543 | -const handleCheckInSubmit = async () => { | ||
| 544 | - if (!selectedCheckIn.value) { | ||
| 545 | - showToast('请选择打卡项目'); | ||
| 546 | - return; | ||
| 547 | - } | ||
| 548 | - | ||
| 549 | - isCheckingIn.value = true; | ||
| 550 | - try { | ||
| 551 | - const { code } = await checkinTaskAPI({ task_id: selectedCheckIn.value.id }); | ||
| 552 | - if (code) { | ||
| 553 | - checkInSuccess.value = true; | ||
| 554 | - // 重置表单 | ||
| 555 | - setTimeout(() => { | ||
| 556 | - checkInSuccess.value = false; | ||
| 557 | - selectedCheckIn.value = null; | ||
| 558 | - showCheckInDialog.value = false; | ||
| 559 | - }, 1500); | ||
| 560 | - } | ||
| 561 | - } catch (error) { | ||
| 562 | - console.error('打卡失败:', error); | ||
| 563 | - showToast('打卡失败,请重试'); | ||
| 564 | - } finally { | ||
| 565 | - isCheckingIn.value = false; | ||
| 566 | - } | ||
| 567 | -}; | ||
| 568 | - | ||
| 569 | const goToCheckin = () => { | 458 | const goToCheckin = () => { |
| 570 | - if(!default_list.value.length) { | 459 | + if(!(task_list.value.length || timeout_task_list.value.length)) { |
| 571 | showToast('暂无打卡任务'); | 460 | showToast('暂无打卡任务'); |
| 572 | return; | 461 | return; |
| 573 | } | 462 | } |
| 574 | showCheckInDialog.value = true; | 463 | showCheckInDialog.value = true; |
| 575 | }; | 464 | }; |
| 576 | 465 | ||
| 577 | -const toggleTask = (type) => { | 466 | +/** |
| 578 | - if(type === 'today') { | 467 | + * 处理打卡成功 |
| 579 | - showTaskList.value = true; | 468 | + * 注释:统一弹窗触发成功事件后,页面提示成功。 |
| 580 | - showTimeoutTaskList.value = false; | 469 | + */ |
| 581 | - default_list.value = task_list.value; | 470 | +const handleCheckInSuccess = () => { |
| 582 | - } else { | 471 | + showToast('打卡成功'); |
| 583 | - showTaskList.value = false; | ||
| 584 | - showTimeoutTaskList.value = true; | ||
| 585 | - default_list.value = timeout_task_list.value; | ||
| 586 | - } | ||
| 587 | -} | ||
| 588 | - | ||
| 589 | -const closeCheckInDialog = () => { | ||
| 590 | - showCheckInDialog.value = false; | ||
| 591 | - // 切换到今日任务 | ||
| 592 | - showTaskList.value = true; | ||
| 593 | - showTimeoutTaskList.value = false; | ||
| 594 | - default_list.value = task_list.value; | ||
| 595 | } | 472 | } |
| 596 | </script> | 473 | </script> |
| 597 | 474 | ... | ... |
This diff is collapsed. Click to expand it.
-
Please register or login to post a comment