hookehuyr

feat(文件预览): 添加音频和视频文件预览功能

实现文件点击预览功能,支持音频和视频文件的播放
添加音频播放器和视频播放器组件
处理多种文件URL获取方式
优化文件列表项的交互样式
......@@ -63,6 +63,7 @@
:before-read="beforeRead"
:after-read="afterRead"
@delete="onDelete"
@click-preview="onClickPreview"
multiple
:accept="getAcceptType()"
result-type="file"
......@@ -72,7 +73,7 @@
<!-- 文件列表显示 -->
<div v-if="fileList.length > 0" class="file-list">
<div v-for="(item, index) in fileList" :key="index" class="file-item">
<div class="file-info">
<div class="file-info" @click="previewFile(item)">
<van-icon :name="getFileIcon()" size="1rem" />
<span class="file-name">{{ item.name || item.file?.name }}</span>
<span class="file-status" :class="item.status">{{ item.message }}</span>
......@@ -116,16 +117,79 @@
<van-loading vertical color="#FFFFFF">上传中...</van-loading>
</div>
</van-overlay>
<!-- 音频播放器弹窗 -->
<van-popup
v-model:show="audioShow"
position="bottom"
round
closeable
:style="{ height: '60%', width: '100%' }"
>
<div class="p-4">
<h3 class="text-lg font-medium mb-4 text-center">{{ audioTitle }}</h3>
<AudioPlayer
v-if="audioShow && audioUrl"
:songs="[{ title: audioTitle, url: audioUrl }]"
class="w-full"
/>
</div>
</van-popup>
<!-- 视频播放器弹窗 -->
<van-popup
v-model:show="videoShow"
position="center"
round
closeable
:style="{ width: '95%', maxHeight: '80vh' }"
@close="stopVideoPlay"
>
<div class="p-4">
<h3 class="text-lg font-medium mb-4 text-center">视频预览</h3>
<div class="relative w-full bg-black rounded-lg overflow-hidden" style="aspect-ratio: 16/9;">
<!-- 视频封面 -->
<div
v-show="!isVideoPlaying"
class="absolute inset-0 flex items-center justify-center cursor-pointer"
@click="startVideoPlay"
>
<img :src="videoCover || 'https://cdn.ipadbiz.cn/mlaj/images/cover_video_2.png'"
:alt="videoTitle" class="w-full h-full object-cover" />
<div class="absolute inset-0 flex items-center justify-center bg-black/20">
<div
class="w-16 h-16 rounded-full bg-black/50 flex items-center justify-center hover:bg-black/70 transition-colors">
<van-icon name="play-circle-o" class="text-white" size="40" />
</div>
</div>
</div>
<!-- 视频播放器 -->
<VideoPlayer
v-show="isVideoPlaying"
ref="videoPlayerRef"
:video-url="videoUrl"
:video-id="videoTitle"
:autoplay="false"
class="w-full h-full"
@play="handleVideoPlay"
@pause="handleVideoPause"
/>
</div>
</div>
</van-popup>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { ref, computed, onMounted, nextTick } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { getTaskDetailAPI } from "@/api/checkin"
import { getTeacherFindSettingsAPI } from '@/api/teacher'
import { useTitle } from '@vueuse/core'
import { useCheckin } from '@/composables/useCheckin'
import AudioPlayer from '@/components/ui/AudioPlayer.vue'
import VideoPlayer from '@/components/ui/VideoPlayer.vue'
import { showToast } from 'vant'
import dayjs from 'dayjs'
const route = useRoute()
......@@ -159,6 +223,17 @@ const attachmentTypeOptions = ref([])
// 是否为编辑模式
const isEditMode = computed(() => route.query.status === 'edit')
// 预览相关变量
const audioShow = ref(false)
const audioTitle = ref('')
const audioUrl = ref('')
const videoShow = ref(false)
const videoTitle = ref('')
const videoUrl = ref('')
const videoCover = ref('')
const isVideoPlaying = ref(false)
const videoPlayerRef = ref(null)
/**
* 返回上一页
*/
......@@ -305,6 +380,247 @@ const getTaskDetail = async (month) => {
}
/**
* van-uploader点击预览事件处理
* @param {Object} file - 文件对象
* @param {Object} detail - 详细信息
*/
const onClickPreview = (file, detail) => {
console.log('onClickPreview - file:', file)
console.log('onClickPreview - detail:', detail)
console.log('file对象的所有属性:', Object.keys(file))
const fileName = file.name || file.file?.name || ''
// 尝试多种方式获取文件URL
let fileUrl = ''
// 方式1: 直接从file对象获取
if (file.url) {
fileUrl = file.url
console.log('从file.url获取URL:', fileUrl)
}
// 方式2: 从file.content获取
else if (file.content) {
fileUrl = file.content
console.log('从file.content获取URL:', fileUrl)
}
// 方式3: 从file.objectURL获取
else if (file.objectURL) {
fileUrl = file.objectURL
console.log('从file.objectURL获取URL:', fileUrl)
}
// 方式4: 从file.file获取
else if (file.file) {
if (file.file.url) {
fileUrl = file.file.url
console.log('从file.file.url获取URL:', fileUrl)
} else {
// 创建临时URL
try {
fileUrl = URL.createObjectURL(file.file)
console.log('通过URL.createObjectURL创建URL:', fileUrl)
} catch (error) {
console.error('创建ObjectURL失败:', error)
}
}
}
// 方式5: 检查是否有其他可能的URL字段
else {
const possibleUrlFields = ['src', 'path', 'value', 'href', 'link']
for (const field of possibleUrlFields) {
if (file[field]) {
fileUrl = file[field]
console.log(`从file.${field}获取URL:`, fileUrl)
break
}
}
}
console.log('最终提取的文件名:', fileName)
console.log('最终提取的文件URL:', fileUrl)
if (!fileUrl) {
console.warn('文件URL不存在,文件对象完整结构:', JSON.stringify(file, null, 2))
showToast('无法获取文件URL,请检查文件是否上传成功')
return
}
// 根据打卡类型或文件扩展名判断文件类型
if (activeType.value === 'audio' || isAudioFile(fileName)) {
console.log('准备播放音频:', fileName, fileUrl)
showAudio(fileName, fileUrl)
} else if (activeType.value === 'video' || isVideoFile(fileName)) {
console.log('准备播放视频:', fileName, fileUrl)
showVideo(fileName, fileUrl)
} else {
console.log('该文件类型不支持预览,文件名:', fileName, '打卡类型:', activeType.value)
showToast('该文件类型不支持预览')
}
}
/**
* 预览文件
* @param {Object} item - 文件项
*/
const previewFile = (item) => {
console.log('previewFile - item:', item)
console.log('previewFile - item对象的所有属性:', Object.keys(item))
const fileName = item.name || item.file?.name || ''
// 尝试多种方式获取文件URL
let fileUrl = ''
// 方式1: 直接从item对象获取
if (item.url) {
fileUrl = item.url
console.log('从item.url获取URL:', fileUrl)
}
// 方式2: 从item.value获取
else if (item.value) {
fileUrl = item.value
console.log('从item.value获取URL:', fileUrl)
}
// 方式3: 从item.content获取
else if (item.content) {
fileUrl = item.content
console.log('从item.content获取URL:', fileUrl)
}
// 方式4: 从item.objectURL获取
else if (item.objectURL) {
fileUrl = item.objectURL
console.log('从item.objectURL获取URL:', fileUrl)
}
// 方式5: 从item.file获取
else if (item.file) {
if (item.file.url) {
fileUrl = item.file.url
console.log('从item.file.url获取URL:', fileUrl)
} else {
// 创建临时URL
try {
fileUrl = URL.createObjectURL(item.file)
console.log('通过URL.createObjectURL创建URL:', fileUrl)
} catch (error) {
console.error('创建ObjectURL失败:', error)
}
}
}
// 方式6: 检查是否有其他可能的URL字段
else {
const possibleUrlFields = ['src', 'path', 'href', 'link', 'downloadUrl', 'previewUrl']
for (const field of possibleUrlFields) {
if (item[field]) {
fileUrl = item[field]
console.log(`从item.${field}获取URL:`, fileUrl)
break
}
}
}
console.log('最终提取的文件名:', fileName)
console.log('最终提取的文件URL:', fileUrl)
if (!fileUrl) {
console.warn('文件URL不存在,文件对象完整结构:', JSON.stringify(item, null, 2))
showToast('无法获取文件URL,请检查文件是否上传成功')
return
}
// 根据打卡类型或文件扩展名判断文件类型
if (activeType.value === 'audio' || isAudioFile(fileName)) {
console.log('准备播放音频:', fileName, fileUrl)
showAudio(fileName, fileUrl)
} else if (activeType.value === 'video' || isVideoFile(fileName)) {
console.log('准备播放视频:', fileName, fileUrl)
showVideo(fileName, fileUrl)
} else {
console.log('该文件类型不支持预览,文件名:', fileName, '打卡类型:', activeType.value)
showToast('该文件类型不支持预览')
}
}
/**
* 判断是否为音频文件
* @param {string} fileName - 文件名
* @returns {boolean}
*/
const isAudioFile = (fileName) => {
const audioExtensions = ['.mp3', '.wav', '.ogg', '.aac', '.m4a', '.flac']
return audioExtensions.some(ext => fileName.toLowerCase().includes(ext))
}
/**
* 判断是否为视频文件
* @param {string} fileName - 文件名
* @returns {boolean}
*/
const isVideoFile = (fileName) => {
const videoExtensions = ['.mp4', '.avi', '.mov', '.wmv', '.flv', '.webm', '.mkv']
return videoExtensions.some(ext => fileName.toLowerCase().includes(ext))
}
/**
* 显示音频播放器
* @param {string} title - 音频标题
* @param {string} url - 音频URL
*/
const showAudio = (title, url) => {
audioTitle.value = title
audioUrl.value = url
audioShow.value = true
}
/**
* 显示视频播放器
* @param {string} title - 视频标题
* @param {string} url - 视频URL
* @param {string} cover - 视频封面URL(可选)
*/
const showVideo = (title, url, cover = '') => {
videoTitle.value = title
videoUrl.value = url
videoCover.value = cover
videoShow.value = true
isVideoPlaying.value = false // 重置播放状态
}
/**
* 开始播放视频
*/
const startVideoPlay = async () => {
isVideoPlaying.value = true
await nextTick()
if (videoPlayerRef.value) {
videoPlayerRef.value.play()
}
}
/**
* 处理视频播放事件
*/
const handleVideoPlay = () => {
isVideoPlaying.value = true
}
/**
* 处理视频暂停事件
*/
const handleVideoPause = () => {
// 保持视频播放器可见,只在初始状态显示封面
}
/**
* 停止视频播放
*/
const stopVideoPlay = () => {
if (videoPlayerRef.value && typeof videoPlayerRef.value.pause === 'function') {
videoPlayerRef.value.pause()
}
isVideoPlaying.value = false
}
/**
* 页面挂载时的初始化逻辑
*/
onMounted(async () => {
......@@ -597,6 +913,14 @@ onMounted(async () => {
align-items: center;
flex: 1;
gap: 0.5rem;
cursor: pointer;
padding: 0.5rem;
border-radius: 0.5rem;
transition: background-color 0.2s;
&:hover {
background-color: #f5f5f5;
}
}
.file-name {
......