feat(视频上传): 新增视频上传功能及相关组件
新增视频上传页面、弹窗组件及视频播放器自动播放配置。添加相关依赖以支持视频上传功能。
Showing
7 changed files
with
258 additions
and
4 deletions
This diff is collapsed. Click to expand it.
| ... | @@ -24,6 +24,7 @@ declare module 'vue' { | ... | @@ -24,6 +24,7 @@ declare module 'vue' { |
| 24 | SearchBar: typeof import('./components/ui/SearchBar.vue')['default'] | 24 | SearchBar: typeof import('./components/ui/SearchBar.vue')['default'] |
| 25 | SummerCampCard: typeof import('./components/ui/SummerCampCard.vue')['default'] | 25 | SummerCampCard: typeof import('./components/ui/SummerCampCard.vue')['default'] |
| 26 | TermsPopup: typeof import('./components/ui/TermsPopup.vue')['default'] | 26 | TermsPopup: typeof import('./components/ui/TermsPopup.vue')['default'] |
| 27 | + UploadVideoPopup: typeof import('./components/ui/UploadVideoPopup.vue')['default'] | ||
| 27 | VanButton: typeof import('vant/es')['Button'] | 28 | VanButton: typeof import('vant/es')['Button'] |
| 28 | VanCellGroup: typeof import('vant/es')['CellGroup'] | 29 | VanCellGroup: typeof import('vant/es')['CellGroup'] |
| 29 | VanCheckbox: typeof import('vant/es')['Checkbox'] | 30 | VanCheckbox: typeof import('vant/es')['Checkbox'] |
| ... | @@ -33,6 +34,7 @@ declare module 'vue' { | ... | @@ -33,6 +34,7 @@ declare module 'vue' { |
| 33 | VanIcon: typeof import('vant/es')['Icon'] | 34 | VanIcon: typeof import('vant/es')['Icon'] |
| 34 | VanImage: typeof import('vant/es')['Image'] | 35 | VanImage: typeof import('vant/es')['Image'] |
| 35 | VanList: typeof import('vant/es')['List'] | 36 | VanList: typeof import('vant/es')['List'] |
| 37 | + VanNavBar: typeof import('vant/es')['NavBar'] | ||
| 36 | VanPickerGroup: typeof import('vant/es')['PickerGroup'] | 38 | VanPickerGroup: typeof import('vant/es')['PickerGroup'] |
| 37 | VanPopup: typeof import('vant/es')['Popup'] | 39 | VanPopup: typeof import('vant/es')['Popup'] |
| 38 | VanProgress: typeof import('vant/es')['Progress'] | 40 | VanProgress: typeof import('vant/es')['Progress'] | ... | ... |
src/components/ui/UploadVideoPopup.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <van-popup | ||
| 3 | + v-model:show="show" | ||
| 4 | + position="bottom" | ||
| 5 | + :style="{ height: '100%' }" | ||
| 6 | + > | ||
| 7 | + <div class="upload-video-popup"> | ||
| 8 | + <van-nav-bar | ||
| 9 | + title="上传视频" | ||
| 10 | + left-text="取消" | ||
| 11 | + right-text="提交" | ||
| 12 | + @click-left="onCancel" | ||
| 13 | + @click-right="onSubmit" | ||
| 14 | + /> | ||
| 15 | + | ||
| 16 | + <div class="upload-content"> | ||
| 17 | + <van-uploader | ||
| 18 | + :max-count="1" | ||
| 19 | + :max-size="maxSize" | ||
| 20 | + :before-read="beforeRead" | ||
| 21 | + :after-read="afterRead" | ||
| 22 | + accept="video/*" | ||
| 23 | + @oversize="onOversize" | ||
| 24 | + > | ||
| 25 | + <van-button icon="plus" type="primary">上传视频</van-button> | ||
| 26 | + </van-uploader> | ||
| 27 | + | ||
| 28 | + <div v-if="uploadProgress > 0 && uploadProgress < 100" class="progress"> | ||
| 29 | + <van-progress :percentage="uploadProgress" :show-pivot="true" /> | ||
| 30 | + </div> | ||
| 31 | + | ||
| 32 | + <div v-if="videoUrl" class="video-preview"> | ||
| 33 | + <VideoPlayer :video-url="videoUrl" :autoplay="false" /> | ||
| 34 | + </div> | ||
| 35 | + </div> | ||
| 36 | + </div> | ||
| 37 | + </van-popup> | ||
| 38 | +</template> | ||
| 39 | + | ||
| 40 | +<script setup> | ||
| 41 | +import { ref, defineProps, defineEmits, watch } from 'vue'; | ||
| 42 | +import { showToast } from 'vant'; | ||
| 43 | +import VideoPlayer from '@/components/ui/VideoPlayer.vue'; | ||
| 44 | +import { v4 as uuidv4 } from 'uuid'; | ||
| 45 | + | ||
| 46 | +const props = defineProps({ | ||
| 47 | + modelValue: { | ||
| 48 | + type: Boolean, | ||
| 49 | + required: true | ||
| 50 | + } | ||
| 51 | +}); | ||
| 52 | + | ||
| 53 | +const emit = defineEmits(['update:modelValue', 'submit', 'cancel']); | ||
| 54 | + | ||
| 55 | +const show = ref(false); | ||
| 56 | + | ||
| 57 | +watch(() => props.modelValue, (newVal) => { | ||
| 58 | + show.value = newVal; | ||
| 59 | + if (newVal) { | ||
| 60 | + // 重置所有状态 | ||
| 61 | + videoUrl.value = ''; | ||
| 62 | + videoId.value = ''; | ||
| 63 | + videoName.value = ''; | ||
| 64 | + uploadProgress.value = 0; | ||
| 65 | + } | ||
| 66 | +}); | ||
| 67 | + | ||
| 68 | +watch(show, (newVal) => { | ||
| 69 | + emit('update:modelValue', newVal); | ||
| 70 | +}); | ||
| 71 | + | ||
| 72 | +const videoUrl = ref(''); | ||
| 73 | +const videoId = ref(''); | ||
| 74 | +const videoName = ref(''); | ||
| 75 | +const uploadProgress = ref(0); | ||
| 76 | +const maxSize = 100 * 1024 * 1024; // 100MB | ||
| 77 | + | ||
| 78 | +const beforeRead = (file) => { | ||
| 79 | + if (!file.type.includes('video/')) { | ||
| 80 | + showToast('请上传视频文件'); | ||
| 81 | + return false; | ||
| 82 | + } | ||
| 83 | + return true; | ||
| 84 | +}; | ||
| 85 | + | ||
| 86 | +const afterRead = async (file) => { | ||
| 87 | + const formData = new FormData(); | ||
| 88 | + formData.append('file', file.file); | ||
| 89 | + | ||
| 90 | + try { | ||
| 91 | + // 模拟上传进度 | ||
| 92 | + const timer = setInterval(() => { | ||
| 93 | + uploadProgress.value += 10; | ||
| 94 | + if (uploadProgress.value >= 100) { | ||
| 95 | + clearInterval(timer); | ||
| 96 | + // 模拟上传成功后的视频URL | ||
| 97 | + videoUrl.value = URL.createObjectURL(file.file); | ||
| 98 | + videoId.value = uuidv4(); | ||
| 99 | + videoName.value = file.file.name; | ||
| 100 | + } | ||
| 101 | + }, 300); | ||
| 102 | + | ||
| 103 | + // TODO: 实际的上传逻辑 | ||
| 104 | + // const response = await uploadVideo(formData); | ||
| 105 | + // videoUrl.value = response.data.url; | ||
| 106 | + // videoId.value = uuidv4(); | ||
| 107 | + // videoName.value = file.file.name; | ||
| 108 | + } catch (error) { | ||
| 109 | + showToast('上传失败'); | ||
| 110 | + console.error('Upload error:', error); | ||
| 111 | + } | ||
| 112 | +}; | ||
| 113 | + | ||
| 114 | +const onOversize = () => { | ||
| 115 | + showToast('文件大小不能超过100MB'); | ||
| 116 | +}; | ||
| 117 | + | ||
| 118 | +const onSubmit = () => { | ||
| 119 | + if (!videoUrl.value || !videoId.value) { | ||
| 120 | + showToast('请先上传视频'); | ||
| 121 | + return; | ||
| 122 | + } | ||
| 123 | + emit('submit', { | ||
| 124 | + url: videoUrl.value, | ||
| 125 | + id: videoId.value, | ||
| 126 | + name: videoName.value | ||
| 127 | + }); | ||
| 128 | + emit('update:modelValue', false); | ||
| 129 | +}; | ||
| 130 | + | ||
| 131 | +const onCancel = () => { | ||
| 132 | + emit('cancel'); | ||
| 133 | + emit('update:modelValue', false); | ||
| 134 | +}; | ||
| 135 | +</script> | ||
| 136 | + | ||
| 137 | +<style scoped> | ||
| 138 | +.upload-video-popup { | ||
| 139 | + display: flex; | ||
| 140 | + flex-direction: column; | ||
| 141 | + height: 100%; | ||
| 142 | +} | ||
| 143 | + | ||
| 144 | +.upload-content { | ||
| 145 | + flex: 1; | ||
| 146 | + padding: 16px; | ||
| 147 | + overflow-y: auto; | ||
| 148 | +} | ||
| 149 | + | ||
| 150 | +.progress { | ||
| 151 | + margin: 16px 0; | ||
| 152 | +} | ||
| 153 | + | ||
| 154 | +.video-preview { | ||
| 155 | + margin-top: 16px; | ||
| 156 | + width: 100%; | ||
| 157 | + max-width: 600px; | ||
| 158 | +} | ||
| 159 | +</style> |
| ... | @@ -28,6 +28,11 @@ const props = defineProps({ | ... | @@ -28,6 +28,11 @@ const props = defineProps({ |
| 28 | type: String, | 28 | type: String, |
| 29 | required: true, | 29 | required: true, |
| 30 | }, | 30 | }, |
| 31 | + autoplay: { | ||
| 32 | + type: Boolean, | ||
| 33 | + required: false, | ||
| 34 | + default: true, | ||
| 35 | + }, | ||
| 31 | }); | 36 | }); |
| 32 | 37 | ||
| 33 | const emit = defineEmits(["onPlay", "onPause"]); | 38 | const emit = defineEmits(["onPlay", "onPause"]); |
| ... | @@ -39,7 +44,7 @@ const videoOptions = computed(() => ({ | ... | @@ -39,7 +44,7 @@ const videoOptions = computed(() => ({ |
| 39 | controls: true, | 44 | controls: true, |
| 40 | preload: "auto", | 45 | preload: "auto", |
| 41 | responsive: true, | 46 | responsive: true, |
| 42 | - autoplay: true, | 47 | + autoplay: props.autoplay, |
| 43 | sources: [ | 48 | sources: [ |
| 44 | { | 49 | { |
| 45 | src: props.videoUrl, | 50 | src: props.videoUrl, |
| ... | @@ -68,7 +73,10 @@ const handleMounted = (payload) => { | ... | @@ -68,7 +73,10 @@ const handleMounted = (payload) => { |
| 68 | state.value = payload.state; | 73 | state.value = payload.state; |
| 69 | player.value = payload.player; | 74 | player.value = payload.player; |
| 70 | if (player.value) { | 75 | if (player.value) { |
| 71 | - player.value.play(); | 76 | + // TAG: 自动播放 |
| 77 | + if (props.autoplay) { | ||
| 78 | + player.value.play(); | ||
| 79 | + } | ||
| 72 | if (!wxInfo().isPc) { | 80 | if (!wxInfo().isPc) { |
| 73 | // 添加touchstart事件监听 | 81 | // 添加touchstart事件监听 |
| 74 | player.value.on('touchstart', (event) => { | 82 | player.value.on('touchstart', (event) => { | ... | ... |
| 1 | /* | 1 | /* |
| 2 | * @Date: 2025-03-20 20:36:36 | 2 | * @Date: 2025-03-20 20:36:36 |
| 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com | 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com |
| 4 | - * @LastEditTime: 2025-03-21 17:58:10 | 4 | + * @LastEditTime: 2025-03-25 09:55:59 |
| 5 | * @FilePath: /mlaj/src/router/index.js | 5 | * @FilePath: /mlaj/src/router/index.js |
| 6 | * @Description: 文件描述 | 6 | * @Description: 文件描述 |
| 7 | */ | 7 | */ |
| ... | @@ -175,6 +175,12 @@ const routes = [ | ... | @@ -175,6 +175,12 @@ const routes = [ |
| 175 | component: () => import('../views/test.vue'), | 175 | component: () => import('../views/test.vue'), |
| 176 | meta: { title: 'test' }, | 176 | meta: { title: 'test' }, |
| 177 | }, | 177 | }, |
| 178 | + { | ||
| 179 | + path: '/upload_video', | ||
| 180 | + name: 'upload_video', | ||
| 181 | + component: () => import('../views/upload_video.vue'), | ||
| 182 | + meta: { title: 'upload_video' }, | ||
| 183 | + }, | ||
| 178 | ...checkinRoutes, | 184 | ...checkinRoutes, |
| 179 | ] | 185 | ] |
| 180 | 186 | ... | ... |
| 1 | <!-- | 1 | <!-- |
| 2 | * @Date: 2025-03-24 13:04:21 | 2 | * @Date: 2025-03-24 13:04:21 |
| 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com | 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com |
| 4 | - * @LastEditTime: 2025-03-24 14:05:18 | 4 | + * @LastEditTime: 2025-03-25 09:56:14 |
| 5 | * @FilePath: /mlaj/src/views/profile/SettingsPage.vue | 5 | * @FilePath: /mlaj/src/views/profile/SettingsPage.vue |
| 6 | * @Description: 用户设置页面 | 6 | * @Description: 用户设置页面 |
| 7 | --> | 7 | --> |
| ... | @@ -53,6 +53,17 @@ | ... | @@ -53,6 +53,17 @@ |
| 53 | <ChevronRightIcon class="w-5 h-5 text-gray-400" /> | 53 | <ChevronRightIcon class="w-5 h-5 text-gray-400" /> |
| 54 | </div> | 54 | </div> |
| 55 | </div> | 55 | </div> |
| 56 | + | ||
| 57 | + <!-- 视频上传 --> | ||
| 58 | + <div class="p-4" @click="router.push('/upload_video')"> | ||
| 59 | + <div class="flex items-center justify-between"> | ||
| 60 | + <div> | ||
| 61 | + <h3 class="text-base font-medium text-gray-900">视频上传</h3> | ||
| 62 | + <p class="text-sm text-gray-500">视频上传</p> | ||
| 63 | + </div> | ||
| 64 | + <ChevronRightIcon class="w-5 h-5 text-gray-400" /> | ||
| 65 | + </div> | ||
| 66 | + </div> | ||
| 56 | </div> | 67 | </div> |
| 57 | </FrostedGlass> | 68 | </FrostedGlass> |
| 58 | </div> | 69 | </div> | ... | ... |
src/views/upload_video.vue
0 → 100644
| 1 | +<template> | ||
| 2 | + <div class="upload-video-container"> | ||
| 3 | + <van-button icon="plus" type="primary" @click="showUploadPopup = true">上传视频</van-button> | ||
| 4 | + | ||
| 5 | + <div class="video-list"> | ||
| 6 | + <div v-for="video in videos" :key="video.id" class="video-preview"> | ||
| 7 | + <VideoPlayer :video-url="video.url" :autoplay="false" /> | ||
| 8 | + <div class="video-info"> | ||
| 9 | + <span class="video-name">{{ video.name }}</span> | ||
| 10 | + <van-button icon="delete" type="danger" size="small" class="delete-btn" @click="deleteVideo(video.id)">删除</van-button> | ||
| 11 | + </div> | ||
| 12 | + </div> | ||
| 13 | + </div> | ||
| 14 | + | ||
| 15 | + <UploadVideoPopup | ||
| 16 | + v-model="showUploadPopup" | ||
| 17 | + @submit="onVideoUploaded" | ||
| 18 | + @cancel="showUploadPopup = false" | ||
| 19 | + /> | ||
| 20 | + </div> | ||
| 21 | +</template> | ||
| 22 | + | ||
| 23 | +<script setup> | ||
| 24 | +import { ref } from 'vue'; | ||
| 25 | +import VideoPlayer from '@/components/ui/VideoPlayer.vue'; | ||
| 26 | +import UploadVideoPopup from '@/components/ui/UploadVideoPopup.vue'; | ||
| 27 | + | ||
| 28 | +const showUploadPopup = ref(false); | ||
| 29 | +const videos = ref([]); | ||
| 30 | + | ||
| 31 | +const onVideoUploaded = (videoInfo) => { | ||
| 32 | + videos.value.push(videoInfo); | ||
| 33 | +}; | ||
| 34 | + | ||
| 35 | +const deleteVideo = (id) => { | ||
| 36 | + const index = videos.value.findIndex(video => video.id === id); | ||
| 37 | + if (index !== -1) { | ||
| 38 | + const newVideos = [...videos.value]; | ||
| 39 | + newVideos.splice(index, 1); | ||
| 40 | + videos.value = newVideos; | ||
| 41 | + } | ||
| 42 | +}; | ||
| 43 | +</script> | ||
| 44 | + | ||
| 45 | +<style scoped> | ||
| 46 | +.upload-video-container { | ||
| 47 | + padding: 16px; | ||
| 48 | +} | ||
| 49 | + | ||
| 50 | +.video-list { | ||
| 51 | + margin-top: 16px; | ||
| 52 | + display: grid; | ||
| 53 | + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); | ||
| 54 | + gap: 16px; | ||
| 55 | +} | ||
| 56 | + | ||
| 57 | +.video-preview { | ||
| 58 | + position: relative; | ||
| 59 | + width: 100%; | ||
| 60 | +} | ||
| 61 | + | ||
| 62 | +.delete-btn { | ||
| 63 | + position: absolute; | ||
| 64 | + top: 8px; | ||
| 65 | + right: 8px; | ||
| 66 | + z-index: 1; | ||
| 67 | +} | ||
| 68 | +</style> |
-
Please register or login to post a comment