hookehuyr

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

将图片和视频预览相关的状态和方法从 AlbumList 和 Dashboard 组件中提取到 useMediaPreview composable
在 Dashboard 组件中实现媒体预览功能
import { ref } from 'vue';
import Taro from '@tarojs/taro';
/**
* 媒体预览 composable
* 提供图片和视频预览功能
*/
export function useMediaPreview() {
// 图片预览相关状态
const previewVisible = ref(false);
const previewImages = ref([]);
const previewIndex = ref(0);
// 视频预览相关状态
const videoVisible = ref(false);
const currentVideo = ref(null);
const videoId = ref(Date.now());
/**
* 处理媒体项目点击事件
* @param {Object} item - 媒体项目
* @param {Array} mediaList - 完整的媒体列表
*/
const handleMediaClick = (item, mediaList = []) => {
if (item.type === 'image') {
// 图片预览
const imageItems = mediaList.filter(media => media.type === 'image');
previewImages.value = imageItems.map(img => ({ src: img.url }));
// 计算当前图片在图片列表中的索引
const imageIndex = imageItems.findIndex(img => img.url === item.url);
previewIndex.value = imageIndex >= 0 ? imageIndex : 0;
previewVisible.value = true;
} else if (item.type === 'video') {
// 视频播放
currentVideo.value = item;
videoId.value = Date.now(); // 生成新的视频ID
videoVisible.value = true;
}
};
/**
* 预览单张图片
* @param {string} imageUrl - 图片URL
*/
const previewSingleImage = (imageUrl) => {
previewImages.value = [{ src: imageUrl }];
previewIndex.value = 0;
previewVisible.value = true;
};
/**
* 预览多张图片
* @param {Array} images - 图片URL数组
* @param {number} index - 初始显示的图片索引
*/
const previewMultipleImages = (images, index = 0) => {
previewImages.value = images.map(url => ({ src: url }));
previewIndex.value = index;
previewVisible.value = true;
};
/**
* 播放视频
* @param {Object} video - 视频对象 {url, thumbnail, duration}
*/
const playVideo = (video) => {
currentVideo.value = video;
videoId.value = Date.now();
videoVisible.value = true;
};
/**
* 关闭图片预览
*/
const closePreview = () => {
previewVisible.value = false;
};
/**
* 关闭视频播放
*/
const closeVideo = () => {
videoVisible.value = false;
currentVideo.value = null;
};
/**
* 处理视频播放
*/
const handleVideoPlay = () => {
// 视频开始播放
};
/**
* 处理视频暂停
*/
const handleVideoPause = () => {
// 视频暂停播放
};
/**
* 处理全屏状态变化
*/
const handleFullscreenChange = () => {
// 全屏状态变化
};
/**
* 处理视频播放错误
* @param {Event} error - 错误事件
*/
const handleVideoError = (error) => {
console.error('视频播放错误:', error);
Taro.showToast({
title: '视频播放失败',
icon: 'error',
duration: 2000
});
// 关闭视频弹框
closeVideo();
};
/**
* 格式化视频时长
* @param {number} seconds - 秒数
* @returns {string} 格式化后的时长
*/
const formatDuration = (seconds) => {
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
};
return {
// 状态
previewVisible,
previewImages,
previewIndex,
videoVisible,
currentVideo,
videoId,
// 方法
handleMediaClick,
previewSingleImage,
previewMultipleImages,
playVideo,
closePreview,
closeVideo,
handleVideoPlay,
handleVideoPause,
handleFullscreenChange,
handleVideoError,
formatDuration
};
}
\ No newline at end of file
......@@ -100,15 +100,28 @@
import { ref, onMounted } from 'vue';
import Taro from '@tarojs/taro';
import { Left, Service, Photograph, Close } from '@nutui/icons-vue-taro';
import { useMediaPreview } from '@/composables/useMediaPreview';
// 响应式数据
const albumList = ref([]);
const previewVisible = ref(false);
const previewImages = ref([]);
const previewIndex = ref(0);
const videoVisible = ref(false);
const currentVideo = ref(null);
const videoId = ref(Date.now());
// 使用媒体预览 composable
const {
previewVisible,
previewImages,
previewIndex,
videoVisible,
currentVideo,
videoId,
handleMediaClick,
closePreview,
closeVideo,
handleVideoPlay,
handleVideoPause,
handleFullscreenChange,
handleVideoError,
formatDuration
} = useMediaPreview();
/**
* 页面加载时设置标题和初始化数据
......@@ -183,84 +196,7 @@ const initMockData = () => {
* @param {number} index - 项目索引
*/
const handleItemClick = (item, index) => {
if (item.type === 'image') {
// 图片预览
const imageItems = albumList.value.filter(item => item.type === 'image');
previewImages.value = imageItems.map(img => ({ src: img.url }));
// 计算当前图片在图片列表中的索引
const imageIndex = imageItems.findIndex(img => img.url === item.url);
previewIndex.value = imageIndex;
previewVisible.value = true;
} else if (item.type === 'video') {
// 视频播放
currentVideo.value = item;
videoId.value = Date.now(); // 生成新的视频ID
videoVisible.value = true;
}
};
/**
* 格式化视频时长
* @param {number} seconds - 秒数
* @returns {string} 格式化后的时长
*/
const formatDuration = (seconds) => {
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
};
/**
* 关闭图片预览
*/
const closePreview = () => {
previewVisible.value = false;
};
/**
* 关闭视频播放
*/
const closeVideo = () => {
videoVisible.value = false;
currentVideo.value = null;
};
/**
* 处理视频播放
*/
const handleVideoPlay = () => {
console.log('视频开始播放');
};
/**
* 处理视频暂停
*/
const handleVideoPause = () => {
console.log('视频暂停播放');
};
/**
* 处理全屏状态变化
* @param {Event} event - 全屏事件
*/
const handleFullscreenChange = (event) => {
console.log('全屏状态变化:', event.detail);
};
/**
* 处理视频播放错误
* @param {Event} error - 错误事件
*/
const handleVideoError = (error) => {
console.error('视频播放错误:', error);
Taro.showToast({
title: '视频播放失败',
icon: 'error',
duration: 2000
});
// 关闭视频弹框
closeVideo();
handleMediaClick(item, albumList.value);
};
/**
......
......@@ -89,26 +89,85 @@
</view>
<p class="text-sm text-gray-500 mb-3">记录每一个家庭活动瞬间</p>
<view class="grid grid-cols-2 gap-3">
<view class="rounded-lg overflow-hidden">
<image src="https://placehold.co/400x400/e2f3ff/0369a1?text=LFX&font=roboto" alt="家庭活动照片" class="w-full h-32 object-cover" />
</view>
<view class="rounded-lg overflow-hidden">
<image src="https://placehold.co/400x400/e2f3ff/0369a1?text=LFX&font=roboto" alt="家庭活动照片" class="w-full h-32 object-cover" />
<view
v-for="(item, index) in albumData"
:key="index"
class="rounded-lg overflow-hidden h-32 relative cursor-pointer"
@click="handleMediaClick(item, albumData)"
>
<image
:src="item.type === 'video' ? item.thumbnail : item.url"
alt="家庭活动照片"
class="w-full h-full object-cover rounded-lg"
/>
<!-- 视频标识 -->
<view
v-if="item.type === 'video'"
class="absolute top-2 left-2 px-2 py-1 bg-black bg-opacity-70 rounded text-white text-xs"
>
视频
</view>
</view>
</view>
</view>
<BottomNav />
<!-- 图片预览 -->
<nut-image-preview
v-model:show="previewVisible"
:images="previewImages"
:init-no="previewIndex"
@close="closePreview"
/>
<!-- 视频播放器 -->
<view
v-if="videoVisible"
class="fixed inset-0 bg-black"
style="z-index: 9999;"
@click="closeVideo"
>
<!-- 关闭按钮 -->
<view
@click.stop="closeVideo"
class="absolute top-4 right-4 w-10 h-10 bg-black bg-opacity-50 rounded-full flex items-center justify-center"
style="z-index: 10000;"
>
<Close size="24" class="text-white" />
</view>
<!-- 视频播放器 -->
<video
v-if="currentVideo"
:id="'dashboard-video-' + videoId"
:src="currentVideo.url"
:poster="currentVideo.thumbnail"
:controls="true"
:autoplay="false"
:show-center-play-btn="true"
:show-play-btn="true"
:object-fit="'contain'"
:show-fullscreen-btn="true"
style="width: 100vw; height: 100vh; position: absolute; top: 0; left: 0;"
@click.stop
@play="handleVideoPlay"
@pause="handleVideoPause"
@error="handleVideoError"
@fullscreenchange="handleFullscreenChange"
/>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue';
import Taro, { useDidShow } from '@tarojs/taro';
import { Setting, Photograph, Right } from '@nutui/icons-vue-taro';
import { Setting, Photograph, Right, Close } from '@nutui/icons-vue-taro';
import BottomNav from '../../components/BottomNav.vue';
import PointsCollector from '@/components/PointsCollector.vue'
import WeRunAuth from '@/components/WeRunAuth.vue'
import { useMediaPreview } from '@/composables/useMediaPreview';
const todaySteps = ref(0);
const isWeRunAuthorized = ref(false);
......@@ -119,6 +178,39 @@ const familyName = ref('')
const familySlogn = ref('')
const familyCover = ref('')
// 使用媒体预览 composable
const {
previewVisible,
previewImages,
previewIndex,
videoVisible,
currentVideo,
videoId,
handleMediaClick,
closePreview,
closeVideo,
handleVideoPlay,
handleVideoPause,
handleFullscreenChange,
handleVideoError
} = useMediaPreview();
// 家庭相册数据
const albumData = ref([
{
type: 'image',
url: 'https://cdn.ipadbiz.cn/hager/0513-1_FsxMk28AGz6N_D1zZFFOl_EaRdss.png',
createTime: '2024-01-15 10:30'
},
{
type: 'video',
url: 'https://vjs.zencdn.net/v/oceans.mp4',
thumbnail: 'https://cdn.ipadbiz.cn/hager/0513-1_FsxMk28AGz6N_D1zZFFOl_EaRdss.png',
duration: 125,
createTime: '2024-01-14 16:20'
}
]);
/**
* 触发积分收集组件的一键收取
*/
......