hookehuyr

refactor(打卡弹窗): 将打卡弹窗统一为通用组件 CheckInDialog

重构 CourseDetailPage、StudyCoursePage 和 StudyDetailPage 中的打卡弹窗逻辑,提取为通用组件 CheckInDialog
组件支持通过 props 传入今日和历史打卡任务列表,并统一处理弹窗显隐和交互逻辑
移除各页面中冗余的打卡相关状态和方法,简化代码结构
...@@ -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>&nbsp;&nbsp;&nbsp;&nbsp;
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>&nbsp;&nbsp;&nbsp;&nbsp;
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>&nbsp;&nbsp;&nbsp;&nbsp;
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 &nbsp; 177 &nbsp;
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>&nbsp;&nbsp;&nbsp;&nbsp;
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 - 文件大小(字节)
......