hookehuyr

feat(视频上传): 新增视频上传功能及相关组件

新增视频上传页面、弹窗组件及视频播放器自动播放配置。添加相关依赖以支持视频上传功能。
This diff is collapsed. Click to expand it.
......@@ -24,6 +24,7 @@ declare module 'vue' {
SearchBar: typeof import('./components/ui/SearchBar.vue')['default']
SummerCampCard: typeof import('./components/ui/SummerCampCard.vue')['default']
TermsPopup: typeof import('./components/ui/TermsPopup.vue')['default']
UploadVideoPopup: typeof import('./components/ui/UploadVideoPopup.vue')['default']
VanButton: typeof import('vant/es')['Button']
VanCellGroup: typeof import('vant/es')['CellGroup']
VanCheckbox: typeof import('vant/es')['Checkbox']
......@@ -33,6 +34,7 @@ declare module 'vue' {
VanIcon: typeof import('vant/es')['Icon']
VanImage: typeof import('vant/es')['Image']
VanList: typeof import('vant/es')['List']
VanNavBar: typeof import('vant/es')['NavBar']
VanPickerGroup: typeof import('vant/es')['PickerGroup']
VanPopup: typeof import('vant/es')['Popup']
VanProgress: typeof import('vant/es')['Progress']
......
<template>
<van-popup
v-model:show="show"
position="bottom"
:style="{ height: '100%' }"
>
<div class="upload-video-popup">
<van-nav-bar
title="上传视频"
left-text="取消"
right-text="提交"
@click-left="onCancel"
@click-right="onSubmit"
/>
<div class="upload-content">
<van-uploader
:max-count="1"
:max-size="maxSize"
:before-read="beforeRead"
:after-read="afterRead"
accept="video/*"
@oversize="onOversize"
>
<van-button icon="plus" type="primary">上传视频</van-button>
</van-uploader>
<div v-if="uploadProgress > 0 && uploadProgress < 100" class="progress">
<van-progress :percentage="uploadProgress" :show-pivot="true" />
</div>
<div v-if="videoUrl" class="video-preview">
<VideoPlayer :video-url="videoUrl" :autoplay="false" />
</div>
</div>
</div>
</van-popup>
</template>
<script setup>
import { ref, defineProps, defineEmits, watch } from 'vue';
import { showToast } from 'vant';
import VideoPlayer from '@/components/ui/VideoPlayer.vue';
import { v4 as uuidv4 } from 'uuid';
const props = defineProps({
modelValue: {
type: Boolean,
required: true
}
});
const emit = defineEmits(['update:modelValue', 'submit', 'cancel']);
const show = ref(false);
watch(() => props.modelValue, (newVal) => {
show.value = newVal;
if (newVal) {
// 重置所有状态
videoUrl.value = '';
videoId.value = '';
videoName.value = '';
uploadProgress.value = 0;
}
});
watch(show, (newVal) => {
emit('update:modelValue', newVal);
});
const videoUrl = ref('');
const videoId = ref('');
const videoName = ref('');
const uploadProgress = ref(0);
const maxSize = 100 * 1024 * 1024; // 100MB
const beforeRead = (file) => {
if (!file.type.includes('video/')) {
showToast('请上传视频文件');
return false;
}
return true;
};
const afterRead = async (file) => {
const formData = new FormData();
formData.append('file', file.file);
try {
// 模拟上传进度
const timer = setInterval(() => {
uploadProgress.value += 10;
if (uploadProgress.value >= 100) {
clearInterval(timer);
// 模拟上传成功后的视频URL
videoUrl.value = URL.createObjectURL(file.file);
videoId.value = uuidv4();
videoName.value = file.file.name;
}
}, 300);
// TODO: 实际的上传逻辑
// const response = await uploadVideo(formData);
// videoUrl.value = response.data.url;
// videoId.value = uuidv4();
// videoName.value = file.file.name;
} catch (error) {
showToast('上传失败');
console.error('Upload error:', error);
}
};
const onOversize = () => {
showToast('文件大小不能超过100MB');
};
const onSubmit = () => {
if (!videoUrl.value || !videoId.value) {
showToast('请先上传视频');
return;
}
emit('submit', {
url: videoUrl.value,
id: videoId.value,
name: videoName.value
});
emit('update:modelValue', false);
};
const onCancel = () => {
emit('cancel');
emit('update:modelValue', false);
};
</script>
<style scoped>
.upload-video-popup {
display: flex;
flex-direction: column;
height: 100%;
}
.upload-content {
flex: 1;
padding: 16px;
overflow-y: auto;
}
.progress {
margin: 16px 0;
}
.video-preview {
margin-top: 16px;
width: 100%;
max-width: 600px;
}
</style>
......@@ -28,6 +28,11 @@ const props = defineProps({
type: String,
required: true,
},
autoplay: {
type: Boolean,
required: false,
default: true,
},
});
const emit = defineEmits(["onPlay", "onPause"]);
......@@ -39,7 +44,7 @@ const videoOptions = computed(() => ({
controls: true,
preload: "auto",
responsive: true,
autoplay: true,
autoplay: props.autoplay,
sources: [
{
src: props.videoUrl,
......@@ -68,7 +73,10 @@ const handleMounted = (payload) => {
state.value = payload.state;
player.value = payload.player;
if (player.value) {
// TAG: 自动播放
if (props.autoplay) {
player.value.play();
}
if (!wxInfo().isPc) {
// 添加touchstart事件监听
player.value.on('touchstart', (event) => {
......
/*
* @Date: 2025-03-20 20:36:36
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-03-21 17:58:10
* @LastEditTime: 2025-03-25 09:55:59
* @FilePath: /mlaj/src/router/index.js
* @Description: 文件描述
*/
......@@ -175,6 +175,12 @@ const routes = [
component: () => import('../views/test.vue'),
meta: { title: 'test' },
},
{
path: '/upload_video',
name: 'upload_video',
component: () => import('../views/upload_video.vue'),
meta: { title: 'upload_video' },
},
...checkinRoutes,
]
......
<!--
* @Date: 2025-03-24 13:04:21
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-03-24 14:05:18
* @LastEditTime: 2025-03-25 09:56:14
* @FilePath: /mlaj/src/views/profile/SettingsPage.vue
* @Description: 用户设置页面
-->
......@@ -53,6 +53,17 @@
<ChevronRightIcon class="w-5 h-5 text-gray-400" />
</div>
</div>
<!-- 视频上传 -->
<div class="p-4" @click="router.push('/upload_video')">
<div class="flex items-center justify-between">
<div>
<h3 class="text-base font-medium text-gray-900">视频上传</h3>
<p class="text-sm text-gray-500">视频上传</p>
</div>
<ChevronRightIcon class="w-5 h-5 text-gray-400" />
</div>
</div>
</div>
</FrostedGlass>
</div>
......
<template>
<div class="upload-video-container">
<van-button icon="plus" type="primary" @click="showUploadPopup = true">上传视频</van-button>
<div class="video-list">
<div v-for="video in videos" :key="video.id" class="video-preview">
<VideoPlayer :video-url="video.url" :autoplay="false" />
<div class="video-info">
<span class="video-name">{{ video.name }}</span>
<van-button icon="delete" type="danger" size="small" class="delete-btn" @click="deleteVideo(video.id)">删除</van-button>
</div>
</div>
</div>
<UploadVideoPopup
v-model="showUploadPopup"
@submit="onVideoUploaded"
@cancel="showUploadPopup = false"
/>
</div>
</template>
<script setup>
import { ref } from 'vue';
import VideoPlayer from '@/components/ui/VideoPlayer.vue';
import UploadVideoPopup from '@/components/ui/UploadVideoPopup.vue';
const showUploadPopup = ref(false);
const videos = ref([]);
const onVideoUploaded = (videoInfo) => {
videos.value.push(videoInfo);
};
const deleteVideo = (id) => {
const index = videos.value.findIndex(video => video.id === id);
if (index !== -1) {
const newVideos = [...videos.value];
newVideos.splice(index, 1);
videos.value = newVideos;
}
};
</script>
<style scoped>
.upload-video-container {
padding: 16px;
}
.video-list {
margin-top: 16px;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 16px;
}
.video-preview {
position: relative;
width: 100%;
}
.delete-btn {
position: absolute;
top: 8px;
right: 8px;
z-index: 1;
}
</style>