|
|
@@ -0,0 +1,875 @@
|
|
|
+<template>
|
|
|
+ <view class="media-uploader">
|
|
|
+ <view class="media-grid">
|
|
|
+ <view class="media-item" v-for="(item, index) in mediaList" :key="index">
|
|
|
+ <!-- 图片预览 -->
|
|
|
+ <image v-if="item.type === 'image'" :src="item.url" mode="aspectFill" @tap="previewMedia(index)"></image>
|
|
|
+ <!-- 视频预览 -->
|
|
|
+ <video v-else-if="item.type === 'video'" :id="'video-' + index" :src="item.url" class="video-preview" controls
|
|
|
+ show-center-play-btn enable-play-gesture show-fullscreen-btn object-fit="cover" :initial-time="0"
|
|
|
+ :show-mute-btn="true" :enable-progress-gesture="true" :vslide-gesture="true"
|
|
|
+ :vslide-gesture-in-fullscreen="true" @tap="previewVideo(index)" :poster="item.thumbnail"></video>
|
|
|
+ <!-- 删除按钮 -->
|
|
|
+ <view class="delete-btn" @tap.stop="deleteMedia(index)">
|
|
|
+ <uni-icons type="close" size="12" color="#fff"></uni-icons>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <!-- 上传按钮 -->
|
|
|
+ <view class="upload-trigger" v-if="mediaList.length < maxCount">
|
|
|
+ <view class="upload-box" @tap="chooseMedia">
|
|
|
+ <uni-icons type="upload" size="48" color="#999"></uni-icons>
|
|
|
+ <text class="upload-text">{{ mediaList.length }}/{{ maxCount }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <text class="upload-tip">最多上传{{ maxCount }}个文件</text>
|
|
|
+ </view>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+
|
|
|
+export default {
|
|
|
+ name: 'MediaUploader', // 组件名称
|
|
|
+ props: {
|
|
|
+ // 最大上传数量限制
|
|
|
+ maxCount: {
|
|
|
+ type: Number,
|
|
|
+ default: 9 // 默认最多9个文件
|
|
|
+ },
|
|
|
+ // 已有的媒体列表,支持v-model双向绑定
|
|
|
+ value: {
|
|
|
+ type: Array,
|
|
|
+ default: () => [] // 默认为空数组
|
|
|
+ },
|
|
|
+ // 文件上传的服务器地址(可选)
|
|
|
+ uploadUrl: {
|
|
|
+ type: String,
|
|
|
+ required: false // 不传则使用本地文件路径
|
|
|
+ },
|
|
|
+ // 认证token,用于上传时的身份验证(可选)
|
|
|
+ token: {
|
|
|
+ type: String,
|
|
|
+ required: false // 不传则不上传认证头
|
|
|
+ },
|
|
|
+ // 媒体类型限制,控制用户只能选择什么类型的文件
|
|
|
+ mediaType: {
|
|
|
+ type: String,
|
|
|
+ default: 'all', // 默认支持所有类型
|
|
|
+ validator: function (value) {
|
|
|
+ // 验证传入的值是否合法
|
|
|
+ return ['all', 'image', 'video'].includes(value)
|
|
|
+ }
|
|
|
+ },
|
|
|
+ // 是否启用媒体模式,true时显示"拍摄/相册"选择,false时直接选择文件
|
|
|
+ media: {
|
|
|
+ type: Boolean,
|
|
|
+ default: true // 默认启用媒体模式
|
|
|
+ }
|
|
|
+ },
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ mediaList: [] // 内部维护的媒体文件列表
|
|
|
+ }
|
|
|
+ },
|
|
|
+ watch: {
|
|
|
+ // 监听外部传入的value值变化
|
|
|
+ value: {
|
|
|
+ handler(newVal) {
|
|
|
+ // 确保传入的是数组格式
|
|
|
+ if (!Array.isArray(newVal)) {
|
|
|
+ console.warn('MediaUploader: value must be an array');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 将外部数据转换为内部格式
|
|
|
+ this.mediaList = newVal.map(item => {
|
|
|
+ // 如果已经是正确的对象格式(包含url和type),直接使用
|
|
|
+ if (typeof item === 'object' && item.url && item.type) {
|
|
|
+ return {
|
|
|
+ url: item.url,
|
|
|
+ type: item.type,
|
|
|
+ backendData: item.backendData || null // 保留后端数据
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果是字符串或其他格式,转换为标准对象格式
|
|
|
+ const url = typeof item === 'object' ? item.url : item;
|
|
|
+ return {
|
|
|
+ url: url, // 文件路径
|
|
|
+ type: this.getMediaType(item), // 自动判断文件类型
|
|
|
+ backendData: null // 没有后端数据
|
|
|
+ }
|
|
|
+ })
|
|
|
+ },
|
|
|
+ immediate: true // 组件创建时立即执行一次
|
|
|
+ }
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ /**
|
|
|
+ * 选择媒体文件的主入口方法
|
|
|
+ * 根据不同的配置和平台,调用相应的选择方法
|
|
|
+ */
|
|
|
+ async chooseMedia() {
|
|
|
+ try {
|
|
|
+ // 检查是否已达到最大上传数量限制
|
|
|
+ if (this.mediaList.length >= this.maxCount) {
|
|
|
+ uni.showToast({
|
|
|
+ title: `最多只能上传${this.maxCount}个文件`,
|
|
|
+ icon: 'none'
|
|
|
+ });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取当前运行平台信息,用于平台差异化处理
|
|
|
+ const platform = uni.getSystemInfoSync().platform;
|
|
|
+ const isH5 = platform === 'h5';
|
|
|
+
|
|
|
+ // 如果启用了媒体模式,显示"拍摄/相册"选择菜单
|
|
|
+ if (this.media) {
|
|
|
+ // 使用Promise包装uni.showActionSheet,确保跨平台兼容性
|
|
|
+ const res = await new Promise((resolve, reject) => {
|
|
|
+ uni.showActionSheet({
|
|
|
+ itemList: ['拍摄', '从相册选择'], // 显示两个选项
|
|
|
+ success: resolve,
|
|
|
+ fail: reject
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ if (res.tapIndex === 0) {
|
|
|
+ // 用户选择了"拍摄"选项
|
|
|
+ if (this.mediaType === 'video') {
|
|
|
+ this.captureVideo(); // 调用视频拍摄方法
|
|
|
+ } else {
|
|
|
+ this.captureImage(); // 调用图片拍摄方法
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 用户选择了"从相册选择"选项
|
|
|
+ this.chooseFromAlbum(); // 调用相册选择方法
|
|
|
+ }
|
|
|
+ return; // 媒体模式下处理完毕,直接返回
|
|
|
+ }
|
|
|
+
|
|
|
+ // 非媒体模式下的处理逻辑
|
|
|
+ if (this.mediaType === 'image') {
|
|
|
+ // 如果只允许选择图片,直接调用图片选择方法
|
|
|
+ this.chooseImage();
|
|
|
+ } else if (this.mediaType === 'video') {
|
|
|
+ // 如果只允许选择视频,根据平台调用不同的视频选择方法
|
|
|
+ if (isH5) {
|
|
|
+ this.chooseVideoH5(); // H5平台使用专门的视频选择方法
|
|
|
+ } else {
|
|
|
+ this.chooseVideoMini(); // 小程序和APP使用通用视频选择方法
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 如果允许选择所有类型('all'),显示类型选择菜单
|
|
|
+ const res = await new Promise((resolve, reject) => {
|
|
|
+ uni.showActionSheet({
|
|
|
+ itemList: ['选择图片', '选择视频'], // 让用户选择文件类型
|
|
|
+ success: resolve,
|
|
|
+ fail: reject
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ if (res.tapIndex === 0) {
|
|
|
+ // 用户选择了"选择图片"
|
|
|
+ this.chooseImage();
|
|
|
+ } else {
|
|
|
+ // 用户选择了"选择视频"
|
|
|
+ if (isH5) {
|
|
|
+ this.chooseVideoH5();
|
|
|
+ } else {
|
|
|
+ this.chooseVideoMini();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ // 捕获并记录选择过程中的错误
|
|
|
+ console.error('选择媒体文件失败:', e);
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 选择图片方法
|
|
|
+ * 调用系统图片选择器,支持从相册选择或拍照
|
|
|
+ */
|
|
|
+ async chooseImage() {
|
|
|
+ try {
|
|
|
+ // 使用Promise包装uni.chooseImage,确保跨平台兼容性
|
|
|
+ const res = await new Promise((resolve, reject) => {
|
|
|
+ uni.chooseImage({
|
|
|
+ count: this.maxCount - this.mediaList.length, // 可选择的图片数量
|
|
|
+ sizeType: ['original', 'compressed'], // 支持原图和压缩图
|
|
|
+ sourceType: ['album', 'camera'], // 支持从相册选择和拍照
|
|
|
+ success: resolve,
|
|
|
+ fail: reject
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ // 遍历选择的图片文件,逐个处理
|
|
|
+ for (let tempFilePath of res.tempFilePaths) {
|
|
|
+ try {
|
|
|
+ // 调用上传方法处理文件(可能是上传或本地路径)
|
|
|
+ const uploadResult = await this.uploadFile(tempFilePath);
|
|
|
+ // 创建新的媒体对象
|
|
|
+ const newMedia = {
|
|
|
+ url: uploadResult.data.link, // 使用前端文件路径进行预览
|
|
|
+ type: 'image', // 标记为图片类型
|
|
|
+ backendData: uploadResult.data.backendData // 保存后端返回的数据
|
|
|
+ };
|
|
|
+ // 更新媒体列表
|
|
|
+ this.updateMediaList([...this.mediaList, newMedia]);
|
|
|
+ } catch (uploadError) {
|
|
|
+ // 处理单个图片上传失败的情况
|
|
|
+ console.error('图片上传失败:', uploadError);
|
|
|
+ uni.showToast({
|
|
|
+ title: '图片上传失败',
|
|
|
+ icon: 'none'
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ // 处理图片选择失败的情况
|
|
|
+ console.error('选择图片失败:', e);
|
|
|
+ uni.showToast({
|
|
|
+ title: '选择图片失败',
|
|
|
+ icon: 'none'
|
|
|
+ });
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * H5平台专用视频选择方法
|
|
|
+ * H5平台对视频处理有特殊要求,需要单独处理
|
|
|
+ */
|
|
|
+ async chooseVideoH5() {
|
|
|
+ try {
|
|
|
+ // 使用Promise包装uni.chooseVideo,确保跨平台兼容性
|
|
|
+ const res = await new Promise((resolve, reject) => {
|
|
|
+ uni.chooseVideo({
|
|
|
+ sourceType: ['album', 'camera'], // 支持从相册选择和拍摄
|
|
|
+ compressed: true, // 启用压缩
|
|
|
+ camera: 'back', // 默认使用后置摄像头
|
|
|
+ success: resolve,
|
|
|
+ fail: reject
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ // 显示加载提示
|
|
|
+ uni.showLoading({
|
|
|
+ title: '处理中...'
|
|
|
+ });
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 处理视频文件(上传或本地路径)
|
|
|
+ const videoResult = await this.uploadFile(res.tempFilePath);
|
|
|
+
|
|
|
+ // 创建视频媒体对象
|
|
|
+ const newMedia = {
|
|
|
+ url: videoResult.data.link, // 使用前端文件路径进行预览
|
|
|
+ type: 'video', // 标记为视频类型
|
|
|
+ duration: Math.round(res.duration), // 视频时长(秒)
|
|
|
+ // H5平台暂时不支持视频预览图,可以后续在服务端处理
|
|
|
+ thumbnail: '',
|
|
|
+ backendData: videoResult.data.backendData // 保存后端返回的数据
|
|
|
+ };
|
|
|
+
|
|
|
+ // 更新媒体列表
|
|
|
+ this.updateMediaList([...this.mediaList, newMedia]);
|
|
|
+ uni.hideLoading(); // 隐藏加载提示
|
|
|
+ } catch (error) {
|
|
|
+ // 处理视频处理失败的情况
|
|
|
+ uni.hideLoading();
|
|
|
+ uni.showToast({
|
|
|
+ title: '视频处理失败',
|
|
|
+ icon: 'none'
|
|
|
+ });
|
|
|
+ console.error('视频处理失败:', error);
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ // 处理视频选择失败的情况
|
|
|
+ console.error('选择视频失败:', e);
|
|
|
+ uni.showToast({
|
|
|
+ title: '选择视频失败',
|
|
|
+ icon: 'none'
|
|
|
+ });
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 非H5平台视频选择方法(小程序和APP)
|
|
|
+ * 不同平台使用不同的API,需要条件编译处理
|
|
|
+ */
|
|
|
+ async chooseVideoMini() {
|
|
|
+ try {
|
|
|
+ // #ifdef MP-WEIXIN
|
|
|
+ // 微信小程序使用 chooseMedia API
|
|
|
+ wx.chooseMedia({
|
|
|
+ count: 1, // 一次只能选择一个视频
|
|
|
+ mediaType: ['video'], // 只选择视频类型
|
|
|
+ success: async (res) => {
|
|
|
+ const tempFile = res.tempFiles[0]; // 获取选择的视频文件
|
|
|
+ try {
|
|
|
+ // 显示加载提示
|
|
|
+ uni.showLoading({
|
|
|
+ title: '处理中...'
|
|
|
+ });
|
|
|
+
|
|
|
+ // 处理视频文件(上传或本地路径)
|
|
|
+ const videoResult = await this.uploadFile(tempFile.tempFilePath);
|
|
|
+
|
|
|
+ // 处理视频预览图(微信小程序会自动生成)
|
|
|
+ const thumbResult = await this.uploadFile(tempFile.thumbTempFilePath);
|
|
|
+
|
|
|
+ // 创建视频媒体对象
|
|
|
+ const newMedia = {
|
|
|
+ url: videoResult.data.link, // 使用前端文件路径进行预览
|
|
|
+ type: 'video', // 标记为视频类型
|
|
|
+ duration: Math.round(tempFile.duration), // 视频时长
|
|
|
+ thumbnail: thumbResult.data.backendData ? thumbResult.data.backendData.link : thumbResult.data.link, // 预览图路径
|
|
|
+ backendData: videoResult.data.backendData // 保存后端返回的数据
|
|
|
+ };
|
|
|
+
|
|
|
+ // 更新媒体列表
|
|
|
+ this.updateMediaList([...this.mediaList, newMedia]);
|
|
|
+ uni.hideLoading(); // 隐藏加载提示
|
|
|
+ } catch (error) {
|
|
|
+ // 处理视频处理失败的情况
|
|
|
+ uni.hideLoading();
|
|
|
+ uni.showToast({
|
|
|
+ title: '视频处理失败',
|
|
|
+ icon: 'none'
|
|
|
+ });
|
|
|
+ console.error('视频处理失败:', error);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ fail: (err) => {
|
|
|
+ // 处理视频选择失败的情况
|
|
|
+ console.error('选择视频失败:', err);
|
|
|
+ uni.showToast({
|
|
|
+ title: '选择视频失败',
|
|
|
+ icon: 'none'
|
|
|
+ });
|
|
|
+ }
|
|
|
+ });
|
|
|
+ // #endif
|
|
|
+
|
|
|
+ // #ifdef APP-PLUS
|
|
|
+ // APP端使用 uni.chooseVideo API
|
|
|
+ uni.chooseVideo({
|
|
|
+ sourceType: ['camera', 'album'], // 支持拍摄和从相册选择
|
|
|
+ compressed: true, // 启用压缩
|
|
|
+ success: async (res) => {
|
|
|
+ try {
|
|
|
+ // 显示加载提示
|
|
|
+ uni.showLoading({
|
|
|
+ title: '处理中...'
|
|
|
+ });
|
|
|
+
|
|
|
+ // 处理视频文件(上传或本地路径)
|
|
|
+ const videoResult = await this.uploadFile(res.tempFilePath);
|
|
|
+
|
|
|
+ // 获取视频缩略图(APP端需要手动获取)
|
|
|
+ const thumbPath = await this.getVideoThumb(res.tempFilePath);
|
|
|
+ let thumbnail = '';
|
|
|
+
|
|
|
+ if (thumbPath) {
|
|
|
+ try {
|
|
|
+ // 处理缩略图文件
|
|
|
+ const thumbResult = await this.uploadFile(thumbPath);
|
|
|
+ thumbnail = thumbResult.data.link; // 使用前端文件路径
|
|
|
+ } catch (error) {
|
|
|
+ console.error('缩略图上传失败:', error);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 创建视频媒体对象
|
|
|
+ const newMedia = {
|
|
|
+ url: videoResult.data.link, // 使用前端文件路径进行预览
|
|
|
+ type: 'video', // 标记为视频类型
|
|
|
+ duration: Math.round(res.duration || 0), // 视频时长
|
|
|
+ thumbnail: thumbnail || videoResult.data.link, // 如果没有缩略图,使用视频地址作为预览图
|
|
|
+ backendData: videoResult.data.backendData // 保存后端返回的数据
|
|
|
+ };
|
|
|
+
|
|
|
+ // 更新媒体列表
|
|
|
+ this.updateMediaList([...this.mediaList, newMedia]);
|
|
|
+ uni.hideLoading(); // 隐藏加载提示
|
|
|
+ } catch (error) {
|
|
|
+ // 处理视频处理失败的情况
|
|
|
+ uni.hideLoading();
|
|
|
+ uni.showToast({
|
|
|
+ title: '视频处理失败',
|
|
|
+ icon: 'none'
|
|
|
+ });
|
|
|
+ console.error('视频处理失败:', error);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ fail: (err) => {
|
|
|
+ // 处理视频选择失败的情况
|
|
|
+ console.error('选择视频失败:', err);
|
|
|
+ uni.showToast({
|
|
|
+ title: '选择视频失败',
|
|
|
+ icon: 'none'
|
|
|
+ });
|
|
|
+ }
|
|
|
+ });
|
|
|
+ // #endif
|
|
|
+
|
|
|
+ // #ifdef H5
|
|
|
+ // 如果在H5环境下不小心调用了这个方法,就调用H5的方法
|
|
|
+ this.chooseVideoH5();
|
|
|
+ // #endif
|
|
|
+ } catch (e) {
|
|
|
+ console.error('选择视频失败:', e);
|
|
|
+ uni.showToast({
|
|
|
+ title: '选择视频失败',
|
|
|
+ icon: 'none'
|
|
|
+ });
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 拍摄图片方法
|
|
|
+ * 调用相机拍摄新图片
|
|
|
+ */
|
|
|
+ async captureImage() {
|
|
|
+ try {
|
|
|
+ // 使用Promise包装uni.chooseImage,确保跨平台兼容性
|
|
|
+ const res = await new Promise((resolve, reject) => {
|
|
|
+ uni.chooseImage({
|
|
|
+ count: 1, // 一次只能拍摄一张图片
|
|
|
+ sizeType: ['original', 'compressed'], // 支持原图和压缩图
|
|
|
+ sourceType: ['camera'], // 只使用相机拍摄
|
|
|
+ success: resolve,
|
|
|
+ fail: reject
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ // 处理拍摄的图片文件
|
|
|
+ for (let tempFilePath of res.tempFilePaths) {
|
|
|
+ try {
|
|
|
+ // 处理图片文件(上传或本地路径)
|
|
|
+ const uploadResult = await this.uploadFile(tempFilePath);
|
|
|
+ // 创建图片媒体对象
|
|
|
+ const newMedia = {
|
|
|
+ url: uploadResult.data.link, // 使用前端文件路径进行预览
|
|
|
+ type: 'image', // 标记为图片类型
|
|
|
+ backendData: uploadResult.data.backendData // 保存后端返回的数据
|
|
|
+ };
|
|
|
+ // 更新媒体列表
|
|
|
+ this.updateMediaList([...this.mediaList, newMedia]);
|
|
|
+ } catch (uploadError) {
|
|
|
+ // 处理图片处理失败的情况
|
|
|
+ console.error('图片上传失败:', uploadError);
|
|
|
+ uni.showToast({
|
|
|
+ title: '图片上传失败',
|
|
|
+ icon: 'none'
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ // 处理拍摄失败的情况
|
|
|
+ console.error('拍摄图片失败:', e);
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 从相册选择文件方法(支持图片和视频)
|
|
|
+ * 不同平台使用不同的API,需要条件编译处理
|
|
|
+ */
|
|
|
+ async chooseFromAlbum() {
|
|
|
+ // #ifdef MP-WEIXIN
|
|
|
+ // 微信小程序使用 chooseMedia API
|
|
|
+ try {
|
|
|
+ const res = await new Promise((resolve, reject) => {
|
|
|
+ wx.chooseMedia({
|
|
|
+ count: this.maxCount - this.mediaList.length, // 可选择的文件数量
|
|
|
+ mediaType: ['image', 'video'], // 支持图片和视频
|
|
|
+ sourceType: ['album'], // 只从相册选择
|
|
|
+ success: resolve,
|
|
|
+ fail: reject
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ // 遍历选择的文件,逐个处理
|
|
|
+ for (let tempFile of res.tempFiles) {
|
|
|
+ try {
|
|
|
+ // 显示加载提示
|
|
|
+ uni.showLoading({ title: '处理中...' });
|
|
|
+
|
|
|
+ // 处理文件(上传或本地路径)
|
|
|
+ const fileResult = await this.uploadFile(tempFile.tempFilePath);
|
|
|
+
|
|
|
+ if (tempFile.fileType === 'video') {
|
|
|
+ // 处理视频文件
|
|
|
+ const thumbPath = tempFile.thumbTempFilePath; // 微信小程序自动生成的预览图
|
|
|
+ let thumbnail = '';
|
|
|
+ if (thumbPath) {
|
|
|
+ try {
|
|
|
+ // 处理预览图文件
|
|
|
+ const thumbResult = await this.uploadFile(thumbPath);
|
|
|
+ thumbnail = thumbResult.data.link; // 使用前端文件路径
|
|
|
+ } catch (error) {
|
|
|
+ console.error('缩略图上传失败:', error);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 创建视频媒体对象
|
|
|
+ const newMedia = {
|
|
|
+ url: fileResult.data.link, // 使用前端文件路径进行预览
|
|
|
+ type: 'video', // 标记为视频类型
|
|
|
+ duration: Math.round(tempFile.duration || 0), // 视频时长
|
|
|
+ thumbnail: thumbnail || fileResult.data.link, // 预览图路径
|
|
|
+ backendData: fileResult.data.backendData // 保存后端返回的数据
|
|
|
+ };
|
|
|
+ this.updateMediaList([...this.mediaList, newMedia]);
|
|
|
+ } else {
|
|
|
+ // 处理图片文件
|
|
|
+ const newMedia = {
|
|
|
+ url: fileResult.data.link, // 使用前端文件路径进行预览
|
|
|
+ type: 'image', // 标记为图片类型
|
|
|
+ backendData: fileResult.data.backendData // 保存后端返回的数据
|
|
|
+ };
|
|
|
+ this.updateMediaList([...this.mediaList, newMedia]);
|
|
|
+ }
|
|
|
+
|
|
|
+ uni.hideLoading(); // 隐藏加载提示
|
|
|
+ } catch (error) {
|
|
|
+ // 处理文件处理失败的情况
|
|
|
+ uni.hideLoading();
|
|
|
+ uni.showToast({
|
|
|
+ title: '上传失败',
|
|
|
+ icon: 'none'
|
|
|
+ });
|
|
|
+ console.error('上传失败:', error);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ console.error('选择失败:', e);
|
|
|
+ uni.showToast({
|
|
|
+ title: '选择失败',
|
|
|
+ icon: 'none'
|
|
|
+ });
|
|
|
+ }
|
|
|
+ // #endif
|
|
|
+
|
|
|
+ // #ifdef APP-PLUS || H5
|
|
|
+ // APP和H5使用系统相册
|
|
|
+ try {
|
|
|
+ const res = await uni.chooseImage({
|
|
|
+ count: this.maxCount - this.mediaList.length,
|
|
|
+ sizeType: ['original', 'compressed'],
|
|
|
+ sourceType: ['album']
|
|
|
+ });
|
|
|
+
|
|
|
+ // 上传图片
|
|
|
+ for (let tempFilePath of res.tempFilePaths) {
|
|
|
+ try {
|
|
|
+ const uploadResult = await this.uploadFile(tempFilePath);
|
|
|
+ const newMedia = {
|
|
|
+ url: uploadResult.data.link, // 使用前端文件路径进行预览
|
|
|
+ type: 'image',
|
|
|
+ backendData: uploadResult.data.backendData // 保存后端返回的数据
|
|
|
+ };
|
|
|
+ this.updateMediaList([...this.mediaList, newMedia]);
|
|
|
+ } catch (uploadError) {
|
|
|
+ console.error('上传失败:', uploadError);
|
|
|
+ uni.showToast({
|
|
|
+ title: '上传失败',
|
|
|
+ icon: 'none'
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ console.error('选择图片失败:', e);
|
|
|
+ uni.showToast({
|
|
|
+ title: '选择图片失败',
|
|
|
+ icon: 'none'
|
|
|
+ });
|
|
|
+ }
|
|
|
+ // #endif
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取视频缩略图方法
|
|
|
+ * 不同平台使用不同的API获取视频预览图
|
|
|
+ * @param {string} videoPath 视频文件路径
|
|
|
+ * @returns {Promise<string>} 缩略图路径
|
|
|
+ */
|
|
|
+ async getVideoThumb(videoPath) {
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ try {
|
|
|
+ // #ifdef APP-PLUS
|
|
|
+ // APP端优先使用 plus.io.getVideoInfo API
|
|
|
+ plus.io.getVideoInfo({
|
|
|
+ filePath: videoPath,
|
|
|
+ success: (info) => {
|
|
|
+ if (info.thumbnail) {
|
|
|
+ resolve(info.thumbnail); // 返回获取到的缩略图路径
|
|
|
+ } else {
|
|
|
+ // 如果获取不到缩略图,尝试使用 uni.getVideoInfo
|
|
|
+ this.getVideoThumbUnified(videoPath).then(resolve).catch(() => resolve(''));
|
|
|
+ }
|
|
|
+ },
|
|
|
+ error: () => {
|
|
|
+ // 如果 plus.io.getVideoInfo 失败,尝试使用 uni.getVideoInfo
|
|
|
+ this.getVideoThumbUnified(videoPath).then(resolve).catch(() => resolve(''));
|
|
|
+ }
|
|
|
+ });
|
|
|
+ // #endif
|
|
|
+
|
|
|
+ // #ifndef APP-PLUS
|
|
|
+ // 非APP平台直接使用 uni.getVideoInfo
|
|
|
+ this.getVideoThumbUnified(videoPath).then(resolve).catch(() => resolve(''));
|
|
|
+ // #endif
|
|
|
+ } catch (e) {
|
|
|
+ // 处理获取缩略图失败的情况
|
|
|
+ console.error('获取视频缩略图失败:', e);
|
|
|
+ resolve(''); // 失败时返回空字符串
|
|
|
+ }
|
|
|
+ });
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 统一的视频缩略图获取方法
|
|
|
+ * 使用 uni.getVideoInfo API 获取视频信息
|
|
|
+ * @param {string} videoPath 视频文件路径
|
|
|
+ * @returns {Promise<string>} 缩略图路径
|
|
|
+ */
|
|
|
+ async getVideoThumbUnified(videoPath) {
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ uni.getVideoInfo({
|
|
|
+ src: videoPath, // 视频文件路径
|
|
|
+ success: (res) => {
|
|
|
+ if (res.thumbTempFilePath) {
|
|
|
+ resolve(res.thumbTempFilePath); // 返回缩略图路径
|
|
|
+ } else {
|
|
|
+ reject(new Error('No thumbnail')); // 没有缩略图时抛出错误
|
|
|
+ }
|
|
|
+ },
|
|
|
+ fail: reject // 获取失败时抛出错误
|
|
|
+ });
|
|
|
+ });
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 文件上传方法
|
|
|
+ * 支持上传到服务器或使用本地文件路径
|
|
|
+ * @param {string} filePath 文件路径
|
|
|
+ * @returns {Promise<Object>} 上传结果,包含文件链接和后端数据
|
|
|
+ */
|
|
|
+ async uploadFile(filePath) {
|
|
|
+ // 如果没有配置上传URL,直接返回本地文件路径(不上传)
|
|
|
+ if (!this.uploadUrl) {
|
|
|
+ return {
|
|
|
+ data: {
|
|
|
+ link: filePath, // 直接使用本地文件路径
|
|
|
+ backendData: null // 没有后端数据
|
|
|
+ }
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ // 有上传URL时,执行文件上传
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ // 构建请求头
|
|
|
+ const header = {};
|
|
|
+ if (this.token) {
|
|
|
+ header["Authorization"] = "Bearer " + this.token; // 添加认证头
|
|
|
+ }
|
|
|
+
|
|
|
+ // 执行文件上传
|
|
|
+ uni.uploadFile({
|
|
|
+ url: this.uploadUrl, // 上传地址
|
|
|
+ filePath: filePath, // 文件路径
|
|
|
+ header: header, // 请求头
|
|
|
+ name: 'file', // 文件字段名
|
|
|
+ success: (res) => {
|
|
|
+ const responseData = JSON.parse(res.data);
|
|
|
+ resolve({
|
|
|
+ data: {
|
|
|
+ link: filePath, // 使用前端文件路径进行预览
|
|
|
+ backendData: responseData // 保存后端返回的完整数据
|
|
|
+ }
|
|
|
+ });
|
|
|
+ },
|
|
|
+ fail: reject // 上传失败时抛出错误
|
|
|
+ });
|
|
|
+ });
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 删除媒体文件方法
|
|
|
+ * @param {number} index 要删除的文件索引
|
|
|
+ */
|
|
|
+ deleteMedia(index) {
|
|
|
+ const newList = [...this.mediaList]; // 创建新数组
|
|
|
+ newList.splice(index, 1); // 删除指定索引的文件
|
|
|
+ this.updateMediaList(newList); // 更新媒体列表
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 预览媒体文件方法
|
|
|
+ * 目前只支持图片预览,视频预览由视频组件自己处理
|
|
|
+ * @param {number} index 要预览的文件索引
|
|
|
+ */
|
|
|
+ previewMedia(index) {
|
|
|
+ const item = this.mediaList[index]; // 获取要预览的文件
|
|
|
+ if (item.type === 'image') {
|
|
|
+ // 图片预览:获取所有图片的URL列表
|
|
|
+ const imageUrls = this.mediaList
|
|
|
+ .filter(item => item.type === 'image') // 过滤出所有图片
|
|
|
+ .map(item => item.url); // 提取图片URL
|
|
|
+ const currentIndex = imageUrls.indexOf(item.url); // 计算当前图片在列表中的索引
|
|
|
+
|
|
|
+ // 调用系统图片预览
|
|
|
+ uni.previewImage({
|
|
|
+ urls: imageUrls, // 所有图片的URL列表
|
|
|
+ current: currentIndex // 当前显示的图片索引
|
|
|
+ });
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 预览视频方法
|
|
|
+ * @param {number} index 要预览的视频索引
|
|
|
+ */
|
|
|
+ previewVideo(index) {
|
|
|
+ const item = this.mediaList[index];
|
|
|
+ if (item.type === 'video') {
|
|
|
+ // H5环境下不需要特殊处理,让视频组件自己处理全屏
|
|
|
+ // #ifdef MP-WEIXIN
|
|
|
+ const videoContext = uni.createVideoContext(`video-${index}`, this);
|
|
|
+ if (videoContext) {
|
|
|
+ videoContext.requestFullScreen();
|
|
|
+ }
|
|
|
+ // #endif
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 更新媒体列表方法
|
|
|
+ * 更新内部数据并触发事件通知父组件
|
|
|
+ * @param {Array} list 新的媒体列表
|
|
|
+ */
|
|
|
+ updateMediaList(list) {
|
|
|
+ this.mediaList = list; // 更新内部数据
|
|
|
+ this.$emit('input', list.map(item => item.url)); // 触发v-model更新,只传递URL数组
|
|
|
+ this.$emit('change', list); // 触发change事件,传递完整的媒体对象数组(包含后端数据)
|
|
|
+ this.$emit('backendData', list.map(item => item.backendData).filter(data => data !== null)); // 触发后端数据事件
|
|
|
+ },
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取媒体类型方法
|
|
|
+ * 根据文件URL或对象判断是图片还是视频
|
|
|
+ * @param {string|Object} url 文件URL或媒体对象
|
|
|
+ * @returns {string} 媒体类型:'image' 或 'video'
|
|
|
+ */
|
|
|
+ getMediaType(url) {
|
|
|
+ // 如果已经是对象格式且包含type属性,直接返回其类型
|
|
|
+ if (typeof url === 'object' && url.type) {
|
|
|
+ return url.type;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果是字符串,则通过文件扩展名判断类型
|
|
|
+ if (typeof url === 'string') {
|
|
|
+ const videoExts = ['.mp4', '.mov', '.avi', '.wmv']; // 支持的视频格式
|
|
|
+ const ext = url.substring(url.lastIndexOf('.')).toLowerCase(); // 获取文件扩展名
|
|
|
+ return videoExts.includes(ext) ? 'video' : 'image'; // 根据扩展名判断类型
|
|
|
+ }
|
|
|
+
|
|
|
+ // 默认返回图片类型
|
|
|
+ return 'image';
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+.media-uploader {
|
|
|
+ padding: 15px;
|
|
|
+
|
|
|
+ .media-grid {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 10px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .media-item {
|
|
|
+ position: relative;
|
|
|
+ width: calc((100% - 20px) / 3);
|
|
|
+ height: 0;
|
|
|
+ padding-bottom: calc((100% - 20px) / 3);
|
|
|
+ background-color: #f8f8f8;
|
|
|
+ border-radius: 8px;
|
|
|
+ overflow: hidden;
|
|
|
+
|
|
|
+ image,
|
|
|
+ video {
|
|
|
+ position: absolute;
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ object-fit: cover;
|
|
|
+ }
|
|
|
+
|
|
|
+ .delete-btn {
|
|
|
+ position: absolute;
|
|
|
+ top: 5px;
|
|
|
+ right: 5px;
|
|
|
+ width: 20px;
|
|
|
+ height: 20px;
|
|
|
+ background: rgba(0, 0, 0, 0.5);
|
|
|
+ border-radius: 50%;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ z-index: 2;
|
|
|
+ }
|
|
|
+
|
|
|
+ .media-mask {
|
|
|
+ position: absolute;
|
|
|
+ bottom: 0;
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
+ height: 30px;
|
|
|
+ background: linear-gradient(to top, rgba(0, 0, 0, 0.5), transparent);
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ z-index: 1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .upload-trigger {
|
|
|
+ width: calc((100% - 20px) / 3);
|
|
|
+ height: 0;
|
|
|
+ padding-bottom: calc((100% - 20px) / 3);
|
|
|
+ position: relative;
|
|
|
+
|
|
|
+ .upload-box {
|
|
|
+ position: absolute;
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ background-color: #f8f8f8;
|
|
|
+ border-radius: 8px;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+
|
|
|
+ .upload-text {
|
|
|
+ margin-top: 5px;
|
|
|
+ font-size: 12px;
|
|
|
+ color: #999;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .upload-tip {
|
|
|
+ margin-top: 10px;
|
|
|
+ font-size: 12px;
|
|
|
+ color: #999;
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|