refactor(打卡弹窗): 将打卡弹窗统一为通用组件 CheckInDialog
重构 CourseDetailPage、StudyCoursePage 和 StudyDetailPage 中的打卡弹窗逻辑,提取为通用组件 CheckInDialog 组件支持通过 props 传入今日和历史打卡任务列表,并统一处理弹窗显隐和交互逻辑 移除各页面中冗余的打卡相关状态和方法,简化代码结构
Showing
5 changed files
with
208 additions
and
591 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 | ... | ... |
| ... | @@ -30,7 +30,8 @@ | ... | @@ -30,7 +30,8 @@ |
| 30 | <div v-if="course.course_type === 'audio'" class="w-full relative" | 30 | <div v-if="course.course_type === 'audio'" class="w-full relative" |
| 31 | style="border-bottom: 1px solid #F3F4F6;"> | 31 | style="border-bottom: 1px solid #F3F4F6;"> |
| 32 | <!-- 音频播放器 --> | 32 | <!-- 音频播放器 --> |
| 33 | - <AudioPlayer ref="audioPlayerRef" v-if="audioList.length" :songs="audioList" @play="onAudioPlay" @pause="onAudioPause" /> | 33 | + <AudioPlayer ref="audioPlayerRef" v-if="audioList.length" :songs="audioList" @play="onAudioPlay" |
| 34 | + @pause="onAudioPause" /> | ||
| 34 | </div> | 35 | </div> |
| 35 | <!-- 图片列表展示区域 --> | 36 | <!-- 图片列表展示区域 --> |
| 36 | <div v-if="course.course_type === 'image'" class="w-full relative"> | 37 | <div v-if="course.course_type === 'image'" class="w-full relative"> |
| ... | @@ -74,14 +75,17 @@ | ... | @@ -74,14 +75,17 @@ |
| 74 | </div> | 75 | </div> |
| 75 | <!-- 标签页区域 --> | 76 | <!-- 标签页区域 --> |
| 76 | <div class="px-4 py-3 bg-white" style="position: relative;"> | 77 | <div class="px-4 py-3 bg-white" style="position: relative;"> |
| 77 | - <van-tabs v-model:active="activeTab" sticky animated swipeable shrink color="#4caf50" @change="handleTabChange"> | 78 | + <van-tabs v-model:active="activeTab" sticky animated swipeable shrink color="#4caf50" |
| 79 | + @change="handleTabChange"> | ||
| 78 | <van-tab title="介绍" name="intro"> | 80 | <van-tab title="介绍" name="intro"> |
| 79 | </van-tab> | 81 | </van-tab> |
| 80 | <van-tab :title-style="{ 'min-width': '50%' }" name="comments"> | 82 | <van-tab :title-style="{ 'min-width': '50%' }" name="comments"> |
| 81 | <template #title>评论({{ commentCount }})</template> | 83 | <template #title>评论({{ commentCount }})</template> |
| 82 | </van-tab> | 84 | </van-tab> |
| 83 | </van-tabs> | 85 | </van-tabs> |
| 84 | - <div v-if="task_list.length > 0" @click="goToCheckin" style="position: absolute; right: 1rem; top: 1.5rem; font-size: 0.875rem; color: #666;">打卡互动</div> | 86 | + <div v-if="task_list.length > 0" @click="goToCheckin" |
| 87 | + style="position: absolute; right: 1rem; top: 1.5rem; font-size: 0.875rem; color: #666;">打卡互动 | ||
| 88 | + </div> | ||
| 85 | </div> | 89 | </div> |
| 86 | </div> | 90 | </div> |
| 87 | 91 | ||
| ... | @@ -91,7 +95,9 @@ | ... | @@ -91,7 +95,9 @@ |
| 91 | <div id="intro" class="py-4 px-4"> | 95 | <div id="intro" class="py-4 px-4"> |
| 92 | <h1 class="text-lg font-bold mb-2">{{ course.title }}</h1> | 96 | <h1 class="text-lg font-bold mb-2">{{ course.title }}</h1> |
| 93 | <div class="text-gray-500 text-sm flex items-center gap-2"> | 97 | <div class="text-gray-500 text-sm flex items-center gap-2"> |
| 94 | - <span>开课时间 {{ course.schedule_time ? dayjs(course.schedule_time).format('YYYY-MM-DD HH:mm:ss') : '暂无' }}</span> | 98 | + <span>开课时间 {{ course.schedule_time ? dayjs(course.schedule_time).format('YYYY-MM-DD HH:mm:ss') : |
| 99 | + '暂无' | ||
| 100 | + }}</span> | ||
| 95 | <!-- <span class="text-gray-300">|</span> --> | 101 | <!-- <span class="text-gray-300">|</span> --> |
| 96 | <!-- <span>没有字段{{ course.studyCount || 0 }}次学习</span> --> | 102 | <!-- <span>没有字段{{ course.studyCount || 0 }}次学习</span> --> |
| 97 | </div> | 103 | </div> |
| ... | @@ -163,10 +169,11 @@ | ... | @@ -163,10 +169,11 @@ |
| 163 | style="margin-right: 0.5rem;" /> | 169 | style="margin-right: 0.5rem;" /> |
| 164 | <div class="flex-1 ml-3"> | 170 | <div class="flex-1 ml-3"> |
| 165 | <div class="flex justify-between items-center mb-1"> | 171 | <div class="flex justify-between items-center mb-1"> |
| 166 | - <span class="font-medium text-gray-900">{{ comment.name || '匿名用户' }}</span> | 172 | + <span class="font-medium text-gray-900">{{ comment.name || '匿名用户' |
| 173 | + }}</span> | ||
| 167 | <div class="flex items-center space-x-1"> | 174 | <div class="flex items-center space-x-1"> |
| 168 | <span class="text-sm text-gray-500">{{ comment.like_count | 175 | <span class="text-sm text-gray-500">{{ comment.like_count |
| 169 | - }}</span> | 176 | + }}</span> |
| 170 | | 177 | |
| 171 | <van-icon :name="comment.is_like ? 'like' : 'like-o'" | 178 | <van-icon :name="comment.is_like ? 'like' : 'like-o'" |
| 172 | :class="{ 'text-red-500': comment.is_like, 'text-gray-400': !comment.is_like }" | 179 | :class="{ 'text-red-500': comment.is_like, 'text-gray-400': !comment.is_like }" |
| ... | @@ -176,7 +183,7 @@ | ... | @@ -176,7 +183,7 @@ |
| 176 | </div> | 183 | </div> |
| 177 | <p class="text-gray-700 text-sm mb-1">{{ comment.note }}</p> | 184 | <p class="text-gray-700 text-sm mb-1">{{ comment.note }}</p> |
| 178 | <div class="text-gray-400 text-xs">{{ formatDate(comment.updated_time) | 185 | <div class="text-gray-400 text-xs">{{ formatDate(comment.updated_time) |
| 179 | - }}</div> | 186 | + }}</div> |
| 180 | </div> | 187 | </div> |
| 181 | </div> | 188 | </div> |
| 182 | </div> | 189 | </div> |
| ... | @@ -218,12 +225,8 @@ | ... | @@ -218,12 +225,8 @@ |
| 218 | </div> | 225 | </div> |
| 219 | 226 | ||
| 220 | <!-- 图片预览组件 --> | 227 | <!-- 图片预览组件 --> |
| 221 | - <van-image-preview | 228 | + <van-image-preview v-model:show="showPreview" :images="previewImages" :show-index="false" |
| 222 | - v-model:show="showPreview" | 229 | + :close-on-click-image="false"> |
| 223 | - :images="previewImages" | ||
| 224 | - :show-index="false" | ||
| 225 | - :close-on-click-image="false" | ||
| 226 | - > | ||
| 227 | <template #image="{ src, style, onLoad }"> | 230 | <template #image="{ src, style, onLoad }"> |
| 228 | <img :src="src" :style="[{ width: '100%' }, style]" @load="onLoad" /> | 231 | <img :src="src" :style="[{ width: '100%' }, style]" @load="onLoad" /> |
| 229 | </template> | 232 | </template> |
| ... | @@ -252,9 +255,14 @@ | ... | @@ -252,9 +255,14 @@ |
| 252 | <div v-if="lesson.progress > 0 && lesson.progress < 100" | 255 | <div v-if="lesson.progress > 0 && lesson.progress < 100" |
| 253 | class="absolute top-2 right-2 px-2 py-1 bg-green-100 text-green-600 text-xs rounded"> | 256 | class="absolute top-2 right-2 px-2 py-1 bg-green-100 text-green-600 text-xs rounded"> |
| 254 | 上次看到</div> | 257 | 上次看到</div> |
| 255 | - <div class="text-black text-base mb-2" :class="{ 'text-green-600 font-medium' : courseId == lesson.id }">{{ lesson.title }} • <span class="text-sm text-gray-500">{{ course_type_maps[lesson.course_type] }}</span></div> | 258 | + <div class="text-black text-base mb-2" |
| 259 | + :class="{ 'text-green-600 font-medium': courseId == lesson.id }">{{ lesson.title }} • | ||
| 260 | + <span class="text-sm text-gray-500">{{ course_type_maps[lesson.course_type] }}</span> | ||
| 261 | + </div> | ||
| 256 | <div class="flex items-center text-sm text-gray-500"> | 262 | <div class="flex items-center text-sm text-gray-500"> |
| 257 | - <span>开课时间: {{ lesson.schedule_time ? dayjs(lesson.schedule_time).format('YYYY-MM-DD') : '暂无' }}</span> | 263 | + <span>开课时间: {{ lesson.schedule_time ? dayjs(lesson.schedule_time).format('YYYY-MM-DD') : |
| 264 | + '暂无' | ||
| 265 | + }}</span> | ||
| 258 | <span class="mx-2">|</span> | 266 | <span class="mx-2">|</span> |
| 259 | <span v-if="lesson.duration">建议时长: {{ lesson.duration }} 分钟</span> | 267 | <span v-if="lesson.duration">建议时长: {{ lesson.duration }} 分钟</span> |
| 260 | </div> | 268 | </div> |
| ... | @@ -268,13 +276,7 @@ | ... | @@ -268,13 +276,7 @@ |
| 268 | <!-- PDF预览改为独立页面,点击资源时跳转到 /pdfPreview --> | 276 | <!-- PDF预览改为独立页面,点击资源时跳转到 /pdfPreview --> |
| 269 | 277 | ||
| 270 | <!-- Office 文档预览弹窗 --> | 278 | <!-- Office 文档预览弹窗 --> |
| 271 | - <van-popup | 279 | + <van-popup v-model:show="officeShow" position="center" round closeable :style="{ height: '80%', width: '90%' }"> |
| 272 | - v-model:show="officeShow" | ||
| 273 | - position="center" | ||
| 274 | - round | ||
| 275 | - closeable | ||
| 276 | - :style="{ height: '80%', width: '90%' }" | ||
| 277 | - > | ||
| 278 | <div class="h-full flex flex-col"> | 280 | <div class="h-full flex flex-col"> |
| 279 | <div class="p-4 border-b border-gray-200"> | 281 | <div class="p-4 border-b border-gray-200"> |
| 280 | <h3 class="text-lg font-medium text-center truncate">{{ officeTitle }}</h3> | 282 | <h3 class="text-lg font-medium text-center truncate">{{ officeTitle }}</h3> |
| ... | @@ -294,135 +296,45 @@ | ... | @@ -294,135 +296,45 @@ |
| 294 | </van-popup> | 296 | </van-popup> |
| 295 | 297 | ||
| 296 | <!-- 音频播放器弹窗 --> | 298 | <!-- 音频播放器弹窗 --> |
| 297 | - <van-popup | 299 | + <van-popup v-model:show="audioShow" position="bottom" round closeable :style="{ height: '60%', width: '100%' }"> |
| 298 | - v-model:show="audioShow" | ||
| 299 | - position="bottom" | ||
| 300 | - round | ||
| 301 | - closeable | ||
| 302 | - :style="{ height: '60%', width: '100%' }" | ||
| 303 | - > | ||
| 304 | <div class="p-4"> | 300 | <div class="p-4"> |
| 305 | <h3 class="text-lg font-medium mb-4 text-center">{{ audioTitle }}</h3> | 301 | <h3 class="text-lg font-medium mb-4 text-center">{{ audioTitle }}</h3> |
| 306 | - <AudioPlayer | 302 | + <AudioPlayer v-if="audioShow && audioUrl" :songs="[{ title: audioTitle, url: audioUrl }]" |
| 307 | - v-if="audioShow && audioUrl" | 303 | + class="w-full" /> |
| 308 | - :songs="[{ title: audioTitle, url: audioUrl }]" | ||
| 309 | - class="w-full" | ||
| 310 | - /> | ||
| 311 | </div> | 304 | </div> |
| 312 | </van-popup> | 305 | </van-popup> |
| 313 | 306 | ||
| 314 | <!-- 视频播放器弹窗 --> | 307 | <!-- 视频播放器弹窗 --> |
| 315 | - <van-popup | 308 | + <van-popup v-model:show="videoShow" position="center" round closeable |
| 316 | - v-model:show="videoShow" | 309 | + :style="{ width: '95%', maxHeight: '80vh' }" @close="stopPopupVideoPlay"> |
| 317 | - position="center" | ||
| 318 | - round | ||
| 319 | - closeable | ||
| 320 | - :style="{ width: '95%', maxHeight: '80vh' }" | ||
| 321 | - @close="stopPopupVideoPlay" | ||
| 322 | - > | ||
| 323 | <div class="p-4"> | 310 | <div class="p-4"> |
| 324 | <h3 class="text-lg font-medium mb-4 text-center">视频预览</h3> | 311 | <h3 class="text-lg font-medium mb-4 text-center">视频预览</h3> |
| 325 | <div class="relative w-full bg-black rounded-lg overflow-hidden" style="aspect-ratio: 16/9;"> | 312 | <div class="relative w-full bg-black rounded-lg overflow-hidden" style="aspect-ratio: 16/9;"> |
| 326 | <!-- 视频封面 --> | 313 | <!-- 视频封面 --> |
| 327 | - <div | 314 | + <div v-show="!isPopupVideoPlaying" |
| 328 | - v-show="!isPopupVideoPlaying" | ||
| 329 | class="absolute inset-0 bg-black flex items-center justify-center cursor-pointer" | 315 | class="absolute inset-0 bg-black flex items-center justify-center cursor-pointer" |
| 330 | - @click="startPopupVideoPlay" | 316 | + @click="startPopupVideoPlay"> |
| 331 | - > | ||
| 332 | <div class="w-16 h-16 bg-white bg-opacity-80 rounded-full flex items-center justify-center"> | 317 | <div class="w-16 h-16 bg-white bg-opacity-80 rounded-full flex items-center justify-center"> |
| 333 | <svg class="w-8 h-8 text-black ml-1" fill="currentColor" viewBox="0 0 24 24"> | 318 | <svg class="w-8 h-8 text-black ml-1" fill="currentColor" viewBox="0 0 24 24"> |
| 334 | - <path d="M8 5v14l11-7z"/> | 319 | + <path d="M8 5v14l11-7z" /> |
| 335 | </svg> | 320 | </svg> |
| 336 | </div> | 321 | </div> |
| 337 | </div> | 322 | </div> |
| 338 | <!-- 视频播放器 --> | 323 | <!-- 视频播放器 --> |
| 339 | - <VideoPlayer | 324 | + <VideoPlayer v-show="isPopupVideoPlaying" ref="popupVideoPlayerRef" :video-url="videoUrl" |
| 340 | - v-show="isPopupVideoPlaying" | 325 | + :video-id="videoTitle" :autoplay="false" class="w-full h-full" @play="handlePopupVideoPlay" |
| 341 | - ref="popupVideoPlayerRef" | 326 | + @pause="handlePopupVideoPause" /> |
| 342 | - :video-url="videoUrl" | ||
| 343 | - :video-id="videoTitle" | ||
| 344 | - :autoplay="false" | ||
| 345 | - class="w-full h-full" | ||
| 346 | - @play="handlePopupVideoPlay" | ||
| 347 | - @pause="handlePopupVideoPause" | ||
| 348 | - /> | ||
| 349 | </div> | 327 | </div> |
| 350 | </div> | 328 | </div> |
| 351 | </van-popup> | 329 | </van-popup> |
| 352 | 330 | ||
| 353 | <!-- 打卡弹窗 --> | 331 | <!-- 打卡弹窗 --> |
| 354 | - <van-popup | 332 | + <CheckInDialog v-model:show="showCheckInDialog" :items_today="task_list" :items_history="timeout_task_list" |
| 355 | - v-model:show="showCheckInDialog" | 333 | + @check-in-success="handleCheckInSuccess" /> |
| 356 | - round | ||
| 357 | - position="bottom" | ||
| 358 | - @close="closeCheckInDialog" | ||
| 359 | - :style="{ minHeight: '30%', maxHeight: '80%', width: '100%' }" | ||
| 360 | - > | ||
| 361 | - <div class="p-4"> | ||
| 362 | - <div class="flex justify-between items-center mb-3"> | ||
| 363 | - <h3 class="font-medium"> | ||
| 364 | - <span :class="{ 'text-green-500' : showTaskList }" @click="toggleTask('today')">今日打卡</span> | ||
| 365 | - <span :class="{ 'text-green-500' : showTimeoutTaskList }" @click="toggleTask('timeout')">历史打卡</span> | ||
| 366 | - </h3> | ||
| 367 | - <van-icon name="cross" @click="showCheckInDialog = false" /> | ||
| 368 | - </div> | ||
| 369 | - | ||
| 370 | - <div v-if="checkInSuccess" class="bg-green-50 border border-green-200 rounded-lg p-4 text-center"> | ||
| 371 | - <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"> | ||
| 372 | - <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" /> | ||
| 373 | - </svg> | ||
| 374 | - <h4 class="text-green-700 font-medium mb-1">打卡成功!</h4> | ||
| 375 | - </div> | ||
| 376 | - <template v-else> | ||
| 377 | - <div class="grid grid-cols-2 gap-4 py-2"> | ||
| 378 | - <button | ||
| 379 | - v-for="checkInType in default_list" | ||
| 380 | - :key="checkInType.id" | ||
| 381 | - class="flex flex-col items-center p-2 rounded-lg border transition-colors | ||
| 382 | - bg-white/70 border-gray-100 hover:bg-white" | ||
| 383 | - :class="{ | ||
| 384 | - 'bg-green-100 border-green-200': selectedCheckIn?.id === checkInType.id | ||
| 385 | - }" | ||
| 386 | - @click="handleCheckInSelect(checkInType)" | ||
| 387 | - > | ||
| 388 | - <div class="w-12 h-12 rounded-full flex items-center justify-center mb-1 transition-colors | ||
| 389 | - bg-gray-100 text-gray-500" | ||
| 390 | - :class="{ | ||
| 391 | - 'bg-green-500 text-white': selectedCheckIn?.id === checkInType.id | ||
| 392 | - }" | ||
| 393 | - > | ||
| 394 | - <van-icon v-if="checkInType.task_type === 'checkin'" name="edit" size="1.5rem" :color="checkInType.is_gray ? 'gray' : ''" /> | ||
| 395 | - <van-icon v-if="checkInType.task_type === 'upload'" name="tosend" size="1.5rem" :color="checkInType.is_gray ? 'gray' : ''" /> | ||
| 396 | - </div> | ||
| 397 | - <span :class="['text-xs', checkInType.is_gray ? 'text-gray-500' : '']">{{ checkInType.name }}</span> | ||
| 398 | - </button> | ||
| 399 | - </div> | ||
| 400 | - | ||
| 401 | - <div v-if="selectedCheckIn" class="mt-3"> | ||
| 402 | - <button | ||
| 403 | - 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" | ||
| 404 | - @click="handleCheckInSubmit" | ||
| 405 | - :disabled="isCheckingIn" | ||
| 406 | - > | ||
| 407 | - <template v-if="isCheckingIn"> | ||
| 408 | - <div class="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin mr-2"></div> | ||
| 409 | - 提交中... | ||
| 410 | - </template> | ||
| 411 | - <template v-else>提交打卡</template> | ||
| 412 | - </button> | ||
| 413 | - </div> | ||
| 414 | - </template> | ||
| 415 | - </div> | ||
| 416 | - </van-popup> | ||
| 417 | 334 | ||
| 418 | <!-- 下载失败提示弹窗 --> | 335 | <!-- 下载失败提示弹窗 --> |
| 419 | - <van-popup | 336 | + <van-popup v-model:show="showDownloadFailDialog" position="center" round closeable |
| 420 | - v-model:show="showDownloadFailDialog" | 337 | + :style="{ width: '85%', maxWidth: '400px' }"> |
| 421 | - position="center" | ||
| 422 | - round | ||
| 423 | - closeable | ||
| 424 | - :style="{ width: '85%', maxWidth: '400px' }" | ||
| 425 | - > | ||
| 426 | <div class="p-6"> | 338 | <div class="p-6"> |
| 427 | <div class="text-center mb-4"> | 339 | <div class="text-center mb-4"> |
| 428 | <van-icon name="warning-o" size="48" color="#ff6b6b" class="mb-2" /> | 340 | <van-icon name="warning-o" size="48" color="#ff6b6b" class="mb-2" /> |
| ... | @@ -449,20 +361,10 @@ | ... | @@ -449,20 +361,10 @@ |
| 449 | </div> | 361 | </div> |
| 450 | 362 | ||
| 451 | <div class="flex gap-3"> | 363 | <div class="flex gap-3"> |
| 452 | - <van-button | 364 | + <van-button block type="default" @click="showDownloadFailDialog = false" class="flex-1"> |
| 453 | - block | ||
| 454 | - type="default" | ||
| 455 | - @click="showDownloadFailDialog = false" | ||
| 456 | - class="flex-1" | ||
| 457 | - > | ||
| 458 | 关闭 | 365 | 关闭 |
| 459 | </van-button> | 366 | </van-button> |
| 460 | - <van-button | 367 | + <van-button block type="primary" @click="copyToClipboard(downloadFailInfo.fileUrl)" class="flex-1"> |
| 461 | - block | ||
| 462 | - type="primary" | ||
| 463 | - @click="copyToClipboard(downloadFailInfo.fileUrl)" | ||
| 464 | - class="flex-1" | ||
| 465 | - > | ||
| 466 | 复制链接 | 368 | 复制链接 |
| 467 | </van-button> | 369 | </van-button> |
| 468 | </div> | 370 | </div> |
| ... | @@ -470,13 +372,8 @@ | ... | @@ -470,13 +372,8 @@ |
| 470 | </van-popup> | 372 | </van-popup> |
| 471 | 373 | ||
| 472 | <!-- 学习资料全屏弹窗 --> | 374 | <!-- 学习资料全屏弹窗 --> |
| 473 | - <van-popup | 375 | + <van-popup v-model:show="showMaterialsPopup" position="bottom" :style="{ width: '100%', height: '100%' }" |
| 474 | - v-model:show="showMaterialsPopup" | 376 | + :close-on-click-overlay="true" :lock-scroll="true"> |
| 475 | - position="bottom" | ||
| 476 | - :style="{ width: '100%', height: '100%' }" | ||
| 477 | - :close-on-click-overlay="true" | ||
| 478 | - :lock-scroll="true" | ||
| 479 | - > | ||
| 480 | <div class="flex flex-col h-full bg-gray-50"> | 377 | <div class="flex flex-col h-full bg-gray-50"> |
| 481 | <!-- 头部导航栏 --> | 378 | <!-- 头部导航栏 --> |
| 482 | <div class="bg-white shadow-sm border-b border-gray-100"> | 379 | <div class="bg-white shadow-sm border-b border-gray-100"> |
| ... | @@ -502,13 +399,8 @@ | ... | @@ -502,13 +399,8 @@ |
| 502 | <!-- <span class="text-blue-600 text-sm font-medium"> | 399 | <!-- <span class="text-blue-600 text-sm font-medium"> |
| 503 | {{ courseFile?.list ? courseFile.list.length : 0 }} | 400 | {{ courseFile?.list ? courseFile.list.length : 0 }} |
| 504 | </span> --> | 401 | </span> --> |
| 505 | - <van-button | 402 | + <van-button @click="showMaterialsPopup = false" type="default" size="small" round |
| 506 | - @click="showMaterialsPopup = false" | 403 | + class="w-8 h-8 p-0 bg-gray-100 border-0"> |
| 507 | - type="default" | ||
| 508 | - size="small" | ||
| 509 | - round | ||
| 510 | - class="w-8 h-8 p-0 bg-gray-100 border-0" | ||
| 511 | - > | ||
| 512 | <van-icon name="cross" size="16" class="text-gray-600" /> | 404 | <van-icon name="cross" size="16" class="text-gray-600" /> |
| 513 | </van-button> | 405 | </van-button> |
| 514 | </div> | 406 | </div> |
| ... | @@ -518,29 +410,26 @@ | ... | @@ -518,29 +410,26 @@ |
| 518 | <!-- 文件列表 --> | 410 | <!-- 文件列表 --> |
| 519 | <div class="flex-1 overflow-y-auto p-4 pb-safe"> | 411 | <div class="flex-1 overflow-y-auto p-4 pb-safe"> |
| 520 | <div v-if="courseFile?.list && courseFile.list.length > 0" class="space-y-4"> | 412 | <div v-if="courseFile?.list && courseFile.list.length > 0" class="space-y-4"> |
| 521 | - <FrostedGlass | 413 | + <FrostedGlass v-for="(file, index) in courseFile.list" :key="index" :bgOpacity="70" |
| 522 | - v-for="(file, index) in courseFile.list" | ||
| 523 | - :key="index" | ||
| 524 | - :bgOpacity="70" | ||
| 525 | blurLevel="md" | 414 | blurLevel="md" |
| 526 | - className="p-5 hover:bg-white/80 transition-all duration-300 hover:shadow-xl hover:scale-[1.02] transform" | 415 | + className="p-5 hover:bg-white/80 transition-all duration-300 hover:shadow-xl hover:scale-[1.02] transform"> |
| 527 | - > | ||
| 528 | <!-- 文件信息 --> | 416 | <!-- 文件信息 --> |
| 529 | <div class="flex items-start gap-4 mb-4 p-2"> | 417 | <div class="flex items-start gap-4 mb-4 p-2"> |
| 530 | - <div class="w-12 h-12 bg-gradient-to-br from-blue-50 to-indigo-100 rounded-xl flex items-center justify-center flex-shrink-0 shadow-sm"> | 418 | + <div |
| 531 | - <van-icon | 419 | + class="w-12 h-12 bg-gradient-to-br from-blue-50 to-indigo-100 rounded-xl flex items-center justify-center flex-shrink-0 shadow-sm"> |
| 532 | - :name="getFileIcon(file.title || file.name)" | 420 | + <van-icon :name="getFileIcon(file.title || file.name)" class="text-blue-600" |
| 533 | - class="text-blue-600" | 421 | + :size="22" /> |
| 534 | - :size="22" | ||
| 535 | - /> | ||
| 536 | </div> | 422 | </div> |
| 537 | <div class="flex-1 min-w-0"> | 423 | <div class="flex-1 min-w-0"> |
| 538 | - <h3 class="text-base font-semibold text-gray-900 mb-2 line-clamp-2">{{ file.title || file.name }}</h3> | 424 | + <h3 class="text-base font-semibold text-gray-900 mb-2 line-clamp-2">{{ file.title || |
| 425 | + file.name }}</h3> | ||
| 539 | <div class="flex items-center justify-between gap-4 text-sm text-gray-600"> | 426 | <div class="flex items-center justify-between gap-4 text-sm text-gray-600"> |
| 540 | <div class="flex items-center gap-1"> | 427 | <div class="flex items-center gap-1"> |
| 541 | - <van-icon name="label-o" size="12" style="margin-right: 0.25rem;"/> | 428 | + <van-icon name="label-o" size="12" style="margin-right: 0.25rem;" /> |
| 542 | <span>{{ getFileType(file.title || file.name) }}</span> | 429 | <span>{{ getFileType(file.title || file.name) }}</span> |
| 543 | - <span class="ml-2">{{ file.size ? (file.size / 1024 / 1024).toFixed(2) + 'MB' : '' }}</span> | 430 | + <span class="ml-2">{{ file.size ? (file.size / 1024 / 1024).toFixed(2) + |
| 431 | + 'MB' : '' | ||
| 432 | + }}</span> | ||
| 544 | </div> | 433 | </div> |
| 545 | <!-- 复制地址按钮 --> | 434 | <!-- 复制地址按钮 --> |
| 546 | <!-- <button | 435 | <!-- <button |
| ... | @@ -577,8 +466,8 @@ | ... | @@ -577,8 +466,8 @@ |
| 577 | </template> --> | 466 | </template> --> |
| 578 | 467 | ||
| 579 | <!-- 统一使用移动端操作逻辑:根据文件类型显示不同的预览按钮 --> | 468 | <!-- 统一使用移动端操作逻辑:根据文件类型显示不同的预览按钮 --> |
| 580 | - <!-- Office 文档显示预览按钮 --> | 469 | + <!-- Office 文档显示预览按钮 --> |
| 581 | - <!-- <button | 470 | + <!-- <button |
| 582 | v-if="isOfficeFile(file.url)" | 471 | v-if="isOfficeFile(file.url)" |
| 583 | @click="showOfficeDocument(file)" | 472 | @click="showOfficeDocument(file)" |
| 584 | class="btn-primary flex-1 py-2.5 text-sm font-medium flex items-center justify-center gap-2" | 473 | class="btn-primary flex-1 py-2.5 text-sm font-medium flex items-center justify-center gap-2" |
| ... | @@ -586,44 +475,33 @@ | ... | @@ -586,44 +475,33 @@ |
| 586 | <van-icon name="description" size="16" /> | 475 | <van-icon name="description" size="16" /> |
| 587 | 文档预览 | 476 | 文档预览 |
| 588 | </button> --> | 477 | </button> --> |
| 589 | - <!-- PDF文件显示在线查看按钮 --> | 478 | + <!-- PDF文件显示在线查看按钮 --> |
| 590 | - <button | 479 | + <button v-if="file.url && file.url.toLowerCase().includes('.pdf')" |
| 591 | - v-if="file.url && file.url.toLowerCase().includes('.pdf')" | 480 | + @click="showPdf(file)" |
| 592 | - @click="showPdf(file)" | 481 | + class="btn-primary flex-1 py-2.5 text-sm font-medium flex items-center justify-center gap-2"> |
| 593 | - class="btn-primary flex-1 py-2.5 text-sm font-medium flex items-center justify-center gap-2" | 482 | + <van-icon name="eye-o" size="16" /> |
| 594 | - > | 483 | + 在线查看 |
| 595 | - <van-icon name="eye-o" size="16" /> | 484 | + </button> |
| 596 | - 在线查看 | 485 | + <!-- 音频文件显示音频播放按钮 --> |
| 597 | - </button> | 486 | + <button v-else-if="isAudioFile(file.url)" @click="showAudio(file)" |
| 598 | - <!-- 音频文件显示音频播放按钮 --> | 487 | + class="btn-primary flex-1 py-2.5 text-sm font-medium flex items-center justify-center gap-2"> |
| 599 | - <button | 488 | + <van-icon name="music-o" size="16" /> |
| 600 | - v-else-if="isAudioFile(file.url)" | 489 | + 音频播放 |
| 601 | - @click="showAudio(file)" | 490 | + </button> |
| 602 | - class="btn-primary flex-1 py-2.5 text-sm font-medium flex items-center justify-center gap-2" | 491 | + <!-- 视频文件显示视频播放按钮 --> |
| 603 | - > | 492 | + <button v-else-if="isVideoFile(file.url)" @click="showVideo(file)" |
| 604 | - <van-icon name="music-o" size="16" /> | 493 | + class="btn-primary flex-1 py-2.5 text-sm font-medium flex items-center justify-center gap-2"> |
| 605 | - 音频播放 | 494 | + <van-icon name="video-o" size="16" /> |
| 606 | - </button> | 495 | + 视频播放 |
| 607 | - <!-- 视频文件显示视频播放按钮 --> | 496 | + </button> |
| 608 | - <button | 497 | + <!-- 图片文件显示图片预览按钮 --> |
| 609 | - v-else-if="isVideoFile(file.url)" | 498 | + <button v-else-if="isImageFile(file.url)" @click="showImage(file)" |
| 610 | - @click="showVideo(file)" | 499 | + class="btn-primary flex-1 py-2.5 text-sm font-medium flex items-center justify-center gap-2"> |
| 611 | - class="btn-primary flex-1 py-2.5 text-sm font-medium flex items-center justify-center gap-2" | 500 | + <van-icon name="photo-o" size="16" /> |
| 612 | - > | 501 | + 图片预览 |
| 613 | - <van-icon name="video-o" size="16" /> | 502 | + </button> |
| 614 | - 视频播放 | 503 | + <!-- 其他文件显示下载按钮 --> |
| 615 | - </button> | 504 | + <!-- <button |
| 616 | - <!-- 图片文件显示图片预览按钮 --> | ||
| 617 | - <button | ||
| 618 | - v-else-if="isImageFile(file.url)" | ||
| 619 | - @click="showImage(file)" | ||
| 620 | - class="btn-primary flex-1 py-2.5 text-sm font-medium flex items-center justify-center gap-2" | ||
| 621 | - > | ||
| 622 | - <van-icon name="photo-o" size="16" /> | ||
| 623 | - 图片预览 | ||
| 624 | - </button> | ||
| 625 | - <!-- 其他文件显示下载按钮 --> | ||
| 626 | - <!-- <button | ||
| 627 | @click="downloadFile(file)" | 505 | @click="downloadFile(file)" |
| 628 | class="btn-secondary flex-1 py-2.5 text-sm font-medium flex items-center justify-center gap-2" | 506 | class="btn-secondary flex-1 py-2.5 text-sm font-medium flex items-center justify-center gap-2" |
| 629 | > | 507 | > |
| ... | @@ -636,7 +514,8 @@ | ... | @@ -636,7 +514,8 @@ |
| 636 | 514 | ||
| 637 | <!-- 空状态 --> | 515 | <!-- 空状态 --> |
| 638 | <div v-else class="flex flex-col items-center justify-center py-16 px-4"> | 516 | <div v-else class="flex flex-col items-center justify-center py-16 px-4"> |
| 639 | - <div class="w-16 h-16 sm:w-20 sm:h-20 bg-gray-100 rounded-full flex items-center justify-center mb-4"> | 517 | + <div |
| 518 | + class="w-16 h-16 sm:w-20 sm:h-20 bg-gray-100 rounded-full flex items-center justify-center mb-4"> | ||
| 640 | <van-icon name="folder-o" :size="28" class="text-gray-400" /> | 519 | <van-icon name="folder-o" :size="28" class="text-gray-400" /> |
| 641 | </div> | 520 | </div> |
| 642 | <p class="text-gray-500 text-base sm:text-lg mb-2 text-center">暂无学习资料</p> | 521 | <p class="text-gray-500 text-base sm:text-lg mb-2 text-center">暂无学习资料</p> |
| ... | @@ -655,6 +534,7 @@ import { useTitle } from '@vueuse/core'; | ... | @@ -655,6 +534,7 @@ import { useTitle } from '@vueuse/core'; |
| 655 | import VideoPlayer from '@/components/ui/VideoPlayer.vue'; | 534 | import VideoPlayer from '@/components/ui/VideoPlayer.vue'; |
| 656 | import AudioPlayer from '@/components/ui/AudioPlayer.vue'; | 535 | import AudioPlayer from '@/components/ui/AudioPlayer.vue'; |
| 657 | import FrostedGlass from '@/components/ui/FrostedGlass.vue'; | 536 | import FrostedGlass from '@/components/ui/FrostedGlass.vue'; |
| 537 | +import CheckInDialog from '@/components/ui/CheckInDialog.vue'; | ||
| 658 | // import OfficeViewer from '@/components/ui/OfficeViewer.vue'; | 538 | // import OfficeViewer from '@/components/ui/OfficeViewer.vue'; |
| 659 | import dayjs from 'dayjs'; | 539 | import dayjs from 'dayjs'; |
| 660 | import { formatDate, wxInfo } from '@/utils/tools' | 540 | import { formatDate, wxInfo } from '@/utils/tools' |
| ... | @@ -667,7 +547,6 @@ import { showToast } from 'vant'; | ... | @@ -667,7 +547,6 @@ import { showToast } from 'vant'; |
| 667 | // 导入接口 | 547 | // 导入接口 |
| 668 | import { getScheduleCourseAPI, getGroupCommentListAPI, addGroupCommentAPI, addGroupCommentLikeAPI, delGroupCommentLikeAPI, getCourseDetailAPI } from '@/api/course'; | 548 | import { getScheduleCourseAPI, getGroupCommentListAPI, addGroupCommentAPI, addGroupCommentLikeAPI, delGroupCommentLikeAPI, getCourseDetailAPI } from '@/api/course'; |
| 669 | import { addStudyRecordAPI } from "@/api/record"; | 549 | import { addStudyRecordAPI } from "@/api/record"; |
| 670 | -import { checkinTaskAPI } from '@/api/checkin'; | ||
| 671 | 550 | ||
| 672 | const route = useRoute(); | 551 | const route = useRoute(); |
| 673 | const router = useRouter(); | 552 | const router = useRouter(); |
| ... | @@ -763,6 +642,16 @@ const closeImagePreview = () => { | ... | @@ -763,6 +642,16 @@ const closeImagePreview = () => { |
| 763 | showPreview.value = false; | 642 | showPreview.value = false; |
| 764 | }; | 643 | }; |
| 765 | 644 | ||
| 645 | +/** | ||
| 646 | + * @function handleCheckInSuccess | ||
| 647 | + * @description 打卡成功后提示并进行轻反馈 | ||
| 648 | + * @returns {void} | ||
| 649 | + */ | ||
| 650 | +const handleCheckInSuccess = () => { | ||
| 651 | + // 打卡成功轻提示 | ||
| 652 | + showToast('打卡成功'); | ||
| 653 | +}; | ||
| 654 | + | ||
| 766 | // 评论列表分页参数 | 655 | // 评论列表分页参数 |
| 767 | const popupCommentList = ref([]); | 656 | const popupCommentList = ref([]); |
| 768 | const popupLoading = ref(false); | 657 | const popupLoading = ref(false); |
| ... | @@ -824,7 +713,7 @@ const handleLessonClick = async (lesson) => { | ... | @@ -824,7 +713,7 @@ const handleLessonClick = async (lesson) => { |
| 824 | title: item.title || '未命名音频', | 713 | title: item.title || '未命名音频', |
| 825 | artist: '', | 714 | artist: '', |
| 826 | url: item.url, | 715 | url: item.url, |
| 827 | - cover: item.cover || 'https://cdn.ipadbiz.cn/mlaj/images/audio_d_cover.jpg' | 716 | + cover: item.cover || 'https://cdn.ipadbiz.cn/mlaj/images/audio_d_cover.jpg' |
| 828 | })); | 717 | })); |
| 829 | } else { | 718 | } else { |
| 830 | audioList.value = []; | 719 | audioList.value = []; |
| ... | @@ -851,7 +740,7 @@ const handleLessonClick = async (lesson) => { | ... | @@ -851,7 +740,7 @@ const handleLessonClick = async (lesson) => { |
| 851 | 740 | ||
| 852 | // 图片附件或者附件不存在 | 741 | // 图片附件或者附件不存在 |
| 853 | // 进入后直接执行学习时长埋点 | 742 | // 进入后直接执行学习时长埋点 |
| 854 | - if(course.value?.course_type === 'image' || !course.value?.course_type) { | 743 | + if (course.value?.course_type === 'image' || !course.value?.course_type) { |
| 855 | // 新增记录 | 744 | // 新增记录 |
| 856 | let paramsObj = { | 745 | let paramsObj = { |
| 857 | schedule_id: courseId.value, | 746 | schedule_id: courseId.value, |
| ... | @@ -1112,13 +1001,12 @@ onMounted(async () => { | ... | @@ -1112,13 +1001,12 @@ onMounted(async () => { |
| 1112 | }); | 1001 | }); |
| 1113 | } | 1002 | } |
| 1114 | 1003 | ||
| 1115 | - default_list.value = task_list.value; | 1004 | + // 统一弹窗组件后不再维护 default_list 与切换状态 |
| 1116 | - showTaskList.value = true; | ||
| 1117 | } | 1005 | } |
| 1118 | } | 1006 | } |
| 1119 | // 图片附件或者附件不存在 | 1007 | // 图片附件或者附件不存在 |
| 1120 | // 进入后直接执行学习时长埋点 | 1008 | // 进入后直接执行学习时长埋点 |
| 1121 | - if(course.value.course_type === 'image' || !course.value.course_type) { | 1009 | + if (course.value.course_type === 'image' || !course.value.course_type) { |
| 1122 | // 新增记录 | 1010 | // 新增记录 |
| 1123 | let paramsObj = { | 1011 | let paramsObj = { |
| 1124 | schedule_id: courseId.value, | 1012 | schedule_id: courseId.value, |
| ... | @@ -1655,7 +1543,7 @@ const getOfficeFileType = (fileName) => { | ... | @@ -1655,7 +1543,7 @@ const getOfficeFileType = (fileName) => { |
| 1655 | const onAudioPlay = (audio, meta_id) => { | 1543 | const onAudioPlay = (audio, meta_id) => { |
| 1656 | console.log('开始播放音频', audio); | 1544 | console.log('开始播放音频', audio); |
| 1657 | // 学习时长埋点开始 | 1545 | // 学习时长埋点开始 |
| 1658 | - startAction({meta_id}); | 1546 | + startAction({ meta_id }); |
| 1659 | } | 1547 | } |
| 1660 | 1548 | ||
| 1661 | /** | 1549 | /** |
| ... | @@ -1808,90 +1696,19 @@ watch(showCatalog, (newVal) => { | ... | @@ -1808,90 +1696,19 @@ watch(showCatalog, (newVal) => { |
| 1808 | 1696 | ||
| 1809 | // 打卡相关状态 | 1697 | // 打卡相关状态 |
| 1810 | const showCheckInDialog = ref(false); | 1698 | const showCheckInDialog = ref(false); |
| 1811 | -const selectedCheckIn = ref(null); | ||
| 1812 | -const isCheckingIn = ref(false); | ||
| 1813 | -const checkInSuccess = ref(false); | ||
| 1814 | const task_list = ref([]); | 1699 | const task_list = ref([]); |
| 1815 | const timeout_task_list = ref([]); | 1700 | const timeout_task_list = ref([]); |
| 1816 | -const default_list = ref([]); | 1701 | +// 统一弹窗后不再维护默认列表与切换状态 |
| 1817 | -const showTaskList = ref(true); | ||
| 1818 | -const showTimeoutTaskList = ref(false); | ||
| 1819 | 1702 | ||
| 1820 | // 处理打卡选择 | 1703 | // 处理打卡选择 |
| 1821 | -const handleCheckInSelect = (type) => { | ||
| 1822 | - if (type.is_gray && type.task_type === 'checkin') { | ||
| 1823 | - showToast('您已经完成了今天的打卡'); | ||
| 1824 | - return; | ||
| 1825 | - } | ||
| 1826 | - if (type.task_type === 'upload') { | ||
| 1827 | - router.push({ | ||
| 1828 | - path: '/checkin/index', | ||
| 1829 | - query: { | ||
| 1830 | - id: type.id | ||
| 1831 | - } | ||
| 1832 | - }); | ||
| 1833 | - showCheckInDialog.value = false; | ||
| 1834 | - return; | ||
| 1835 | - } else { | ||
| 1836 | - selectedCheckIn.value = type; | ||
| 1837 | - } | ||
| 1838 | -}; | ||
| 1839 | - | ||
| 1840 | -// 处理打卡提交 | ||
| 1841 | -const handleCheckInSubmit = async () => { | ||
| 1842 | - if (!selectedCheckIn.value) { | ||
| 1843 | - showToast('请选择打卡项目'); | ||
| 1844 | - return; | ||
| 1845 | - } | ||
| 1846 | - | ||
| 1847 | - isCheckingIn.value = true; | ||
| 1848 | - try { | ||
| 1849 | - const { code } = await checkinTaskAPI({ task_id: selectedCheckIn.value.id }); | ||
| 1850 | - if (code) { | ||
| 1851 | - checkInSuccess.value = true; | ||
| 1852 | - // 重置表单 | ||
| 1853 | - setTimeout(() => { | ||
| 1854 | - checkInSuccess.value = false; | ||
| 1855 | - selectedCheckIn.value = null; | ||
| 1856 | - showCheckInDialog.value = false; | ||
| 1857 | - }, 1500); | ||
| 1858 | - } | ||
| 1859 | - } catch (error) { | ||
| 1860 | - console.error('打卡失败:', error); | ||
| 1861 | - showToast('打卡失败,请重试'); | ||
| 1862 | - } finally { | ||
| 1863 | - isCheckingIn.value = false; | ||
| 1864 | - } | ||
| 1865 | -}; | ||
| 1866 | - | ||
| 1867 | const goToCheckin = () => { | 1704 | const goToCheckin = () => { |
| 1868 | - if(!default_list.value.length) { | 1705 | + if (!(task_list.value.length || timeout_task_list.value.length)) { |
| 1869 | showToast('暂无打卡任务'); | 1706 | showToast('暂无打卡任务'); |
| 1870 | return; | 1707 | return; |
| 1871 | } | 1708 | } |
| 1872 | showCheckInDialog.value = true; | 1709 | showCheckInDialog.value = true; |
| 1873 | }; | 1710 | }; |
| 1874 | 1711 | ||
| 1875 | -const toggleTask = (type) => { | ||
| 1876 | - if(type === 'today') { | ||
| 1877 | - showTaskList.value = true; | ||
| 1878 | - showTimeoutTaskList.value = false; | ||
| 1879 | - default_list.value = task_list.value; | ||
| 1880 | - } else { | ||
| 1881 | - showTaskList.value = false; | ||
| 1882 | - showTimeoutTaskList.value = true; | ||
| 1883 | - default_list.value = timeout_task_list.value; | ||
| 1884 | - } | ||
| 1885 | -} | ||
| 1886 | - | ||
| 1887 | -const closeCheckInDialog = () => { | ||
| 1888 | - showCheckInDialog.value = false; | ||
| 1889 | - // 切换到今日任务 | ||
| 1890 | - showTaskList.value = true; | ||
| 1891 | - showTimeoutTaskList.value = false; | ||
| 1892 | - default_list.value = task_list.value; | ||
| 1893 | -} | ||
| 1894 | - | ||
| 1895 | /** | 1712 | /** |
| 1896 | * 格式化文件大小 | 1713 | * 格式化文件大小 |
| 1897 | * @param {number} size - 文件大小(字节) | 1714 | * @param {number} size - 文件大小(字节) | ... | ... |
-
Please register or login to post a comment