hookehuyr

refactor(media): 提取媒体预览逻辑到可复用 composable

将图片和视频预览相关的状态和方法从 AlbumList 和 Dashboard 组件中提取到 useMediaPreview composable
在 Dashboard 组件中实现媒体预览功能
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 */
......