hookehuyr

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

实现媒体文件上传功能,包括图片和视频的选择、预览和上传
移除Dashboard页面的上传逻辑,改为跳转到新页面
添加文件大小和时长限制校验
......@@ -29,6 +29,7 @@ export default {
'pages/AlbumList/index',
'pages/ActivitiesCover/index',
'pages/PointsList/index',
'pages/UploadMedia/index',
],
window: {
backgroundTextStyle: 'light',
......
......@@ -60,7 +60,6 @@
<Photograph size="20" class="mr-2" />
拍照留念,奖励积分
</view>
<view class="text-xs text-gray-200 mt-1">支持jpg、png格式图片(≤10MB)或60秒内视频</view>
</view>
</view>
......@@ -206,36 +205,10 @@ const uploadFile = (filePath) => {
});
};
/**
* 打开拍照上传页面
*/
const openCamera = () => {
Taro.chooseMedia({
count: 1,
mediaType: ['image', 'video'],
sourceType: ['album', 'camera'],
maxDuration: 60,
sizeType: ['compressed'],
success: function (res) {
const tempFile = res.tempFiles[0];
const { tempFilePath, size, duration, fileType } = tempFile;
if (fileType === 'image') {
if (size > 10 * 1024 * 1024) {
showToast('图片大小不能超过10MB', 'error');
return;
}
}
if (fileType === 'video') {
if (duration > 60) {
showToast('视频时长不能超过60秒', 'error');
return;
}
}
uploadFile(tempFilePath);
},
fail: function () {
showToast('选择文件失败', 'error');
}
});
Taro.navigateTo({ url: '/pages/UploadMedia/index' });
}
</script>
......
export default {
navigationBarTitleText: '拍照留念',
navigationBarBackgroundColor: '#ffffff',
navigationBarTextStyle: 'black',
backgroundColor: '#f9fafb'
}
\ No newline at end of file
.upload-media-page {
min-height: 100vh;
background-color: #f9fafb;
}
.upload-area {
padding: 1rem;
}
.upload-button {
border: 2px dashed #d1d5db;
border-radius: 0.5rem;
padding: 2rem;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin-bottom: 1rem;
background-color: #ffffff;
transition: all 0.3s ease;
&:active {
background-color: #f3f4f6;
border-color: #9ca3af;
}
}
.preview-container {
margin-bottom: 1rem;
.preview-item {
position: relative;
border-radius: 0.5rem;
overflow: hidden;
background-color: #ffffff;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
}
.remove-button {
position: absolute;
top: 0.5rem;
right: 0.5rem;
width: 2rem;
height: 2rem;
background-color: rgba(0, 0, 0, 0.5);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
}
.video-overlay {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
.play-button {
width: 4rem;
height: 4rem;
background-color: rgba(0, 0, 0, 0.6);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
}
}
.file-info {
margin-top: 0.75rem;
padding: 0.75rem;
background-color: #ffffff;
border-radius: 0.5rem;
}
.action-buttons {
display: flex;
gap: 0.75rem;
.button {
flex: 1;
padding: 0.75rem;
border-radius: 0.5rem;
text-align: center;
font-weight: 500;
transition: all 0.3s ease;
&.secondary {
background-color: #f3f4f6;
color: #374151;
&:active {
background-color: #e5e7eb;
}
}
&.primary {
background-color: #3b82f6;
color: #ffffff;
&:active {
background-color: #2563eb;
}
}
}
}
.video-modal {
position: fixed;
inset: 0;
background-color: #000000;
z-index: 9999;
.close-button {
position: absolute;
top: 1rem;
right: 1rem;
width: 2.5rem;
height: 2.5rem;
background-color: rgba(0, 0, 0, 0.5);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
}
video {
width: 100vw;
height: 100vh;
position: absolute;
top: 0;
left: 0;
}
}
.preview-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 1);
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
.preview-container {
position: relative;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.preview-image {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
.close-btn {
position: absolute;
top: 20rpx;
right: 20rpx;
width: 60rpx;
height: 60rpx;
background: rgba(0, 0, 0, 0.5);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
}
}
\ No newline at end of file
<template>
<view class="min-h-screen bg-gray-50">
<!-- Upload Area -->
<view class="p-4">
<!-- Upload Button -->
<view
v-if="!uploadedFile"
class="border border-dashed border-gray-300 rounded-lg p-8 flex flex-col items-center justify-center mb-4 bg-white"
@click="chooseMedia"
>
<view class="text-gray-400 mb-4">
<Photograph size="48" />
</view>
<view class="text-center text-gray-600 mb-2">选择图片或视频</view>
<view class="text-center text-gray-400 text-sm">
支持图片格式(jpg、png)最大10MB或60秒内视频
</view>
</view>
<!-- Preview Area -->
<view v-if="uploadedFile" class="mb-4">
<!-- Image Preview -->
<view v-if="uploadedFile.type === 'image'" class="relative rounded-lg overflow-hidden bg-white shadow-sm">
<image
:src="uploadedFile.url"
class="w-full h-64 object-cover cursor-pointer"
mode="aspectFit"
@click="previewImage"
/>
<view
@click="removeFile"
class="absolute top-2 right-2 w-8 h-8 bg-black bg-opacity-50 rounded-full flex items-center justify-center"
>
<Close size="16" class="text-white" />
</view>
</view>
<!-- Video Preview -->
<view v-if="uploadedFile.type === 'video'" class="relative rounded-lg overflow-hidden bg-white shadow-sm">
<view
class="relative w-full h-64 bg-black rounded-lg flex items-center justify-center"
@click="playVideo"
>
<image
v-if="uploadedFile.thumbnail"
:src="uploadedFile.thumbnail"
class="w-full h-full object-cover"
mode="aspectFit"
/>
<view class="absolute inset-0 flex items-center justify-center">
<view class="w-16 h-16 bg-black bg-opacity-60 rounded-full flex items-center justify-center">
<text class="text-white text-2xl">▶</text>
</view>
</view>
</view>
<view
@click="removeFile"
class="absolute top-2 right-2 w-8 h-8 bg-black bg-opacity-50 rounded-full flex items-center justify-center"
>
<Close size="16" class="text-white" />
</view>
</view>
<!-- File Info -->
<view class="mt-3 p-3 bg-white rounded-lg">
<view class="text-sm text-gray-600">文件大小: {{ formatFileSize(uploadedFile.size) }}</view>
<view v-if="uploadedFile.type === 'video'" class="text-sm text-gray-600 mt-1">
时长: {{ formatDuration(uploadedFile.duration) }}
</view>
</view>
</view>
<!-- Action Buttons -->
<view class="flex gap-3">
<view
@click="chooseMedia"
class="flex-1 bg-gray-100 text-gray-700 py-3 rounded-lg text-center"
>
{{ uploadedFile ? '重新选择' : '选择文件' }}
</view>
<view
v-if="uploadedFile"
@click="saveMedia"
class="flex-1 bg-blue-500 text-white py-3 rounded-lg text-center"
>
保存
</view>
</view>
</view>
<!-- Video Player Modal -->
<view
v-if="videoVisible"
class="fixed inset-0 bg-black"
style="z-index: 9999;"
@click="closeVideo"
>
<!-- Close Button -->
<view
@click.stop="closeVideo"
class="absolute top-4 right-4 w-10 h-10 bg-black bg-opacity-50 rounded-full flex items-center justify-center"
style="z-index: 10000;"
>
<Close size="24" class="text-white" />
</view>
<!-- Video Player -->
<video
v-if="uploadedFile && uploadedFile.type === 'video'"
:id="'upload-video-' + videoId"
:src="uploadedFile.url"
:poster="uploadedFile.thumbnail"
:controls="true"
:autoplay="false"
:show-center-play-btn="true"
:show-play-btn="true"
:object-fit="'contain'"
:show-fullscreen-btn="true"
style="width: 100vw; height: 100vh; position: absolute; top: 0; left: 0;"
@click.stop
@play="handleVideoPlay"
@pause="handleVideoPause"
@error="handleVideoError"
@fullscreenchange="handleFullscreenChange"
/>
</view>
<!-- Image Preview Modal -->
<view
v-if="previewVisible"
class="fixed inset-0 bg-black"
style="z-index: 9999;"
@click="closePreview"
>
<!-- Close Button -->
<view
@click.stop="closePreview"
class="absolute top-4 right-4 w-10 h-10 bg-black bg-opacity-50 rounded-full flex items-center justify-center"
style="z-index: 10000;"
>
<Close size="24" class="text-white" />
</view>
<!-- Image Preview -->
<image
:src="previewImages[previewIndex]?.src"
class="w-full h-full object-contain"
mode="aspectFit"
@click.stop
/>
</view>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import Taro from '@tarojs/taro';
import { Left, Photograph, Close } from '@nutui/icons-vue-taro';
import BASE_URL from '@/utils/config';
// 响应式数据
const uploadedFile = ref(null);
const videoVisible = ref(false);
const videoId = ref(Date.now());
// 图片预览相关
const previewVisible = ref(false);
const previewImages = ref([]);
const previewIndex = ref(0);
/**
* 页面加载时设置标题
*/
onMounted(() => {
Taro.setNavigationBarTitle({ title: '拍照留念' });
});
/**
* 选择媒体文件(图片或视频)
*/
const chooseMedia = () => {
Taro.chooseMedia({
count: 1,
mediaType: ['image', 'video'],
sourceType: ['album', 'camera'],
maxDuration: 60,
sizeType: ['compressed'],
camera: 'back',
success: (res) => {
const file = res.tempFiles[0];
// 检查文件大小(10MB限制)
if (file.size > 10 * 1024 * 1024) {
Taro.showToast({
title: '文件大小不能超过10MB',
icon: 'error',
duration: 2000
});
return;
}
// 根据文件类型设置不同的信息
if (file.fileType === 'image') {
uploadedFile.value = {
type: 'image',
url: file.tempFilePath,
size: file.size,
name: `image_${Date.now()}.jpg`
};
} else if (file.fileType === 'video') {
uploadedFile.value = {
type: 'video',
url: file.tempFilePath,
thumbnail: file.thumbTempFilePath,
duration: Math.floor(file.duration),
size: file.size,
name: `video_${Date.now()}.mp4`
};
}
},
fail: (err) => {
console.error('选择媒体文件失败:', err);
Taro.showToast({
title: '选择文件失败',
icon: 'error',
duration: 2000
});
}
});
};
/**
* 移除文件
*/
const removeFile = () => {
uploadedFile.value = null;
};
/**
* 播放视频
*/
const playVideo = () => {
if (uploadedFile.value && uploadedFile.value.type === 'video') {
videoId.value = Date.now();
videoVisible.value = true;
}
};
/**
* 关闭视频播放
*/
const closeVideo = () => {
videoVisible.value = false;
};
/**
* 预览图片
*/
const previewImage = () => {
if (!uploadedFile.value || uploadedFile.value.type !== 'image') {
Taro.showToast({
title: '暂无图片可预览',
icon: 'error',
duration: 2000
});
return;
}
previewImages.value = [{ src: uploadedFile.value.url }];
previewIndex.value = 0;
previewVisible.value = true;
};
/**
* 关闭图片预览
*/
const closePreview = () => {
previewVisible.value = false;
};
/**
* 处理视频播放
*/
const handleVideoPlay = () => {
console.log('视频开始播放');
};
/**
* 处理视频暂停
*/
const handleVideoPause = () => {
console.log('视频暂停播放');
};
/**
* 处理全屏状态变化
* @param {Event} event - 全屏事件
*/
const handleFullscreenChange = (event) => {
console.log('全屏状态变化:', event.detail);
};
/**
* 处理视频播放错误
* @param {Event} error - 错误事件
*/
const handleVideoError = (error) => {
console.error('视频播放错误:', error);
Taro.showToast({
title: '视频播放失败',
icon: 'error',
duration: 2000
});
closeVideo();
};
/**
* 格式化文件大小
* @param {number} bytes - 字节数
* @returns {string} 格式化后的文件大小
*/
const formatFileSize = (bytes) => {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
/**
* 格式化视频时长
* @param {number} seconds - 秒数
* @returns {string} 格式化后的时长
*/
const formatDuration = (seconds) => {
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
};
/**
* 上传文件到服务器
* @param {string} filePath - 文件路径
*/
const uploadFileToServer = (filePath) => {
return new Promise((resolve, reject) => {
wx.uploadFile({
url: BASE_URL + '/admin/?m=srv&a=upload',
filePath,
name: 'file',
header: {
'content-type': 'multipart/form-data',
},
success: function (res) {
try {
let upload_data = JSON.parse(res.data);
if (res.statusCode === 200 && upload_data.data) {
resolve(upload_data.data.src);
} else {
reject(new Error('服务器错误'));
}
} catch (error) {
reject(new Error('解析响应数据失败'));
}
},
fail: function (error) {
reject(error);
}
});
});
};
/**
* 保存媒体文件
*/
const saveMedia = async () => {
if (!uploadedFile.value) {
Taro.showToast({
title: '请先选择文件',
icon: 'error',
duration: 2000
});
return;
}
Taro.showLoading({
title: '上传中...',
mask: true
});
try {
// 上传文件到服务器
const serverUrl = await uploadFileToServer(uploadedFile.value.url);
// 更新文件信息,保存服务器返回的URL
uploadedFile.value.serverUrl = serverUrl;
Taro.hideLoading();
Taro.showToast({
title: '保存成功,获得积分奖励!',
icon: 'success',
duration: 2000
});
// 延迟返回Dashboard页面
setTimeout(() => {
Taro.navigateBack();
}, 2000);
} catch (error) {
console.error('上传失败:', error);
Taro.hideLoading();
Taro.showToast({
title: '上传失败,请重试',
icon: 'error',
duration: 2000
});
}
};
/**
* 返回上一页
*/
const goBack = () => {
Taro.navigateBack();
};
</script>
<style scoped>
/* 自定义样式 */
</style>