refactor(media): 提取媒体预览逻辑到可复用 composable
将图片和视频预览相关的状态和方法从 AlbumList 和 Dashboard 组件中提取到 useMediaPreview composable 在 Dashboard 组件中实现媒体预览功能
Showing
3 changed files
with
275 additions
and
90 deletions
src/composables/useMediaPreview.js
0 → 100644
| 1 | +import { ref } from 'vue'; | ||
| 2 | +import Taro from '@tarojs/taro'; | ||
| 3 | + | ||
| 4 | +/** | ||
| 5 | + * 媒体预览 composable | ||
| 6 | + * 提供图片和视频预览功能 | ||
| 7 | + */ | ||
| 8 | +export function useMediaPreview() { | ||
| 9 | + // 图片预览相关状态 | ||
| 10 | + const previewVisible = ref(false); | ||
| 11 | + const previewImages = ref([]); | ||
| 12 | + const previewIndex = ref(0); | ||
| 13 | + | ||
| 14 | + // 视频预览相关状态 | ||
| 15 | + const videoVisible = ref(false); | ||
| 16 | + const currentVideo = ref(null); | ||
| 17 | + const videoId = ref(Date.now()); | ||
| 18 | + | ||
| 19 | + /** | ||
| 20 | + * 处理媒体项目点击事件 | ||
| 21 | + * @param {Object} item - 媒体项目 | ||
| 22 | + * @param {Array} mediaList - 完整的媒体列表 | ||
| 23 | + */ | ||
| 24 | + const handleMediaClick = (item, mediaList = []) => { | ||
| 25 | + if (item.type === 'image') { | ||
| 26 | + // 图片预览 | ||
| 27 | + const imageItems = mediaList.filter(media => media.type === 'image'); | ||
| 28 | + previewImages.value = imageItems.map(img => ({ src: img.url })); | ||
| 29 | + | ||
| 30 | + // 计算当前图片在图片列表中的索引 | ||
| 31 | + const imageIndex = imageItems.findIndex(img => img.url === item.url); | ||
| 32 | + previewIndex.value = imageIndex >= 0 ? imageIndex : 0; | ||
| 33 | + previewVisible.value = true; | ||
| 34 | + } else if (item.type === 'video') { | ||
| 35 | + // 视频播放 | ||
| 36 | + currentVideo.value = item; | ||
| 37 | + videoId.value = Date.now(); // 生成新的视频ID | ||
| 38 | + videoVisible.value = true; | ||
| 39 | + } | ||
| 40 | + }; | ||
| 41 | + | ||
| 42 | + /** | ||
| 43 | + * 预览单张图片 | ||
| 44 | + * @param {string} imageUrl - 图片URL | ||
| 45 | + */ | ||
| 46 | + const previewSingleImage = (imageUrl) => { | ||
| 47 | + previewImages.value = [{ src: imageUrl }]; | ||
| 48 | + previewIndex.value = 0; | ||
| 49 | + previewVisible.value = true; | ||
| 50 | + }; | ||
| 51 | + | ||
| 52 | + /** | ||
| 53 | + * 预览多张图片 | ||
| 54 | + * @param {Array} images - 图片URL数组 | ||
| 55 | + * @param {number} index - 初始显示的图片索引 | ||
| 56 | + */ | ||
| 57 | + const previewMultipleImages = (images, index = 0) => { | ||
| 58 | + previewImages.value = images.map(url => ({ src: url })); | ||
| 59 | + previewIndex.value = index; | ||
| 60 | + previewVisible.value = true; | ||
| 61 | + }; | ||
| 62 | + | ||
| 63 | + /** | ||
| 64 | + * 播放视频 | ||
| 65 | + * @param {Object} video - 视频对象 {url, thumbnail, duration} | ||
| 66 | + */ | ||
| 67 | + const playVideo = (video) => { | ||
| 68 | + currentVideo.value = video; | ||
| 69 | + videoId.value = Date.now(); | ||
| 70 | + videoVisible.value = true; | ||
| 71 | + }; | ||
| 72 | + | ||
| 73 | + /** | ||
| 74 | + * 关闭图片预览 | ||
| 75 | + */ | ||
| 76 | + const closePreview = () => { | ||
| 77 | + previewVisible.value = false; | ||
| 78 | + }; | ||
| 79 | + | ||
| 80 | + /** | ||
| 81 | + * 关闭视频播放 | ||
| 82 | + */ | ||
| 83 | + const closeVideo = () => { | ||
| 84 | + videoVisible.value = false; | ||
| 85 | + currentVideo.value = null; | ||
| 86 | + }; | ||
| 87 | + | ||
| 88 | + /** | ||
| 89 | + * 处理视频播放 | ||
| 90 | + */ | ||
| 91 | + const handleVideoPlay = () => { | ||
| 92 | + // 视频开始播放 | ||
| 93 | + }; | ||
| 94 | + | ||
| 95 | + /** | ||
| 96 | + * 处理视频暂停 | ||
| 97 | + */ | ||
| 98 | + const handleVideoPause = () => { | ||
| 99 | + // 视频暂停播放 | ||
| 100 | + }; | ||
| 101 | + | ||
| 102 | + /** | ||
| 103 | + * 处理全屏状态变化 | ||
| 104 | + */ | ||
| 105 | + const handleFullscreenChange = () => { | ||
| 106 | + // 全屏状态变化 | ||
| 107 | + }; | ||
| 108 | + | ||
| 109 | + /** | ||
| 110 | + * 处理视频播放错误 | ||
| 111 | + * @param {Event} error - 错误事件 | ||
| 112 | + */ | ||
| 113 | + const handleVideoError = (error) => { | ||
| 114 | + console.error('视频播放错误:', error); | ||
| 115 | + Taro.showToast({ | ||
| 116 | + title: '视频播放失败', | ||
| 117 | + icon: 'error', | ||
| 118 | + duration: 2000 | ||
| 119 | + }); | ||
| 120 | + // 关闭视频弹框 | ||
| 121 | + closeVideo(); | ||
| 122 | + }; | ||
| 123 | + | ||
| 124 | + /** | ||
| 125 | + * 格式化视频时长 | ||
| 126 | + * @param {number} seconds - 秒数 | ||
| 127 | + * @returns {string} 格式化后的时长 | ||
| 128 | + */ | ||
| 129 | + const formatDuration = (seconds) => { | ||
| 130 | + const minutes = Math.floor(seconds / 60); | ||
| 131 | + const remainingSeconds = seconds % 60; | ||
| 132 | + return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`; | ||
| 133 | + }; | ||
| 134 | + | ||
| 135 | + return { | ||
| 136 | + // 状态 | ||
| 137 | + previewVisible, | ||
| 138 | + previewImages, | ||
| 139 | + previewIndex, | ||
| 140 | + videoVisible, | ||
| 141 | + currentVideo, | ||
| 142 | + videoId, | ||
| 143 | + | ||
| 144 | + // 方法 | ||
| 145 | + handleMediaClick, | ||
| 146 | + previewSingleImage, | ||
| 147 | + previewMultipleImages, | ||
| 148 | + playVideo, | ||
| 149 | + closePreview, | ||
| 150 | + closeVideo, | ||
| 151 | + handleVideoPlay, | ||
| 152 | + handleVideoPause, | ||
| 153 | + handleFullscreenChange, | ||
| 154 | + handleVideoError, | ||
| 155 | + formatDuration | ||
| 156 | + }; | ||
| 157 | +} | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
| ... | @@ -100,15 +100,28 @@ | ... | @@ -100,15 +100,28 @@ |
| 100 | import { ref, onMounted } from 'vue'; | 100 | import { ref, onMounted } from 'vue'; |
| 101 | import Taro from '@tarojs/taro'; | 101 | import Taro from '@tarojs/taro'; |
| 102 | import { Left, Service, Photograph, Close } from '@nutui/icons-vue-taro'; | 102 | import { Left, Service, Photograph, Close } from '@nutui/icons-vue-taro'; |
| 103 | +import { useMediaPreview } from '@/composables/useMediaPreview'; | ||
| 103 | 104 | ||
| 104 | // 响应式数据 | 105 | // 响应式数据 |
| 105 | const albumList = ref([]); | 106 | const albumList = ref([]); |
| 106 | -const previewVisible = ref(false); | 107 | + |
| 107 | -const previewImages = ref([]); | 108 | +// 使用媒体预览 composable |
| 108 | -const previewIndex = ref(0); | 109 | +const { |
| 109 | -const videoVisible = ref(false); | 110 | + previewVisible, |
| 110 | -const currentVideo = ref(null); | 111 | + previewImages, |
| 111 | -const videoId = ref(Date.now()); | 112 | + previewIndex, |
| 113 | + videoVisible, | ||
| 114 | + currentVideo, | ||
| 115 | + videoId, | ||
| 116 | + handleMediaClick, | ||
| 117 | + closePreview, | ||
| 118 | + closeVideo, | ||
| 119 | + handleVideoPlay, | ||
| 120 | + handleVideoPause, | ||
| 121 | + handleFullscreenChange, | ||
| 122 | + handleVideoError, | ||
| 123 | + formatDuration | ||
| 124 | +} = useMediaPreview(); | ||
| 112 | 125 | ||
| 113 | /** | 126 | /** |
| 114 | * 页面加载时设置标题和初始化数据 | 127 | * 页面加载时设置标题和初始化数据 |
| ... | @@ -183,84 +196,7 @@ const initMockData = () => { | ... | @@ -183,84 +196,7 @@ const initMockData = () => { |
| 183 | * @param {number} index - 项目索引 | 196 | * @param {number} index - 项目索引 |
| 184 | */ | 197 | */ |
| 185 | const handleItemClick = (item, index) => { | 198 | const handleItemClick = (item, index) => { |
| 186 | - if (item.type === 'image') { | 199 | + handleMediaClick(item, albumList.value); |
| 187 | - // 图片预览 | ||
| 188 | - const imageItems = albumList.value.filter(item => item.type === 'image'); | ||
| 189 | - previewImages.value = imageItems.map(img => ({ src: img.url })); | ||
| 190 | - | ||
| 191 | - // 计算当前图片在图片列表中的索引 | ||
| 192 | - const imageIndex = imageItems.findIndex(img => img.url === item.url); | ||
| 193 | - previewIndex.value = imageIndex; | ||
| 194 | - previewVisible.value = true; | ||
| 195 | - } else if (item.type === 'video') { | ||
| 196 | - // 视频播放 | ||
| 197 | - currentVideo.value = item; | ||
| 198 | - videoId.value = Date.now(); // 生成新的视频ID | ||
| 199 | - videoVisible.value = true; | ||
| 200 | - } | ||
| 201 | -}; | ||
| 202 | - | ||
| 203 | -/** | ||
| 204 | - * 格式化视频时长 | ||
| 205 | - * @param {number} seconds - 秒数 | ||
| 206 | - * @returns {string} 格式化后的时长 | ||
| 207 | - */ | ||
| 208 | -const formatDuration = (seconds) => { | ||
| 209 | - const minutes = Math.floor(seconds / 60); | ||
| 210 | - const remainingSeconds = seconds % 60; | ||
| 211 | - return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`; | ||
| 212 | -}; | ||
| 213 | - | ||
| 214 | -/** | ||
| 215 | - * 关闭图片预览 | ||
| 216 | - */ | ||
| 217 | -const closePreview = () => { | ||
| 218 | - previewVisible.value = false; | ||
| 219 | -}; | ||
| 220 | - | ||
| 221 | -/** | ||
| 222 | - * 关闭视频播放 | ||
| 223 | - */ | ||
| 224 | -const closeVideo = () => { | ||
| 225 | - videoVisible.value = false; | ||
| 226 | - currentVideo.value = null; | ||
| 227 | -}; | ||
| 228 | - | ||
| 229 | -/** | ||
| 230 | - * 处理视频播放 | ||
| 231 | - */ | ||
| 232 | -const handleVideoPlay = () => { | ||
| 233 | - console.log('视频开始播放'); | ||
| 234 | -}; | ||
| 235 | - | ||
| 236 | -/** | ||
| 237 | - * 处理视频暂停 | ||
| 238 | - */ | ||
| 239 | -const handleVideoPause = () => { | ||
| 240 | - console.log('视频暂停播放'); | ||
| 241 | -}; | ||
| 242 | - | ||
| 243 | -/** | ||
| 244 | - * 处理全屏状态变化 | ||
| 245 | - * @param {Event} event - 全屏事件 | ||
| 246 | - */ | ||
| 247 | -const handleFullscreenChange = (event) => { | ||
| 248 | - console.log('全屏状态变化:', event.detail); | ||
| 249 | -}; | ||
| 250 | - | ||
| 251 | -/** | ||
| 252 | - * 处理视频播放错误 | ||
| 253 | - * @param {Event} error - 错误事件 | ||
| 254 | - */ | ||
| 255 | -const handleVideoError = (error) => { | ||
| 256 | - console.error('视频播放错误:', error); | ||
| 257 | - Taro.showToast({ | ||
| 258 | - title: '视频播放失败', | ||
| 259 | - icon: 'error', | ||
| 260 | - duration: 2000 | ||
| 261 | - }); | ||
| 262 | - // 关闭视频弹框 | ||
| 263 | - closeVideo(); | ||
| 264 | }; | 200 | }; |
| 265 | 201 | ||
| 266 | /** | 202 | /** | ... | ... |
| ... | @@ -89,26 +89,85 @@ | ... | @@ -89,26 +89,85 @@ |
| 89 | </view> | 89 | </view> |
| 90 | <p class="text-sm text-gray-500 mb-3">记录每一个家庭活动瞬间</p> | 90 | <p class="text-sm text-gray-500 mb-3">记录每一个家庭活动瞬间</p> |
| 91 | <view class="grid grid-cols-2 gap-3"> | 91 | <view class="grid grid-cols-2 gap-3"> |
| 92 | - <view class="rounded-lg overflow-hidden"> | 92 | + <view |
| 93 | - <image src="https://placehold.co/400x400/e2f3ff/0369a1?text=LFX&font=roboto" alt="家庭活动照片" class="w-full h-32 object-cover" /> | 93 | + v-for="(item, index) in albumData" |
| 94 | - </view> | 94 | + :key="index" |
| 95 | - <view class="rounded-lg overflow-hidden"> | 95 | + class="rounded-lg overflow-hidden h-32 relative cursor-pointer" |
| 96 | - <image src="https://placehold.co/400x400/e2f3ff/0369a1?text=LFX&font=roboto" alt="家庭活动照片" class="w-full h-32 object-cover" /> | 96 | + @click="handleMediaClick(item, albumData)" |
| 97 | + > | ||
| 98 | + <image | ||
| 99 | + :src="item.type === 'video' ? item.thumbnail : item.url" | ||
| 100 | + alt="家庭活动照片" | ||
| 101 | + class="w-full h-full object-cover rounded-lg" | ||
| 102 | + /> | ||
| 103 | + <!-- 视频标识 --> | ||
| 104 | + <view | ||
| 105 | + v-if="item.type === 'video'" | ||
| 106 | + class="absolute top-2 left-2 px-2 py-1 bg-black bg-opacity-70 rounded text-white text-xs" | ||
| 107 | + > | ||
| 108 | + 视频 | ||
| 109 | + </view> | ||
| 97 | </view> | 110 | </view> |
| 98 | </view> | 111 | </view> |
| 99 | </view> | 112 | </view> |
| 100 | 113 | ||
| 101 | <BottomNav /> | 114 | <BottomNav /> |
| 115 | + | ||
| 116 | + <!-- 图片预览 --> | ||
| 117 | + <nut-image-preview | ||
| 118 | + v-model:show="previewVisible" | ||
| 119 | + :images="previewImages" | ||
| 120 | + :init-no="previewIndex" | ||
| 121 | + @close="closePreview" | ||
| 122 | + /> | ||
| 123 | + | ||
| 124 | + <!-- 视频播放器 --> | ||
| 125 | + <view | ||
| 126 | + v-if="videoVisible" | ||
| 127 | + class="fixed inset-0 bg-black" | ||
| 128 | + style="z-index: 9999;" | ||
| 129 | + @click="closeVideo" | ||
| 130 | + > | ||
| 131 | + <!-- 关闭按钮 --> | ||
| 132 | + <view | ||
| 133 | + @click.stop="closeVideo" | ||
| 134 | + class="absolute top-4 right-4 w-10 h-10 bg-black bg-opacity-50 rounded-full flex items-center justify-center" | ||
| 135 | + style="z-index: 10000;" | ||
| 136 | + > | ||
| 137 | + <Close size="24" class="text-white" /> | ||
| 138 | + </view> | ||
| 139 | + | ||
| 140 | + <!-- 视频播放器 --> | ||
| 141 | + <video | ||
| 142 | + v-if="currentVideo" | ||
| 143 | + :id="'dashboard-video-' + videoId" | ||
| 144 | + :src="currentVideo.url" | ||
| 145 | + :poster="currentVideo.thumbnail" | ||
| 146 | + :controls="true" | ||
| 147 | + :autoplay="false" | ||
| 148 | + :show-center-play-btn="true" | ||
| 149 | + :show-play-btn="true" | ||
| 150 | + :object-fit="'contain'" | ||
| 151 | + :show-fullscreen-btn="true" | ||
| 152 | + style="width: 100vw; height: 100vh; position: absolute; top: 0; left: 0;" | ||
| 153 | + @click.stop | ||
| 154 | + @play="handleVideoPlay" | ||
| 155 | + @pause="handleVideoPause" | ||
| 156 | + @error="handleVideoError" | ||
| 157 | + @fullscreenchange="handleFullscreenChange" | ||
| 158 | + /> | ||
| 159 | + </view> | ||
| 102 | </view> | 160 | </view> |
| 103 | </template> | 161 | </template> |
| 104 | 162 | ||
| 105 | <script setup> | 163 | <script setup> |
| 106 | import { ref, computed, onMounted } from 'vue'; | 164 | import { ref, computed, onMounted } from 'vue'; |
| 107 | import Taro, { useDidShow } from '@tarojs/taro'; | 165 | import Taro, { useDidShow } from '@tarojs/taro'; |
| 108 | -import { Setting, Photograph, Right } from '@nutui/icons-vue-taro'; | 166 | +import { Setting, Photograph, Right, Close } from '@nutui/icons-vue-taro'; |
| 109 | import BottomNav from '../../components/BottomNav.vue'; | 167 | import BottomNav from '../../components/BottomNav.vue'; |
| 110 | import PointsCollector from '@/components/PointsCollector.vue' | 168 | import PointsCollector from '@/components/PointsCollector.vue' |
| 111 | import WeRunAuth from '@/components/WeRunAuth.vue' | 169 | import WeRunAuth from '@/components/WeRunAuth.vue' |
| 170 | +import { useMediaPreview } from '@/composables/useMediaPreview'; | ||
| 112 | 171 | ||
| 113 | const todaySteps = ref(0); | 172 | const todaySteps = ref(0); |
| 114 | const isWeRunAuthorized = ref(false); | 173 | const isWeRunAuthorized = ref(false); |
| ... | @@ -119,6 +178,39 @@ const familyName = ref('') | ... | @@ -119,6 +178,39 @@ const familyName = ref('') |
| 119 | const familySlogn = ref('') | 178 | const familySlogn = ref('') |
| 120 | const familyCover = ref('') | 179 | const familyCover = ref('') |
| 121 | 180 | ||
| 181 | +// 使用媒体预览 composable | ||
| 182 | +const { | ||
| 183 | + previewVisible, | ||
| 184 | + previewImages, | ||
| 185 | + previewIndex, | ||
| 186 | + videoVisible, | ||
| 187 | + currentVideo, | ||
| 188 | + videoId, | ||
| 189 | + handleMediaClick, | ||
| 190 | + closePreview, | ||
| 191 | + closeVideo, | ||
| 192 | + handleVideoPlay, | ||
| 193 | + handleVideoPause, | ||
| 194 | + handleFullscreenChange, | ||
| 195 | + handleVideoError | ||
| 196 | +} = useMediaPreview(); | ||
| 197 | + | ||
| 198 | +// 家庭相册数据 | ||
| 199 | +const albumData = ref([ | ||
| 200 | + { | ||
| 201 | + type: 'image', | ||
| 202 | + url: 'https://cdn.ipadbiz.cn/hager/0513-1_FsxMk28AGz6N_D1zZFFOl_EaRdss.png', | ||
| 203 | + createTime: '2024-01-15 10:30' | ||
| 204 | + }, | ||
| 205 | + { | ||
| 206 | + type: 'video', | ||
| 207 | + url: 'https://vjs.zencdn.net/v/oceans.mp4', | ||
| 208 | + thumbnail: 'https://cdn.ipadbiz.cn/hager/0513-1_FsxMk28AGz6N_D1zZFFOl_EaRdss.png', | ||
| 209 | + duration: 125, | ||
| 210 | + createTime: '2024-01-14 16:20' | ||
| 211 | + } | ||
| 212 | +]); | ||
| 213 | + | ||
| 122 | /** | 214 | /** |
| 123 | * 触发积分收集组件的一键收取 | 215 | * 触发积分收集组件的一键收取 |
| 124 | */ | 216 | */ | ... | ... |
-
Please register or login to post a comment