hookehuyr

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

新增视频上传页面、弹窗组件及视频播放器自动播放配置。添加相关依赖以支持视频上传功能。
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']
......
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) {
76 + // TAG: 自动播放
77 + if (props.autoplay) {
71 player.value.play(); 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>
......
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>