hookehuyr

feat(上传): 添加独立的媒体上传页面

实现媒体文件上传功能,包括图片和视频的选择、预览和上传
移除Dashboard页面的上传逻辑,改为跳转到新页面
添加文件大小和时长限制校验
...@@ -29,6 +29,7 @@ export default { ...@@ -29,6 +29,7 @@ export default {
29 'pages/AlbumList/index', 29 'pages/AlbumList/index',
30 'pages/ActivitiesCover/index', 30 'pages/ActivitiesCover/index',
31 'pages/PointsList/index', 31 'pages/PointsList/index',
32 + 'pages/UploadMedia/index',
32 ], 33 ],
33 window: { 34 window: {
34 backgroundTextStyle: 'light', 35 backgroundTextStyle: 'light',
......
...@@ -60,7 +60,6 @@ ...@@ -60,7 +60,6 @@
60 <Photograph size="20" class="mr-2" /> 60 <Photograph size="20" class="mr-2" />
61 拍照留念,奖励积分 61 拍照留念,奖励积分
62 </view> 62 </view>
63 - <view class="text-xs text-gray-200 mt-1">支持jpg、png格式图片(≤10MB)或60秒内视频</view>
64 </view> 63 </view>
65 </view> 64 </view>
66 65
...@@ -206,36 +205,10 @@ const uploadFile = (filePath) => { ...@@ -206,36 +205,10 @@ const uploadFile = (filePath) => {
206 }); 205 });
207 }; 206 };
208 207
208 +/**
209 + * 打开拍照上传页面
210 + */
209 const openCamera = () => { 211 const openCamera = () => {
210 - Taro.chooseMedia({ 212 + Taro.navigateTo({ url: '/pages/UploadMedia/index' });
211 - count: 1,
212 - mediaType: ['image', 'video'],
213 - sourceType: ['album', 'camera'],
214 - maxDuration: 60,
215 - sizeType: ['compressed'],
216 - success: function (res) {
217 - const tempFile = res.tempFiles[0];
218 - const { tempFilePath, size, duration, fileType } = tempFile;
219 -
220 - if (fileType === 'image') {
221 - if (size > 10 * 1024 * 1024) {
222 - showToast('图片大小不能超过10MB', 'error');
223 - return;
224 - }
225 - }
226 -
227 - if (fileType === 'video') {
228 - if (duration > 60) {
229 - showToast('视频时长不能超过60秒', 'error');
230 - return;
231 - }
232 - }
233 -
234 - uploadFile(tempFilePath);
235 - },
236 - fail: function () {
237 - showToast('选择文件失败', 'error');
238 - }
239 - });
240 } 213 }
241 </script> 214 </script>
......
1 +export default {
2 + navigationBarTitleText: '拍照留念',
3 + navigationBarBackgroundColor: '#ffffff',
4 + navigationBarTextStyle: 'black',
5 + backgroundColor: '#f9fafb'
6 +}
...\ No newline at end of file ...\ No newline at end of file
1 +.upload-media-page {
2 + min-height: 100vh;
3 + background-color: #f9fafb;
4 +}
5 +
6 +.upload-area {
7 + padding: 1rem;
8 +}
9 +
10 +.upload-button {
11 + border: 2px dashed #d1d5db;
12 + border-radius: 0.5rem;
13 + padding: 2rem;
14 + display: flex;
15 + flex-direction: column;
16 + align-items: center;
17 + justify-content: center;
18 + margin-bottom: 1rem;
19 + background-color: #ffffff;
20 + transition: all 0.3s ease;
21 +
22 + &:active {
23 + background-color: #f3f4f6;
24 + border-color: #9ca3af;
25 + }
26 +}
27 +
28 +.preview-container {
29 + margin-bottom: 1rem;
30 +
31 + .preview-item {
32 + position: relative;
33 + border-radius: 0.5rem;
34 + overflow: hidden;
35 + background-color: #ffffff;
36 + box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
37 + }
38 +
39 + .remove-button {
40 + position: absolute;
41 + top: 0.5rem;
42 + right: 0.5rem;
43 + width: 2rem;
44 + height: 2rem;
45 + background-color: rgba(0, 0, 0, 0.5);
46 + border-radius: 50%;
47 + display: flex;
48 + align-items: center;
49 + justify-content: center;
50 + z-index: 10;
51 + }
52 +
53 + .video-overlay {
54 + position: absolute;
55 + inset: 0;
56 + display: flex;
57 + align-items: center;
58 + justify-content: center;
59 +
60 + .play-button {
61 + width: 4rem;
62 + height: 4rem;
63 + background-color: rgba(0, 0, 0, 0.6);
64 + border-radius: 50%;
65 + display: flex;
66 + align-items: center;
67 + justify-content: center;
68 + }
69 + }
70 +}
71 +
72 +.file-info {
73 + margin-top: 0.75rem;
74 + padding: 0.75rem;
75 + background-color: #ffffff;
76 + border-radius: 0.5rem;
77 +}
78 +
79 +.action-buttons {
80 + display: flex;
81 + gap: 0.75rem;
82 +
83 + .button {
84 + flex: 1;
85 + padding: 0.75rem;
86 + border-radius: 0.5rem;
87 + text-align: center;
88 + font-weight: 500;
89 + transition: all 0.3s ease;
90 +
91 + &.secondary {
92 + background-color: #f3f4f6;
93 + color: #374151;
94 +
95 + &:active {
96 + background-color: #e5e7eb;
97 + }
98 + }
99 +
100 + &.primary {
101 + background-color: #3b82f6;
102 + color: #ffffff;
103 +
104 + &:active {
105 + background-color: #2563eb;
106 + }
107 + }
108 + }
109 +}
110 +
111 +.video-modal {
112 + position: fixed;
113 + inset: 0;
114 + background-color: #000000;
115 + z-index: 9999;
116 +
117 + .close-button {
118 + position: absolute;
119 + top: 1rem;
120 + right: 1rem;
121 + width: 2.5rem;
122 + height: 2.5rem;
123 + background-color: rgba(0, 0, 0, 0.5);
124 + border-radius: 50%;
125 + display: flex;
126 + align-items: center;
127 + justify-content: center;
128 + z-index: 10000;
129 + }
130 +
131 + video {
132 + width: 100vw;
133 + height: 100vh;
134 + position: absolute;
135 + top: 0;
136 + left: 0;
137 + }
138 +}
139 +
140 +.preview-modal {
141 + position: fixed;
142 + top: 0;
143 + left: 0;
144 + width: 100%;
145 + height: 100%;
146 + background: rgba(0, 0, 0, 1);
147 + z-index: 9999;
148 + display: flex;
149 + align-items: center;
150 + justify-content: center;
151 +
152 + .preview-container {
153 + position: relative;
154 + width: 100%;
155 + height: 100%;
156 + display: flex;
157 + align-items: center;
158 + justify-content: center;
159 + }
160 +
161 + .preview-image {
162 + max-width: 100%;
163 + max-height: 100%;
164 + object-fit: contain;
165 + }
166 +
167 + .close-btn {
168 + position: absolute;
169 + top: 20rpx;
170 + right: 20rpx;
171 + width: 60rpx;
172 + height: 60rpx;
173 + background: rgba(0, 0, 0, 0.5);
174 + border-radius: 50%;
175 + display: flex;
176 + align-items: center;
177 + justify-content: center;
178 + z-index: 10000;
179 + }
180 +}
...\ No newline at end of file ...\ No newline at end of file
1 +<template>
2 + <view class="min-h-screen bg-gray-50">
3 +
4 + <!-- Upload Area -->
5 + <view class="p-4">
6 + <!-- Upload Button -->
7 + <view
8 + v-if="!uploadedFile"
9 + class="border border-dashed border-gray-300 rounded-lg p-8 flex flex-col items-center justify-center mb-4 bg-white"
10 + @click="chooseMedia"
11 + >
12 + <view class="text-gray-400 mb-4">
13 + <Photograph size="48" />
14 + </view>
15 + <view class="text-center text-gray-600 mb-2">选择图片或视频</view>
16 + <view class="text-center text-gray-400 text-sm">
17 + 支持图片格式(jpg、png)最大10MB或60秒内视频
18 + </view>
19 + </view>
20 +
21 + <!-- Preview Area -->
22 + <view v-if="uploadedFile" class="mb-4">
23 + <!-- Image Preview -->
24 + <view v-if="uploadedFile.type === 'image'" class="relative rounded-lg overflow-hidden bg-white shadow-sm">
25 + <image
26 + :src="uploadedFile.url"
27 + class="w-full h-64 object-cover cursor-pointer"
28 + mode="aspectFit"
29 + @click="previewImage"
30 + />
31 + <view
32 + @click="removeFile"
33 + class="absolute top-2 right-2 w-8 h-8 bg-black bg-opacity-50 rounded-full flex items-center justify-center"
34 + >
35 + <Close size="16" class="text-white" />
36 + </view>
37 + </view>
38 +
39 + <!-- Video Preview -->
40 + <view v-if="uploadedFile.type === 'video'" class="relative rounded-lg overflow-hidden bg-white shadow-sm">
41 + <view
42 + class="relative w-full h-64 bg-black rounded-lg flex items-center justify-center"
43 + @click="playVideo"
44 + >
45 + <image
46 + v-if="uploadedFile.thumbnail"
47 + :src="uploadedFile.thumbnail"
48 + class="w-full h-full object-cover"
49 + mode="aspectFit"
50 + />
51 + <view class="absolute inset-0 flex items-center justify-center">
52 + <view class="w-16 h-16 bg-black bg-opacity-60 rounded-full flex items-center justify-center">
53 + <text class="text-white text-2xl">▶</text>
54 + </view>
55 + </view>
56 + </view>
57 + <view
58 + @click="removeFile"
59 + class="absolute top-2 right-2 w-8 h-8 bg-black bg-opacity-50 rounded-full flex items-center justify-center"
60 + >
61 + <Close size="16" class="text-white" />
62 + </view>
63 + </view>
64 +
65 + <!-- File Info -->
66 + <view class="mt-3 p-3 bg-white rounded-lg">
67 + <view class="text-sm text-gray-600">文件大小: {{ formatFileSize(uploadedFile.size) }}</view>
68 + <view v-if="uploadedFile.type === 'video'" class="text-sm text-gray-600 mt-1">
69 + 时长: {{ formatDuration(uploadedFile.duration) }}
70 + </view>
71 + </view>
72 + </view>
73 +
74 + <!-- Action Buttons -->
75 + <view class="flex gap-3">
76 + <view
77 + @click="chooseMedia"
78 + class="flex-1 bg-gray-100 text-gray-700 py-3 rounded-lg text-center"
79 + >
80 + {{ uploadedFile ? '重新选择' : '选择文件' }}
81 + </view>
82 + <view
83 + v-if="uploadedFile"
84 + @click="saveMedia"
85 + class="flex-1 bg-blue-500 text-white py-3 rounded-lg text-center"
86 + >
87 + 保存
88 + </view>
89 + </view>
90 + </view>
91 +
92 + <!-- Video Player Modal -->
93 + <view
94 + v-if="videoVisible"
95 + class="fixed inset-0 bg-black"
96 + style="z-index: 9999;"
97 + @click="closeVideo"
98 + >
99 + <!-- Close Button -->
100 + <view
101 + @click.stop="closeVideo"
102 + class="absolute top-4 right-4 w-10 h-10 bg-black bg-opacity-50 rounded-full flex items-center justify-center"
103 + style="z-index: 10000;"
104 + >
105 + <Close size="24" class="text-white" />
106 + </view>
107 +
108 + <!-- Video Player -->
109 + <video
110 + v-if="uploadedFile && uploadedFile.type === 'video'"
111 + :id="'upload-video-' + videoId"
112 + :src="uploadedFile.url"
113 + :poster="uploadedFile.thumbnail"
114 + :controls="true"
115 + :autoplay="false"
116 + :show-center-play-btn="true"
117 + :show-play-btn="true"
118 + :object-fit="'contain'"
119 + :show-fullscreen-btn="true"
120 + style="width: 100vw; height: 100vh; position: absolute; top: 0; left: 0;"
121 + @click.stop
122 + @play="handleVideoPlay"
123 + @pause="handleVideoPause"
124 + @error="handleVideoError"
125 + @fullscreenchange="handleFullscreenChange"
126 + />
127 + </view>
128 +
129 + <!-- Image Preview Modal -->
130 + <view
131 + v-if="previewVisible"
132 + class="fixed inset-0 bg-black"
133 + style="z-index: 9999;"
134 + @click="closePreview"
135 + >
136 + <!-- Close Button -->
137 + <view
138 + @click.stop="closePreview"
139 + class="absolute top-4 right-4 w-10 h-10 bg-black bg-opacity-50 rounded-full flex items-center justify-center"
140 + style="z-index: 10000;"
141 + >
142 + <Close size="24" class="text-white" />
143 + </view>
144 +
145 + <!-- Image Preview -->
146 + <image
147 + :src="previewImages[previewIndex]?.src"
148 + class="w-full h-full object-contain"
149 + mode="aspectFit"
150 + @click.stop
151 + />
152 + </view>
153 + </view>
154 +</template>
155 +
156 +<script setup>
157 +import { ref, onMounted } from 'vue';
158 +import Taro from '@tarojs/taro';
159 +import { Left, Photograph, Close } from '@nutui/icons-vue-taro';
160 +import BASE_URL from '@/utils/config';
161 +
162 +// 响应式数据
163 +const uploadedFile = ref(null);
164 +const videoVisible = ref(false);
165 +const videoId = ref(Date.now());
166 +
167 +// 图片预览相关
168 +const previewVisible = ref(false);
169 +const previewImages = ref([]);
170 +const previewIndex = ref(0);
171 +
172 +/**
173 + * 页面加载时设置标题
174 + */
175 +onMounted(() => {
176 + Taro.setNavigationBarTitle({ title: '拍照留念' });
177 +});
178 +
179 +/**
180 + * 选择媒体文件(图片或视频)
181 + */
182 +const chooseMedia = () => {
183 + Taro.chooseMedia({
184 + count: 1,
185 + mediaType: ['image', 'video'],
186 + sourceType: ['album', 'camera'],
187 + maxDuration: 60,
188 + sizeType: ['compressed'],
189 + camera: 'back',
190 + success: (res) => {
191 + const file = res.tempFiles[0];
192 +
193 + // 检查文件大小(10MB限制)
194 + if (file.size > 10 * 1024 * 1024) {
195 + Taro.showToast({
196 + title: '文件大小不能超过10MB',
197 + icon: 'error',
198 + duration: 2000
199 + });
200 + return;
201 + }
202 +
203 + // 根据文件类型设置不同的信息
204 + if (file.fileType === 'image') {
205 + uploadedFile.value = {
206 + type: 'image',
207 + url: file.tempFilePath,
208 + size: file.size,
209 + name: `image_${Date.now()}.jpg`
210 + };
211 + } else if (file.fileType === 'video') {
212 + uploadedFile.value = {
213 + type: 'video',
214 + url: file.tempFilePath,
215 + thumbnail: file.thumbTempFilePath,
216 + duration: Math.floor(file.duration),
217 + size: file.size,
218 + name: `video_${Date.now()}.mp4`
219 + };
220 + }
221 + },
222 + fail: (err) => {
223 + console.error('选择媒体文件失败:', err);
224 + Taro.showToast({
225 + title: '选择文件失败',
226 + icon: 'error',
227 + duration: 2000
228 + });
229 + }
230 + });
231 +};
232 +
233 +/**
234 + * 移除文件
235 + */
236 +const removeFile = () => {
237 + uploadedFile.value = null;
238 +};
239 +
240 +/**
241 + * 播放视频
242 + */
243 +const playVideo = () => {
244 + if (uploadedFile.value && uploadedFile.value.type === 'video') {
245 + videoId.value = Date.now();
246 + videoVisible.value = true;
247 + }
248 +};
249 +
250 +/**
251 + * 关闭视频播放
252 + */
253 +const closeVideo = () => {
254 + videoVisible.value = false;
255 +};
256 +
257 +/**
258 + * 预览图片
259 + */
260 +const previewImage = () => {
261 + if (!uploadedFile.value || uploadedFile.value.type !== 'image') {
262 + Taro.showToast({
263 + title: '暂无图片可预览',
264 + icon: 'error',
265 + duration: 2000
266 + });
267 + return;
268 + }
269 + previewImages.value = [{ src: uploadedFile.value.url }];
270 + previewIndex.value = 0;
271 + previewVisible.value = true;
272 +};
273 +
274 +/**
275 + * 关闭图片预览
276 + */
277 +const closePreview = () => {
278 + previewVisible.value = false;
279 +};
280 +
281 +/**
282 + * 处理视频播放
283 + */
284 +const handleVideoPlay = () => {
285 + console.log('视频开始播放');
286 +};
287 +
288 +/**
289 + * 处理视频暂停
290 + */
291 +const handleVideoPause = () => {
292 + console.log('视频暂停播放');
293 +};
294 +
295 +/**
296 + * 处理全屏状态变化
297 + * @param {Event} event - 全屏事件
298 + */
299 +const handleFullscreenChange = (event) => {
300 + console.log('全屏状态变化:', event.detail);
301 +};
302 +
303 +/**
304 + * 处理视频播放错误
305 + * @param {Event} error - 错误事件
306 + */
307 +const handleVideoError = (error) => {
308 + console.error('视频播放错误:', error);
309 + Taro.showToast({
310 + title: '视频播放失败',
311 + icon: 'error',
312 + duration: 2000
313 + });
314 + closeVideo();
315 +};
316 +
317 +/**
318 + * 格式化文件大小
319 + * @param {number} bytes - 字节数
320 + * @returns {string} 格式化后的文件大小
321 + */
322 +const formatFileSize = (bytes) => {
323 + if (bytes === 0) return '0 B';
324 + const k = 1024;
325 + const sizes = ['B', 'KB', 'MB', 'GB'];
326 + const i = Math.floor(Math.log(bytes) / Math.log(k));
327 + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
328 +};
329 +
330 +/**
331 + * 格式化视频时长
332 + * @param {number} seconds - 秒数
333 + * @returns {string} 格式化后的时长
334 + */
335 +const formatDuration = (seconds) => {
336 + const minutes = Math.floor(seconds / 60);
337 + const remainingSeconds = seconds % 60;
338 + return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
339 +};
340 +
341 +/**
342 + * 上传文件到服务器
343 + * @param {string} filePath - 文件路径
344 + */
345 +const uploadFileToServer = (filePath) => {
346 + return new Promise((resolve, reject) => {
347 + wx.uploadFile({
348 + url: BASE_URL + '/admin/?m=srv&a=upload',
349 + filePath,
350 + name: 'file',
351 + header: {
352 + 'content-type': 'multipart/form-data',
353 + },
354 + success: function (res) {
355 + try {
356 + let upload_data = JSON.parse(res.data);
357 + if (res.statusCode === 200 && upload_data.data) {
358 + resolve(upload_data.data.src);
359 + } else {
360 + reject(new Error('服务器错误'));
361 + }
362 + } catch (error) {
363 + reject(new Error('解析响应数据失败'));
364 + }
365 + },
366 + fail: function (error) {
367 + reject(error);
368 + }
369 + });
370 + });
371 +};
372 +
373 +/**
374 + * 保存媒体文件
375 + */
376 +const saveMedia = async () => {
377 + if (!uploadedFile.value) {
378 + Taro.showToast({
379 + title: '请先选择文件',
380 + icon: 'error',
381 + duration: 2000
382 + });
383 + return;
384 + }
385 +
386 + Taro.showLoading({
387 + title: '上传中...',
388 + mask: true
389 + });
390 +
391 + try {
392 + // 上传文件到服务器
393 + const serverUrl = await uploadFileToServer(uploadedFile.value.url);
394 +
395 + // 更新文件信息,保存服务器返回的URL
396 + uploadedFile.value.serverUrl = serverUrl;
397 +
398 + Taro.hideLoading();
399 + Taro.showToast({
400 + title: '保存成功,获得积分奖励!',
401 + icon: 'success',
402 + duration: 2000
403 + });
404 +
405 + // 延迟返回Dashboard页面
406 + setTimeout(() => {
407 + Taro.navigateBack();
408 + }, 2000);
409 + } catch (error) {
410 + console.error('上传失败:', error);
411 + Taro.hideLoading();
412 + Taro.showToast({
413 + title: '上传失败,请重试',
414 + icon: 'error',
415 + duration: 2000
416 + });
417 + }
418 +};
419 +
420 +/**
421 + * 返回上一页
422 + */
423 +const goBack = () => {
424 + Taro.navigateBack();
425 +};
426 +</script>
427 +
428 +<style scoped>
429 +/* 自定义样式 */
430 +</style>