docs: 为多个组件和工具函数添加详细的JSDoc注释
为AppLayout、CourseCard等组件和useShare、useTracking等工具函数添加了详细的JSDoc注释,包括功能描述、参数说明和返回值类型。这些注释将提升代码可读性和维护性,帮助开发者快速理解组件和函数的用途及使用方法。 新增的注释涵盖了组件props、methods、events等关键部分,并遵循了统一的文档格式标准。对于复杂逻辑的函数,添加了详细的实现说明和使用示例。 这些文档更新不会影响现有功能,但会显著改善开发体验和代码可维护性。
Showing
42 changed files
with
867 additions
and
162 deletions
| 1 | <template> | 1 | <template> |
| 2 | <div class="post-card shadow-md"> | 2 | <div class="post-card shadow-md"> |
| 3 | - <!-- Header --> | 3 | + <!-- 头部信息 --> |
| 4 | <div class="post-header"> | 4 | <div class="post-header"> |
| 5 | <van-row> | 5 | <van-row> |
| 6 | <van-col span="4"> | 6 | <van-col span="4"> |
| ... | @@ -12,7 +12,7 @@ | ... | @@ -12,7 +12,7 @@ |
| 12 | <div class="user-info"> | 12 | <div class="user-info"> |
| 13 | <div class="username"> | 13 | <div class="username"> |
| 14 | {{ post.user.name }} | 14 | {{ post.user.name }} |
| 15 | - <!-- Makeup Tag --> | 15 | + <!-- 补卡标记 --> |
| 16 | <span v-if="post.user.is_makeup" | 16 | <span v-if="post.user.is_makeup" |
| 17 | class="MakeupTag inline-flex items-center ml-2 px-2 py-0.5 rounded-full text-xs font-medium bg-green-50 text-green-600 border border-green-300">补打卡</span> | 17 | class="MakeupTag inline-flex items-center ml-2 px-2 py-0.5 rounded-full text-xs font-medium bg-green-50 text-green-600 border border-green-300">补打卡</span> |
| 18 | <slot name="header-tags"></slot> | 18 | <slot name="header-tags"></slot> |
| ... | @@ -23,7 +23,7 @@ | ... | @@ -23,7 +23,7 @@ |
| 23 | <van-col span="3"> | 23 | <van-col span="3"> |
| 24 | <div v-if="post.is_my" class="post-menu"> | 24 | <div v-if="post.is_my" class="post-menu"> |
| 25 | <slot name="menu"> | 25 | <slot name="menu"> |
| 26 | - <!-- Default menu items if needed, or left empty for parent to fill --> | 26 | + <!-- 默认菜单项,或留空供父组件填充 --> |
| 27 | <van-icon name="edit" @click="emit('edit', post)" class="mr-2" color="#4caf50" /> | 27 | <van-icon name="edit" @click="emit('edit', post)" class="mr-2" color="#4caf50" /> |
| 28 | <van-icon name="delete-o" @click="emit('delete', post)" color="#f44336" /> | 28 | <van-icon name="delete-o" @click="emit('delete', post)" color="#f44336" /> |
| 29 | </slot> | 29 | </slot> |
| ... | @@ -33,15 +33,15 @@ | ... | @@ -33,15 +33,15 @@ |
| 33 | </van-row> | 33 | </van-row> |
| 34 | </div> | 34 | </div> |
| 35 | 35 | ||
| 36 | - <!-- Content --> | 36 | + <!-- 内容区域 --> |
| 37 | <div class="post-content"> | 37 | <div class="post-content"> |
| 38 | <slot name="content-top"></slot> | 38 | <slot name="content-top"></slot> |
| 39 | <PostCountModel :post-data="post" /> | 39 | <PostCountModel :post-data="post" /> |
| 40 | <div class="post-text">{{ post.content }}</div> | 40 | <div class="post-text">{{ post.content }}</div> |
| 41 | 41 | ||
| 42 | - <!-- Media --> | 42 | + <!-- 媒体内容 --> |
| 43 | <div class="post-media"> | 43 | <div class="post-media"> |
| 44 | - <!-- Images --> | 44 | + <!-- 图片列表 --> |
| 45 | <div v-if="post.images.length" class="post-images"> | 45 | <div v-if="post.images.length" class="post-images"> |
| 46 | <div class="post-image-item" v-for="(image, index) in post.images" :key="index"> | 46 | <div class="post-image-item" v-for="(image, index) in post.images" :key="index"> |
| 47 | <van-image width="100%" height="100%" fit="cover" :src="getOptimizedUrl(image)" radius="5" | 47 | <van-image width="100%" height="100%" fit="cover" :src="getOptimizedUrl(image)" radius="5" |
| ... | @@ -51,9 +51,9 @@ | ... | @@ -51,9 +51,9 @@ |
| 51 | <van-image-preview v-if="post.images.length" v-model:show="showLocalImagePreview" :images="post.images" | 51 | <van-image-preview v-if="post.images.length" v-model:show="showLocalImagePreview" :images="post.images" |
| 52 | :start-position="localStartPosition" :show-index="true" /> | 52 | :start-position="localStartPosition" :show-index="true" /> |
| 53 | 53 | ||
| 54 | - <!-- Videos --> | 54 | + <!-- 视频列表 --> |
| 55 | <div v-for="(v, idx) in post.videoList" :key="idx"> | 55 | <div v-for="(v, idx) in post.videoList" :key="idx"> |
| 56 | - <!-- Cover --> | 56 | + <!-- 封面图 --> |
| 57 | <div v-if="v.video && !v.isPlaying" class="relative w-full rounded-lg overflow-hidden" | 57 | <div v-if="v.video && !v.isPlaying" class="relative w-full rounded-lg overflow-hidden" |
| 58 | style="aspect-ratio: 16/9; margin-bottom: 1rem;"> | 58 | style="aspect-ratio: 16/9; margin-bottom: 1rem;"> |
| 59 | <img :src="getOptimizedUrl(v.videoCover || 'https://cdn.ipadbiz.cn/mlaj/images/cover_video_2.png')" | 59 | <img :src="getOptimizedUrl(v.videoCover || 'https://cdn.ipadbiz.cn/mlaj/images/cover_video_2.png')" |
| ... | @@ -66,28 +66,28 @@ | ... | @@ -66,28 +66,28 @@ |
| 66 | </div> | 66 | </div> |
| 67 | </div> | 67 | </div> |
| 68 | </div> | 68 | </div> |
| 69 | - <!-- Video Player --> | 69 | + <!-- 视频播放器 --> |
| 70 | <VideoPlayer v-if="v.video && v.isPlaying" :video-url="v.video" | 70 | <VideoPlayer v-if="v.video && v.isPlaying" :video-url="v.video" |
| 71 | :video-id="v.id || `video-${post.id}-${idx}`" :use-native-on-ios="false" class="post-video rounded-lg overflow-hidden" | 71 | :video-id="v.id || `video-${post.id}-${idx}`" :use-native-on-ios="false" class="post-video rounded-lg overflow-hidden" |
| 72 | :ref="(el) => setVideoRef(el, v.id)" @onPlay="(player) => handleVideoPlay(player, v)" | 72 | :ref="(el) => setVideoRef(el, v.id)" @onPlay="(player) => handleVideoPlay(player, v)" |
| 73 | @onPause="handleVideoPause" /> | 73 | @onPause="handleVideoPause" /> |
| 74 | </div> | 74 | </div> |
| 75 | 75 | ||
| 76 | - <!-- Audio --> | 76 | + <!-- 音频播放器 --> |
| 77 | <AudioPlayer v-if="post.audio && post.audio.length" :songs="post.audio" class="post-audio" :id="post.id" | 77 | <AudioPlayer v-if="post.audio && post.audio.length" :songs="post.audio" class="post-audio" :id="post.id" |
| 78 | :ref="(el) => setAudioRef(el, post.id)" @play="handleAudioPlay" /> | 78 | :ref="(el) => setAudioRef(el, post.id)" @play="handleAudioPlay" /> |
| 79 | </div> | 79 | </div> |
| 80 | </div> | 80 | </div> |
| 81 | 81 | ||
| 82 | - <!-- Footer --> | 82 | + <!-- 底部操作栏 --> |
| 83 | <div class="post-footer flex items-center justify-between"> | 83 | <div class="post-footer flex items-center justify-between"> |
| 84 | - <!-- Left: Like --> | 84 | + <!-- 左侧:点赞 --> |
| 85 | <div class="flex items-center"> | 85 | <div class="flex items-center"> |
| 86 | <van-icon @click="emit('like', post)" name="good-job" class="like-icon" | 86 | <van-icon @click="emit('like', post)" name="good-job" class="like-icon" |
| 87 | :color="post.is_liked ? 'red' : ''" /> | 87 | :color="post.is_liked ? 'red' : ''" /> |
| 88 | <span class="like-count ml-1">{{ post.likes }}</span> | 88 | <span class="like-count ml-1">{{ post.likes }}</span> |
| 89 | </div> | 89 | </div> |
| 90 | - <!-- Right: Custom Actions --> | 90 | + <!-- 右侧:自定义操作 --> |
| 91 | <div class="flex items-center"> | 91 | <div class="flex items-center"> |
| 92 | <slot name="footer-right"></slot> | 92 | <slot name="footer-right"></slot> |
| 93 | </div> | 93 | </div> |
| ... | @@ -102,25 +102,47 @@ import VideoPlayer from "@/components/ui/VideoPlayer.vue"; | ... | @@ -102,25 +102,47 @@ import VideoPlayer from "@/components/ui/VideoPlayer.vue"; |
| 102 | import AudioPlayer from "@/components/ui/AudioPlayer.vue"; | 102 | import AudioPlayer from "@/components/ui/AudioPlayer.vue"; |
| 103 | 103 | ||
| 104 | const props = defineProps({ | 104 | const props = defineProps({ |
| 105 | + /** | ||
| 106 | + * 帖子数据对象 | ||
| 107 | + * @property {number|string} id - 帖子ID | ||
| 108 | + * @property {Object} user - 用户信息 | ||
| 109 | + * @property {string} content - 帖子内容 | ||
| 110 | + * @property {Array} images - 图片列表 | ||
| 111 | + * @property {Array} videoList - 视频列表 | ||
| 112 | + * @property {Array} audio - 音频列表 | ||
| 113 | + * @property {number} likes - 点赞数 | ||
| 114 | + * @property {boolean} is_liked - 是否已点赞 | ||
| 115 | + * @property {boolean} is_my - 是否是自己的帖子 | ||
| 116 | + */ | ||
| 105 | post: { type: Object, required: true }, | 117 | post: { type: Object, required: true }, |
| 118 | + /** 是否使用 CDN 优化链接 */ | ||
| 106 | useCdnOptimization: { type: Boolean, default: false } | 119 | useCdnOptimization: { type: Boolean, default: false } |
| 107 | }) | 120 | }) |
| 108 | 121 | ||
| 109 | const emit = defineEmits(['like', 'edit', 'delete', 'video-play', 'audio-play']) | 122 | const emit = defineEmits(['like', 'edit', 'delete', 'video-play', 'audio-play']) |
| 110 | 123 | ||
| 111 | -// Image Preview State | 124 | +// 图片预览状态 |
| 112 | const showLocalImagePreview = ref(false) | 125 | const showLocalImagePreview = ref(false) |
| 113 | const localStartPosition = ref(0) | 126 | const localStartPosition = ref(0) |
| 114 | 127 | ||
| 128 | +/** | ||
| 129 | + * @description 打开图片预览 | ||
| 130 | + * @param {number} index 图片索引 | ||
| 131 | + */ | ||
| 115 | const openImagePreview = (index) => { | 132 | const openImagePreview = (index) => { |
| 116 | localStartPosition.value = index | 133 | localStartPosition.value = index |
| 117 | showLocalImagePreview.value = true | 134 | showLocalImagePreview.value = true |
| 118 | } | 135 | } |
| 119 | 136 | ||
| 120 | -// Media Refs | 137 | +// 媒体引用 |
| 121 | const videoRefs = ref(new Map()) | 138 | const videoRefs = ref(new Map()) |
| 122 | const audioRefs = ref(new Map()) | 139 | const audioRefs = ref(new Map()) |
| 123 | 140 | ||
| 141 | +/** | ||
| 142 | + * @description 设置视频引用 | ||
| 143 | + * @param {HTMLElement} el 元素 | ||
| 144 | + * @param {string|number} id 视频ID | ||
| 145 | + */ | ||
| 124 | const setVideoRef = (el, id) => { | 146 | const setVideoRef = (el, id) => { |
| 125 | if (el) { | 147 | if (el) { |
| 126 | videoRefs.value.set(id, el) | 148 | videoRefs.value.set(id, el) |
| ... | @@ -128,6 +150,12 @@ const setVideoRef = (el, id) => { | ... | @@ -128,6 +150,12 @@ const setVideoRef = (el, id) => { |
| 128 | videoRefs.value.delete(id) | 150 | videoRefs.value.delete(id) |
| 129 | } | 151 | } |
| 130 | } | 152 | } |
| 153 | + | ||
| 154 | +/** | ||
| 155 | + * @description 设置音频引用 | ||
| 156 | + * @param {HTMLElement} el 元素 | ||
| 157 | + * @param {string|number} id 音频ID | ||
| 158 | + */ | ||
| 131 | const setAudioRef = (el, id) => { | 159 | const setAudioRef = (el, id) => { |
| 132 | if (el) { | 160 | if (el) { |
| 133 | audioRefs.value.set(id, el) | 161 | audioRefs.value.set(id, el) |
| ... | @@ -136,41 +164,65 @@ const setAudioRef = (el, id) => { | ... | @@ -136,41 +164,65 @@ const setAudioRef = (el, id) => { |
| 136 | } | 164 | } |
| 137 | } | 165 | } |
| 138 | 166 | ||
| 139 | -// Optimization | 167 | +// CDN 链接优化 |
| 168 | +/** | ||
| 169 | + * @description 获取优化后的 CDN 链接 | ||
| 170 | + * @param {string} url 原始链接 | ||
| 171 | + * @returns {string} 优化后的链接 | ||
| 172 | + */ | ||
| 140 | const getOptimizedUrl = (url) => { | 173 | const getOptimizedUrl = (url) => { |
| 141 | if (!props.useCdnOptimization) return url | 174 | if (!props.useCdnOptimization) return url |
| 142 | if (url.includes('?')) return url | 175 | if (url.includes('?')) return url |
| 143 | return `${url}?imageMogr2/thumbnail/200x/strip/quality/70` | 176 | return `${url}?imageMogr2/thumbnail/200x/strip/quality/70` |
| 144 | } | 177 | } |
| 145 | 178 | ||
| 146 | -// Video Logic | 179 | +// 视频播放逻辑 |
| 180 | +/** | ||
| 181 | + * @description 开始播放视频 | ||
| 182 | + * @param {Object} v 视频对象 | ||
| 183 | + */ | ||
| 147 | const startPlay = (v) => { | 184 | const startPlay = (v) => { |
| 148 | - // Pause other videos in this card | 185 | + // 暂停当前卡片中的其他视频 |
| 149 | props.post.videoList.forEach(item => { | 186 | props.post.videoList.forEach(item => { |
| 150 | if (item.id !== v.id) item.isPlaying = false | 187 | if (item.id !== v.id) item.isPlaying = false |
| 151 | }) | 188 | }) |
| 152 | v.isPlaying = true | 189 | v.isPlaying = true |
| 153 | } | 190 | } |
| 154 | 191 | ||
| 192 | +/** | ||
| 193 | + * @description 处理视频播放事件 | ||
| 194 | + * @param {Object} player 播放器实例 | ||
| 195 | + * @param {Object} v 视频对象 | ||
| 196 | + */ | ||
| 155 | const handleVideoPlay = (player, v) => { | 197 | const handleVideoPlay = (player, v) => { |
| 156 | - // Stop local audio | 198 | + // 停止本地音频 |
| 157 | stopLocalAudio() | 199 | stopLocalAudio() |
| 158 | - // Emit to parent to stop other cards | 200 | + // 通知父组件停止其他卡片的播放 |
| 159 | emit('video-play', { post: props.post, player, videoId: v.id }) | 201 | emit('video-play', { post: props.post, player, videoId: v.id }) |
| 160 | } | 202 | } |
| 161 | 203 | ||
| 204 | +/** | ||
| 205 | + * @description 处理视频暂停事件 | ||
| 206 | + */ | ||
| 162 | const handleVideoPause = () => { | 207 | const handleVideoPause = () => { |
| 163 | - // do nothing | 208 | + // 暂无操作 |
| 164 | } | 209 | } |
| 165 | 210 | ||
| 166 | -// Audio Logic | 211 | +// 音频播放逻辑 |
| 212 | +/** | ||
| 213 | + * @description 处理音频播放事件 | ||
| 214 | + * @param {Object} player 播放器实例 | ||
| 215 | + */ | ||
| 167 | const handleAudioPlay = (player) => { | 216 | const handleAudioPlay = (player) => { |
| 168 | - // Stop local videos | 217 | + // 停止本地视频 |
| 169 | stopLocalVideos() | 218 | stopLocalVideos() |
| 170 | - // Emit to parent | 219 | + // 通知父组件 |
| 171 | emit('audio-play', { post: props.post, player }) | 220 | emit('audio-play', { post: props.post, player }) |
| 172 | } | 221 | } |
| 173 | 222 | ||
| 223 | +/** | ||
| 224 | + * @description 停止当前卡片内的所有视频 | ||
| 225 | + */ | ||
| 174 | const stopLocalVideos = () => { | 226 | const stopLocalVideos = () => { |
| 175 | videoRefs.value.forEach(player => { | 227 | videoRefs.value.forEach(player => { |
| 176 | if (player && typeof player.pause === 'function') { | 228 | if (player && typeof player.pause === 'function') { |
| ... | @@ -180,25 +232,25 @@ const stopLocalVideos = () => { | ... | @@ -180,25 +232,25 @@ const stopLocalVideos = () => { |
| 180 | props.post.videoList.forEach(v => v.isPlaying = false) | 232 | props.post.videoList.forEach(v => v.isPlaying = false) |
| 181 | } | 233 | } |
| 182 | 234 | ||
| 235 | +/** | ||
| 236 | + * @description 停止当前卡片内的所有音频 | ||
| 237 | + */ | ||
| 183 | const stopLocalAudio = () => { | 238 | const stopLocalAudio = () => { |
| 184 | audioRefs.value.forEach(player => { | 239 | audioRefs.value.forEach(player => { |
| 185 | if (player && typeof player.pause === 'function') { | 240 | if (player && typeof player.pause === 'function') { |
| 186 | player.pause() | 241 | player.pause() |
| 187 | } | 242 | } |
| 188 | }) | 243 | }) |
| 189 | - // Also update isPlaying state if AudioPlayer doesn't handle it fully self-contained | 244 | + // 同时也需要暂停 AudioPlayer 组件实例 |
| 190 | - // But AudioPlayer usually manages its own state or we don't track audio isPlaying on post object for audio lists? | ||
| 191 | - // Looking at IndexCheckInPage: post.audio is array. AudioPlayer takes songs. | ||
| 192 | - // So we just pause the player component. | ||
| 193 | } | 245 | } |
| 194 | 246 | ||
| 195 | -// Expose methods for parent | 247 | +// 暴露方法给父组件 |
| 196 | defineExpose({ | 248 | defineExpose({ |
| 197 | stopAllMedia: () => { | 249 | stopAllMedia: () => { |
| 198 | stopLocalVideos() | 250 | stopLocalVideos() |
| 199 | stopLocalAudio() | 251 | stopLocalAudio() |
| 200 | }, | 252 | }, |
| 201 | - // Also expose post id for convenience | 253 | + // 暴露 post id 以便父组件查找 |
| 202 | id: props.post.id | 254 | id: props.post.id |
| 203 | }) | 255 | }) |
| 204 | </script> | 256 | </script> | ... | ... |
| 1 | <!-- | 1 | <!-- |
| 2 | * @Date: 2025-12-16 11:44:27 | 2 | * @Date: 2025-12-16 11:44:27 |
| 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com | 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com |
| 4 | - * @LastEditTime: 2026-01-21 10:04:55 | 4 | + * @LastEditTime: 2026-01-22 16:45:04 |
| 5 | * @FilePath: /mlaj/src/components/count/CheckinTargetList.vue | 5 | * @FilePath: /mlaj/src/components/count/CheckinTargetList.vue |
| 6 | * @Description: 打卡动态对象列表组件 | 6 | * @Description: 打卡动态对象列表组件 |
| 7 | --> | 7 | --> |
| ... | @@ -101,6 +101,11 @@ const props = defineProps({ | ... | @@ -101,6 +101,11 @@ const props = defineProps({ |
| 101 | }, | 101 | }, |
| 102 | /** | 102 | /** |
| 103 | * 所有可用的对象列表 | 103 | * 所有可用的对象列表 |
| 104 | + * @property {number|string} id - 目标ID | ||
| 105 | + * @property {string} name - 目标名称 | ||
| 106 | + * @property {number} count - 当前计数 | ||
| 107 | + * @property {number} target_count - 目标计数 | ||
| 108 | + * @property {string} unit - 单位 | ||
| 104 | */ | 109 | */ |
| 105 | targetList: { | 110 | targetList: { |
| 106 | type: Array, | 111 | type: Array, |
| ... | @@ -158,7 +163,16 @@ onMounted(() => { | ... | @@ -158,7 +163,16 @@ onMounted(() => { |
| 158 | checkHeight() | 163 | checkHeight() |
| 159 | }) | 164 | }) |
| 160 | 165 | ||
| 161 | -const emit = defineEmits(['add', 'toggle', 'edit', 'delete']) | 166 | +const emit = defineEmits([ |
| 167 | + 'add', | ||
| 168 | + 'toggle', | ||
| 169 | + /** | ||
| 170 | + * 编辑目标 | ||
| 171 | + * @property {Object} item - 目标对象 | ||
| 172 | + */ | ||
| 173 | + 'edit', | ||
| 174 | + 'delete' | ||
| 175 | +]) | ||
| 162 | 176 | ||
| 163 | /** | 177 | /** |
| 164 | * 点击添加按钮 | 178 | * 点击添加按钮 |
| ... | @@ -175,6 +189,7 @@ const currentItem = ref(null) | ... | @@ -175,6 +189,7 @@ const currentItem = ref(null) |
| 175 | 189 | ||
| 176 | const actions = [ | 190 | const actions = [ |
| 177 | { name: '编辑', color: '#1989fa', action: 'edit' }, | 191 | { name: '编辑', color: '#1989fa', action: 'edit' }, |
| 192 | + // 没有加删除功能, 那个接口也是不存在的 | ||
| 178 | ] | 193 | ] |
| 179 | 194 | ||
| 180 | const startLongPress = (item) => { | 195 | const startLongPress = (item) => { | ... | ... |
| ... | @@ -15,42 +15,42 @@ const isWxMobile = isMobile && isWeiXin; | ... | @@ -15,42 +15,42 @@ const isWxMobile = isMobile && isWeiXin; |
| 15 | import { ref, onMounted, onUnmounted, computed } from 'vue'; | 15 | import { ref, onMounted, onUnmounted, computed } from 'vue'; |
| 16 | 16 | ||
| 17 | const props = defineProps({ | 17 | const props = defineProps({ |
| 18 | - // 背景颜色 | 18 | + /** 背景颜色 */ |
| 19 | bgColor: { | 19 | bgColor: { |
| 20 | type: String, | 20 | type: String, |
| 21 | default: 'white' | 21 | default: 'white' |
| 22 | }, | 22 | }, |
| 23 | - // 背景图片 URL | 23 | + /** 背景图片 URL */ |
| 24 | bgImage: { | 24 | bgImage: { |
| 25 | type: String, | 25 | type: String, |
| 26 | default: 'https://cdn.ipadbiz.cn/mlaj/recall/img/bg01@2x.png' | 26 | default: 'https://cdn.ipadbiz.cn/mlaj/recall/img/bg01@2x.png' |
| 27 | }, | 27 | }, |
| 28 | - // 星星数量 | 28 | + /** 星星数量 */ |
| 29 | starCount: { | 29 | starCount: { |
| 30 | type: Number, | 30 | type: Number, |
| 31 | default: isWxMobile ? 500 : 500 | 31 | default: isWxMobile ? 500 : 500 |
| 32 | }, | 32 | }, |
| 33 | - // 星星颜色 (RGBA 前缀,例如 '255,255,255') | 33 | + /** 星星颜色 (RGBA 前缀,例如 '255,255,255') */ |
| 34 | starColor: { | 34 | starColor: { |
| 35 | type: String, | 35 | type: String, |
| 36 | default: '255,255,255' | 36 | default: '255,255,255' |
| 37 | }, | 37 | }, |
| 38 | - // 星星移动速度系数 (值越大移动越快) | 38 | + /** 星星移动速度系数 (值越大移动越快) */ |
| 39 | starSpeed: { | 39 | starSpeed: { |
| 40 | type: Number, | 40 | type: Number, |
| 41 | default: isWxMobile ? 0.3 : 0.1 | 41 | default: isWxMobile ? 0.3 : 0.1 |
| 42 | }, | 42 | }, |
| 43 | - // 流星速度 - 水平分量 (负值向左,正值向右) | 43 | + /** 流星速度 - 水平分量 (负值向左,正值向右) */ |
| 44 | meteorVx: { | 44 | meteorVx: { |
| 45 | type: Number, | 45 | type: Number, |
| 46 | default: isWxMobile ? -10 : -2 | 46 | default: isWxMobile ? -10 : -2 |
| 47 | }, | 47 | }, |
| 48 | - // 流星速度 - 垂直分量 (正值向下) | 48 | + /** 流星速度 - 垂直分量 (正值向下) */ |
| 49 | meteorVy: { | 49 | meteorVy: { |
| 50 | type: Number, | 50 | type: Number, |
| 51 | default: isWxMobile ? 10 : 2 | 51 | default: isWxMobile ? 10 : 2 |
| 52 | }, | 52 | }, |
| 53 | - // 流星拖尾长度系数 (值越大尾巴越长) | 53 | + /** 流星拖尾长度系数 (值越大尾巴越长) */ |
| 54 | meteorTrail: { | 54 | meteorTrail: { |
| 55 | type: Number, | 55 | type: Number, |
| 56 | default: isWxMobile ? 4 : 10 | 56 | default: isWxMobile ? 4 : 10 |
| ... | @@ -80,7 +80,10 @@ let animationFrameId = null; | ... | @@ -80,7 +80,10 @@ let animationFrameId = null; |
| 80 | let meteorTimeoutId = null; | 80 | let meteorTimeoutId = null; |
| 81 | let meteorIndex = -1; // 当前流星的索引 | 81 | let meteorIndex = -1; // 当前流星的索引 |
| 82 | 82 | ||
| 83 | -// 创建星星纹理(放射效果) | 83 | +/** |
| 84 | + * @description 创建星星纹理(放射效果) | ||
| 85 | + * @returns {HTMLCanvasElement} 包含星星纹理的 canvas 元素 | ||
| 86 | + */ | ||
| 84 | const createStarTexture = () => { | 87 | const createStarTexture = () => { |
| 85 | const canvas = document.createElement('canvas'); | 88 | const canvas = document.createElement('canvas'); |
| 86 | canvas.width = 32; | 89 | canvas.width = 32; |
| ... | @@ -115,7 +118,9 @@ const createStarTexture = () => { | ... | @@ -115,7 +118,9 @@ const createStarTexture = () => { |
| 115 | return canvas; | 118 | return canvas; |
| 116 | }; | 119 | }; |
| 117 | 120 | ||
| 118 | -// 初始化星星 | 121 | +/** |
| 122 | + * @description 初始化星星数组 | ||
| 123 | + */ | ||
| 119 | const initStars = () => { | 124 | const initStars = () => { |
| 120 | stars = []; | 125 | stars = []; |
| 121 | starTexture = createStarTexture(); // 生成纹理 | 126 | starTexture = createStarTexture(); // 生成纹理 |
| ... | @@ -135,7 +140,9 @@ const initStars = () => { | ... | @@ -135,7 +140,9 @@ const initStars = () => { |
| 135 | } | 140 | } |
| 136 | }; | 141 | }; |
| 137 | 142 | ||
| 138 | -// 渲染函数 | 143 | +/** |
| 144 | + * @description 渲染动画帧 | ||
| 145 | + */ | ||
| 139 | const render = () => { | 146 | const render = () => { |
| 140 | if (!context) return; | 147 | if (!context) return; |
| 141 | 148 | ||
| ... | @@ -234,7 +241,9 @@ const render = () => { | ... | @@ -234,7 +241,9 @@ const render = () => { |
| 234 | animationFrameId = requestAnimationFrame(render); | 241 | animationFrameId = requestAnimationFrame(render); |
| 235 | }; | 242 | }; |
| 236 | 243 | ||
| 237 | -// 流星触发逻辑 | 244 | +/** |
| 245 | + * @description 触发流星动画 | ||
| 246 | + */ | ||
| 238 | const triggerMeteor = () => { | 247 | const triggerMeteor = () => { |
| 239 | const time = Math.round(Math.random() * 3000 + 33); | 248 | const time = Math.round(Math.random() * 3000 + 33); |
| 240 | meteorTimeoutId = setTimeout(() => { | 249 | meteorTimeoutId = setTimeout(() => { | ... | ... |
| ... | @@ -36,20 +36,22 @@ import { ref, watch, nextTick, onMounted, onUnmounted } from 'vue' | ... | @@ -36,20 +36,22 @@ import { ref, watch, nextTick, onMounted, onUnmounted } from 'vue' |
| 36 | * 组件属性定义 | 36 | * 组件属性定义 |
| 37 | */ | 37 | */ |
| 38 | const props = defineProps({ | 38 | const props = defineProps({ |
| 39 | - // 控制弹窗显示状态 | 39 | + /** 控制弹窗显示状态 */ |
| 40 | show: { | 40 | show: { |
| 41 | type: Boolean, | 41 | type: Boolean, |
| 42 | default: false | 42 | default: false |
| 43 | }, | 43 | }, |
| 44 | - // iframe的src地址 | 44 | + /** iframe的src地址 */ |
| 45 | iframeSrc: { | 45 | iframeSrc: { |
| 46 | type: String, | 46 | type: String, |
| 47 | required: true | 47 | required: true |
| 48 | }, | 48 | }, |
| 49 | + /** 弹窗标题 */ | ||
| 49 | title: { | 50 | title: { |
| 50 | type: String, | 51 | type: String, |
| 51 | default: '个人信息录入' | 52 | default: '个人信息录入' |
| 52 | }, | 53 | }, |
| 54 | + /** 操作类型: add | edit */ | ||
| 53 | type: { | 55 | type: { |
| 54 | type: String, | 56 | type: String, |
| 55 | default: 'add' | 57 | default: 'add' | ... | ... |
| ... | @@ -55,6 +55,9 @@ const props = defineProps({ | ... | @@ -55,6 +55,9 @@ const props = defineProps({ |
| 55 | } | 55 | } |
| 56 | }) | 56 | }) |
| 57 | 57 | ||
| 58 | +/** | ||
| 59 | + * @description 处理返回按钮点击事件 | ||
| 60 | + */ | ||
| 58 | const handleBack = () => { | 61 | const handleBack = () => { |
| 59 | if (props.onBack) { | 62 | if (props.onBack) { |
| 60 | props.onBack() | 63 | props.onBack() | ... | ... |
| ... | @@ -49,12 +49,19 @@ const router = useRouter() | ... | @@ -49,12 +49,19 @@ const router = useRouter() |
| 49 | const { currentUser } = useAuth() | 49 | const { currentUser } = useAuth() |
| 50 | const { getUserInfoFromLocal } = useUserInfo() | 50 | const { getUserInfoFromLocal } = useUserInfo() |
| 51 | 51 | ||
| 52 | +/** | ||
| 53 | + * @description 未读消息数量 | ||
| 54 | + * @type {import('vue').ComputedRef<number>} | ||
| 55 | + */ | ||
| 52 | const unread_msg_count = computed(() => { | 56 | const unread_msg_count = computed(() => { |
| 53 | const userInfo = currentUser.value || getUserInfoFromLocal() | 57 | const userInfo = currentUser.value || getUserInfoFromLocal() |
| 54 | const count = Number(userInfo?.unread_msg_count || 0) | 58 | const count = Number(userInfo?.unread_msg_count || 0) |
| 55 | return Number.isFinite(count) ? count : 0 | 59 | return Number.isFinite(count) ? count : 0 |
| 56 | }) | 60 | }) |
| 57 | 61 | ||
| 62 | +/** | ||
| 63 | + * @description 底部导航配置项 | ||
| 64 | + */ | ||
| 58 | const navItems = [ | 65 | const navItems = [ |
| 59 | { | 66 | { |
| 60 | name: '首页', | 67 | name: '首页', |
| ... | @@ -79,10 +86,19 @@ const navItems = [ | ... | @@ -79,10 +86,19 @@ const navItems = [ |
| 79 | } | 86 | } |
| 80 | ] | 87 | ] |
| 81 | 88 | ||
| 89 | +/** | ||
| 90 | + * @description 判断导航项是否激活 | ||
| 91 | + * @param {string} path 路由路径 | ||
| 92 | + * @returns {boolean} | ||
| 93 | + */ | ||
| 82 | const isActive = (path) => { | 94 | const isActive = (path) => { |
| 83 | return route.path === path || (path !== '/' && route.path.startsWith(path)) | 95 | return route.path === path || (path !== '/' && route.path.startsWith(path)) |
| 84 | } | 96 | } |
| 85 | 97 | ||
| 98 | +/** | ||
| 99 | + * @description 处理导航点击事件 | ||
| 100 | + * @param {Object} item 导航项配置 | ||
| 101 | + */ | ||
| 86 | const handleNavClick = (item) => { | 102 | const handleNavClick = (item) => { |
| 87 | if (item.path === '/activity') { | 103 | if (item.path === '/activity') { |
| 88 | // showToast('功能暂未开放') | 104 | // showToast('功能暂未开放') | ... | ... |
| ... | @@ -102,7 +102,11 @@ const initWxPay = () => { | ... | @@ -102,7 +102,11 @@ const initWxPay = () => { |
| 102 | }) | 102 | }) |
| 103 | } | 103 | } |
| 104 | 104 | ||
| 105 | -// 检查支付状态 | 105 | +/** |
| 106 | + * @description 检查支付状态 | ||
| 107 | + * @param {string} orderId 订单ID | ||
| 108 | + * @returns {Promise<boolean>} 支付是否成功 | ||
| 109 | + */ | ||
| 106 | const checkPaymentStatus = async (orderId) => { | 110 | const checkPaymentStatus = async (orderId) => { |
| 107 | try { | 111 | try { |
| 108 | const payStatus = await wxPayCheckAPI({ order_id: orderId }) | 112 | const payStatus = await wxPayCheckAPI({ order_id: orderId }) |
| ... | @@ -117,7 +121,13 @@ const checkPaymentStatus = async (orderId) => { | ... | @@ -117,7 +121,13 @@ const checkPaymentStatus = async (orderId) => { |
| 117 | } | 121 | } |
| 118 | } | 122 | } |
| 119 | 123 | ||
| 120 | -// 处理支付结果 | 124 | +/** |
| 125 | + * @description 处理支付结果 | ||
| 126 | + * 1. 如果微信返回成功,进入轮询验证流程 | ||
| 127 | + * 2. 轮询 checkPaymentStatus 确认后端状态 | ||
| 128 | + * 3. 如果微信返回取消或失败,直接更新状态 | ||
| 129 | + * @param {Object} res 微信支付回调结果 | ||
| 130 | + */ | ||
| 121 | const handlePaymentResult = async (res) => { | 131 | const handlePaymentResult = async (res) => { |
| 122 | if (res.err_msg === "get_brand_wcpay_request:ok") { | 132 | if (res.err_msg === "get_brand_wcpay_request:ok") { |
| 123 | // 支付成功,验证支付状态 | 133 | // 支付成功,验证支付状态 |
| ... | @@ -149,7 +159,13 @@ const handlePaymentResult = async (res) => { | ... | @@ -149,7 +159,13 @@ const handlePaymentResult = async (res) => { |
| 149 | } | 159 | } |
| 150 | } | 160 | } |
| 151 | 161 | ||
| 152 | -// 处理支付流程 | 162 | +/** |
| 163 | + * @description 处理支付流程 | ||
| 164 | + * 1. 初始化支付状态 | ||
| 165 | + * 2. 调用后端接口获取支付参数 | ||
| 166 | + * 3. 初始化微信JSBridge | ||
| 167 | + * 4. 调用微信支付 | ||
| 168 | + */ | ||
| 153 | const handlePayment = async () => { | 169 | const handlePayment = async () => { |
| 154 | try { | 170 | try { |
| 155 | // 重置支付状态 | 171 | // 重置支付状态 | ... | ... |
| 1 | +<!-- | ||
| 2 | + * @Date: 2025-12-27 23:56:28 | ||
| 3 | + * @LastEditors: hookehuyr hookehuyr@gmail.com | ||
| 4 | + * @LastEditTime: 2026-01-22 15:59:35 | ||
| 5 | + * @FilePath: /mlaj/src/components/studyDetail/StudyCatalogPopup.vue | ||
| 6 | + * @Description: 课程目录弹窗 | ||
| 7 | +--> | ||
| 1 | <template> | 8 | <template> |
| 2 | <van-popup v-model:show="show_catalog_model" position="bottom" round closeable safe-area-inset-bottom | 9 | <van-popup v-model:show="show_catalog_model" position="bottom" round closeable safe-area-inset-bottom |
| 3 | style="height: 80%"> | 10 | style="height: 80%"> |
| ... | @@ -75,9 +82,11 @@ watch(show_catalog_model, (newVal) => { | ... | @@ -75,9 +82,11 @@ watch(show_catalog_model, (newVal) => { |
| 75 | const container = scroll_container_ref.value; | 82 | const container = scroll_container_ref.value; |
| 76 | if (!container) return; | 83 | if (!container) return; |
| 77 | 84 | ||
| 85 | + // 查找当前激活的课程项 | ||
| 78 | const active_item = container.querySelector('.lesson-item.is-active'); | 86 | const active_item = container.querySelector('.lesson-item.is-active'); |
| 79 | if (!active_item) return; | 87 | if (!active_item) return; |
| 80 | 88 | ||
| 89 | + // 计算滚动位置,使选中项居中 | ||
| 81 | const container_height = container.clientHeight; | 90 | const container_height = container.clientHeight; |
| 82 | const item_top = active_item.offsetTop; | 91 | const item_top = active_item.offsetTop; |
| 83 | const item_height = active_item.clientHeight; | 92 | const item_height = active_item.clientHeight; | ... | ... |
| ... | @@ -85,34 +85,42 @@ import { formatDate } from '@/utils/tools' | ... | @@ -85,34 +85,42 @@ import { formatDate } from '@/utils/tools' |
| 85 | import { delGroupCommentAPI } from '@/api/course' | 85 | import { delGroupCommentAPI } from '@/api/course' |
| 86 | 86 | ||
| 87 | const props = defineProps({ | 87 | const props = defineProps({ |
| 88 | + /** 评论总数 */ | ||
| 88 | commentCount: { | 89 | commentCount: { |
| 89 | type: Number, | 90 | type: Number, |
| 90 | default: 0 | 91 | default: 0 |
| 91 | }, | 92 | }, |
| 93 | + /** 评论列表数据 */ | ||
| 92 | commentList: { | 94 | commentList: { |
| 93 | type: Array, | 95 | type: Array, |
| 94 | default: () => [] | 96 | default: () => [] |
| 95 | }, | 97 | }, |
| 98 | + /** 是否显示评论弹窗 (v-model) */ | ||
| 96 | showCommentPopup: { | 99 | showCommentPopup: { |
| 97 | type: Boolean, | 100 | type: Boolean, |
| 98 | default: false | 101 | default: false |
| 99 | }, | 102 | }, |
| 103 | + /** 弹窗内的评论列表数据 */ | ||
| 100 | popupCommentList: { | 104 | popupCommentList: { |
| 101 | type: Array, | 105 | type: Array, |
| 102 | default: () => [] | 106 | default: () => [] |
| 103 | }, | 107 | }, |
| 108 | + /** 弹窗加载状态 (v-model) */ | ||
| 104 | popupLoading: { | 109 | popupLoading: { |
| 105 | type: Boolean, | 110 | type: Boolean, |
| 106 | default: false | 111 | default: false |
| 107 | }, | 112 | }, |
| 113 | + /** 弹窗加载是否完成 */ | ||
| 108 | popupFinished: { | 114 | popupFinished: { |
| 109 | type: Boolean, | 115 | type: Boolean, |
| 110 | default: false | 116 | default: false |
| 111 | }, | 117 | }, |
| 118 | + /** 弹窗评论输入框内容 (v-model) */ | ||
| 112 | popupComment: { | 119 | popupComment: { |
| 113 | type: String, | 120 | type: String, |
| 114 | default: '' | 121 | default: '' |
| 115 | }, | 122 | }, |
| 123 | + /** 底部留白高度,防止被固定元素遮挡 */ | ||
| 116 | bottomWrapperHeight: { | 124 | bottomWrapperHeight: { |
| 117 | type: String, | 125 | type: String, |
| 118 | default: '0px' | 126 | default: '0px' |
| ... | @@ -120,32 +128,45 @@ const props = defineProps({ | ... | @@ -120,32 +128,45 @@ const props = defineProps({ |
| 120 | }); | 128 | }); |
| 121 | 129 | ||
| 122 | const emit = defineEmits([ | 130 | const emit = defineEmits([ |
| 131 | + /** 更新评论弹窗显示状态 */ | ||
| 123 | 'update:showCommentPopup', | 132 | 'update:showCommentPopup', |
| 133 | + /** 更新弹窗加载状态 */ | ||
| 124 | 'update:popupLoading', | 134 | 'update:popupLoading', |
| 135 | + /** 更新弹窗评论内容 */ | ||
| 125 | 'update:popupComment', | 136 | 'update:popupComment', |
| 137 | + /** 切换点赞状态 */ | ||
| 126 | 'toggleLike', | 138 | 'toggleLike', |
| 139 | + /** 弹窗加载更多 */ | ||
| 127 | 'popupLoad', | 140 | 'popupLoad', |
| 141 | + /** 提交弹窗评论 */ | ||
| 128 | 'submitPopupComment', | 142 | 'submitPopupComment', |
| 143 | + /** 评论删除成功 */ | ||
| 129 | 'commentDeleted' | 144 | 'commentDeleted' |
| 130 | ]); | 145 | ]); |
| 131 | 146 | ||
| 147 | +/** @type {import('vue').WritableComputedRef<boolean>} 弹窗显示状态双向绑定 */ | ||
| 132 | const show_comment_popup_model = computed({ | 148 | const show_comment_popup_model = computed({ |
| 133 | get: () => props.showCommentPopup, | 149 | get: () => props.showCommentPopup, |
| 134 | set: (val) => emit('update:showCommentPopup', val) | 150 | set: (val) => emit('update:showCommentPopup', val) |
| 135 | }); | 151 | }); |
| 136 | 152 | ||
| 153 | +/** @type {import('vue').WritableComputedRef<boolean>} 弹窗加载状态双向绑定 */ | ||
| 137 | const popup_loading_model = computed({ | 154 | const popup_loading_model = computed({ |
| 138 | get: () => props.popupLoading, | 155 | get: () => props.popupLoading, |
| 139 | set: (val) => emit('update:popupLoading', val) | 156 | set: (val) => emit('update:popupLoading', val) |
| 140 | }); | 157 | }); |
| 141 | 158 | ||
| 159 | +/** @type {import('vue').WritableComputedRef<string>} 弹窗评论内容双向绑定 */ | ||
| 142 | const popup_comment_model = computed({ | 160 | const popup_comment_model = computed({ |
| 143 | get: () => props.popupComment, | 161 | get: () => props.popupComment, |
| 144 | set: (val) => emit('update:popupComment', val) | 162 | set: (val) => emit('update:popupComment', val) |
| 145 | }); | 163 | }); |
| 146 | 164 | ||
| 165 | +/** @type {import('vue').Ref<boolean>} 是否显示操作面板 */ | ||
| 147 | const show_actions = ref(false) | 166 | const show_actions = ref(false) |
| 167 | +/** @type {import('vue').Ref<Object|null>} 当前操作的评论对象 */ | ||
| 148 | const current_comment = ref(null) | 168 | const current_comment = ref(null) |
| 169 | +/** 操作面板选项 */ | ||
| 149 | const actions = [ | 170 | const actions = [ |
| 150 | { name: '删除', color: '#ef4444' }, | 171 | { name: '删除', color: '#ef4444' }, |
| 151 | ] | 172 | ] |
| ... | @@ -176,6 +197,10 @@ const on_select_action = (action) => { | ... | @@ -176,6 +197,10 @@ const on_select_action = (action) => { |
| 176 | /** | 197 | /** |
| 177 | * @function confirm_delete_comment | 198 | * @function confirm_delete_comment |
| 178 | * @description 二次确认删除评论并调用接口 | 199 | * @description 二次确认删除评论并调用接口 |
| 200 | + * 1. 检查是否有当前选中的评论ID | ||
| 201 | + * 2. 弹出确认对话框 | ||
| 202 | + * 3. 调用 delGroupCommentAPI 接口 | ||
| 203 | + * 4. 成功后提示并触发 commentDeleted 事件 | ||
| 179 | * @returns {Promise<void>} | 204 | * @returns {Promise<void>} |
| 180 | */ | 205 | */ |
| 181 | const confirm_delete_comment = async () => { | 206 | const confirm_delete_comment = async () => { |
| ... | @@ -188,6 +213,7 @@ const confirm_delete_comment = async () => { | ... | @@ -188,6 +213,7 @@ const confirm_delete_comment = async () => { |
| 188 | message: '确定要删除这条评论吗?', | 213 | message: '确定要删除这条评论吗?', |
| 189 | }) | 214 | }) |
| 190 | } catch (e) { | 215 | } catch (e) { |
| 216 | + // 用户取消删除 | ||
| 191 | return | 217 | return |
| 192 | } | 218 | } |
| 193 | 219 | ... | ... |
| ... | @@ -84,10 +84,12 @@ import { computed } from 'vue'; | ... | @@ -84,10 +84,12 @@ import { computed } from 'vue'; |
| 84 | import FrostedGlass from '@/components/ui/FrostedGlass.vue'; | 84 | import FrostedGlass from '@/components/ui/FrostedGlass.vue'; |
| 85 | 85 | ||
| 86 | const props = defineProps({ | 86 | const props = defineProps({ |
| 87 | + /** 是否显示资料弹窗 (v-model) */ | ||
| 87 | showMaterialsPopup: { | 88 | showMaterialsPopup: { |
| 88 | type: Boolean, | 89 | type: Boolean, |
| 89 | default: false | 90 | default: false |
| 90 | }, | 91 | }, |
| 92 | + /** 资料文件列表 */ | ||
| 91 | files: { | 93 | files: { |
| 92 | type: Array, | 94 | type: Array, |
| 93 | default: () => [] | 95 | default: () => [] |
| ... | @@ -95,23 +97,39 @@ const props = defineProps({ | ... | @@ -95,23 +97,39 @@ const props = defineProps({ |
| 95 | }); | 97 | }); |
| 96 | 98 | ||
| 97 | const emit = defineEmits([ | 99 | const emit = defineEmits([ |
| 100 | + /** 更新弹窗显示状态 */ | ||
| 98 | 'update:showMaterialsPopup', | 101 | 'update:showMaterialsPopup', |
| 102 | + /** 打开PDF文件 */ | ||
| 99 | 'openPdf', | 103 | 'openPdf', |
| 104 | + /** 打开音频文件 */ | ||
| 100 | 'openAudio', | 105 | 'openAudio', |
| 106 | + /** 打开视频文件 */ | ||
| 101 | 'openVideo', | 107 | 'openVideo', |
| 108 | + /** 打开图片文件 */ | ||
| 102 | 'openImage' | 109 | 'openImage' |
| 103 | ]); | 110 | ]); |
| 104 | 111 | ||
| 112 | +/** @type {import('vue').WritableComputedRef<boolean>} 弹窗显示状态双向绑定 */ | ||
| 105 | const show_materials_popup_model = computed({ | 113 | const show_materials_popup_model = computed({ |
| 106 | get: () => props.showMaterialsPopup, | 114 | get: () => props.showMaterialsPopup, |
| 107 | set: (val) => emit('update:showMaterialsPopup', val) | 115 | set: (val) => emit('update:showMaterialsPopup', val) |
| 108 | }); | 116 | }); |
| 109 | 117 | ||
| 118 | +/** | ||
| 119 | + * @description 判断是否为PDF文件 | ||
| 120 | + * @param {string} url 文件URL | ||
| 121 | + * @returns {boolean} | ||
| 122 | + */ | ||
| 110 | const isPdfFile = (url) => { | 123 | const isPdfFile = (url) => { |
| 111 | if (!url || typeof url !== 'string') return false; | 124 | if (!url || typeof url !== 'string') return false; |
| 112 | return url.toLowerCase().includes('.pdf'); | 125 | return url.toLowerCase().includes('.pdf'); |
| 113 | } | 126 | } |
| 114 | 127 | ||
| 128 | +/** | ||
| 129 | + * @description 判断是否为音频文件 | ||
| 130 | + * @param {string} fileName 文件名 | ||
| 131 | + * @returns {boolean} | ||
| 132 | + */ | ||
| 115 | const isAudioFile = (fileName) => { | 133 | const isAudioFile = (fileName) => { |
| 116 | if (!fileName || typeof fileName !== 'string') return false; | 134 | if (!fileName || typeof fileName !== 'string') return false; |
| 117 | const extension = fileName.split('.').pop().toLowerCase(); | 135 | const extension = fileName.split('.').pop().toLowerCase(); |
| ... | @@ -119,6 +137,11 @@ const isAudioFile = (fileName) => { | ... | @@ -119,6 +137,11 @@ const isAudioFile = (fileName) => { |
| 119 | return audioTypes.includes(extension); | 137 | return audioTypes.includes(extension); |
| 120 | } | 138 | } |
| 121 | 139 | ||
| 140 | +/** | ||
| 141 | + * @description 判断是否为视频文件 | ||
| 142 | + * @param {string} fileName 文件名 | ||
| 143 | + * @returns {boolean} | ||
| 144 | + */ | ||
| 122 | const isVideoFile = (fileName) => { | 145 | const isVideoFile = (fileName) => { |
| 123 | if (!fileName || typeof fileName !== 'string') return false; | 146 | if (!fileName || typeof fileName !== 'string') return false; |
| 124 | const extension = fileName.split('.').pop().toLowerCase(); | 147 | const extension = fileName.split('.').pop().toLowerCase(); |
| ... | @@ -126,6 +149,11 @@ const isVideoFile = (fileName) => { | ... | @@ -126,6 +149,11 @@ const isVideoFile = (fileName) => { |
| 126 | return videoTypes.includes(extension); | 149 | return videoTypes.includes(extension); |
| 127 | } | 150 | } |
| 128 | 151 | ||
| 152 | +/** | ||
| 153 | + * @description 判断是否为图片文件 | ||
| 154 | + * @param {string} fileName 文件名 | ||
| 155 | + * @returns {boolean} | ||
| 156 | + */ | ||
| 129 | const isImageFile = (fileName) => { | 157 | const isImageFile = (fileName) => { |
| 130 | if (!fileName || typeof fileName !== 'string') return false; | 158 | if (!fileName || typeof fileName !== 'string') return false; |
| 131 | const extension = fileName.split('.').pop().toLowerCase(); | 159 | const extension = fileName.split('.').pop().toLowerCase(); |
| ... | @@ -133,6 +161,11 @@ const isImageFile = (fileName) => { | ... | @@ -133,6 +161,11 @@ const isImageFile = (fileName) => { |
| 133 | return imageTypes.includes(extension); | 161 | return imageTypes.includes(extension); |
| 134 | } | 162 | } |
| 135 | 163 | ||
| 164 | +/** | ||
| 165 | + * @description 根据文件扩展名获取对应图标 | ||
| 166 | + * @param {string} fileName 文件名 | ||
| 167 | + * @returns {string} Vant图标名称 | ||
| 168 | + */ | ||
| 136 | const getFileIcon = (fileName) => { | 169 | const getFileIcon = (fileName) => { |
| 137 | if (!fileName || typeof fileName !== 'string') { | 170 | if (!fileName || typeof fileName !== 'string') { |
| 138 | return 'description'; | 171 | return 'description'; |
| ... | @@ -166,6 +199,11 @@ const getFileIcon = (fileName) => { | ... | @@ -166,6 +199,11 @@ const getFileIcon = (fileName) => { |
| 166 | return iconMap[extension] || 'description'; | 199 | return iconMap[extension] || 'description'; |
| 167 | } | 200 | } |
| 168 | 201 | ||
| 202 | +/** | ||
| 203 | + * @description 获取文件类型描述 | ||
| 204 | + * @param {string} fileName 文件名 | ||
| 205 | + * @returns {string} 文件类型中文描述 | ||
| 206 | + */ | ||
| 169 | const getFileType = (fileName) => { | 207 | const getFileType = (fileName) => { |
| 170 | if (!fileName || typeof fileName !== 'string') { | 208 | if (!fileName || typeof fileName !== 'string') { |
| 171 | return '未知文件'; | 209 | return '未知文件'; | ... | ... |
| ... | @@ -25,13 +25,22 @@ import { ref, watch, computed } from 'vue' | ... | @@ -25,13 +25,22 @@ import { ref, watch, computed } from 'vue' |
| 25 | import { getTeacherTaskListAPI, getTeacherTaskDetailAPI } from '@/api/teacher' | 25 | import { getTeacherTaskListAPI, getTeacherTaskDetailAPI } from '@/api/teacher' |
| 26 | 26 | ||
| 27 | const props = defineProps({ | 27 | const props = defineProps({ |
| 28 | + /** 班级群组ID */ | ||
| 28 | groupId: { | 29 | groupId: { |
| 29 | type: [String, Number], | 30 | type: [String, Number], |
| 30 | default: '' | 31 | default: '' |
| 31 | } | 32 | } |
| 32 | }) | 33 | }) |
| 33 | 34 | ||
| 34 | -const emit = defineEmits(['change']) | 35 | +const emit = defineEmits([ |
| 36 | + /** | ||
| 37 | + * 筛选条件变更事件 | ||
| 38 | + * @property {Object} payload | ||
| 39 | + * @property {string|number} payload.task_id - 作业ID | ||
| 40 | + * @property {string|number} payload.subtask_id - 子作业ID | ||
| 41 | + */ | ||
| 42 | + 'change' | ||
| 43 | +]) | ||
| 35 | 44 | ||
| 36 | const show = ref(false) | 45 | const show = ref(false) |
| 37 | const cascaderValue = ref('') | 46 | const cascaderValue = ref('') |
| ... | @@ -57,6 +66,10 @@ const selectedLabel = computed(() => { | ... | @@ -57,6 +66,10 @@ const selectedLabel = computed(() => { |
| 57 | }) | 66 | }) |
| 58 | 67 | ||
| 59 | // 获取作业列表(第一级) | 68 | // 获取作业列表(第一级) |
| 69 | +/** | ||
| 70 | + * @description 获取作业列表(第一级) | ||
| 71 | + * @returns {Promise<void>} | ||
| 72 | + */ | ||
| 60 | const fetchTaskList = async () => { | 73 | const fetchTaskList = async () => { |
| 61 | if (!props.groupId) { | 74 | if (!props.groupId) { |
| 62 | options.value = [] | 75 | options.value = [] |
| ... | @@ -73,9 +86,7 @@ const fetchTaskList = async () => { | ... | @@ -73,9 +86,7 @@ const fetchTaskList = async () => { |
| 73 | if (res.code === 1) { | 86 | if (res.code === 1) { |
| 74 | // 构造第一级数据 | 87 | // 构造第一级数据 |
| 75 | // 注意:为了让级联选择器知道还有下一级,需要给children一个空数组 | 88 | // 注意:为了让级联选择器知道还有下一级,需要给children一个空数组 |
| 76 | - // 或者我们可以一次性加载?如果不确定数据量,建议动态加载 | 89 | + // 这里采用动态加载策略:给一个占位符,必须给children。 |
| 77 | - // 这里采用动态加载策略:给一个占位符,或者如果Vant Cascader支持,可以不给children但通过change事件动态添加 | ||
| 78 | - // Vant Cascader如果不给children,就认为是叶子节点。所以必须给children。 | ||
| 79 | options.value = (res.data || []).map(item => ({ | 90 | options.value = (res.data || []).map(item => ({ |
| 80 | text: item.title, | 91 | text: item.title, |
| 81 | value: item.id, | 92 | value: item.id, |
| ... | @@ -97,6 +108,11 @@ const fetchTaskList = async () => { | ... | @@ -97,6 +108,11 @@ const fetchTaskList = async () => { |
| 97 | } | 108 | } |
| 98 | 109 | ||
| 99 | // 获取子作业(第二级) | 110 | // 获取子作业(第二级) |
| 111 | +/** | ||
| 112 | + * @description 获取子作业列表(第二级) | ||
| 113 | + * @param {string|number} taskId 作业ID | ||
| 114 | + * @returns {Promise<Array>} 子作业选项列表 | ||
| 115 | + */ | ||
| 100 | const fetchSubtaskList = async (taskId) => { | 116 | const fetchSubtaskList = async (taskId) => { |
| 101 | try { | 117 | try { |
| 102 | const res = await getTeacherTaskDetailAPI({ id: taskId }) | 118 | const res = await getTeacherTaskDetailAPI({ id: taskId }) |
| ... | @@ -126,6 +142,10 @@ watch(() => props.groupId, () => { | ... | @@ -126,6 +142,10 @@ watch(() => props.groupId, () => { |
| 126 | fetchTaskList() | 142 | fetchTaskList() |
| 127 | }, { immediate: true }) | 143 | }, { immediate: true }) |
| 128 | 144 | ||
| 145 | +/** | ||
| 146 | + * @description 处理级联选择器变化 | ||
| 147 | + * @param {Object} params 包含 value, selectedOptions, tabIndex | ||
| 148 | + */ | ||
| 129 | const onChange = async ({ value, selectedOptions, tabIndex }) => { | 149 | const onChange = async ({ value, selectedOptions, tabIndex }) => { |
| 130 | // 当选中第一级(大作业)时 | 150 | // 当选中第一级(大作业)时 |
| 131 | if (tabIndex === 0) { | 151 | if (tabIndex === 0) { | ... | ... |
| ... | @@ -49,13 +49,27 @@ import { getTeacherTaskListAPI, getTeacherTaskDetailAPI } from '@/api/teacher' | ... | @@ -49,13 +49,27 @@ import { getTeacherTaskListAPI, getTeacherTaskDetailAPI } from '@/api/teacher' |
| 49 | import { showToast } from 'vant' | 49 | import { showToast } from 'vant' |
| 50 | 50 | ||
| 51 | const props = defineProps({ | 51 | const props = defineProps({ |
| 52 | + /** 班级群组ID */ | ||
| 52 | groupId: { | 53 | groupId: { |
| 53 | type: [String, Number], | 54 | type: [String, Number], |
| 54 | default: '' | 55 | default: '' |
| 55 | } | 56 | } |
| 56 | }) | 57 | }) |
| 57 | 58 | ||
| 58 | -const emit = defineEmits(['change', 'popup-visible-change']) | 59 | +const emit = defineEmits([ |
| 60 | + /** | ||
| 61 | + * 筛选条件变更事件 | ||
| 62 | + * @property {Object} payload | ||
| 63 | + * @property {string|number} payload.task_id - 作业ID | ||
| 64 | + * @property {string|number} payload.subtask_id - 子作业ID | ||
| 65 | + */ | ||
| 66 | + 'change', | ||
| 67 | + /** | ||
| 68 | + * 弹窗显示状态变更事件 | ||
| 69 | + * @property {boolean} visible - 是否显示 | ||
| 70 | + */ | ||
| 71 | + 'popup-visible-change' | ||
| 72 | +]) | ||
| 59 | 73 | ||
| 60 | // 状态 | 74 | // 状态 |
| 61 | const showTaskPicker = ref(false) | 75 | const showTaskPicker = ref(false) |
| ... | @@ -94,7 +108,13 @@ const selectedSubtaskName = computed(() => { | ... | @@ -94,7 +108,13 @@ const selectedSubtaskName = computed(() => { |
| 94 | return found ? found.title : '' | 108 | return found ? found.title : '' |
| 95 | }) | 109 | }) |
| 96 | 110 | ||
| 97 | -// 获取大作业列表 | 111 | +/** |
| 112 | + * @description 获取大作业列表 | ||
| 113 | + * 1. 检查 groupId 是否存在 | ||
| 114 | + * 2. 调用 getTeacherTaskListAPI 获取列表 | ||
| 115 | + * 3. 更新 taskList 数据 | ||
| 116 | + * @returns {Promise<void>} | ||
| 117 | + */ | ||
| 98 | const fetchTaskList = async () => { | 118 | const fetchTaskList = async () => { |
| 99 | if (!props.groupId) { | 119 | if (!props.groupId) { |
| 100 | taskList.value = [] | 120 | taskList.value = [] |
| ... | @@ -121,7 +141,11 @@ watch(() => props.groupId, (newVal) => { | ... | @@ -121,7 +141,11 @@ watch(() => props.groupId, (newVal) => { |
| 121 | fetchTaskList() | 141 | fetchTaskList() |
| 122 | }, { immediate: true }) | 142 | }, { immediate: true }) |
| 123 | 143 | ||
| 124 | -// 获取大作业详情(含小作业) | 144 | +/** |
| 145 | + * @description 获取作业详情 | ||
| 146 | + * @param {number|string} taskId 作业ID | ||
| 147 | + * @returns {Promise<void>} | ||
| 148 | + */ | ||
| 125 | const fetchTaskDetail = async (taskId) => { | 149 | const fetchTaskDetail = async (taskId) => { |
| 126 | if (!taskId) { | 150 | if (!taskId) { |
| 127 | subtaskList.value = [] | 151 | subtaskList.value = [] |
| ... | @@ -138,7 +162,11 @@ const fetchTaskDetail = async (taskId) => { | ... | @@ -138,7 +162,11 @@ const fetchTaskDetail = async (taskId) => { |
| 138 | } | 162 | } |
| 139 | } | 163 | } |
| 140 | 164 | ||
| 141 | -// 事件处理 | 165 | +/** |
| 166 | + * @description 确认选择作业 | ||
| 167 | + * @param {Object} params | ||
| 168 | + * @param {Array} params.selectedOptions - 选中的选项数组 | ||
| 169 | + */ | ||
| 142 | const onConfirmTask = async ({ selectedOptions }) => { | 170 | const onConfirmTask = async ({ selectedOptions }) => { |
| 143 | const option = selectedOptions[0] | 171 | const option = selectedOptions[0] |
| 144 | if (selectedTaskId.value !== option.value) { | 172 | if (selectedTaskId.value !== option.value) { |
| ... | @@ -156,6 +184,11 @@ const onConfirmTask = async ({ selectedOptions }) => { | ... | @@ -156,6 +184,11 @@ const onConfirmTask = async ({ selectedOptions }) => { |
| 156 | showTaskPicker.value = false | 184 | showTaskPicker.value = false |
| 157 | } | 185 | } |
| 158 | 186 | ||
| 187 | +/** | ||
| 188 | + * @description 确认选择子作业 | ||
| 189 | + * @param {Object} params | ||
| 190 | + * @param {Array} params.selectedOptions - 选中的选项数组 | ||
| 191 | + */ | ||
| 159 | const onConfirmSubtask = ({ selectedOptions }) => { | 192 | const onConfirmSubtask = ({ selectedOptions }) => { |
| 160 | const option = selectedOptions[0] | 193 | const option = selectedOptions[0] |
| 161 | selectedSubtaskId.value = option.value | 194 | selectedSubtaskId.value = option.value |
| ... | @@ -163,6 +196,10 @@ const onConfirmSubtask = ({ selectedOptions }) => { | ... | @@ -163,6 +196,10 @@ const onConfirmSubtask = ({ selectedOptions }) => { |
| 163 | showSubtaskPicker.value = false | 196 | showSubtaskPicker.value = false |
| 164 | } | 197 | } |
| 165 | 198 | ||
| 199 | +/** | ||
| 200 | + * @description 处理子作业点击 | ||
| 201 | + * 如果未选择作业,提示用户 | ||
| 202 | + */ | ||
| 166 | const handleSubtaskClick = () => { | 203 | const handleSubtaskClick = () => { |
| 167 | if (!selectedTaskId.value) { | 204 | if (!selectedTaskId.value) { |
| 168 | showToast('请先选择作业') | 205 | showToast('请先选择作业') |
| ... | @@ -171,6 +208,9 @@ const handleSubtaskClick = () => { | ... | @@ -171,6 +208,9 @@ const handleSubtaskClick = () => { |
| 171 | showSubtaskPicker.value = true | 208 | showSubtaskPicker.value = true |
| 172 | } | 209 | } |
| 173 | 210 | ||
| 211 | +/** | ||
| 212 | + * @description 触发 change 事件 | ||
| 213 | + */ | ||
| 174 | const emitChange = () => { | 214 | const emitChange = () => { |
| 175 | emit('change', { | 215 | emit('change', { |
| 176 | task_id: selectedTaskId.value, | 216 | task_id: selectedTaskId.value, | ... | ... |
| ... | @@ -151,12 +151,17 @@ | ... | @@ -151,12 +151,17 @@ |
| 151 | </van-popup> | 151 | </van-popup> |
| 152 | </template> | 152 | </template> |
| 153 | 153 | ||
| 154 | +<!-- | ||
| 155 | + * @component ActivityApplyHistoryPopup | ||
| 156 | + * @description 活动申请历史记录弹窗,支持增删改查 | ||
| 157 | +--> | ||
| 154 | <script setup> | 158 | <script setup> |
| 155 | import { ref, onMounted } from 'vue' | 159 | import { ref, onMounted } from 'vue' |
| 156 | import { showToast, showConfirmDialog } from 'vant' | 160 | import { showToast, showConfirmDialog } from 'vant' |
| 157 | import { getSupplementListAPI, supplementAddAPI, supplementEditAPI, supplementDelAPI } from '@/api/recall_users' | 161 | import { getSupplementListAPI, supplementAddAPI, supplementEditAPI, supplementDelAPI } from '@/api/recall_users' |
| 158 | 162 | ||
| 159 | const props = defineProps({ | 163 | const props = defineProps({ |
| 164 | + /** 是否显示弹窗 */ | ||
| 160 | show: { | 165 | show: { |
| 161 | type: Boolean, | 166 | type: Boolean, |
| 162 | default: false | 167 | default: false |
| ... | @@ -165,6 +170,7 @@ const props = defineProps({ | ... | @@ -165,6 +170,7 @@ const props = defineProps({ |
| 165 | 170 | ||
| 166 | const emit = defineEmits(['update:show']) | 171 | const emit = defineEmits(['update:show']) |
| 167 | 172 | ||
| 173 | +/** @type {import('vue').Ref<Array>} 申请记录列表 */ | ||
| 168 | const apply_records = ref([]) | 174 | const apply_records = ref([]) |
| 169 | 175 | ||
| 170 | const show_form_popup = ref(false) | 176 | const show_form_popup = ref(false) |
| ... | @@ -177,6 +183,9 @@ const form = ref({ | ... | @@ -177,6 +183,9 @@ const form = ref({ |
| 177 | paid_amount: '' | 183 | paid_amount: '' |
| 178 | }) | 184 | }) |
| 179 | 185 | ||
| 186 | +/** | ||
| 187 | + * @description 重置表单数据 | ||
| 188 | + */ | ||
| 180 | const reset_form = () => { | 189 | const reset_form = () => { |
| 181 | form.value = { | 190 | form.value = { |
| 182 | name: '', | 191 | name: '', |
| ... | @@ -187,6 +196,11 @@ const reset_form = () => { | ... | @@ -187,6 +196,11 @@ const reset_form = () => { |
| 187 | editing_id.value = null | 196 | editing_id.value = null |
| 188 | } | 197 | } |
| 189 | 198 | ||
| 199 | +/** | ||
| 200 | + * @description 获取申请记录列表 | ||
| 201 | + * 调用 getSupplementListAPI 接口并映射数据字段 | ||
| 202 | + * @returns {Promise<void>} | ||
| 203 | + */ | ||
| 190 | const fetch_records = async () => { | 204 | const fetch_records = async () => { |
| 191 | const res = await getSupplementListAPI() | 205 | const res = await getSupplementListAPI() |
| 192 | if (res && res.code) { | 206 | if (res && res.code) { |
| ... | @@ -203,12 +217,19 @@ const fetch_records = async () => { | ... | @@ -203,12 +217,19 @@ const fetch_records = async () => { |
| 203 | } | 217 | } |
| 204 | } | 218 | } |
| 205 | 219 | ||
| 220 | +/** | ||
| 221 | + * @description 打开新增表单 | ||
| 222 | + */ | ||
| 206 | const open_add = () => { | 223 | const open_add = () => { |
| 207 | form_mode.value = 'add' | 224 | form_mode.value = 'add' |
| 208 | reset_form() | 225 | reset_form() |
| 209 | show_form_popup.value = true | 226 | show_form_popup.value = true |
| 210 | } | 227 | } |
| 211 | 228 | ||
| 229 | +/** | ||
| 230 | + * @description 打开编辑表单 | ||
| 231 | + * @param {Object} item 记录对象 | ||
| 232 | + */ | ||
| 212 | const open_edit = (item) => { | 233 | const open_edit = (item) => { |
| 213 | form_mode.value = 'edit' | 234 | form_mode.value = 'edit' |
| 214 | editing_id.value = item.id | 235 | editing_id.value = item.id |
| ... | @@ -221,10 +242,21 @@ const open_edit = (item) => { | ... | @@ -221,10 +242,21 @@ const open_edit = (item) => { |
| 221 | show_form_popup.value = true | 242 | show_form_popup.value = true |
| 222 | } | 243 | } |
| 223 | 244 | ||
| 245 | +/** | ||
| 246 | + * @description 关闭表单弹窗 | ||
| 247 | + */ | ||
| 224 | const close_form_popup = () => { | 248 | const close_form_popup = () => { |
| 225 | show_form_popup.value = false | 249 | show_form_popup.value = false |
| 226 | } | 250 | } |
| 227 | 251 | ||
| 252 | +/** | ||
| 253 | + * @description 提交表单(新增或编辑) | ||
| 254 | + * 1. 验证必填项 | ||
| 255 | + * 2. 构造参数 | ||
| 256 | + * 3. 根据模式调用 add 或 edit 接口 | ||
| 257 | + * 4. 成功后刷新列表 | ||
| 258 | + * @returns {Promise<void>} | ||
| 259 | + */ | ||
| 228 | const submit_form = async () => { | 260 | const submit_form = async () => { |
| 229 | if (!String(form.value.name || '').trim()) { | 261 | if (!String(form.value.name || '').trim()) { |
| 230 | showToast('请输入活动名称') | 262 | showToast('请输入活动名称') |
| ... | @@ -278,6 +310,11 @@ const submit_form = async () => { | ... | @@ -278,6 +310,11 @@ const submit_form = async () => { |
| 278 | } | 310 | } |
| 279 | } | 311 | } |
| 280 | 312 | ||
| 313 | +/** | ||
| 314 | + * @description 确认删除记录 | ||
| 315 | + * @param {Object} item 记录对象 | ||
| 316 | + * @returns {Promise<void>} | ||
| 317 | + */ | ||
| 281 | const confirm_delete = async (item) => { | 318 | const confirm_delete = async (item) => { |
| 282 | try { | 319 | try { |
| 283 | await showConfirmDialog({ | 320 | await showConfirmDialog({ |
| ... | @@ -300,6 +337,11 @@ const confirm_delete = async (item) => { | ... | @@ -300,6 +337,11 @@ const confirm_delete = async (item) => { |
| 300 | } | 337 | } |
| 301 | } | 338 | } |
| 302 | 339 | ||
| 340 | +/** | ||
| 341 | + * @description 格式化实付金额 | ||
| 342 | + * @param {string|number} val 金额 | ||
| 343 | + * @returns {string} 格式化后的金额字符串 | ||
| 344 | + */ | ||
| 303 | const format_paid_amount = (val) => { | 345 | const format_paid_amount = (val) => { |
| 304 | const str = String(val ?? '').trim() | 346 | const str = String(val ?? '').trim() |
| 305 | if (!str) return '¥0.00' | 347 | if (!str) return '¥0.00' | ... | ... |
| 1 | <!-- | 1 | <!-- |
| 2 | * @Date: 2025-03-20 20:36:36 | 2 | * @Date: 2025-03-20 20:36:36 |
| 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com | 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com |
| 4 | - * @LastEditTime: 2025-12-04 10:40:05 | 4 | + * @LastEditTime: 2026-01-22 16:35:54 |
| 5 | * @FilePath: /mlaj/src/components/ui/ActivityCard.vue | 5 | * @FilePath: /mlaj/src/components/ui/ActivityCard.vue |
| 6 | - * @Description: 文件描述 | 6 | + * @Description: 活动卡片组件 |
| 7 | --> | 7 | --> |
| 8 | <template> | 8 | <template> |
| 9 | <div @click="navigateToActivity" class="block mb-4"> | 9 | <div @click="navigateToActivity" class="block mb-4"> |
| ... | @@ -84,11 +84,18 @@ const props = defineProps({ | ... | @@ -84,11 +84,18 @@ const props = defineProps({ |
| 84 | } | 84 | } |
| 85 | }) | 85 | }) |
| 86 | 86 | ||
| 87 | +/** | ||
| 88 | + * @description 跳转到活动详情页 | ||
| 89 | + */ | ||
| 87 | const navigateToActivity = () => { | 90 | const navigateToActivity = () => { |
| 88 | - // window.open(`https://example.com/activities/${props.activity.id}`, '_blank') | ||
| 89 | window.open(`${props.activity.mock_link}`, '_blank') | 91 | window.open(`${props.activity.mock_link}`, '_blank') |
| 90 | } | 92 | } |
| 91 | 93 | ||
| 94 | +/** | ||
| 95 | + * @description 获取活动状态对应的样式类 | ||
| 96 | + * @param {string} status 活动状态 | ||
| 97 | + * @returns {string} 样式类名 | ||
| 98 | + */ | ||
| 92 | const getStatusClass = (status) => { | 99 | const getStatusClass = (status) => { |
| 93 | switch (status) { | 100 | switch (status) { |
| 94 | case '活动中': | 101 | case '活动中': | ... | ... |
| 1 | <!-- | 1 | <!-- |
| 2 | * @Date: 2025-04-07 12:35:35 | 2 | * @Date: 2025-04-07 12:35:35 |
| 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com | 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com |
| 4 | - * @LastEditTime: 2025-12-27 23:56:18 | 4 | + * @LastEditTime: 2026-01-22 16:04:13 |
| 5 | * @FilePath: /mlaj/src/components/ui/AudioPlayer.vue | 5 | * @FilePath: /mlaj/src/components/ui/AudioPlayer.vue |
| 6 | * @Description: 音频播放器组件,支持播放控制、进度条调节、音量控制、播放列表等功能 | 6 | * @Description: 音频播放器组件,支持播放控制、进度条调节、音量控制、播放列表等功能 |
| 7 | --> | 7 | --> |
| ... | @@ -171,7 +171,10 @@ onMounted(() => { | ... | @@ -171,7 +171,10 @@ onMounted(() => { |
| 171 | audio.value?.addEventListener('ended', handleEnded) | 171 | audio.value?.addEventListener('ended', handleEnded) |
| 172 | }) | 172 | }) |
| 173 | 173 | ||
| 174 | -// 核心方法:音频加载 | 174 | +/** |
| 175 | + * @description 加载音频资源 | ||
| 176 | + * @returns {Promise<boolean>} 加载是否成功 | ||
| 177 | + */ | ||
| 175 | const loadAudio = async () => { | 178 | const loadAudio = async () => { |
| 176 | // 如果没有当前歌曲,直接返回 | 179 | // 如果没有当前歌曲,直接返回 |
| 177 | if (!currentSong.value) { | 180 | if (!currentSong.value) { |
| ... | @@ -247,7 +250,9 @@ const checkIfEnded = () => { | ... | @@ -247,7 +250,9 @@ const checkIfEnded = () => { |
| 247 | // 定义组件事件 | 250 | // 定义组件事件 |
| 248 | const emit = defineEmits(['play', 'pause']) | 251 | const emit = defineEmits(['play', 'pause']) |
| 249 | 252 | ||
| 250 | -// 播放控制:切换播放/暂停状态 | 253 | +/** |
| 254 | + * @description 切换播放/暂停状态 | ||
| 255 | + */ | ||
| 251 | const togglePlay = async () => { | 256 | const togglePlay = async () => { |
| 252 | // 如果正在加载中,不执行任何操作 | 257 | // 如果正在加载中,不执行任何操作 |
| 253 | if (isLoading.value) return | 258 | if (isLoading.value) return |
| ... | @@ -304,7 +309,9 @@ const handleEnded = () => { | ... | @@ -304,7 +309,9 @@ const handleEnded = () => { |
| 304 | nextSong() | 309 | nextSong() |
| 305 | } | 310 | } |
| 306 | 311 | ||
| 307 | -// 控制方法:切换到上一首 | 312 | +/** |
| 313 | + * @description 播放上一首 | ||
| 314 | + */ | ||
| 308 | const prevSong = async () => { | 315 | const prevSong = async () => { |
| 309 | try { | 316 | try { |
| 310 | // 如果音频实例存在,先暂停并清除 | 317 | // 如果音频实例存在,先暂停并清除 |
| ... | @@ -344,7 +351,9 @@ const prevSong = async () => { | ... | @@ -344,7 +351,9 @@ const prevSong = async () => { |
| 344 | } | 351 | } |
| 345 | } | 352 | } |
| 346 | 353 | ||
| 347 | -// 控制方法:切换到下一首 | 354 | +/** |
| 355 | + * @description 播放下一首 | ||
| 356 | + */ | ||
| 348 | const nextSong = async () => { | 357 | const nextSong = async () => { |
| 349 | try { | 358 | try { |
| 350 | // 如果音频实例存在,先暂停并清除 | 359 | // 如果音频实例存在,先暂停并清除 |
| ... | @@ -396,7 +405,9 @@ const handleProgressChange = (e) => { | ... | @@ -396,7 +405,9 @@ const handleProgressChange = (e) => { |
| 396 | progress.value = parseFloat(target.value) | 405 | progress.value = parseFloat(target.value) |
| 397 | } | 406 | } |
| 398 | 407 | ||
| 399 | -// 跳转到指定进度 | 408 | +/** |
| 409 | + * @description 调整播放进度 | ||
| 410 | + */ | ||
| 400 | const seekTo = () => { | 411 | const seekTo = () => { |
| 401 | if (!audio.value || !duration.value) return | 412 | if (!audio.value || !duration.value) return |
| 402 | audio.value.currentTime = (progress.value / 100) * duration.value | 413 | audio.value.currentTime = (progress.value / 100) * duration.value | ... | ... |
| ... | @@ -35,9 +35,21 @@ const router = useRouter() | ... | @@ -35,9 +35,21 @@ const router = useRouter() |
| 35 | const props = defineProps({ | 35 | const props = defineProps({ |
| 36 | /** 弹窗显隐 */ | 36 | /** 弹窗显隐 */ |
| 37 | show: { type: Boolean, required: true, default: false }, | 37 | show: { type: Boolean, required: true, default: false }, |
| 38 | - /** 今日打卡任务(外部传入,可选) */ | 38 | + /** |
| 39 | + * 今日打卡任务(外部传入,可选) | ||
| 40 | + * @property {number|string} id - 任务ID | ||
| 41 | + * @property {string} name - 任务名称 | ||
| 42 | + * @property {string} task_type - 任务类型 (checkin/upload) | ||
| 43 | + * @property {boolean} is_gray - 是否置灰(已完成) | ||
| 44 | + */ | ||
| 39 | items_today: { type: Array, default: () => [] }, | 45 | items_today: { type: Array, default: () => [] }, |
| 40 | - /** 历史打卡任务(外部传入,可选) */ | 46 | + /** |
| 47 | + * 历史打卡任务(外部传入,可选) | ||
| 48 | + * @property {number|string} id - 任务ID | ||
| 49 | + * @property {string} name - 任务名称 | ||
| 50 | + * @property {string} task_type - 任务类型 (checkin/upload) | ||
| 51 | + * @property {boolean} is_gray - 是否置灰(已完成) | ||
| 52 | + */ | ||
| 41 | items_history: { type: Array, default: () => [] } | 53 | items_history: { type: Array, default: () => [] } |
| 42 | }) | 54 | }) |
| 43 | 55 | ||
| ... | @@ -99,6 +111,9 @@ const handleListSuccess = async () => { | ... | @@ -99,6 +111,9 @@ const handleListSuccess = async () => { |
| 99 | setTimeout(() => emit('update:show', false), 1500) | 111 | setTimeout(() => emit('update:show', false), 1500) |
| 100 | } | 112 | } |
| 101 | 113 | ||
| 114 | +/** | ||
| 115 | + * @description 关闭弹窗 | ||
| 116 | + */ | ||
| 102 | const handleClose = () => { | 117 | const handleClose = () => { |
| 103 | emit('update:show', false) | 118 | emit('update:show', false) |
| 104 | } | 119 | } | ... | ... |
| ... | @@ -125,18 +125,22 @@ import { normalizeAttachmentTypeConfig } from '@/utils/tools' | ... | @@ -125,18 +125,22 @@ import { normalizeAttachmentTypeConfig } from '@/utils/tools' |
| 125 | 125 | ||
| 126 | // Props定义 | 126 | // Props定义 |
| 127 | const props = defineProps({ | 127 | const props = defineProps({ |
| 128 | + /** 标题 */ | ||
| 128 | title: { | 129 | title: { |
| 129 | type: String, | 130 | type: String, |
| 130 | default: '选择日期' | 131 | default: '选择日期' |
| 131 | }, | 132 | }, |
| 133 | + /** 日历格式化函数 */ | ||
| 132 | formatter: { | 134 | formatter: { |
| 133 | type: Function, | 135 | type: Function, |
| 134 | required: true | 136 | required: true |
| 135 | }, | 137 | }, |
| 138 | + /** 当前选中日期 (v-model) */ | ||
| 136 | modelValue: { | 139 | modelValue: { |
| 137 | type: [String, Date], | 140 | type: [String, Date], |
| 138 | default: null | 141 | default: null |
| 139 | }, | 142 | }, |
| 143 | + /** 子作业列表,用于筛选 */ | ||
| 140 | subtaskList: { | 144 | subtaskList: { |
| 141 | type: Array, | 145 | type: Array, |
| 142 | default: () => [] | 146 | default: () => [] | ... | ... |
| ... | @@ -30,18 +30,25 @@ | ... | @@ -30,18 +30,25 @@ |
| 30 | </Teleport> | 30 | </Teleport> |
| 31 | </template> | 31 | </template> |
| 32 | 32 | ||
| 33 | +<!-- | ||
| 34 | + * @component ConfirmDialog | ||
| 35 | + * @description 确认对话框组件,基于 FrostedGlass | ||
| 36 | +--> | ||
| 33 | <script setup> | 37 | <script setup> |
| 34 | import FrostedGlass from './FrostedGlass.vue' | 38 | import FrostedGlass from './FrostedGlass.vue' |
| 35 | 39 | ||
| 36 | const props = defineProps({ | 40 | const props = defineProps({ |
| 41 | + /** 是否显示弹窗 (v-model) */ | ||
| 37 | show: { | 42 | show: { |
| 38 | type: Boolean, | 43 | type: Boolean, |
| 39 | default: false | 44 | default: false |
| 40 | }, | 45 | }, |
| 46 | + /** 标题 */ | ||
| 41 | title: { | 47 | title: { |
| 42 | type: String, | 48 | type: String, |
| 43 | default: '确认' | 49 | default: '确认' |
| 44 | }, | 50 | }, |
| 51 | + /** 消息内容 */ | ||
| 45 | message: { | 52 | message: { |
| 46 | type: String, | 53 | type: String, |
| 47 | required: true | 54 | required: true |
| ... | @@ -50,11 +57,17 @@ const props = defineProps({ | ... | @@ -50,11 +57,17 @@ const props = defineProps({ |
| 50 | 57 | ||
| 51 | const emit = defineEmits(['confirm', 'cancel', 'update:show']) | 58 | const emit = defineEmits(['confirm', 'cancel', 'update:show']) |
| 52 | 59 | ||
| 60 | +/** | ||
| 61 | + * @description 确认操作 | ||
| 62 | + */ | ||
| 53 | const handleConfirm = () => { | 63 | const handleConfirm = () => { |
| 54 | emit('confirm') | 64 | emit('confirm') |
| 55 | emit('update:show', false) | 65 | emit('update:show', false) |
| 56 | } | 66 | } |
| 57 | 67 | ||
| 68 | +/** | ||
| 69 | + * @description 取消操作 | ||
| 70 | + */ | ||
| 58 | const handleCancel = () => { | 71 | const handleCancel = () => { |
| 59 | emit('cancel') | 72 | emit('cancel') |
| 60 | emit('update:show', false) | 73 | emit('update:show', false) | ... | ... |
| ... | @@ -44,6 +44,11 @@ | ... | @@ -44,6 +44,11 @@ |
| 44 | </template> | 44 | </template> |
| 45 | 45 | ||
| 46 | <script setup> | 46 | <script setup> |
| 47 | +/** | ||
| 48 | + * @description 课程卡片组件 | ||
| 49 | + * @property {Object} course 课程对象 | ||
| 50 | + * @property {string} linkTo 跳转链接(可选) | ||
| 51 | + */ | ||
| 47 | defineProps({ | 52 | defineProps({ |
| 48 | course: { | 53 | course: { |
| 49 | type: Object, | 54 | type: Object, | ... | ... |
| 1 | <!-- | 1 | <!-- |
| 2 | * @Date: 2025-01-21 14:31:21 | 2 | * @Date: 2025-01-21 14:31:21 |
| 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com | 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com |
| 4 | - * @LastEditTime: 2025-10-20 17:56:14 | 4 | + * @LastEditTime: 2026-01-22 16:17:22 |
| 5 | * @FilePath: /mlaj/src/components/ui/CourseImageCard.vue | 5 | * @FilePath: /mlaj/src/components/ui/CourseImageCard.vue |
| 6 | * @Description: 课程图片卡片组件 - 一行显示一张图片,高度自适应 | 6 | * @Description: 课程图片卡片组件 - 一行显示一张图片,高度自适应 |
| 7 | --> | 7 | --> |
| ... | @@ -52,12 +52,22 @@ | ... | @@ -52,12 +52,22 @@ |
| 52 | * 课程图片卡片组件的属性定义 | 52 | * 课程图片卡片组件的属性定义 |
| 53 | */ | 53 | */ |
| 54 | defineProps({ | 54 | defineProps({ |
| 55 | - // 课程数据对象 | 55 | + /** |
| 56 | + * 课程数据对象 | ||
| 57 | + * @property {number|string} id - 课程ID | ||
| 58 | + * @property {string} title - 课程标题 | ||
| 59 | + * @property {string} subtitle - 副标题 | ||
| 60 | + * @property {string} cover - 封面图片URL | ||
| 61 | + * @property {string} price - 价格 | ||
| 62 | + * @property {boolean} is_buy - 是否已购买 | ||
| 63 | + * @property {number} count - 更新期数 | ||
| 64 | + * @property {number} buy_count - 订阅人数 | ||
| 65 | + */ | ||
| 56 | course: { | 66 | course: { |
| 57 | type: Object, | 67 | type: Object, |
| 58 | required: true | 68 | required: true |
| 59 | }, | 69 | }, |
| 60 | - // 自定义链接地址 | 70 | + /** 自定义链接地址 (可选,覆盖默认课程详情链接) */ |
| 61 | linkTo: { | 71 | linkTo: { |
| 62 | type: String, | 72 | type: String, |
| 63 | default: '' | 73 | default: '' | ... | ... |
| ... | @@ -3,7 +3,7 @@ | ... | @@ -3,7 +3,7 @@ |
| 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com | 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com |
| 4 | * @LastEditTime: 2025-03-21 15:37:45 | 4 | * @LastEditTime: 2025-03-21 15:37:45 |
| 5 | * @FilePath: /mlaj/src/components/ui/FrostedGlass.vue | 5 | * @FilePath: /mlaj/src/components/ui/FrostedGlass.vue |
| 6 | - * @Description: 文件描述 | 6 | + * @Description: 毛玻璃效果背景组件 |
| 7 | --> | 7 | --> |
| 8 | <template> | 8 | <template> |
| 9 | <div | 9 | <div |
| ... | @@ -18,15 +18,25 @@ | ... | @@ -18,15 +18,25 @@ |
| 18 | 18 | ||
| 19 | <script setup> | 19 | <script setup> |
| 20 | defineProps({ | 20 | defineProps({ |
| 21 | + /** | ||
| 22 | + * 自定义类名 | ||
| 23 | + */ | ||
| 21 | className: { | 24 | className: { |
| 22 | type: String, | 25 | type: String, |
| 23 | default: '' | 26 | default: '' |
| 24 | }, | 27 | }, |
| 28 | + /** | ||
| 29 | + * 背景透明度 (0-100) | ||
| 30 | + */ | ||
| 25 | bgOpacity: { | 31 | bgOpacity: { |
| 26 | type: Number, | 32 | type: Number, |
| 27 | default: 80, | 33 | default: 80, |
| 28 | validator: (value) => value >= 0 && value <= 100 | 34 | validator: (value) => value >= 0 && value <= 100 |
| 29 | }, | 35 | }, |
| 36 | + /** | ||
| 37 | + * 模糊等级 | ||
| 38 | + * @values 'none', 'sm', 'md', 'lg', 'xl', '2xl', '3xl' | ||
| 39 | + */ | ||
| 30 | blurLevel: { | 40 | blurLevel: { |
| 31 | type: String, | 41 | type: String, |
| 32 | default: 'sm', | 42 | default: 'sm', | ... | ... |
| ... | @@ -24,21 +24,25 @@ | ... | @@ -24,21 +24,25 @@ |
| 24 | 24 | ||
| 25 | <script setup> | 25 | <script setup> |
| 26 | defineProps({ | 26 | defineProps({ |
| 27 | + /** 标题文本 */ | ||
| 27 | title: { | 28 | title: { |
| 28 | type: String, | 29 | type: String, |
| 29 | required: true | 30 | required: true |
| 30 | }, | 31 | }, |
| 32 | + /** 是否显示返回按钮 */ | ||
| 31 | showBackButton: { | 33 | showBackButton: { |
| 32 | type: Boolean, | 34 | type: Boolean, |
| 33 | default: false | 35 | default: false |
| 34 | }, | 36 | }, |
| 37 | + /** 返回按钮点击回调函数 */ | ||
| 35 | onBack: { | 38 | onBack: { |
| 36 | type: Function, | 39 | type: Function, |
| 37 | default: () => {} | 40 | default: () => {} |
| 38 | }, | 41 | }, |
| 42 | + /** 右侧内容配置对象 */ | ||
| 39 | rightContent: { | 43 | rightContent: { |
| 40 | type: Object, | 44 | type: Object, |
| 41 | - default: null | 45 | + default: null, |
| 42 | - } | 46 | + }, |
| 43 | }) | 47 | }) |
| 44 | </script> | 48 | </script> | ... | ... |
| ... | @@ -3,7 +3,7 @@ | ... | @@ -3,7 +3,7 @@ |
| 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com | 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com |
| 4 | * @LastEditTime: 2025-03-21 15:38:03 | 4 | * @LastEditTime: 2025-03-21 15:38:03 |
| 5 | * @FilePath: /mlaj/src/components/ui/LiveStreamCard.vue | 5 | * @FilePath: /mlaj/src/components/ui/LiveStreamCard.vue |
| 6 | - * @Description: 文件描述 | 6 | + * @Description: 直播流展示卡片组件 |
| 7 | --> | 7 | --> |
| 8 | <template> | 8 | <template> |
| 9 | <div class="relative"> | 9 | <div class="relative"> |
| ... | @@ -44,6 +44,7 @@ | ... | @@ -44,6 +44,7 @@ |
| 44 | 44 | ||
| 45 | <script setup> | 45 | <script setup> |
| 46 | defineProps({ | 46 | defineProps({ |
| 47 | + /** 直播流数据对象 */ | ||
| 47 | stream: { | 48 | stream: { |
| 48 | type: Object, | 49 | type: Object, |
| 49 | required: true | 50 | required: true | ... | ... |
| ... | @@ -98,18 +98,22 @@ | ... | @@ -98,18 +98,22 @@ |
| 98 | 98 | ||
| 99 | <script setup> | 99 | <script setup> |
| 100 | defineProps({ | 100 | defineProps({ |
| 101 | + /** 图标名称 (Iconify 或本地图标) */ | ||
| 101 | icon: { | 102 | icon: { |
| 102 | type: String, | 103 | type: String, |
| 103 | required: true | 104 | required: true |
| 104 | }, | 105 | }, |
| 106 | + /** 菜单标题 */ | ||
| 105 | title: { | 107 | title: { |
| 106 | type: String, | 108 | type: String, |
| 107 | required: true | 109 | required: true |
| 108 | }, | 110 | }, |
| 111 | + /** 跳转路径 */ | ||
| 109 | path: { | 112 | path: { |
| 110 | type: String, | 113 | type: String, |
| 111 | required: true | 114 | required: true |
| 112 | }, | 115 | }, |
| 116 | + /** 徽标内容 (可选) */ | ||
| 113 | badge: { | 117 | badge: { |
| 114 | type: String, | 118 | type: String, |
| 115 | default: '' | 119 | default: '' | ... | ... |
| ... | @@ -94,7 +94,7 @@ const error = ref('') | ... | @@ -94,7 +94,7 @@ const error = ref('') |
| 94 | const containerHeight = computed(() => props.height) | 94 | const containerHeight = computed(() => props.height) |
| 95 | 95 | ||
| 96 | /** | 96 | /** |
| 97 | - * 文档渲染完成回调 | 97 | + * @description 文档渲染完成回调 |
| 98 | */ | 98 | */ |
| 99 | const onRendered = () => { | 99 | const onRendered = () => { |
| 100 | console.log('Office document rendered successfully') | 100 | console.log('Office document rendered successfully') |
| ... | @@ -104,7 +104,7 @@ const onRendered = () => { | ... | @@ -104,7 +104,7 @@ const onRendered = () => { |
| 104 | } | 104 | } |
| 105 | 105 | ||
| 106 | /** | 106 | /** |
| 107 | - * 文档渲染错误回调 | 107 | + * @description 文档渲染错误回调 |
| 108 | * @param {Error} err - 错误对象 | 108 | * @param {Error} err - 错误对象 |
| 109 | */ | 109 | */ |
| 110 | const onError = (err) => { | 110 | const onError = (err) => { |
| ... | @@ -115,7 +115,7 @@ const onError = (err) => { | ... | @@ -115,7 +115,7 @@ const onError = (err) => { |
| 115 | } | 115 | } |
| 116 | 116 | ||
| 117 | /** | 117 | /** |
| 118 | - * 重试加载文档 | 118 | + * @description 重试加载文档 |
| 119 | */ | 119 | */ |
| 120 | const retry = () => { | 120 | const retry = () => { |
| 121 | console.log('Retrying to load office document') | 121 | console.log('Retrying to load office document') |
| ... | @@ -125,7 +125,7 @@ const retry = () => { | ... | @@ -125,7 +125,7 @@ const retry = () => { |
| 125 | } | 125 | } |
| 126 | 126 | ||
| 127 | /** | 127 | /** |
| 128 | - * 监听 src 变化,重新加载文档 | 128 | + * @description 监听 src 变化,重新加载文档 |
| 129 | */ | 129 | */ |
| 130 | watch(() => props.src, (newSrc) => { | 130 | watch(() => props.src, (newSrc) => { |
| 131 | console.log('Office document src changed:', newSrc) | 131 | console.log('Office document src changed:', newSrc) |
| ... | @@ -151,13 +151,14 @@ watch(() => props.src, (newSrc) => { | ... | @@ -151,13 +151,14 @@ watch(() => props.src, (newSrc) => { |
| 151 | }, { immediate: true }) | 151 | }, { immediate: true }) |
| 152 | 152 | ||
| 153 | /** | 153 | /** |
| 154 | - * 监听 fileType 变化 | 154 | + * @description 监听 fileType 变化 |
| 155 | */ | 155 | */ |
| 156 | watch(() => props.fileType, (newType) => { | 156 | watch(() => props.fileType, (newType) => { |
| 157 | console.log('Office document fileType changed:', newType) | 157 | console.log('Office document fileType changed:', newType) |
| 158 | }) | 158 | }) |
| 159 | 159 | ||
| 160 | // 响应式数据 | 160 | // 响应式数据 |
| 161 | +/** @type {import('vue').Ref<boolean>} 加载状态 */ | ||
| 161 | const loading = ref(false) | 162 | const loading = ref(false) |
| 162 | 163 | ||
| 163 | // 组件挂载时的初始化 | 164 | // 组件挂载时的初始化 | ... | ... |
| ... | @@ -540,12 +540,13 @@ const handleClose = () => { | ... | @@ -540,12 +540,13 @@ const handleClose = () => { |
| 540 | }; | 540 | }; |
| 541 | 541 | ||
| 542 | /** | 542 | /** |
| 543 | - * 放大PDF | 543 | + * @description 放大PDF |
| 544 | */ | 544 | */ |
| 545 | const zoomIn = () => { | 545 | const zoomIn = () => { |
| 546 | - if (zoomLevel.value < 3) { // 最大放大到300% | 546 | + if (zoomLevel.value < 3) { |
| 547 | const pdfContainer = document.querySelector('.pdf-viewer-container .pdf-content'); | 547 | const pdfContainer = document.querySelector('.pdf-viewer-container .pdf-content'); |
| 548 | if (pdfContainer) { | 548 | if (pdfContainer) { |
| 549 | + // 记录缩放前的滚动位置和视口中心 | ||
| 549 | const oldW = pdfContainer.scrollWidth; | 550 | const oldW = pdfContainer.scrollWidth; |
| 550 | const oldH = pdfContainer.scrollHeight; | 551 | const oldH = pdfContainer.scrollHeight; |
| 551 | const viewportW = pdfContainer.clientWidth; | 552 | const viewportW = pdfContainer.clientWidth; |
| ... | @@ -554,6 +555,7 @@ const zoomIn = () => { | ... | @@ -554,6 +555,7 @@ const zoomIn = () => { |
| 554 | const centerX = pdfContainer.scrollLeft + viewportW / 2; | 555 | const centerX = pdfContainer.scrollLeft + viewportW / 2; |
| 555 | const centerY = pdfContainer.scrollTop + viewportH / 2; | 556 | const centerY = pdfContainer.scrollTop + viewportH / 2; |
| 556 | 557 | ||
| 558 | + // 计算锚点比例 | ||
| 557 | const anchorRatioX = oldW > 0 ? centerX / oldW : 0; | 559 | const anchorRatioX = oldW > 0 ? centerX / oldW : 0; |
| 558 | const anchorRatioY = oldH > 0 ? centerY / oldH : 0; | 560 | const anchorRatioY = oldH > 0 ? centerY / oldH : 0; |
| 559 | 561 | ||
| ... | @@ -563,16 +565,17 @@ const zoomIn = () => { | ... | @@ -563,16 +565,17 @@ const zoomIn = () => { |
| 563 | const newW = pdfContainer.scrollWidth; | 565 | const newW = pdfContainer.scrollWidth; |
| 564 | const newH = pdfContainer.scrollHeight; | 566 | const newH = pdfContainer.scrollHeight; |
| 565 | 567 | ||
| 568 | + // 计算新的中心点位置 | ||
| 566 | let targetCenterX = newW * anchorRatioX; | 569 | let targetCenterX = newW * anchorRatioX; |
| 567 | let targetCenterY = newH * anchorRatioY; | 570 | let targetCenterY = newH * anchorRatioY; |
| 568 | 571 | ||
| 569 | let newScrollLeft = Math.max(0, targetCenterX - viewportW / 2); | 572 | let newScrollLeft = Math.max(0, targetCenterX - viewportW / 2); |
| 570 | let newScrollTop = Math.max(0, targetCenterY - viewportH / 2); | 573 | let newScrollTop = Math.max(0, targetCenterY - viewportH / 2); |
| 571 | 574 | ||
| 575 | + // 边界检查 | ||
| 572 | newScrollLeft = Math.min(newScrollLeft, Math.max(0, newW - viewportW)); | 576 | newScrollLeft = Math.min(newScrollLeft, Math.max(0, newW - viewportW)); |
| 573 | newScrollTop = Math.min(newScrollTop, Math.max(0, newH - viewportH)); | 577 | newScrollTop = Math.min(newScrollTop, Math.max(0, newH - viewportH)); |
| 574 | 578 | ||
| 575 | - // 直接赋值避免平滑滚动导致的偏移抖动 | ||
| 576 | pdfContainer.scrollLeft = newScrollLeft; | 579 | pdfContainer.scrollLeft = newScrollLeft; |
| 577 | pdfContainer.scrollTop = newScrollTop; | 580 | pdfContainer.scrollTop = newScrollTop; |
| 578 | }); | 581 | }); |
| ... | @@ -583,12 +586,13 @@ const zoomIn = () => { | ... | @@ -583,12 +586,13 @@ const zoomIn = () => { |
| 583 | }; | 586 | }; |
| 584 | 587 | ||
| 585 | /** | 588 | /** |
| 586 | - * 缩小PDF | 589 | + * @description 缩小PDF |
| 587 | */ | 590 | */ |
| 588 | const zoomOut = () => { | 591 | const zoomOut = () => { |
| 589 | - if (zoomLevel.value > 0.5) { // 最小缩小到50% | 592 | + if (zoomLevel.value > 0.5) { |
| 590 | const pdfContainer = document.querySelector('.pdf-viewer-container .pdf-content'); | 593 | const pdfContainer = document.querySelector('.pdf-viewer-container .pdf-content'); |
| 591 | if (pdfContainer) { | 594 | if (pdfContainer) { |
| 595 | + // 记录缩放前的滚动位置 | ||
| 592 | const oldW = pdfContainer.scrollWidth; | 596 | const oldW = pdfContainer.scrollWidth; |
| 593 | const oldH = pdfContainer.scrollHeight; | 597 | const oldH = pdfContainer.scrollHeight; |
| 594 | const viewportW = pdfContainer.clientWidth; | 598 | const viewportW = pdfContainer.clientWidth; | ... | ... |
| ... | @@ -13,7 +13,7 @@ | ... | @@ -13,7 +13,7 @@ |
| 13 | <div class="mt-4 text-sm font-medium">海报生成中...</div> | 13 | <div class="mt-4 text-sm font-medium">海报生成中...</div> |
| 14 | </div> | 14 | </div> |
| 15 | 15 | ||
| 16 | - <!-- Canvas (Hidden) --> | 16 | + <!-- Canvas (隐藏) --> |
| 17 | <canvas ref="canvasRef" class="hidden"></canvas> | 17 | <canvas ref="canvasRef" class="hidden"></canvas> |
| 18 | </div> | 18 | </div> |
| 19 | </template> | 19 | </template> |
| ... | @@ -24,18 +24,22 @@ import { showToast } from 'vant' | ... | @@ -24,18 +24,22 @@ import { showToast } from 'vant' |
| 24 | // import request from '@/utils/axios' // 移除 axios 依赖,改用 fetch | 24 | // import request from '@/utils/axios' // 移除 axios 依赖,改用 fetch |
| 25 | 25 | ||
| 26 | const props = defineProps({ | 26 | const props = defineProps({ |
| 27 | + /** 海报背景图片 URL */ | ||
| 27 | bgUrl: { | 28 | bgUrl: { |
| 28 | type: String, | 29 | type: String, |
| 29 | required: true | 30 | required: true |
| 30 | }, | 31 | }, |
| 32 | + /** 海报标题 */ | ||
| 31 | title: { | 33 | title: { |
| 32 | type: String, | 34 | type: String, |
| 33 | default: '' | 35 | default: '' |
| 34 | }, | 36 | }, |
| 37 | + /** Logo 图片 URL */ | ||
| 35 | logoUrl: { | 38 | logoUrl: { |
| 36 | type: String, | 39 | type: String, |
| 37 | default: 'https://cdn.ipadbiz.cn/mlaj/recall/poster/kai@2x.png' | 40 | default: 'https://cdn.ipadbiz.cn/mlaj/recall/poster/kai@2x.png' |
| 38 | }, | 41 | }, |
| 42 | + /** 二维码图片 URL */ | ||
| 39 | qrUrl: { | 43 | qrUrl: { |
| 40 | type: String, | 44 | type: String, |
| 41 | default: 'https://cdn.ipadbiz.cn/mlaj/recall/poster/%E4%BA%8C%E7%BB%B4%E7%A0%81@2x.png' | 45 | default: 'https://cdn.ipadbiz.cn/mlaj/recall/poster/%E4%BA%8C%E7%BB%B4%E7%A0%81@2x.png' |
| ... | @@ -84,6 +88,16 @@ const drawRoundedRect = (ctx, x, y, width, height, radius) => { | ... | @@ -84,6 +88,16 @@ const drawRoundedRect = (ctx, x, y, width, height, radius) => { |
| 84 | } | 88 | } |
| 85 | 89 | ||
| 86 | // 工具函数:绘制多行文本 | 90 | // 工具函数:绘制多行文本 |
| 91 | +/** | ||
| 92 | + * 绘制多行文本 | ||
| 93 | + * @param {CanvasRenderingContext2D} ctx - Canvas上下文 | ||
| 94 | + * @param {string} text - 文本内容 | ||
| 95 | + * @param {number} x - x坐标 | ||
| 96 | + * @param {number} y - y坐标 | ||
| 97 | + * @param {number} maxWidth - 最大宽度 | ||
| 98 | + * @param {number} lineHeight - 行高 | ||
| 99 | + * @param {number} maxLines - 最大行数 | ||
| 100 | + */ | ||
| 87 | const wrapText = (ctx, text, x, y, maxWidth, lineHeight, maxLines) => { | 101 | const wrapText = (ctx, text, x, y, maxWidth, lineHeight, maxLines) => { |
| 88 | const words = text.split('') // 中文按字分割 | 102 | const words = text.split('') // 中文按字分割 |
| 89 | let line = '' | 103 | let line = '' | ... | ... |
| ... | @@ -61,6 +61,7 @@ import resolveConfig from 'tailwindcss/resolveConfig'; | ... | @@ -61,6 +61,7 @@ import resolveConfig from 'tailwindcss/resolveConfig'; |
| 61 | import tailwindConfig from '@root/tailwind.config.js'; | 61 | import tailwindConfig from '@root/tailwind.config.js'; |
| 62 | 62 | ||
| 63 | const props = defineProps({ | 63 | const props = defineProps({ |
| 64 | + /** 夏令营项目列表 */ | ||
| 64 | items: { | 65 | items: { |
| 65 | type: Array, | 66 | type: Array, |
| 66 | default: () => [ | 67 | default: () => [ | ... | ... |
| ... | @@ -57,9 +57,21 @@ import { ref, computed, watch } from 'vue' | ... | @@ -57,9 +57,21 @@ import { ref, computed, watch } from 'vue' |
| 57 | * 组件对外暴露的 v-model 值:选中日期(YYYY-MM-DD) | 57 | * 组件对外暴露的 v-model 值:选中日期(YYYY-MM-DD) |
| 58 | */ | 58 | */ |
| 59 | const props = defineProps({ | 59 | const props = defineProps({ |
| 60 | - modelValue: { type: String, default: '' }, | 60 | + /** 当前选中的日期 (v-model) */ |
| 61 | - // 是否不在初始化时默认选中“今日”,用于弹窗场景 | 61 | + modelValue: { |
| 62 | - noDefaultSelect: { type: Boolean, default: false } | 62 | + type: Date, |
| 63 | + default: () => new Date() | ||
| 64 | + }, | ||
| 65 | + /** | ||
| 66 | + * 任务列表数据 | ||
| 67 | + * @property {string} date - 日期字符串 (YYYY-MM-DD) | ||
| 68 | + * @property {number} count - 任务数量 | ||
| 69 | + * @property {boolean} hasUnread - 是否有未读 | ||
| 70 | + */ | ||
| 71 | + tasks: { | ||
| 72 | + type: Array, | ||
| 73 | + default: () => [] | ||
| 74 | + } | ||
| 63 | }) | 75 | }) |
| 64 | const emit = defineEmits(['update:modelValue', 'select']) | 76 | const emit = defineEmits(['update:modelValue', 'select']) |
| 65 | 77 | ... | ... |
| ... | @@ -68,16 +68,19 @@ | ... | @@ -68,16 +68,19 @@ |
| 68 | 68 | ||
| 69 | <script setup> | 69 | <script setup> |
| 70 | defineProps({ | 70 | defineProps({ |
| 71 | + /** 是否显示弹窗 */ | ||
| 71 | show: { | 72 | show: { |
| 72 | type: Boolean, | 73 | type: Boolean, |
| 73 | required: true, | 74 | required: true, |
| 74 | default: false | 75 | default: false |
| 75 | }, | 76 | }, |
| 77 | + /** 协议类型: 'terms' | 'privacy' */ | ||
| 76 | type: { | 78 | type: { |
| 77 | type: String, | 79 | type: String, |
| 78 | required: true, | 80 | required: true, |
| 79 | validator: (value) => ['terms', 'privacy'].includes(value) | 81 | validator: (value) => ['terms', 'privacy'].includes(value) |
| 80 | }, | 82 | }, |
| 83 | + /** 弹窗标题 */ | ||
| 81 | title: { | 84 | title: { |
| 82 | type: String, | 85 | type: String, |
| 83 | required: true | 86 | required: true | ... | ... |
| ... | @@ -44,13 +44,27 @@ import VideoPlayer from '@/components/ui/VideoPlayer.vue'; | ... | @@ -44,13 +44,27 @@ import VideoPlayer from '@/components/ui/VideoPlayer.vue'; |
| 44 | import { uploadFile, validateFile } from '@/utils/upload'; | 44 | import { uploadFile, validateFile } from '@/utils/upload'; |
| 45 | 45 | ||
| 46 | const props = defineProps({ | 46 | const props = defineProps({ |
| 47 | + /** 是否显示弹窗 (v-model) */ | ||
| 47 | modelValue: { | 48 | modelValue: { |
| 48 | type: Boolean, | 49 | type: Boolean, |
| 49 | required: true | 50 | required: true |
| 50 | } | 51 | } |
| 51 | }); | 52 | }); |
| 52 | 53 | ||
| 53 | -const emit = defineEmits(['update:modelValue', 'submit', 'cancel']); | 54 | +const emit = defineEmits([ |
| 55 | + /** 更新显示状态 */ | ||
| 56 | + 'update:modelValue', | ||
| 57 | + /** | ||
| 58 | + * 提交视频事件 | ||
| 59 | + * @property {Object} payload | ||
| 60 | + * @property {string} payload.url - 视频URL | ||
| 61 | + * @property {string} payload.id - 视频ID/Hash | ||
| 62 | + * @property {string} payload.name - 视频文件名 | ||
| 63 | + */ | ||
| 64 | + 'submit', | ||
| 65 | + /** 取消事件 */ | ||
| 66 | + 'cancel' | ||
| 67 | +]); | ||
| 54 | 68 | ||
| 55 | const show = ref(false); | 69 | const show = ref(false); |
| 56 | 70 | ||
| ... | @@ -75,6 +89,11 @@ const videoName = ref(''); | ... | @@ -75,6 +89,11 @@ const videoName = ref(''); |
| 75 | const uploadProgress = ref(0); | 89 | const uploadProgress = ref(0); |
| 76 | const maxSize = 100 * 1024 * 1024; // 100MB | 90 | const maxSize = 100 * 1024 * 1024; // 100MB |
| 77 | 91 | ||
| 92 | +/** | ||
| 93 | + * @description 文件读取前校验 | ||
| 94 | + * @param {File} file 文件对象 | ||
| 95 | + * @returns {boolean} 是否通过校验 | ||
| 96 | + */ | ||
| 78 | const beforeRead = (file) => { | 97 | const beforeRead = (file) => { |
| 79 | const validation = validateFile(file); | 98 | const validation = validateFile(file); |
| 80 | if (!validation.valid) { | 99 | if (!validation.valid) { |
| ... | @@ -84,6 +103,10 @@ const beforeRead = (file) => { | ... | @@ -84,6 +103,10 @@ const beforeRead = (file) => { |
| 84 | return true; | 103 | return true; |
| 85 | }; | 104 | }; |
| 86 | 105 | ||
| 106 | +/** | ||
| 107 | + * @description 文件读取后上传 | ||
| 108 | + * @param {Object} file 读取的文件对象 | ||
| 109 | + */ | ||
| 87 | const afterRead = async (file) => { | 110 | const afterRead = async (file) => { |
| 88 | // 清除之前的上传结果 | 111 | // 清除之前的上传结果 |
| 89 | videoUrl.value = ''; | 112 | videoUrl.value = ''; |
| ... | @@ -112,12 +135,18 @@ const afterRead = async (file) => { | ... | @@ -112,12 +135,18 @@ const afterRead = async (file) => { |
| 112 | } | 135 | } |
| 113 | }; | 136 | }; |
| 114 | 137 | ||
| 138 | +/** | ||
| 139 | + * @description 文件超过大小限制回调 | ||
| 140 | + */ | ||
| 115 | const onOversize = () => { | 141 | const onOversize = () => { |
| 116 | showToast('文件大小不能超过100MB'); | 142 | showToast('文件大小不能超过100MB'); |
| 117 | }; | 143 | }; |
| 118 | 144 | ||
| 119 | const videoPlayerRef = ref(null); | 145 | const videoPlayerRef = ref(null); |
| 120 | 146 | ||
| 147 | +/** | ||
| 148 | + * @description 提交视频 | ||
| 149 | + */ | ||
| 121 | const onSubmit = () => { | 150 | const onSubmit = () => { |
| 122 | if (!videoUrl.value || !videoId.value) { | 151 | if (!videoUrl.value || !videoId.value) { |
| 123 | showToast('请先上传视频'); | 152 | showToast('请先上传视频'); |
| ... | @@ -132,6 +161,9 @@ const onSubmit = () => { | ... | @@ -132,6 +161,9 @@ const onSubmit = () => { |
| 132 | emit('update:modelValue', false); | 161 | emit('update:modelValue', false); |
| 133 | }; | 162 | }; |
| 134 | 163 | ||
| 164 | +/** | ||
| 165 | + * @description 取消上传 | ||
| 166 | + */ | ||
| 135 | const onCancel = () => { | 167 | const onCancel = () => { |
| 136 | videoPlayerRef.value?.pause(); | 168 | videoPlayerRef.value?.pause(); |
| 137 | emit('cancel'); | 169 | emit('cancel'); | ... | ... |
| ... | @@ -55,10 +55,16 @@ import { ref, defineExpose } from 'vue'; | ... | @@ -55,10 +55,16 @@ import { ref, defineExpose } from 'vue'; |
| 55 | 55 | ||
| 56 | const show = ref(false); | 56 | const show = ref(false); |
| 57 | 57 | ||
| 58 | +/** | ||
| 59 | + * @description 关闭协议弹窗 | ||
| 60 | + */ | ||
| 58 | const handleClose = () => { | 61 | const handleClose = () => { |
| 59 | show.value = false; | 62 | show.value = false; |
| 60 | }; | 63 | }; |
| 61 | 64 | ||
| 65 | +/** | ||
| 66 | + * @description 打开协议弹窗 | ||
| 67 | + */ | ||
| 62 | const openAgreement = () => { | 68 | const openAgreement = () => { |
| 63 | show.value = true; | 69 | show.value = true; |
| 64 | }; | 70 | }; | ... | ... |
| ... | @@ -123,9 +123,28 @@ const { | ... | @@ -123,9 +123,28 @@ const { |
| 123 | } = useVideoPlayer(props, emit, videoRef, nativeVideoRef); | 123 | } = useVideoPlayer(props, emit, videoRef, nativeVideoRef); |
| 124 | 124 | ||
| 125 | // 事件处理 | 125 | // 事件处理 |
| 126 | +/** | ||
| 127 | + * @description 处理播放事件 | ||
| 128 | + * @param {Event|Object} payload - 事件对象或数据 | ||
| 129 | + */ | ||
| 126 | const handlePlay = (payload) => emit("onPlay", payload); | 130 | const handlePlay = (payload) => emit("onPlay", payload); |
| 131 | + | ||
| 132 | +/** | ||
| 133 | + * @description 处理暂停事件 | ||
| 134 | + * @param {Event|Object} payload - 事件对象或数据 | ||
| 135 | + */ | ||
| 127 | const handlePause = (payload) => emit("onPause", payload); | 136 | const handlePause = (payload) => emit("onPause", payload); |
| 137 | + | ||
| 138 | +/** | ||
| 139 | + * @description 处理原生播放事件 | ||
| 140 | + * @param {Event} event - 事件对象 | ||
| 141 | + */ | ||
| 128 | const handleNativePlay = (event) => emit("onPlay", event); | 142 | const handleNativePlay = (event) => emit("onPlay", event); |
| 143 | + | ||
| 144 | +/** | ||
| 145 | + * @description 处理原生暂停事件 | ||
| 146 | + * @param {Event} event - 事件对象 | ||
| 147 | + */ | ||
| 129 | const handleNativePause = (event) => emit("onPause", event); | 148 | const handleNativePause = (event) => emit("onPause", event); |
| 130 | 149 | ||
| 131 | // 暴露方法给父组件 | 150 | // 暴露方法给父组件 | ... | ... |
| ... | @@ -7,31 +7,61 @@ import { qiniuFileHash } from '@/utils/qiniuFileHash'; | ... | @@ -7,31 +7,61 @@ import { qiniuFileHash } from '@/utils/qiniuFileHash'; |
| 7 | import { useAuth } from '@/contexts/auth' | 7 | import { useAuth } from '@/contexts/auth' |
| 8 | 8 | ||
| 9 | /** | 9 | /** |
| 10 | - * 打卡功能的composable | 10 | + * 打卡核心逻辑封装 |
| 11 | - * @returns {Object} 打卡相关的状态和方法 | 11 | + * @module useCheckin |
| 12 | + * @description | ||
| 13 | + * 该 Hook 封装了打卡页面的核心业务逻辑,包括: | ||
| 14 | + * 1. 状态管理:上传状态、文件列表、打卡类型、计数器等。 | ||
| 15 | + * 2. 文件处理:文件选择、校验(类型/大小)、MD5计算、七牛云上传。 | ||
| 16 | + * 3. 提交逻辑:表单校验、数据组装、新增/编辑API调用。 | ||
| 17 | + * 4. 数据回显:编辑模式下的数据初始化与恢复。 | ||
| 18 | + * | ||
| 19 | + * @returns {Object} 包含响应式状态和操作方法的对象 | ||
| 12 | */ | 20 | */ |
| 13 | export function useCheckin() { | 21 | export function useCheckin() { |
| 14 | const route = useRoute() | 22 | const route = useRoute() |
| 15 | const router = useRouter() | 23 | const router = useRouter() |
| 16 | const { currentUser } = useAuth() | 24 | const { currentUser } = useAuth() |
| 17 | 25 | ||
| 18 | - // 基础状态 | 26 | + // ==================== 状态定义 ==================== |
| 27 | + | ||
| 28 | + /** @type {import('vue').Ref<boolean>} 上传中状态 */ | ||
| 19 | const uploading = ref(false) | 29 | const uploading = ref(false) |
| 30 | + /** @type {import('vue').Ref<boolean>} 加载中状态 */ | ||
| 20 | const loading = ref(false) | 31 | const loading = ref(false) |
| 32 | + /** @type {import('vue').Ref<string>} 打卡文本内容 */ | ||
| 21 | const message = ref('') | 33 | const message = ref('') |
| 34 | + /** @type {import('vue').Ref<Array>} 文件列表 */ | ||
| 22 | const fileList = ref([]) | 35 | const fileList = ref([]) |
| 23 | - const activeType = ref('') // 当前选中的打卡类型 | 36 | + /** @type {import('vue').Ref<string>} 当前选中的打卡类型 (text/image/video/audio) */ |
| 24 | - const subTaskId = ref('') // 当前选中的任务ID | 37 | + const activeType = ref('') |
| 25 | - const selectedTaskText = ref('') // 选中的任务文本 | 38 | + /** @type {import('vue').Ref<string>} 当前选中的子任务ID */ |
| 26 | - const selectedTaskValue = ref([]) // 选中的任务值(Picker使用) | 39 | + const subTaskId = ref('') |
| 27 | - const isMakeup = ref(false) // 是否为补录作业 | 40 | + /** @type {import('vue').Ref<string>} 选中的任务文本显示 */ |
| 41 | + const selectedTaskText = ref('') | ||
| 42 | + /** @type {import('vue').Ref<Array>} 选中的任务值(Picker使用) */ | ||
| 43 | + const selectedTaskValue = ref([]) | ||
| 44 | + /** @type {import('vue').Ref<boolean>} 是否为补录作业 */ | ||
| 45 | + const isMakeup = ref(false) | ||
| 46 | + /** @type {import('vue').Ref<number>} 最大上传数量 */ | ||
| 28 | const maxCount = ref(5) | 47 | const maxCount = ref(5) |
| 48 | + | ||
| 49 | + /** | ||
| 50 | + * 各类型文件的最大体积限制 (MB) | ||
| 51 | + * @type {import('vue').Ref<Object>} | ||
| 52 | + */ | ||
| 29 | const maxFileSizeMbMap = ref({ | 53 | const maxFileSizeMbMap = ref({ |
| 30 | image: 20, | 54 | image: 20, |
| 31 | video: 20, | 55 | video: 20, |
| 32 | audio: 20 | 56 | audio: 20 |
| 33 | }) | 57 | }) |
| 34 | 58 | ||
| 59 | + // ==================== 计算属性 ==================== | ||
| 60 | + | ||
| 61 | + /** | ||
| 62 | + * 当前类型文件的最大体积限制 | ||
| 63 | + * @returns {number} 体积限制(MB) | ||
| 64 | + */ | ||
| 35 | const maxFileSizeMb = computed(() => { | 65 | const maxFileSizeMb = computed(() => { |
| 36 | const type = String(activeType.value || '') | 66 | const type = String(activeType.value || '') |
| 37 | const raw = maxFileSizeMbMap.value?.[type] | 67 | const raw = maxFileSizeMbMap.value?.[type] |
| ... | @@ -44,7 +74,6 @@ export function useCheckin() { | ... | @@ -44,7 +74,6 @@ export function useCheckin() { |
| 44 | * 设置最大文件大小映射 | 74 | * 设置最大文件大小映射 |
| 45 | * @param {Object} map - 包含 image, video, audio 键的对象 | 75 | * @param {Object} map - 包含 image, video, audio 键的对象 |
| 46 | */ | 76 | */ |
| 47 | - | ||
| 48 | const setMaxFileSizeMbMap = (map = {}) => { | 77 | const setMaxFileSizeMbMap = (map = {}) => { |
| 49 | if (!map || typeof map !== 'object') return | 78 | if (!map || typeof map !== 'object') return |
| 50 | 79 | ||
| ... | @@ -59,10 +88,16 @@ export function useCheckin() { | ... | @@ -59,10 +88,16 @@ export function useCheckin() { |
| 59 | maxFileSizeMbMap.value = next | 88 | maxFileSizeMbMap.value = next |
| 60 | } | 89 | } |
| 61 | 90 | ||
| 62 | - // 打卡类型 | 91 | + /** |
| 92 | + * 从路由获取打卡类型 | ||
| 93 | + * @returns {string} 打卡类型 | ||
| 94 | + */ | ||
| 63 | const checkinType = computed(() => route.query.task_type) | 95 | const checkinType = computed(() => route.query.task_type) |
| 64 | 96 | ||
| 65 | - // 用于记忆不同类型的文件列表 | 97 | + /** |
| 98 | + * 用于记忆不同类型的文件列表,切换类型时恢复 | ||
| 99 | + * @type {import('vue').Ref<Object>} | ||
| 100 | + */ | ||
| 66 | const fileListMemory = ref({ | 101 | const fileListMemory = ref({ |
| 67 | text: [], | 102 | text: [], |
| 68 | image: [], | 103 | image: [], |
| ... | @@ -72,6 +107,12 @@ export function useCheckin() { | ... | @@ -72,6 +107,12 @@ export function useCheckin() { |
| 72 | 107 | ||
| 73 | /** | 108 | /** |
| 74 | * 是否可以提交 | 109 | * 是否可以提交 |
| 110 | + * @description | ||
| 111 | + * 校验逻辑: | ||
| 112 | + * 1. 计数打卡:直接通过,由组件内部校验 | ||
| 113 | + * 2. 文字打卡:必须有内容且长度 >= 10 | ||
| 114 | + * 3. 多媒体打卡:必须有文件上传 | ||
| 115 | + * @returns {boolean} | ||
| 75 | */ | 116 | */ |
| 76 | const canSubmit = computed(() => { | 117 | const canSubmit = computed(() => { |
| 77 | // 如果是计数打卡,交由组件内部校验 | 118 | // 如果是计数打卡,交由组件内部校验 |
| ... | @@ -88,11 +129,13 @@ export function useCheckin() { | ... | @@ -88,11 +129,13 @@ export function useCheckin() { |
| 88 | } | 129 | } |
| 89 | }) | 130 | }) |
| 90 | 131 | ||
| 132 | + // ==================== 文件处理方法 ==================== | ||
| 133 | + | ||
| 91 | /** | 134 | /** |
| 92 | * 获取文件哈希(与七牛云ETag一致) | 135 | * 获取文件哈希(与七牛云ETag一致) |
| 93 | * @param {File} file 文件对象 | 136 | * @param {File} file 文件对象 |
| 94 | * @returns {Promise<string>} 哈希字符串 | 137 | * @returns {Promise<string>} 哈希字符串 |
| 95 | - * 注释:使用 qiniuFileHash 进行计算,替代浏览器MD5方案。 | 138 | + * @description 使用 qiniuFileHash 进行计算,替代浏览器MD5方案,确保与七牛云存储一致性。 |
| 96 | */ | 139 | */ |
| 97 | const getFileMD5 = async (file) => { | 140 | const getFileMD5 = async (file) => { |
| 98 | return await qiniuFileHash(file) | 141 | return await qiniuFileHash(file) |
| ... | @@ -104,6 +147,11 @@ export function useCheckin() { | ... | @@ -104,6 +147,11 @@ export function useCheckin() { |
| 104 | * @param {string} token - 七牛云token | 147 | * @param {string} token - 七牛云token |
| 105 | * @param {string} fileName - 文件名 | 148 | * @param {string} fileName - 文件名 |
| 106 | * @returns {Promise<Object>} 上传结果 | 149 | * @returns {Promise<Object>} 上传结果 |
| 150 | + * @description | ||
| 151 | + * 数据流: | ||
| 152 | + * 1. 构建 FormData,包含 file, token, key | ||
| 153 | + * 2. 根据协议(http/https)选择上传域名 | ||
| 154 | + * 3. 调用 qiniuUploadAPI 执行上传 | ||
| 107 | */ | 155 | */ |
| 108 | const uploadToQiniu = async (file, token, fileName) => { | 156 | const uploadToQiniu = async (file, token, fileName) => { |
| 109 | const formData = new FormData() | 157 | const formData = new FormData() |
| ... | @@ -124,9 +172,16 @@ export function useCheckin() { | ... | @@ -124,9 +172,16 @@ export function useCheckin() { |
| 124 | } | 172 | } |
| 125 | 173 | ||
| 126 | /** | 174 | /** |
| 127 | - * 处理单个文件上传 | 175 | + * 处理单个文件上传流程 |
| 128 | * @param {Object} file - 文件对象 | 176 | * @param {Object} file - 文件对象 |
| 129 | * @returns {Promise<Object|null>} 上传结果 | 177 | * @returns {Promise<Object|null>} 上传结果 |
| 178 | + * @description | ||
| 179 | + * 完整上传流程: | ||
| 180 | + * 1. 计算文件 MD5 | ||
| 181 | + * 2. 调用 qiniuTokenAPI 获取上传凭证或秒传检测 | ||
| 182 | + * 3. 如果已存在(秒传),直接返回文件信息 | ||
| 183 | + * 4. 如果不存在,构造文件名并上传到七牛云 | ||
| 184 | + * 5. 上传成功后调用 saveFileAPI 保存文件记录到后端 | ||
| 130 | */ | 185 | */ |
| 131 | const handleUpload = async (file) => { | 186 | const handleUpload = async (file) => { |
| 132 | loading.value = true | 187 | loading.value = true |
| ... | @@ -193,6 +248,11 @@ export function useCheckin() { | ... | @@ -193,6 +248,11 @@ export function useCheckin() { |
| 193 | * 文件上传前的校验 | 248 | * 文件上传前的校验 |
| 194 | * @param {File|File[]} file - 文件或文件数组 | 249 | * @param {File|File[]} file - 文件或文件数组 |
| 195 | * @returns {boolean} 是否通过校验 | 250 | * @returns {boolean} 是否通过校验 |
| 251 | + * @description | ||
| 252 | + * 校验规则: | ||
| 253 | + * 1. 数量限制:当前已选 + 新选 <= maxCount | ||
| 254 | + * 2. 大小限制:每个文件 <= maxFileSizeMb | ||
| 255 | + * 3. 类型限制:根据 activeType 校验 MIME type | ||
| 196 | */ | 256 | */ |
| 197 | const beforeRead = (file) => { | 257 | const beforeRead = (file) => { |
| 198 | let flag = true | 258 | let flag = true |
| ... | @@ -247,6 +307,7 @@ export function useCheckin() { | ... | @@ -247,6 +307,7 @@ export function useCheckin() { |
| 247 | /** | 307 | /** |
| 248 | * 文件读取后的处理 | 308 | * 文件读取后的处理 |
| 249 | * @param {File|File[]} file - 文件或文件数组 | 309 | * @param {File|File[]} file - 文件或文件数组 |
| 310 | + * @description 遍历文件列表,逐个执行上传,并更新状态 | ||
| 250 | */ | 311 | */ |
| 251 | const afterRead = async (file) => { | 312 | const afterRead = async (file) => { |
| 252 | const files = Array.isArray(file) ? file : [file] | 313 | const files = Array.isArray(file) ? file : [file] |
| ... | @@ -282,7 +343,7 @@ export function useCheckin() { | ... | @@ -282,7 +343,7 @@ export function useCheckin() { |
| 282 | } | 343 | } |
| 283 | 344 | ||
| 284 | /** | 345 | /** |
| 285 | - * 删除文件项 | 346 | + * 删除文件项(别名) |
| 286 | * @param {Object} item - 要删除的文件项 | 347 | * @param {Object} item - 要删除的文件项 |
| 287 | */ | 348 | */ |
| 288 | const delItem = (item) => { | 349 | const delItem = (item) => { |
| ... | @@ -292,9 +353,12 @@ export function useCheckin() { | ... | @@ -292,9 +353,12 @@ export function useCheckin() { |
| 292 | } | 353 | } |
| 293 | } | 354 | } |
| 294 | 355 | ||
| 356 | + // ==================== 提交逻辑 ==================== | ||
| 357 | + | ||
| 295 | /** | 358 | /** |
| 296 | - * 提交打卡 | 359 | + * 递归查找新生成的打卡ID |
| 297 | - * @param {Object} extraData - 额外提交数据 | 360 | + * @param {Object} data API返回的数据对象 |
| 361 | + * @returns {string|null} 找到的ID或null | ||
| 298 | */ | 362 | */ |
| 299 | const get_new_checkin_id = (data) => { | 363 | const get_new_checkin_id = (data) => { |
| 300 | if (!data) return null | 364 | if (!data) return null |
| ... | @@ -337,6 +401,17 @@ export function useCheckin() { | ... | @@ -337,6 +401,17 @@ export function useCheckin() { |
| 337 | return null | 401 | return null |
| 338 | } | 402 | } |
| 339 | 403 | ||
| 404 | + /** | ||
| 405 | + * 提交打卡 | ||
| 406 | + * @param {Object} extraData - 额外提交数据 | ||
| 407 | + * @description | ||
| 408 | + * 提交流程: | ||
| 409 | + * 1. 表单校验(内容长度、是否上传文件) | ||
| 410 | + * 2. 组装数据(note, file_type, meta_id等) | ||
| 411 | + * 3. 根据 route.query.status 判断是新增还是编辑 | ||
| 412 | + * 4. 调用对应 API (addUploadTaskAPI / editUploadTaskInfoAPI) | ||
| 413 | + * 5. 成功后设置 sessionStorage 刷新标记并返回上一页 | ||
| 414 | + */ | ||
| 340 | const onSubmit = async (extraData = {}) => { | 415 | const onSubmit = async (extraData = {}) => { |
| 341 | if (uploading.value) return | 416 | if (uploading.value) return |
| 342 | 417 | ||
| ... | @@ -428,6 +503,7 @@ export function useCheckin() { | ... | @@ -428,6 +503,7 @@ export function useCheckin() { |
| 428 | /** | 503 | /** |
| 429 | * 切换打卡类型 | 504 | * 切换打卡类型 |
| 430 | * @param {string} type - 打卡类型 | 505 | * @param {string} type - 打卡类型 |
| 506 | + * @description 切换时会保存当前类型的文件列表,并恢复新类型的文件列表 | ||
| 431 | */ | 507 | */ |
| 432 | const switchType = (type) => { | 508 | const switchType = (type) => { |
| 433 | if (activeType.value !== type) { | 509 | if (activeType.value !== type) { |
| ... | @@ -468,6 +544,11 @@ export function useCheckin() { | ... | @@ -468,6 +544,11 @@ export function useCheckin() { |
| 468 | * 初始化编辑数据 | 544 | * 初始化编辑数据 |
| 469 | * @param {Array} taskOptions - 任务选项列表 | 545 | * @param {Array} taskOptions - 任务选项列表 |
| 470 | * @param {Object} handlers - 回调处理函数 | 546 | * @param {Object} handlers - 回调处理函数 |
| 547 | + * @description | ||
| 548 | + * 编辑模式下: | ||
| 549 | + * 1. 调用 getUploadTaskInfoAPI 获取详情 | ||
| 550 | + * 2. 回显文本、类型、文件列表 | ||
| 551 | + * 3. 处理计数打卡的数据恢复 | ||
| 471 | */ | 552 | */ |
| 472 | const initEditData = async (taskOptions = [], handlers = {}) => { | 553 | const initEditData = async (taskOptions = [], handlers = {}) => { |
| 473 | if (route.query.status === 'edit') { | 554 | if (route.query.status === 'edit') { | ... | ... |
| 1 | import { ref } from 'vue' | 1 | import { ref } from 'vue' |
| 2 | 2 | ||
| 3 | +/** | ||
| 4 | + * 首页视频播放控制 | ||
| 5 | + * @module useHomeVideoPlayer | ||
| 6 | + * @description 管理首页视频列表的播放状态,确保同一时间只有一个视频在播放。 | ||
| 7 | + * @returns {Object} 包含视频播放索引和控制方法 | ||
| 8 | + */ | ||
| 3 | export const useHomeVideoPlayer = () => { | 9 | export const useHomeVideoPlayer = () => { |
| 10 | + /** @type {import('vue').Ref<number|null>} 当前播放的视频索引 */ | ||
| 4 | const activeVideoIndex = ref(null) | 11 | const activeVideoIndex = ref(null) |
| 5 | 12 | ||
| 13 | + /** | ||
| 14 | + * 播放指定索引的视频 | ||
| 15 | + * @param {number} index - 视频索引 | ||
| 16 | + */ | ||
| 6 | const playVideo = (index) => { | 17 | const playVideo = (index) => { |
| 7 | activeVideoIndex.value = index | 18 | activeVideoIndex.value = index |
| 8 | } | 19 | } |
| 9 | 20 | ||
| 21 | + /** | ||
| 22 | + * 关闭当前视频播放 | ||
| 23 | + */ | ||
| 10 | const closeVideo = () => { | 24 | const closeVideo = () => { |
| 11 | activeVideoIndex.value = null | 25 | activeVideoIndex.value = null |
| 12 | } | 26 | } | ... | ... |
| ... | @@ -3,7 +3,7 @@ | ... | @@ -3,7 +3,7 @@ |
| 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com | 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com |
| 4 | * @LastEditTime: 2025-12-04 13:36:41 | 4 | * @LastEditTime: 2025-12-04 13:36:41 |
| 5 | * @FilePath: /mlaj/src/composables/useShare.js | 5 | * @FilePath: /mlaj/src/composables/useShare.js |
| 6 | - * @Description: 文件描述 | 6 | + * @Description: 微信分享相关逻辑 |
| 7 | */ | 7 | */ |
| 8 | import wx from 'weixin-js-sdk'; | 8 | import wx from 'weixin-js-sdk'; |
| 9 | 9 | ... | ... |
| ... | @@ -2,25 +2,50 @@ import { ref, watch } from 'vue'; | ... | @@ -2,25 +2,50 @@ import { ref, watch } from 'vue'; |
| 2 | import { getGroupCommentListAPI, addGroupCommentAPI, addGroupCommentLikeAPI, delGroupCommentLikeAPI } from '@/api/course'; | 2 | import { getGroupCommentListAPI, addGroupCommentAPI, addGroupCommentLikeAPI, delGroupCommentLikeAPI } from '@/api/course'; |
| 3 | 3 | ||
| 4 | /** | 4 | /** |
| 5 | - * 学习评论跟踪器 | 5 | + * 学习评论功能封装 |
| 6 | - * @param {*} course 课程对象 | 6 | + * @module useStudyComments |
| 7 | - * @returns 学习评论跟踪器对象,包含 commentCount、commentList、newComment、showCommentPopup、popupComment、popupCommentList、popupLoading、popupFinished、popupLimit、popupPage、refreshComments、toggleLike、submitComment 等方法 | 7 | + * @description |
| 8 | + * 管理课程/学习资料的评论列表、发表评论、点赞以及弹窗内的评论加载。 | ||
| 9 | + * | ||
| 10 | + * @param {import('vue').Ref<Object>} course - 课程对象响应式引用 | ||
| 11 | + * @returns {Object} 评论相关状态和方法 | ||
| 8 | */ | 12 | */ |
| 9 | export const useStudyComments = (course) => { | 13 | export const useStudyComments = (course) => { |
| 14 | + // ==================== 状态定义 ==================== | ||
| 15 | + | ||
| 16 | + /** @type {import('vue').Ref<number>} 评论总数 */ | ||
| 10 | const commentCount = ref(0); | 17 | const commentCount = ref(0); |
| 18 | + /** @type {import('vue').Ref<Array>} 评论列表(主页面显示) */ | ||
| 11 | const commentList = ref([]); | 19 | const commentList = ref([]); |
| 12 | - | 20 | + /** @type {import('vue').Ref<string>} 新评论内容(主页面输入框) */ |
| 13 | const newComment = ref(''); | 21 | const newComment = ref(''); |
| 14 | 22 | ||
| 23 | + // 弹窗相关状态 | ||
| 24 | + /** @type {import('vue').Ref<boolean>} 是否显示评论弹窗 */ | ||
| 15 | const showCommentPopup = ref(false); | 25 | const showCommentPopup = ref(false); |
| 26 | + /** @type {import('vue').Ref<string>} 弹窗内的新评论内容 */ | ||
| 16 | const popupComment = ref(''); | 27 | const popupComment = ref(''); |
| 17 | - | 28 | + /** @type {import('vue').Ref<Array>} 弹窗内的评论列表 */ |
| 18 | const popupCommentList = ref([]); | 29 | const popupCommentList = ref([]); |
| 30 | + /** @type {import('vue').Ref<boolean>} 弹窗加载中状态 */ | ||
| 19 | const popupLoading = ref(false); | 31 | const popupLoading = ref(false); |
| 32 | + /** @type {import('vue').Ref<boolean>} 弹窗是否已加载全部 */ | ||
| 20 | const popupFinished = ref(false); | 33 | const popupFinished = ref(false); |
| 34 | + /** @type {import('vue').Ref<number>} 弹窗每页加载数量 */ | ||
| 21 | const popupLimit = ref(5); | 35 | const popupLimit = ref(5); |
| 36 | + /** @type {import('vue').Ref<number>} 弹窗当前页码 */ | ||
| 22 | const popupPage = ref(0); | 37 | const popupPage = ref(0); |
| 23 | 38 | ||
| 39 | + // ==================== 方法定义 ==================== | ||
| 40 | + | ||
| 41 | + /** | ||
| 42 | + * 刷新主页面评论列表 | ||
| 43 | + * @description | ||
| 44 | + * 数据流: | ||
| 45 | + * 1. 检查 course.value 是否包含 group_id 和 schedule_id | ||
| 46 | + * 2. 调用 getGroupCommentListAPI 获取最新评论 | ||
| 47 | + * 3. 更新 commentList 和 commentCount | ||
| 48 | + */ | ||
| 24 | const refreshComments = async () => { | 49 | const refreshComments = async () => { |
| 25 | if (!course.value?.group_id || !course.value?.id) return; | 50 | if (!course.value?.group_id || !course.value?.id) return; |
| 26 | 51 | ||
| ... | @@ -34,6 +59,14 @@ export const useStudyComments = (course) => { | ... | @@ -34,6 +59,14 @@ export const useStudyComments = (course) => { |
| 34 | } | 59 | } |
| 35 | }; | 60 | }; |
| 36 | 61 | ||
| 62 | + /** | ||
| 63 | + * 切换点赞状态 | ||
| 64 | + * @param {Object} comment - 评论对象 | ||
| 65 | + * @description | ||
| 66 | + * 乐观更新:先调用 API,成功后更新本地状态 | ||
| 67 | + * 1. 如果当前未点赞 -> 调用 addGroupCommentLikeAPI -> 成功则 is_like=true, count+1 | ||
| 68 | + * 2. 如果当前已点赞 -> 调用 delGroupCommentLikeAPI -> 成功则 is_like=false, count-1 | ||
| 69 | + */ | ||
| 37 | const toggleLike = async (comment) => { | 70 | const toggleLike = async (comment) => { |
| 38 | try { | 71 | try { |
| 39 | if (!comment.is_like) { | 72 | if (!comment.is_like) { |
| ... | @@ -54,6 +87,13 @@ export const useStudyComments = (course) => { | ... | @@ -54,6 +87,13 @@ export const useStudyComments = (course) => { |
| 54 | } | 87 | } |
| 55 | }; | 88 | }; |
| 56 | 89 | ||
| 90 | + /** | ||
| 91 | + * 提交主页面评论 | ||
| 92 | + * @description | ||
| 93 | + * 1. 校验内容非空 | ||
| 94 | + * 2. 调用 addGroupCommentAPI | ||
| 95 | + * 3. 成功后刷新评论列表并清空输入框 | ||
| 96 | + */ | ||
| 57 | const submitComment = async () => { | 97 | const submitComment = async () => { |
| 58 | if (!newComment.value.trim()) return; | 98 | if (!newComment.value.trim()) return; |
| 59 | if (!course.value?.group_id || !course.value?.id) return; | 99 | if (!course.value?.group_id || !course.value?.id) return; |
| ... | @@ -74,6 +114,15 @@ export const useStudyComments = (course) => { | ... | @@ -74,6 +114,15 @@ export const useStudyComments = (course) => { |
| 74 | } | 114 | } |
| 75 | }; | 115 | }; |
| 76 | 116 | ||
| 117 | + /** | ||
| 118 | + * 弹窗加载更多评论(分页) | ||
| 119 | + * @description | ||
| 120 | + * 用于 Vant List 组件的 @load 事件 | ||
| 121 | + * 1. 计算下一页页码 | ||
| 122 | + * 2. 调用 API 获取分页数据 | ||
| 123 | + * 3. 过滤重复数据并追加到 popupCommentList | ||
| 124 | + * 4. 判断是否已加载全部数据 (finished) | ||
| 125 | + */ | ||
| 77 | const onPopupLoad = async () => { | 126 | const onPopupLoad = async () => { |
| 78 | if (!course.value?.group_id || !course.value?.id) { | 127 | if (!course.value?.group_id || !course.value?.id) { |
| 79 | popupLoading.value = false; | 128 | popupLoading.value = false; |
| ... | @@ -90,9 +139,12 @@ export const useStudyComments = (course) => { | ... | @@ -90,9 +139,12 @@ export const useStudyComments = (course) => { |
| 90 | }); | 139 | }); |
| 91 | if (res.code === 1) { | 140 | if (res.code === 1) { |
| 92 | const newComments = res.data.comment_list; | 141 | const newComments = res.data.comment_list; |
| 142 | + // 去重处理,防止页码错乱导致的数据重复 | ||
| 93 | const existingIds = new Set(popupCommentList.value.map(item => item.id)); | 143 | const existingIds = new Set(popupCommentList.value.map(item => item.id)); |
| 94 | const uniqueNewComments = newComments.filter(item => !existingIds.has(item.id)); | 144 | const uniqueNewComments = newComments.filter(item => !existingIds.has(item.id)); |
| 145 | + | ||
| 95 | popupCommentList.value = [...popupCommentList.value, ...uniqueNewComments]; | 146 | popupCommentList.value = [...popupCommentList.value, ...uniqueNewComments]; |
| 147 | + // 如果返回数量小于限制,说明已无更多数据 | ||
| 96 | popupFinished.value = res.data.comment_list.length < popupLimit.value; | 148 | popupFinished.value = res.data.comment_list.length < popupLimit.value; |
| 97 | popupPage.value = nextPage + 1; | 149 | popupPage.value = nextPage + 1; |
| 98 | } | 150 | } |
| ... | @@ -102,6 +154,14 @@ export const useStudyComments = (course) => { | ... | @@ -102,6 +154,14 @@ export const useStudyComments = (course) => { |
| 102 | popupLoading.value = false; | 154 | popupLoading.value = false; |
| 103 | }; | 155 | }; |
| 104 | 156 | ||
| 157 | + /** | ||
| 158 | + * 提交弹窗内的评论 | ||
| 159 | + * @description | ||
| 160 | + * 1. 调用 API 提交 | ||
| 161 | + * 2. 成功后重置弹窗列表状态(清空列表、页码归零) | ||
| 162 | + * 3. 重新加载第一页数据 | ||
| 163 | + * 4. 同步更新外部评论总数 | ||
| 164 | + */ | ||
| 105 | const submitPopupComment = async () => { | 165 | const submitPopupComment = async () => { |
| 106 | if (!popupComment.value.trim()) return; | 166 | if (!popupComment.value.trim()) return; |
| 107 | if (!course.value?.group_id || !course.value?.id) return; | 167 | if (!course.value?.group_id || !course.value?.id) return; |
| ... | @@ -114,13 +174,14 @@ export const useStudyComments = (course) => { | ... | @@ -114,13 +174,14 @@ export const useStudyComments = (course) => { |
| 114 | }); | 174 | }); |
| 115 | 175 | ||
| 116 | if (code === 1) { | 176 | if (code === 1) { |
| 177 | + // 重置列表状态以重新加载 | ||
| 117 | popupCommentList.value = []; | 178 | popupCommentList.value = []; |
| 118 | popupPage.value = 0; | 179 | popupPage.value = 0; |
| 119 | popupFinished.value = false; | 180 | popupFinished.value = false; |
| 120 | await onPopupLoad(); | 181 | await onPopupLoad(); |
| 121 | 182 | ||
| 122 | commentCount.value = data.comment_count; | 183 | commentCount.value = data.comment_count; |
| 123 | - await refreshComments(); | 184 | + await refreshComments(); // 同步刷新主列表 |
| 124 | popupComment.value = ''; | 185 | popupComment.value = ''; |
| 125 | } | 186 | } |
| 126 | } catch (error) { | 187 | } catch (error) { |
| ... | @@ -128,6 +189,7 @@ export const useStudyComments = (course) => { | ... | @@ -128,6 +189,7 @@ export const useStudyComments = (course) => { |
| 128 | } | 189 | } |
| 129 | }; | 190 | }; |
| 130 | 191 | ||
| 192 | + // 监听弹窗打开,初始化数据 | ||
| 131 | watch(showCommentPopup, async (newVal) => { | 193 | watch(showCommentPopup, async (newVal) => { |
| 132 | if (!newVal) return; | 194 | if (!newVal) return; |
| 133 | if (!course.value?.group_id || !course.value?.id) return; | 195 | if (!course.value?.group_id || !course.value?.id) return; | ... | ... |
| ... | @@ -3,17 +3,27 @@ import { v4 as uuidv4 } from 'uuid'; | ... | @@ -3,17 +3,27 @@ import { v4 as uuidv4 } from 'uuid'; |
| 3 | import { addStudyRecordAPI } from '@/api/record'; | 3 | import { addStudyRecordAPI } from '@/api/record'; |
| 4 | 4 | ||
| 5 | /** | 5 | /** |
| 6 | - * 学习记录跟踪器 | 6 | + * 学习记录埋点跟踪器 |
| 7 | - * @param {*} course 课程对象 | 7 | + * @module useStudyRecordTracker |
| 8 | - * @param {*} courseId 课程ID | 8 | + * @description |
| 9 | - * @param {*} videoPlayerRef 视频播放器引用 | 9 | + * 自动追踪用户的学习行为(视频播放、音频播放),定期上报学习进度。 |
| 10 | - * @param {*} audioPlayerRef 音频播放器引用 | 10 | + * |
| 11 | - * @returns 学习记录跟踪器对象,包含 startAction、addRecord 等方法 | 11 | + * @param {Object} params - 参数对象 |
| 12 | + * @param {import('vue').Ref<Object>} params.course - 课程对象 | ||
| 13 | + * @param {import('vue').Ref<string|number>} params.courseId - 课程ID | ||
| 14 | + * @param {import('vue').Ref<Object>} params.videoPlayerRef - 视频播放器组件引用 | ||
| 15 | + * @param {import('vue').Ref<Object>} params.audioPlayerRef - 音频播放器组件引用 | ||
| 16 | + * @returns {Object} 包含开始/结束追踪和手动添加记录的方法 | ||
| 12 | */ | 17 | */ |
| 13 | export const useStudyRecordTracker = ({ course, courseId, videoPlayerRef, audioPlayerRef }) => { | 18 | export const useStudyRecordTracker = ({ course, courseId, videoPlayerRef, audioPlayerRef }) => { |
| 19 | + /** @type {import('vue').Ref<number|null>} 定时器ID */ | ||
| 14 | const action_timer = ref(null); | 20 | const action_timer = ref(null); |
| 21 | + /** @type {import('vue').Ref<string>} 当前播放会话ID (UUID) */ | ||
| 15 | const playback_id = ref(''); | 22 | const playback_id = ref(''); |
| 16 | 23 | ||
| 24 | + /** | ||
| 25 | + * 清除定时器 | ||
| 26 | + */ | ||
| 17 | const clear_timer = () => { | 27 | const clear_timer = () => { |
| 18 | if (action_timer.value) { | 28 | if (action_timer.value) { |
| 19 | clearInterval(action_timer.value); | 29 | clearInterval(action_timer.value); |
| ... | @@ -21,16 +31,30 @@ export const useStudyRecordTracker = ({ course, courseId, videoPlayerRef, audioP | ... | @@ -21,16 +31,30 @@ export const useStudyRecordTracker = ({ course, courseId, videoPlayerRef, audioP |
| 21 | } | 31 | } |
| 22 | }; | 32 | }; |
| 23 | 33 | ||
| 34 | + /** | ||
| 35 | + * 手动上报学习记录 | ||
| 36 | + * @param {Object} paramsObj - 上报参数 | ||
| 37 | + * @returns {Promise<Object>} API响应 | ||
| 38 | + */ | ||
| 24 | const addRecord = async (paramsObj) => { | 39 | const addRecord = async (paramsObj) => { |
| 25 | if (!paramsObj || !paramsObj.schedule_id) return; | 40 | if (!paramsObj || !paramsObj.schedule_id) return; |
| 26 | return await addStudyRecordAPI(paramsObj); | 41 | return await addStudyRecordAPI(paramsObj); |
| 27 | }; | 42 | }; |
| 28 | 43 | ||
| 44 | + /** | ||
| 45 | + * 获取规范化的课程ID | ||
| 46 | + * @returns {string|number} | ||
| 47 | + */ | ||
| 29 | const get_schedule_id = () => { | 48 | const get_schedule_id = () => { |
| 30 | if (typeof courseId === 'object' && courseId && 'value' in courseId) return courseId.value; | 49 | if (typeof courseId === 'object' && courseId && 'value' in courseId) return courseId.value; |
| 31 | return courseId || ''; | 50 | return courseId || ''; |
| 32 | }; | 51 | }; |
| 33 | 52 | ||
| 53 | + /** | ||
| 54 | + * 获取视频播放状态载荷 | ||
| 55 | + * @description 从 videoPlayerRef 获取当前播放时间、总时长和 meta_id | ||
| 56 | + * @returns {Object|null} | ||
| 57 | + */ | ||
| 34 | const get_video_payload = () => { | 58 | const get_video_payload = () => { |
| 35 | const player = videoPlayerRef?.value?.getPlayer?.(); | 59 | const player = videoPlayerRef?.value?.getPlayer?.(); |
| 36 | if (!player) return null; | 60 | if (!player) return null; |
| ... | @@ -48,6 +72,11 @@ export const useStudyRecordTracker = ({ course, courseId, videoPlayerRef, audioP | ... | @@ -48,6 +72,11 @@ export const useStudyRecordTracker = ({ course, courseId, videoPlayerRef, audioP |
| 48 | }; | 72 | }; |
| 49 | }; | 73 | }; |
| 50 | 74 | ||
| 75 | + /** | ||
| 76 | + * 获取音频播放状态载荷 | ||
| 77 | + * @param {Object} item - 音频项数据 | ||
| 78 | + * @returns {Object|null} | ||
| 79 | + */ | ||
| 51 | const get_audio_payload = (item) => { | 80 | const get_audio_payload = (item) => { |
| 52 | const player = audioPlayerRef?.value?.getPlayer?.(); | 81 | const player = audioPlayerRef?.value?.getPlayer?.(); |
| 53 | if (!player) return null; | 82 | if (!player) return null; |
| ... | @@ -63,6 +92,15 @@ export const useStudyRecordTracker = ({ course, courseId, videoPlayerRef, audioP | ... | @@ -63,6 +92,15 @@ export const useStudyRecordTracker = ({ course, courseId, videoPlayerRef, audioP |
| 63 | }; | 92 | }; |
| 64 | }; | 93 | }; |
| 65 | 94 | ||
| 95 | + /** | ||
| 96 | + * 开始追踪 | ||
| 97 | + * @param {Object} [item] - 当前播放项(用于音频) | ||
| 98 | + * @description | ||
| 99 | + * 1. 生成新的 playback_id | ||
| 100 | + * 2. 启动定时器 (3秒间隔) | ||
| 101 | + * 3. 根据课程类型 (video/audio) 获取对应的播放状态 | ||
| 102 | + * 4. 调用 addRecord 上报 | ||
| 103 | + */ | ||
| 66 | const startAction = (item) => { | 104 | const startAction = (item) => { |
| 67 | clear_timer(); | 105 | clear_timer(); |
| 68 | 106 | ||
| ... | @@ -78,7 +116,9 @@ export const useStudyRecordTracker = ({ course, courseId, videoPlayerRef, audioP | ... | @@ -78,7 +116,9 @@ export const useStudyRecordTracker = ({ course, courseId, videoPlayerRef, audioP |
| 78 | let payload = null; | 116 | let payload = null; |
| 79 | if (is_video) payload = get_video_payload(); | 117 | if (is_video) payload = get_video_payload(); |
| 80 | if (is_audio) payload = get_audio_payload(item); | 118 | if (is_audio) payload = get_audio_payload(item); |
| 119 | + // 兜底尝试 | ||
| 81 | if (!payload) payload = get_video_payload() || get_audio_payload(item); | 120 | if (!payload) payload = get_video_payload() || get_audio_payload(item); |
| 121 | + | ||
| 82 | if (!payload) return; | 122 | if (!payload) return; |
| 83 | 123 | ||
| 84 | addRecord({ | 124 | addRecord({ |
| ... | @@ -88,10 +128,14 @@ export const useStudyRecordTracker = ({ course, courseId, videoPlayerRef, audioP | ... | @@ -88,10 +128,14 @@ export const useStudyRecordTracker = ({ course, courseId, videoPlayerRef, audioP |
| 88 | }, 3000); | 128 | }, 3000); |
| 89 | }; | 129 | }; |
| 90 | 130 | ||
| 131 | + /** | ||
| 132 | + * 结束追踪 | ||
| 133 | + */ | ||
| 91 | const endAction = () => { | 134 | const endAction = () => { |
| 92 | clear_timer(); | 135 | clear_timer(); |
| 93 | }; | 136 | }; |
| 94 | 137 | ||
| 138 | + // 组件卸载时自动清理 | ||
| 95 | onUnmounted(() => { | 139 | onUnmounted(() => { |
| 96 | clear_timer(); | 140 | clear_timer(); |
| 97 | }); | 141 | }); | ... | ... |
| 1 | -/* | 1 | +/** |
| 2 | - * @Date: 2025-12-24 13:50:03 | 2 | + * 埋点系统封装 |
| 3 | - * @LastEditors: hookehuyr hookehuyr@gmail.com | 3 | + * @module useTracking |
| 4 | - * @LastEditTime: 2025-12-24 14:19:52 | 4 | + * @description 提供统一的事件埋点上报功能,支持页面浏览、点击事件和自定义事件。 |
| 5 | - * @FilePath: /mlaj/src/composables/useTracking.js | ||
| 6 | - * @Description: 埋点 Composable, 模拟埋点接口请求 | ||
| 7 | */ | 5 | */ |
| 6 | + | ||
| 8 | /** | 7 | /** |
| 9 | * 模拟埋点接口请求 | 8 | * 模拟埋点接口请求 |
| 10 | * @param {Object} data 埋点数据 | 9 | * @param {Object} data 埋点数据 |
| ... | @@ -31,7 +30,7 @@ export function useTracking() { | ... | @@ -31,7 +30,7 @@ export function useTracking() { |
| 31 | * @param {Object} [extraData={}] - 额外参数 (业务相关的数据) | 30 | * @param {Object} [extraData={}] - 额外参数 (业务相关的数据) |
| 32 | */ | 31 | */ |
| 33 | const trackEvent = async (eventType, actionName, extraData = {}) => { | 32 | const trackEvent = async (eventType, actionName, extraData = {}) => { |
| 34 | - // 获取当前页面路径 (简单获取,如果在组件setup中使用window.location) | 33 | + // 获取当前页面路径 |
| 35 | const pagePath = window.location.pathname + window.location.hash | 34 | const pagePath = window.location.pathname + window.location.hash |
| 36 | 35 | ||
| 37 | const payload = { | 36 | const payload = { | ... | ... |
| 1 | import { ref } from "vue"; | 1 | import { ref } from "vue"; |
| 2 | 2 | ||
| 3 | /** | 3 | /** |
| 4 | - * @description 播放器叠层逻辑:弱网提示、HLS 下载速率(基于 VHS 带宽)与调试信息。 | 4 | + * 视频播放器叠层逻辑管理 |
| 5 | - * @param {{ | 5 | + * @module useVideoPlaybackOverlays |
| 6 | - * props: any, | 6 | + * @description |
| 7 | - * player: import("vue").Ref<any>, | 7 | + * 管理视频播放器上的各种叠层信息,包括: |
| 8 | - * is_m3u8: import("vue").Ref<boolean> | import("vue").ComputedRef<boolean>, | 8 | + * 1. 弱网提示:当网速过慢时显示提示。 |
| 9 | - * use_native_player: import("vue").Ref<boolean> | import("vue").ComputedRef<boolean>, | 9 | + * 2. HLS 下载速率:显示当前 HLS 流的下载速度(调试模式)。 |
| 10 | - * show_error_overlay: import("vue").Ref<boolean>, | 10 | + * 3. 调试信息:显示播放器内核、带宽等技术参数。 |
| 11 | - * has_started_playback: import("vue").Ref<boolean> | 11 | + * |
| 12 | - * }} params | 12 | + * @param {Object} params - 初始化参数 |
| 13 | - * @returns {{ | 13 | + * @param {any} params.props - 组件 props |
| 14 | - * showNetworkSpeedOverlay: import("vue").Ref<boolean>, | 14 | + * @param {import("vue").Ref<any>} params.player - Video.js 实例引用 |
| 15 | - * networkSpeedText: import("vue").Ref<string>, | 15 | + * @param {import("vue").Ref<boolean>} params.is_m3u8 - 是否为 m3u8 格式 |
| 16 | - * hlsDownloadSpeedText: import("vue").Ref<string>, | 16 | + * @param {import("vue").Ref<boolean>} params.use_native_player - 是否使用原生播放器 |
| 17 | - * hlsSpeedDebugText: import("vue").Ref<string>, | 17 | + * @param {import("vue").Ref<boolean>} params.show_error_overlay - 是否显示错误叠层 |
| 18 | - * setHlsDebug: (event_name: string, extra?: string) => void, | 18 | + * @param {import("vue").Ref<boolean>} params.has_started_playback - 是否已开始播放 |
| 19 | - * showNetworkSpeed: () => void, | 19 | + * @returns {Object} 叠层状态和控制方法 |
| 20 | - * hideNetworkSpeed: () => void, | ||
| 21 | - * startHlsDownloadSpeed: () => void, | ||
| 22 | - * stopHlsDownloadSpeed: (reason?: string) => void, | ||
| 23 | - * disposeOverlays: () => void | ||
| 24 | - * }} | ||
| 25 | */ | 20 | */ |
| 26 | export const useVideoPlaybackOverlays = ({ | 21 | export const useVideoPlaybackOverlays = ({ |
| 27 | props, | 22 | props, |
| ... | @@ -31,15 +26,28 @@ export const useVideoPlaybackOverlays = ({ | ... | @@ -31,15 +26,28 @@ export const useVideoPlaybackOverlays = ({ |
| 31 | show_error_overlay, | 26 | show_error_overlay, |
| 32 | has_started_playback, | 27 | has_started_playback, |
| 33 | }) => { | 28 | }) => { |
| 29 | + // ==================== 状态定义 ==================== | ||
| 30 | + | ||
| 31 | + /** @type {import('vue').Ref<boolean>} 是否显示网速叠层 */ | ||
| 34 | const show_network_speed_overlay = ref(false); | 32 | const show_network_speed_overlay = ref(false); |
| 33 | + /** @type {import('vue').Ref<string>} 网速文本 */ | ||
| 35 | const network_speed_text = ref(""); | 34 | const network_speed_text = ref(""); |
| 36 | let network_speed_timer = null; | 35 | let network_speed_timer = null; |
| 37 | let last_weixin_network_type_at = 0; | 36 | let last_weixin_network_type_at = 0; |
| 38 | 37 | ||
| 38 | + /** @type {import('vue').Ref<string>} HLS下载速度文本 */ | ||
| 39 | const hls_download_speed_text = ref(""); | 39 | const hls_download_speed_text = ref(""); |
| 40 | let hls_speed_timer = null; | 40 | let hls_speed_timer = null; |
| 41 | + /** @type {import('vue').Ref<string>} HLS调试信息文本 */ | ||
| 41 | const hls_speed_debug_text = ref(""); | 42 | const hls_speed_debug_text = ref(""); |
| 42 | 43 | ||
| 44 | + // ==================== 方法定义 ==================== | ||
| 45 | + | ||
| 46 | + /** | ||
| 47 | + * 设置 HLS 调试信息 | ||
| 48 | + * @param {string} event_name - 事件名称 | ||
| 49 | + * @param {string} [extra] - 额外信息 | ||
| 50 | + */ | ||
| 43 | const set_hls_debug = (event_name, extra) => { | 51 | const set_hls_debug = (event_name, extra) => { |
| 44 | if (!props || props.debug !== true) return; | 52 | if (!props || props.debug !== true) return; |
| 45 | 53 | ||
| ... | @@ -66,6 +74,10 @@ export const useVideoPlaybackOverlays = ({ | ... | @@ -66,6 +74,10 @@ export const useVideoPlaybackOverlays = ({ |
| 66 | hls_speed_debug_text.value = `${event_name}${extra_text}\nmode:${mode} m3u8:${is_m3u8.value ? "1" : "0"} native:${use_native_player.value ? "1" : "0"}\ntech:${tech_name} vhs:${vhs ? "1" : "0"} bw:${bw_kbps}kbps hls_bw:${hls_bw_kbps}kbps`; | 74 | hls_speed_debug_text.value = `${event_name}${extra_text}\nmode:${mode} m3u8:${is_m3u8.value ? "1" : "0"} native:${use_native_player.value ? "1" : "0"}\ntech:${tech_name} vhs:${vhs ? "1" : "0"} bw:${bw_kbps}kbps hls_bw:${hls_bw_kbps}kbps`; |
| 67 | }; | 75 | }; |
| 68 | 76 | ||
| 77 | + /** | ||
| 78 | + * 更新网速信息 | ||
| 79 | + * @returns {boolean} 是否获取成功 | ||
| 80 | + */ | ||
| 69 | const update_network_speed = () => { | 81 | const update_network_speed = () => { |
| 70 | if (typeof navigator === "undefined") { | 82 | if (typeof navigator === "undefined") { |
| 71 | network_speed_text.value = "未知"; | 83 | network_speed_text.value = "未知"; |
| ... | @@ -85,6 +97,9 @@ export const useVideoPlaybackOverlays = ({ | ... | @@ -85,6 +97,9 @@ export const useVideoPlaybackOverlays = ({ |
| 85 | return effective_type ? true : false; | 97 | return effective_type ? true : false; |
| 86 | }; | 98 | }; |
| 87 | 99 | ||
| 100 | + /** | ||
| 101 | + * 更新微信环境下的网络类型 | ||
| 102 | + */ | ||
| 88 | const update_weixin_network_type = () => { | 103 | const update_weixin_network_type = () => { |
| 89 | if (typeof window === "undefined") return; | 104 | if (typeof window === "undefined") return; |
| 90 | if (!window.WeixinJSBridge || typeof window.WeixinJSBridge.invoke !== "function") return; | 105 | if (!window.WeixinJSBridge || typeof window.WeixinJSBridge.invoke !== "function") return; |
| ... | @@ -100,6 +115,10 @@ export const useVideoPlaybackOverlays = ({ | ... | @@ -100,6 +115,10 @@ export const useVideoPlaybackOverlays = ({ |
| 100 | }); | 115 | }); |
| 101 | }; | 116 | }; |
| 102 | 117 | ||
| 118 | + /** | ||
| 119 | + * 显示网速叠层 | ||
| 120 | + * @description 启动定时器定期获取网速 | ||
| 121 | + */ | ||
| 103 | const show_network_speed = () => { | 122 | const show_network_speed = () => { |
| 104 | // 没有进入过播放阶段时不显示,避免一加载就出现“弱网提示”造成误导 | 123 | // 没有进入过播放阶段时不显示,避免一加载就出现“弱网提示”造成误导 |
| 105 | if (!has_started_playback.value) return; | 124 | if (!has_started_playback.value) return; |
| ... | @@ -117,6 +136,9 @@ export const useVideoPlaybackOverlays = ({ | ... | @@ -117,6 +136,9 @@ export const useVideoPlaybackOverlays = ({ |
| 117 | }, 800); | 136 | }, 800); |
| 118 | }; | 137 | }; |
| 119 | 138 | ||
| 139 | + /** | ||
| 140 | + * 隐藏网速叠层 | ||
| 141 | + */ | ||
| 120 | const hide_network_speed = () => { | 142 | const hide_network_speed = () => { |
| 121 | show_network_speed_overlay.value = false; | 143 | show_network_speed_overlay.value = false; |
| 122 | if (network_speed_timer) { | 144 | if (network_speed_timer) { |
| ... | @@ -125,6 +147,11 @@ export const useVideoPlaybackOverlays = ({ | ... | @@ -125,6 +147,11 @@ export const useVideoPlaybackOverlays = ({ |
| 125 | } | 147 | } |
| 126 | }; | 148 | }; |
| 127 | 149 | ||
| 150 | + /** | ||
| 151 | + * 格式化速度显示 | ||
| 152 | + * @param {number} bytes_per_second | ||
| 153 | + * @returns {string} | ||
| 154 | + */ | ||
| 128 | const format_speed = (bytes_per_second) => { | 155 | const format_speed = (bytes_per_second) => { |
| 129 | const size = Number(bytes_per_second) || 0; | 156 | const size = Number(bytes_per_second) || 0; |
| 130 | if (!size) return ""; | 157 | if (!size) return ""; |
| ... | @@ -136,6 +163,9 @@ export const useVideoPlaybackOverlays = ({ | ... | @@ -136,6 +163,9 @@ export const useVideoPlaybackOverlays = ({ |
| 136 | return `${Math.round(size)}B/s`; | 163 | return `${Math.round(size)}B/s`; |
| 137 | }; | 164 | }; |
| 138 | 165 | ||
| 166 | + /** | ||
| 167 | + * 更新 HLS 下载速度 | ||
| 168 | + */ | ||
| 139 | const update_hls_download_speed = () => { | 169 | const update_hls_download_speed = () => { |
| 140 | if (!player.value || player.value.isDisposed()) { | 170 | if (!player.value || player.value.isDisposed()) { |
| 141 | hls_download_speed_text.value = ""; | 171 | hls_download_speed_text.value = ""; |
| ... | @@ -171,6 +201,9 @@ export const useVideoPlaybackOverlays = ({ | ... | @@ -171,6 +201,9 @@ export const useVideoPlaybackOverlays = ({ |
| 171 | set_hls_debug("update", `speed:${hls_download_speed_text.value}`); | 201 | set_hls_debug("update", `speed:${hls_download_speed_text.value}`); |
| 172 | }; | 202 | }; |
| 173 | 203 | ||
| 204 | + /** | ||
| 205 | + * 开始监控 HLS 下载速度 | ||
| 206 | + */ | ||
| 174 | const start_hls_download_speed = () => { | 207 | const start_hls_download_speed = () => { |
| 175 | if (hls_speed_timer) return; | 208 | if (hls_speed_timer) return; |
| 176 | if (!is_m3u8.value || use_native_player.value) return; | 209 | if (!is_m3u8.value || use_native_player.value) return; |
| ... | @@ -182,6 +215,10 @@ export const useVideoPlaybackOverlays = ({ | ... | @@ -182,6 +215,10 @@ export const useVideoPlaybackOverlays = ({ |
| 182 | }, 1000); | 215 | }, 1000); |
| 183 | }; | 216 | }; |
| 184 | 217 | ||
| 218 | + /** | ||
| 219 | + * 停止监控 HLS 下载速度 | ||
| 220 | + * @param {string} [reason] - 停止原因 | ||
| 221 | + */ | ||
| 185 | const stop_hls_download_speed = (reason) => { | 222 | const stop_hls_download_speed = (reason) => { |
| 186 | if (hls_speed_timer) { | 223 | if (hls_speed_timer) { |
| 187 | clearInterval(hls_speed_timer); | 224 | clearInterval(hls_speed_timer); |
| ... | @@ -191,6 +228,9 @@ export const useVideoPlaybackOverlays = ({ | ... | @@ -191,6 +228,9 @@ export const useVideoPlaybackOverlays = ({ |
| 191 | set_hls_debug("stop", reason || ""); | 228 | set_hls_debug("stop", reason || ""); |
| 192 | }; | 229 | }; |
| 193 | 230 | ||
| 231 | + /** | ||
| 232 | + * 销毁所有叠层 | ||
| 233 | + */ | ||
| 194 | const dispose_overlays = () => { | 234 | const dispose_overlays = () => { |
| 195 | hide_network_speed(); | 235 | hide_network_speed(); |
| 196 | stop_hls_download_speed("dispose"); | 236 | stop_hls_download_speed("dispose"); | ... | ... |
| ... | @@ -3,7 +3,7 @@ | ... | @@ -3,7 +3,7 @@ |
| 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com | 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com |
| 4 | * @LastEditTime: 2026-01-20 17:06:44 | 4 | * @LastEditTime: 2026-01-20 17:06:44 |
| 5 | * @FilePath: /mlaj/src/composables/videoPlayerSource.js | 5 | * @FilePath: /mlaj/src/composables/videoPlayerSource.js |
| 6 | - * @Description: 文件描述 | 6 | + * @Description: 视频播放源处理逻辑 |
| 7 | */ | 7 | */ |
| 8 | /** | 8 | /** |
| 9 | * @description 从 url(或文件名)中提取扩展名(小写,不含点)。支持 base_url 作为 URL 解析基准。 | 9 | * @description 从 url(或文件名)中提取扩展名(小写,不含点)。支持 base_url 作为 URL 解析基准。 | ... | ... |
| ... | @@ -457,22 +457,23 @@ const handleTargetEdit = (item) => { | ... | @@ -457,22 +457,23 @@ const handleTargetEdit = (item) => { |
| 457 | * 处理对象删除 | 457 | * 处理对象删除 |
| 458 | */ | 458 | */ |
| 459 | const handleTargetDelete = async (item) => { | 459 | const handleTargetDelete = async (item) => { |
| 460 | - const { code } = await gratitudeDeleteAPI({ id: item.id }) | 460 | + // 屏蔽删除功能, 那个接口也是不存在的 |
| 461 | - if (code === 1) { | 461 | + // const { code } = await gratitudeDeleteAPI({ id: item.id }) |
| 462 | - // 删除成功,更新本地列表 | 462 | + // if (code === 1) { |
| 463 | - const targetIndex = targetList.value.findIndex(t => t.id === item.id) | 463 | + // // 删除成功,更新本地列表 |
| 464 | - if (targetIndex > -1) { | 464 | + // const targetIndex = targetList.value.findIndex(t => t.id === item.id) |
| 465 | - targetList.value.splice(targetIndex, 1) | 465 | + // if (targetIndex > -1) { |
| 466 | - } | 466 | + // targetList.value.splice(targetIndex, 1) |
| 467 | + // } | ||
| 467 | 468 | ||
| 468 | - // 从选中列表中也删除 | 469 | + // // 从选中列表中也删除 |
| 469 | - const selectedIndex = selectedTargets.value.findIndex(t => t.id === item.id) | 470 | + // const selectedIndex = selectedTargets.value.findIndex(t => t.id === item.id) |
| 470 | - if (selectedIndex > -1) { | 471 | + // if (selectedIndex > -1) { |
| 471 | - selectedTargets.value.splice(selectedIndex, 1) | 472 | + // selectedTargets.value.splice(selectedIndex, 1) |
| 472 | - } | 473 | + // } |
| 473 | 474 | ||
| 474 | - showToast('删除成功') | 475 | + // showToast('删除成功') |
| 475 | - } | 476 | + // } |
| 476 | } | 477 | } |
| 477 | 478 | ||
| 478 | /** | 479 | /** | ... | ... |
-
Please register or login to post a comment