| | |
| | | <template> |
| | | <view class="camera-upload"> |
| | | <!-- 拍照/拍视频按钮 --> |
| | | <view v-if="!disabled" class="camera-buttons"> |
| | | <view v-if="!disabled" |
| | | class="camera-buttons"> |
| | | <view class="button-row"> |
| | | <u-button |
| | | type="primary" |
| | | @click="takePhoto" |
| | | :loading="uploading" |
| | | :disabled="fileList.length >= limit" |
| | | :customStyle="{ marginRight: '10px', flex: 1 }" |
| | | > |
| | | <u-icon name="camera" size="18" color="#fff" style="margin-right: 5px;"></u-icon> |
| | | <u-button type="primary" |
| | | @click="takePhoto" |
| | | :loading="uploading" |
| | | :disabled="fileList.length >= limit" |
| | | :customStyle="{ marginRight: '10px', flex: 1 }"> |
| | | <u-icon name="camera" |
| | | size="18" |
| | | color="#fff" |
| | | style="margin-right: 5px;"></u-icon> |
| | | {{ uploading ? '上传中...' : '拍照' }} |
| | | </u-button> |
| | | <u-button |
| | | <!-- <u-button |
| | | type="success" |
| | | @click="takeVideo" |
| | | :loading="uploading" |
| | |
| | | > |
| | | <u-icon name="video" size="18" color="#fff" style="margin-right: 5px;"></u-icon> |
| | | {{ uploading ? '上传中...' : '拍视频' }} |
| | | </u-button> |
| | | </u-button> --> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- 提示信息 --> |
| | | <view v-if="showTip && !disabled" class="upload-tip"> |
| | | <view v-if="showTip && !disabled" |
| | | class="upload-tip"> |
| | | 请使用相机 |
| | | <text v-if="fileSize" class="tip-text"> |
| | | <text v-if="fileSize" |
| | | class="tip-text"> |
| | | 拍摄大小不超过 <text class="tip-highlight">{{ fileSize }}MB</text> |
| | | </text> |
| | | 的 |
| | | <text class="tip-highlight">照片或视频</text> |
| | | <text class="tip-highlight">照片</text> |
| | | </view> |
| | | |
| | | <!-- 媒体文件列表 --> |
| | | <view class="media-list"> |
| | | <view |
| | | v-for="(file, index) in fileList" |
| | | :key="file.uid || index" |
| | | class="media-item" |
| | | > |
| | | <view v-for="(file, index) in fileList" |
| | | :key="file.uid || index" |
| | | class="media-item"> |
| | | <!-- 预览区域 --> |
| | | <view class="media-preview" @click="previewMedia(file, index)"> |
| | | <image |
| | | v-if="file.type === 'image'" |
| | | :src="file.url || file.tempFilePath" |
| | | class="preview-image" |
| | | mode="aspectFill" |
| | | ></image> |
| | | <video |
| | | v-else-if="file.type === 'video'" |
| | | :src="file.url || file.tempFilePath" |
| | | class="preview-video" |
| | | :controls="false" |
| | | ></video> |
| | | <view class="media-preview" |
| | | @click="previewMedia(file, index)"> |
| | | <image v-if="file.type === 'image'" |
| | | :src="file.url || file.tempFilePath" |
| | | class="preview-image" |
| | | mode="aspectFill"></image> |
| | | <video v-else-if="file.type === 'video'" |
| | | :src="file.url || file.tempFilePath" |
| | | class="preview-video" |
| | | :controls="false"></video> |
| | | <view class="media-type-icon"> |
| | | <u-icon |
| | | :name="file.type === 'image' ? 'photo' : 'video'" |
| | | size="12" |
| | | color="#fff" |
| | | ></u-icon> |
| | | <u-icon :name="file.type === 'image' ? 'photo' : 'video'" |
| | | size="12" |
| | | color="#fff"></u-icon> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- 操作按钮 --> |
| | | <view class="media-actions" v-if="!disabled"> |
| | | <u-button |
| | | type="error" |
| | | size="mini" |
| | | @click="handleDelete(index)" |
| | | :customStyle="{ |
| | | <view class="media-actions" |
| | | v-if="!disabled"> |
| | | <u-button type="error" |
| | | size="mini" |
| | | @click="handleDelete(index)" |
| | | :customStyle="{ |
| | | minWidth: '40px', |
| | | height: '24px', |
| | | fontSize: '10px', |
| | | padding: '0 8px' |
| | | }" |
| | | > |
| | | }"> |
| | | 删除 |
| | | </u-button> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | |
| | | <!-- 上传进度 --> |
| | | <view v-if="uploading" class="upload-progress"> |
| | | <u-line-progress |
| | | :percentage="uploadProgress" |
| | | :showText="true" |
| | | activeColor="#409eff" |
| | | ></u-line-progress> |
| | | <view v-if="uploading" |
| | | class="upload-progress"> |
| | | <u-line-progress :percentage="uploadProgress" |
| | | :showText="true" |
| | | activeColor="#409eff"></u-line-progress> |
| | | </view> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'; |
| | | import { getToken } from "@/utils/auth"; |
| | | import { ref, computed, watch, onMounted, onUnmounted, nextTick } from "vue"; |
| | | import { getToken } from "@/utils/auth"; |
| | | |
| | | // Props 定义 |
| | | const props = defineProps({ |
| | | modelValue: [String, Object, Array], |
| | | action: { type: String, default: "/common/minioUploads" }, |
| | | data: { type: Object }, |
| | | limit: { type: Number, default: 5 }, |
| | | fileSize: { type: Number, default: 10 }, // 默认10MB,适合视频 |
| | | fileType: { |
| | | type: Array, |
| | | default: () => ["jpg", "jpeg", "png", "mp4", "mov"] |
| | | }, |
| | | isShowTip: { type: Boolean, default: true }, |
| | | disabled: { type: Boolean, default: false }, |
| | | drag: { type: Boolean, default: false }, // 拍照不需要拖拽 |
| | | statusType: { type: Number, default: "" }, // 用于区分不同状态的上传 |
| | | maxVideoDuration: { type: Number, default: 30 }, // 最大视频时长(秒) |
| | | }); |
| | | |
| | | // 事件定义 |
| | | const emit = defineEmits(['update:modelValue']); |
| | | |
| | | // 响应式数据 |
| | | const number = ref(0); |
| | | const uploadList = ref([]); |
| | | const fileList = ref([]); |
| | | const uploading = ref(false); |
| | | const uploadProgress = ref(0); |
| | | |
| | | // 计算属性 |
| | | const uploadFileUrl = computed(() => { |
| | | // 获取基础API地址,适配uniapp环境 |
| | | let baseUrl = ''; |
| | | |
| | | // 尝试多种方式获取baseUrl |
| | | if (process.env.VUE_APP_BASE_API) { |
| | | baseUrl = process.env.VUE_APP_BASE_API; |
| | | } else if (process.env.NODE_ENV === 'development') { |
| | | baseUrl = 'http://192.168.1.147:9036'; |
| | | } else { |
| | | baseUrl = 'http://192.168.1.147:9036'; |
| | | } |
| | | |
| | | const fullUrl = baseUrl + props.action; |
| | | return fullUrl; |
| | | }); |
| | | const headers = computed(() => { |
| | | const token = getToken(); |
| | | return token ? { Authorization: "Bearer " + token } : {}; |
| | | }); |
| | | const showTip = computed(() => props.isShowTip && (props.fileType || props.fileSize)); |
| | | // 初始化和编辑初始化方法 |
| | | const init = () => { |
| | | fileList.value = []; |
| | | uploadList.value = []; |
| | | number.value = 0; |
| | | }; |
| | | |
| | | const editInit = (val) => { |
| | | fileList.value = []; |
| | | val.storageBlobDTO.forEach((element) => { |
| | | // 确保文件数据包含所有必要字段,包括id |
| | | const fileData = { |
| | | ...element, |
| | | id: element.id, // 保留服务器返回的id |
| | | url: element.url || element.downloadUrl, |
| | | bucketFilename: element.bucketFilename || element.name, |
| | | downloadUrl: element.downloadUrl || element.url, |
| | | type: element.type || (element.url && element.url.includes('video') ? 'video' : 'image'), |
| | | name: element.name || element.bucketFilename || `文件_${Date.now()}`, |
| | | size: element.size || 0, |
| | | createTime: element.createTime || new Date().getTime(), |
| | | uid: element.uid || new Date().getTime() + Math.random() |
| | | }; |
| | | fileList.value.push(fileData); |
| | | uploadedSuccessfully(); |
| | | // Props 定义 |
| | | const props = defineProps({ |
| | | modelValue: [String, Object, Array], |
| | | action: { type: String, default: "/common/minioUploads" }, |
| | | data: { type: Object }, |
| | | limit: { type: Number, default: 5 }, |
| | | fileSize: { type: Number, default: 10 }, // 默认10MB,适合视频 |
| | | fileType: { |
| | | type: Array, |
| | | default: () => ["jpg", "jpeg", "png", "mp4", "mov"], |
| | | }, |
| | | isShowTip: { type: Boolean, default: true }, |
| | | disabled: { type: Boolean, default: false }, |
| | | drag: { type: Boolean, default: false }, // 拍照不需要拖拽 |
| | | statusType: { type: Number, default: "" }, // 用于区分不同状态的上传 |
| | | maxVideoDuration: { type: Number, default: 30 }, // 最大视频时长(秒) |
| | | }); |
| | | }; |
| | | |
| | | // 测试服务器连接 |
| | | const testServerConnection = () => { |
| | | return new Promise((resolve) => { |
| | | uni.request({ |
| | | url: uploadFileUrl.value.replace('/common/minioUploads', '/common/test'), |
| | | method: 'GET', |
| | | timeout: 5000, |
| | | success: (res) => { |
| | | resolve(true); |
| | | }, |
| | | fail: (err) => { |
| | | resolve(false); |
| | | } |
| | | }); |
| | | }); |
| | | }; |
| | | // 事件定义 |
| | | const emit = defineEmits(["update:modelValue"]); |
| | | |
| | | // 组件销毁时的清理 |
| | | onUnmounted(() => { |
| | | // 清理上传状态 |
| | | if (uploading.value) { |
| | | uploading.value = false |
| | | } |
| | | |
| | | // 隐藏可能显示的加载提示 |
| | | uni.hideLoading() |
| | | uni.hideToast() |
| | | }) |
| | | // 响应式数据 |
| | | const number = ref(0); |
| | | const uploadList = ref([]); |
| | | const fileList = ref([]); |
| | | const uploading = ref(false); |
| | | const uploadProgress = ref(0); |
| | | |
| | | // 暴露方法 |
| | | defineExpose({ init, editInit, testServerConnection }); |
| | | // 计算属性 |
| | | const uploadFileUrl = computed(() => { |
| | | // 获取基础API地址,适配uniapp环境 |
| | | let baseUrl = ""; |
| | | |
| | | // 监听 modelValue 变化 |
| | | watch( |
| | | () => props.modelValue, |
| | | (val) => { |
| | | if (val) { |
| | | let temp = 1; |
| | | let list = []; |
| | | |
| | | if (Array.isArray(val)) { |
| | | list = val; |
| | | } else if (typeof val === "string") { |
| | | list = val.split(",").map(url => ({ url: url.trim() })); |
| | | } |
| | | |
| | | fileList.value = list.map((item) => { |
| | | if (typeof item === "string") { |
| | | item = { name: item, url: item }; |
| | | } |
| | | // 确保每个文件都有必要的属性,包括id |
| | | return { |
| | | ...item, |
| | | id: item.id, // 保留id字段 |
| | | uid: item.uid || new Date().getTime() + temp++, |
| | | type: item.type || (item.url && item.url.includes('video') ? 'video' : 'image'), |
| | | name: item.name || item.bucketFilename || `文件_${Date.now()}`, |
| | | size: item.size || 0, |
| | | createTime: item.createTime || new Date().getTime() |
| | | }; |
| | | }); |
| | | // 尝试多种方式获取baseUrl |
| | | if (process.env.VUE_APP_BASE_API) { |
| | | baseUrl = process.env.VUE_APP_BASE_API; |
| | | } else if (process.env.NODE_ENV === "development") { |
| | | baseUrl = "http://192.168.1.147:9036"; |
| | | } else { |
| | | fileList.value = []; |
| | | baseUrl = "http://192.168.1.147:9036"; |
| | | } |
| | | }, |
| | | { deep: true, immediate: true } |
| | | ); |
| | | |
| | | // 拍照 |
| | | const takePhoto = () => { |
| | | if (fileList.value.length >= props.limit) { |
| | | uni.showToast({ |
| | | title: `最多只能拍摄${props.limit}个文件`, |
| | | icon: 'none' |
| | | const fullUrl = baseUrl + props.action; |
| | | return fullUrl; |
| | | }); |
| | | const headers = computed(() => { |
| | | const token = getToken(); |
| | | return token ? { Authorization: "Bearer " + token } : {}; |
| | | }); |
| | | const showTip = computed( |
| | | () => props.isShowTip && (props.fileType || props.fileSize) |
| | | ); |
| | | // 初始化和编辑初始化方法 |
| | | const init = () => { |
| | | fileList.value = []; |
| | | uploadList.value = []; |
| | | number.value = 0; |
| | | }; |
| | | |
| | | const editInit = val => { |
| | | fileList.value = []; |
| | | val.storageBlobDTO.forEach(element => { |
| | | // 确保文件数据包含所有必要字段,包括id |
| | | const fileData = { |
| | | ...element, |
| | | id: element.id, // 保留服务器返回的id |
| | | url: element.url || element.downloadUrl, |
| | | bucketFilename: element.bucketFilename || element.name, |
| | | downloadUrl: element.downloadUrl || element.url, |
| | | type: |
| | | element.type || |
| | | (element.url && element.url.includes("video") ? "video" : "image"), |
| | | name: element.name || element.bucketFilename || `文件_${Date.now()}`, |
| | | size: element.size || 0, |
| | | createTime: element.createTime || new Date().getTime(), |
| | | uid: element.uid || new Date().getTime() + Math.random(), |
| | | }; |
| | | fileList.value.push(fileData); |
| | | uploadedSuccessfully(); |
| | | }); |
| | | return; |
| | | } |
| | | |
| | | uni.chooseImage({ |
| | | count: 1, |
| | | sizeType: ['compressed', 'original'], |
| | | sourceType: ['camera'], |
| | | success: (res) => { |
| | | try { |
| | | if (!res.tempFilePaths || res.tempFilePaths.length === 0) { |
| | | throw new Error('未获取到图片文件'); |
| | | }; |
| | | |
| | | // 测试服务器连接 |
| | | const testServerConnection = () => { |
| | | return new Promise(resolve => { |
| | | uni.request({ |
| | | url: uploadFileUrl.value.replace("/common/minioUploads", "/common/test"), |
| | | method: "GET", |
| | | timeout: 5000, |
| | | success: res => { |
| | | resolve(true); |
| | | }, |
| | | fail: err => { |
| | | resolve(false); |
| | | }, |
| | | }); |
| | | }); |
| | | }; |
| | | |
| | | // 组件销毁时的清理 |
| | | onUnmounted(() => { |
| | | // 清理上传状态 |
| | | if (uploading.value) { |
| | | uploading.value = false; |
| | | } |
| | | |
| | | // 隐藏可能显示的加载提示 |
| | | uni.hideLoading(); |
| | | uni.hideToast(); |
| | | }); |
| | | |
| | | // 暴露方法 |
| | | defineExpose({ init, editInit, testServerConnection }); |
| | | |
| | | // 监听 modelValue 变化 |
| | | watch( |
| | | () => props.modelValue, |
| | | val => { |
| | | if (val) { |
| | | let temp = 1; |
| | | let list = []; |
| | | |
| | | if (Array.isArray(val)) { |
| | | list = val; |
| | | } else if (typeof val === "string") { |
| | | list = val.split(",").map(url => ({ url: url.trim() })); |
| | | } |
| | | |
| | | const tempFilePath = res.tempFilePaths[0]; |
| | | const tempFile = res.tempFiles && res.tempFiles[0] ? res.tempFiles[0] : {}; |
| | | |
| | | const file = { |
| | | tempFilePath: tempFilePath, |
| | | type: 'image', |
| | | name: `photo_${Date.now()}.jpg`, |
| | | size: tempFile.size || 0, |
| | | createTime: new Date().getTime(), |
| | | uid: Date.now() + Math.random() |
| | | }; |
| | | |
| | | handleBeforeUpload(file); |
| | | } catch (error) { |
| | | console.error('处理拍照结果失败:', error); |
| | | uni.showToast({ |
| | | title: '处理图片失败', |
| | | icon: 'error' |
| | | |
| | | fileList.value = list.map(item => { |
| | | if (typeof item === "string") { |
| | | item = { name: item, url: item }; |
| | | } |
| | | // 确保每个文件都有必要的属性,包括id |
| | | return { |
| | | ...item, |
| | | id: item.id, // 保留id字段 |
| | | uid: item.uid || new Date().getTime() + temp++, |
| | | type: |
| | | item.type || |
| | | (item.url && item.url.includes("video") ? "video" : "image"), |
| | | name: item.name || item.bucketFilename || `文件_${Date.now()}`, |
| | | size: item.size || 0, |
| | | createTime: item.createTime || new Date().getTime(), |
| | | }; |
| | | }); |
| | | } else { |
| | | fileList.value = []; |
| | | } |
| | | }, |
| | | fail: (err) => { |
| | | console.error('拍照失败:', err); |
| | | uni.showToast({ |
| | | title: '拍照失败: ' + (err.errMsg || '未知错误'), |
| | | icon: 'error' |
| | | }); |
| | | } |
| | | }); |
| | | }; |
| | | { deep: true, immediate: true } |
| | | ); |
| | | |
| | | // 拍视频 |
| | | const takeVideo = () => { |
| | | if (fileList.value.length >= props.limit) { |
| | | uni.showToast({ |
| | | title: `最多只能拍摄${props.limit}个文件`, |
| | | icon: 'none' |
| | | }); |
| | | return; |
| | | } |
| | | |
| | | uni.chooseVideo({ |
| | | sourceType: ['camera'], |
| | | maxDuration: props.maxVideoDuration, |
| | | camera: 'back', |
| | | success: (res) => { |
| | | try { |
| | | if (!res.tempFilePath) { |
| | | throw new Error('未获取到视频文件'); |
| | | // 拍照 |
| | | const takePhoto = () => { |
| | | if (fileList.value.length >= props.limit) { |
| | | uni.showToast({ |
| | | title: `最多只能拍摄${props.limit}个文件`, |
| | | icon: "none", |
| | | }); |
| | | return; |
| | | } |
| | | |
| | | uni.chooseImage({ |
| | | count: 1, |
| | | sizeType: ["compressed", "original"], |
| | | sourceType: ["camera"], |
| | | success: res => { |
| | | try { |
| | | if (!res.tempFilePaths || res.tempFilePaths.length === 0) { |
| | | throw new Error("未获取到图片文件"); |
| | | } |
| | | |
| | | const tempFilePath = res.tempFilePaths[0]; |
| | | const tempFile = |
| | | res.tempFiles && res.tempFiles[0] ? res.tempFiles[0] : {}; |
| | | |
| | | const file = { |
| | | tempFilePath: tempFilePath, |
| | | type: "image", |
| | | name: `photo_${Date.now()}.jpg`, |
| | | size: tempFile.size || 0, |
| | | createTime: new Date().getTime(), |
| | | uid: Date.now() + Math.random(), |
| | | }; |
| | | |
| | | handleBeforeUpload(file); |
| | | } catch (error) { |
| | | console.error("处理拍照结果失败:", error); |
| | | uni.showToast({ |
| | | title: "处理图片失败", |
| | | icon: "error", |
| | | }); |
| | | } |
| | | |
| | | const file = { |
| | | tempFilePath: res.tempFilePath, |
| | | type: 'video', |
| | | name: `video_${Date.now()}.mp4`, |
| | | size: res.size || 0, |
| | | duration: res.duration || 0, |
| | | createTime: new Date().getTime(), |
| | | uid: Date.now() + Math.random() |
| | | }; |
| | | |
| | | handleBeforeUpload(file); |
| | | } catch (error) { |
| | | console.error('处理拍视频结果失败:', error); |
| | | }, |
| | | fail: err => { |
| | | console.error("拍照失败:", err); |
| | | uni.showToast({ |
| | | title: '处理视频失败', |
| | | icon: 'error' |
| | | title: "拍照失败: " + (err.errMsg || "未知错误"), |
| | | icon: "error", |
| | | }); |
| | | } |
| | | }, |
| | | fail: (err) => { |
| | | console.error('拍视频失败:', err); |
| | | uni.showToast({ |
| | | title: '拍视频失败: ' + (err.errMsg || '未知错误'), |
| | | icon: 'error' |
| | | }); |
| | | } |
| | | }); |
| | | }; |
| | | }, |
| | | }); |
| | | }; |
| | | |
| | | // 文件上传处理 |
| | | const uploadFile = (file) => { |
| | | uploading.value = true; |
| | | uploadProgress.value = 0; |
| | | number.value++; // 增加上传计数 |
| | | |
| | | // 确保文件路径正确 |
| | | const filePath = file.tempFilePath || file.path; |
| | | if (!filePath) { |
| | | handleUploadError('文件路径不存在'); |
| | | return; |
| | | } |
| | | |
| | | // 确保token存在 |
| | | const token = getToken(); |
| | | if (!token) { |
| | | handleUploadError('用户未登录'); |
| | | return; |
| | | } |
| | | |
| | | // 准备上传参数 |
| | | const uploadParams = { |
| | | url: uploadFileUrl.value, |
| | | filePath: filePath, |
| | | name: 'files', |
| | | formData: { |
| | | type: props.statusType || 0, |
| | | ...(props.data || {}) |
| | | }, |
| | | header: { |
| | | 'Authorization': `Bearer ${token}` |
| | | // 拍视频 |
| | | const takeVideo = () => { |
| | | if (fileList.value.length >= props.limit) { |
| | | uni.showToast({ |
| | | title: `最多只能拍摄${props.limit}个文件`, |
| | | icon: "none", |
| | | }); |
| | | return; |
| | | } |
| | | |
| | | uni.chooseVideo({ |
| | | sourceType: ["camera"], |
| | | maxDuration: props.maxVideoDuration, |
| | | camera: "back", |
| | | success: res => { |
| | | try { |
| | | if (!res.tempFilePath) { |
| | | throw new Error("未获取到视频文件"); |
| | | } |
| | | |
| | | const file = { |
| | | tempFilePath: res.tempFilePath, |
| | | type: "video", |
| | | name: `video_${Date.now()}.mp4`, |
| | | size: res.size || 0, |
| | | duration: res.duration || 0, |
| | | createTime: new Date().getTime(), |
| | | uid: Date.now() + Math.random(), |
| | | }; |
| | | |
| | | handleBeforeUpload(file); |
| | | } catch (error) { |
| | | console.error("处理拍视频结果失败:", error); |
| | | uni.showToast({ |
| | | title: "处理视频失败", |
| | | icon: "error", |
| | | }); |
| | | } |
| | | }, |
| | | fail: err => { |
| | | console.error("拍视频失败:", err); |
| | | uni.showToast({ |
| | | title: "拍视频失败: " + (err.errMsg || "未知错误"), |
| | | icon: "error", |
| | | }); |
| | | }, |
| | | }); |
| | | }; |
| | | |
| | | // 文件上传处理 |
| | | const uploadFile = file => { |
| | | uploading.value = true; |
| | | uploadProgress.value = 0; |
| | | number.value++; // 增加上传计数 |
| | | |
| | | // 确保文件路径正确 |
| | | const filePath = file.tempFilePath || file.path; |
| | | if (!filePath) { |
| | | handleUploadError("文件路径不存在"); |
| | | return; |
| | | } |
| | | |
| | | // 确保token存在 |
| | | const token = getToken(); |
| | | if (!token) { |
| | | handleUploadError("用户未登录"); |
| | | return; |
| | | } |
| | | |
| | | // 准备上传参数 |
| | | const uploadParams = { |
| | | url: uploadFileUrl.value, |
| | | filePath: filePath, |
| | | name: "files", |
| | | formData: { |
| | | type: props.statusType || 0, |
| | | ...(props.data || {}), |
| | | }, |
| | | header: { |
| | | Authorization: `Bearer ${token}`, |
| | | }, |
| | | }; |
| | | |
| | | const uploadTask = uni.uploadFile({ |
| | | ...uploadParams, |
| | | success: res => { |
| | | try { |
| | | if (res.statusCode === 200) { |
| | | const response = JSON.parse(res.data); |
| | | if (response.code === 200) { |
| | | handleUploadSuccess(response, file); |
| | | uni.showToast({ |
| | | title: "上传成功", |
| | | icon: "success", |
| | | }); |
| | | emit("update:modelValue", fileList.value); |
| | | } else { |
| | | handleUploadError(response.msg || "服务器返回错误"); |
| | | } |
| | | } else { |
| | | handleUploadError(`服务器错误,状态码: ${res.statusCode}`); |
| | | } |
| | | } catch (e) { |
| | | console.error("解析响应失败:", e); |
| | | console.error("原始响应数据:", res.data); |
| | | handleUploadError("响应数据解析失败: " + e.message); |
| | | } |
| | | }, |
| | | fail: err => { |
| | | console.error("上传失败:", err.errMsg || err); |
| | | number.value--; // 上传失败时减少计数 |
| | | |
| | | let errorMessage = "上传失败"; |
| | | if (err.errMsg) { |
| | | if (err.errMsg.includes("statusCode: null")) { |
| | | errorMessage = "网络连接失败,请检查网络设置"; |
| | | } else if (err.errMsg.includes("timeout")) { |
| | | errorMessage = "上传超时,请重试"; |
| | | } else if (err.errMsg.includes("fail")) { |
| | | errorMessage = "上传失败,请检查网络连接"; |
| | | } else { |
| | | errorMessage = err.errMsg; |
| | | } |
| | | } |
| | | |
| | | handleUploadError(errorMessage); |
| | | }, |
| | | complete: () => { |
| | | uploading.value = false; |
| | | uploadProgress.value = 0; |
| | | }, |
| | | }); |
| | | |
| | | // 监听上传进度 |
| | | if (uploadTask && uploadTask.onProgressUpdate) { |
| | | uploadTask.onProgressUpdate(res => { |
| | | uploadProgress.value = res.progress; |
| | | }); |
| | | } |
| | | }; |
| | | |
| | | const uploadTask = uni.uploadFile({ |
| | | ...uploadParams, |
| | | success: (res) => { |
| | | try { |
| | | if (res.statusCode === 200) { |
| | | const response = JSON.parse(res.data); |
| | | if (response.code === 200) { |
| | | handleUploadSuccess(response, file); |
| | | uni.showToast({ |
| | | title: '上传成功', |
| | | icon: 'success' |
| | | }); |
| | | emit("update:modelValue", fileList.value); |
| | | } else { |
| | | handleUploadError(response.msg || '服务器返回错误'); |
| | | } |
| | | } else { |
| | | handleUploadError(`服务器错误,状态码: ${res.statusCode}`); |
| | | } |
| | | } catch (e) { |
| | | console.error('解析响应失败:', e); |
| | | console.error('原始响应数据:', res.data); |
| | | handleUploadError('响应数据解析失败: ' + e.message); |
| | | } |
| | | }, |
| | | fail: (err) => { |
| | | console.error('上传失败:', err.errMsg || err); |
| | | number.value--; // 上传失败时减少计数 |
| | | |
| | | let errorMessage = '上传失败'; |
| | | if (err.errMsg) { |
| | | if (err.errMsg.includes('statusCode: null')) { |
| | | errorMessage = '网络连接失败,请检查网络设置'; |
| | | } else if (err.errMsg.includes('timeout')) { |
| | | errorMessage = '上传超时,请重试'; |
| | | } else if (err.errMsg.includes('fail')) { |
| | | errorMessage = '上传失败,请检查网络连接'; |
| | | } else { |
| | | errorMessage = err.errMsg; |
| | | } |
| | | } |
| | | |
| | | handleUploadError(errorMessage); |
| | | }, |
| | | complete: () => { |
| | | uploading.value = false; |
| | | uploadProgress.value = 0; |
| | | // 获取媒体文件名 |
| | | const getMediaName = file => { |
| | | if (file.bucketFilename) { |
| | | return file.bucketFilename.length > 15 |
| | | ? file.bucketFilename.substring(0, 15) + "..." |
| | | : file.bucketFilename; |
| | | } |
| | | }); |
| | | |
| | | // 监听上传进度 |
| | | if (uploadTask && uploadTask.onProgressUpdate) { |
| | | uploadTask.onProgressUpdate((res) => { |
| | | uploadProgress.value = res.progress; |
| | | }); |
| | | } |
| | | }; |
| | | // 获取媒体文件名 |
| | | const getMediaName = (file) => { |
| | | if (file.bucketFilename) { |
| | | return file.bucketFilename.length > 15 |
| | | ? file.bucketFilename.substring(0, 15) + '...' |
| | | : file.bucketFilename; |
| | | } |
| | | if (file.name) { |
| | | return file.name.length > 15 |
| | | ? file.name.substring(0, 15) + '...' |
| | | : file.name; |
| | | } |
| | | return file.type === 'image' ? '照片' : '视频'; |
| | | }; |
| | | if (file.name) { |
| | | return file.name.length > 15 |
| | | ? file.name.substring(0, 15) + "..." |
| | | : file.name; |
| | | } |
| | | return file.type === "image" ? "照片" : "视频"; |
| | | }; |
| | | |
| | | // 格式化文件大小 |
| | | const formatFileSize = (size) => { |
| | | if (!size) return ''; |
| | | if (size < 1024) return size + 'B'; |
| | | if (size < 1024 * 1024) return (size / 1024).toFixed(1) + 'KB'; |
| | | return (size / (1024 * 1024)).toFixed(1) + 'MB'; |
| | | }; |
| | | // 格式化文件大小 |
| | | const formatFileSize = size => { |
| | | if (!size) return ""; |
| | | if (size < 1024) return size + "B"; |
| | | if (size < 1024 * 1024) return (size / 1024).toFixed(1) + "KB"; |
| | | return (size / (1024 * 1024)).toFixed(1) + "MB"; |
| | | }; |
| | | |
| | | // 格式化时间 |
| | | const formatTime = (timestamp) => { |
| | | if (!timestamp) return ''; |
| | | const date = new Date(timestamp); |
| | | const now = new Date(); |
| | | const diff = now - date; |
| | | |
| | | if (diff < 60000) return '刚刚'; |
| | | if (diff < 3600000) return Math.floor(diff / 60000) + '分钟前'; |
| | | if (diff < 86400000) return Math.floor(diff / 3600000) + '小时前'; |
| | | |
| | | return date.toLocaleDateString(); |
| | | }; |
| | | // 格式化时间 |
| | | const formatTime = timestamp => { |
| | | if (!timestamp) return ""; |
| | | const date = new Date(timestamp); |
| | | const now = new Date(); |
| | | const diff = now - date; |
| | | |
| | | // 预览媒体文件 |
| | | const previewMedia = (file, index) => { |
| | | if (file.type === 'image') { |
| | | // 预览图片 |
| | | const urls = fileList.value |
| | | .filter(item => item.type === 'image') |
| | | .map(item => item.url || item.tempFilePath); |
| | | |
| | | uni.previewImage({ |
| | | urls: urls, |
| | | current: file.url || file.tempFilePath |
| | | }); |
| | | } else if (file.type === 'video') { |
| | | // 预览视频 |
| | | uni.previewVideo({ |
| | | sources: [{ |
| | | src: file.url || file.tempFilePath, |
| | | type: 'mp4' |
| | | }], |
| | | current: 0 |
| | | }); |
| | | } |
| | | }; |
| | | if (diff < 60000) return "刚刚"; |
| | | if (diff < 3600000) return Math.floor(diff / 60000) + "分钟前"; |
| | | if (diff < 86400000) return Math.floor(diff / 3600000) + "小时前"; |
| | | |
| | | // 下载文件 |
| | | const handleDownload = (index) => { |
| | | const file = fileList.value[index]; |
| | | const url = file.url || file.downloadUrl; |
| | | |
| | | if (!url) { |
| | | uni.showToast({ |
| | | title: '文件链接不存在,无法下载', |
| | | icon: 'none' |
| | | }); |
| | | return; |
| | | } |
| | | |
| | | // 使用uniapp的下载API |
| | | uni.downloadFile({ |
| | | url: url, |
| | | success: (res) => { |
| | | if (res.statusCode === 200) { |
| | | // 保存到相册或文件系统 |
| | | uni.saveFile({ |
| | | tempFilePath: res.tempFilePath, |
| | | success: (saveRes) => { |
| | | uni.showToast({ |
| | | title: '下载成功', |
| | | icon: 'success' |
| | | }); |
| | | return date.toLocaleDateString(); |
| | | }; |
| | | |
| | | // 预览媒体文件 |
| | | const previewMedia = (file, index) => { |
| | | if (file.type === "image") { |
| | | // 预览图片 |
| | | const urls = fileList.value |
| | | .filter(item => item.type === "image") |
| | | .map(item => item.url || item.tempFilePath); |
| | | |
| | | uni.previewImage({ |
| | | urls: urls, |
| | | current: file.url || file.tempFilePath, |
| | | }); |
| | | } else if (file.type === "video") { |
| | | // 预览视频 |
| | | uni.previewVideo({ |
| | | sources: [ |
| | | { |
| | | src: file.url || file.tempFilePath, |
| | | type: "mp4", |
| | | }, |
| | | fail: (err) => { |
| | | console.error('保存文件失败:', err); |
| | | uni.showToast({ |
| | | title: '保存文件失败', |
| | | icon: 'error' |
| | | }); |
| | | } |
| | | }); |
| | | } |
| | | }, |
| | | fail: (err) => { |
| | | console.error('下载失败:', err); |
| | | uni.showToast({ |
| | | title: '下载失败', |
| | | icon: 'error' |
| | | ], |
| | | current: 0, |
| | | }); |
| | | } |
| | | }); |
| | | }; |
| | | }; |
| | | |
| | | // 检查网络连接 |
| | | const checkNetworkConnection = () => { |
| | | return new Promise((resolve) => { |
| | | uni.getNetworkType({ |
| | | success: (res) => { |
| | | if (res.networkType === 'none') { |
| | | resolve(false); |
| | | } else { |
| | | resolve(true); |
| | | // 下载文件 |
| | | const handleDownload = index => { |
| | | const file = fileList.value[index]; |
| | | const url = file.url || file.downloadUrl; |
| | | |
| | | if (!url) { |
| | | uni.showToast({ |
| | | title: "文件链接不存在,无法下载", |
| | | icon: "none", |
| | | }); |
| | | return; |
| | | } |
| | | |
| | | // 使用uniapp的下载API |
| | | uni.downloadFile({ |
| | | url: url, |
| | | success: res => { |
| | | if (res.statusCode === 200) { |
| | | // 保存到相册或文件系统 |
| | | uni.saveFile({ |
| | | tempFilePath: res.tempFilePath, |
| | | success: saveRes => { |
| | | uni.showToast({ |
| | | title: "下载成功", |
| | | icon: "success", |
| | | }); |
| | | }, |
| | | fail: err => { |
| | | console.error("保存文件失败:", err); |
| | | uni.showToast({ |
| | | title: "保存文件失败", |
| | | icon: "error", |
| | | }); |
| | | }, |
| | | }); |
| | | } |
| | | }, |
| | | fail: () => { |
| | | resolve(false); |
| | | } |
| | | fail: err => { |
| | | console.error("下载失败:", err); |
| | | uni.showToast({ |
| | | title: "下载失败", |
| | | icon: "error", |
| | | }); |
| | | }, |
| | | }); |
| | | }); |
| | | }; |
| | | }; |
| | | |
| | | // 上传前校验 |
| | | const handleBeforeUpload = async (file) => { |
| | | // 检查网络连接 |
| | | const hasNetwork = await checkNetworkConnection(); |
| | | if (!hasNetwork) { |
| | | uni.showToast({ |
| | | title: '网络连接不可用,请检查网络设置', |
| | | icon: 'none' |
| | | const checkNetworkConnection = () => { |
| | | return new Promise(resolve => { |
| | | uni.getNetworkType({ |
| | | success: res => { |
| | | if (res.networkType === "none") { |
| | | resolve(false); |
| | | } else { |
| | | resolve(true); |
| | | } |
| | | }, |
| | | fail: () => { |
| | | resolve(false); |
| | | }, |
| | | }); |
| | | }); |
| | | return false; |
| | | } |
| | | }; |
| | | |
| | | // 校验文件大小 |
| | | if (props.fileSize && file.size) { |
| | | const isLt = file.size / 1024 / 1024 < props.fileSize; |
| | | if (!isLt) { |
| | | // 上传前校验 |
| | | const handleBeforeUpload = async file => { |
| | | // 检查网络连接 |
| | | const hasNetwork = await checkNetworkConnection(); |
| | | if (!hasNetwork) { |
| | | uni.showToast({ |
| | | title: `文件大小不能超过 ${props.fileSize} MB!`, |
| | | icon: 'none' |
| | | title: "网络连接不可用,请检查网络设置", |
| | | icon: "none", |
| | | }); |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | // 校验视频时长 |
| | | if (file.type === 'video' && file.duration && file.duration > props.maxVideoDuration) { |
| | | uni.showToast({ |
| | | title: `视频时长不能超过 ${props.maxVideoDuration} 秒!`, |
| | | icon: 'none' |
| | | }); |
| | | return false; |
| | | } |
| | | |
| | | // 校验文件类型 |
| | | if (props.fileType && Array.isArray(props.fileType) && props.fileType.length > 0) { |
| | | const fileName = file.name || ''; |
| | | const fileExtension = fileName ? fileName.split('.').pop().toLowerCase() : ''; |
| | | |
| | | // 根据文件类型确定期望的扩展名 |
| | | let expectedTypes = []; |
| | | if (file.type === 'image') { |
| | | expectedTypes = ['jpg', 'jpeg', 'png', 'gif', 'webp']; |
| | | } else if (file.type === 'video') { |
| | | expectedTypes = ['mp4', 'mov', 'avi', 'wmv']; |
| | | } |
| | | |
| | | // 检查文件扩展名是否在允许的类型中 |
| | | if (fileExtension && expectedTypes.length > 0) { |
| | | const isAllowed = expectedTypes.some(type => |
| | | props.fileType.includes(type) && type === fileExtension |
| | | ); |
| | | |
| | | if (!isAllowed) { |
| | | // 校验文件大小 |
| | | if (props.fileSize && file.size) { |
| | | const isLt = file.size / 1024 / 1024 < props.fileSize; |
| | | if (!isLt) { |
| | | uni.showToast({ |
| | | title: `文件格式不支持,请拍摄 ${expectedTypes.join('/')} 格式的文件`, |
| | | icon: 'none' |
| | | title: `文件大小不能超过 ${props.fileSize} MB!`, |
| | | icon: "none", |
| | | }); |
| | | return false; |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 校验通过,开始上传 |
| | | uploadFile(file); |
| | | return true; |
| | | }; |
| | | // 校验视频时长 |
| | | if ( |
| | | file.type === "video" && |
| | | file.duration && |
| | | file.duration > props.maxVideoDuration |
| | | ) { |
| | | uni.showToast({ |
| | | title: `视频时长不能超过 ${props.maxVideoDuration} 秒!`, |
| | | icon: "none", |
| | | }); |
| | | return false; |
| | | } |
| | | |
| | | // 上传失败处理 |
| | | const handleUploadError = (message = '上传文件失败', showRetry = true) => { |
| | | if (showRetry) { |
| | | uni.showModal({ |
| | | title: '上传失败', |
| | | content: message + ',是否重试?', |
| | | success: (res) => { |
| | | if (res.confirm) { |
| | | // 用户选择重试,这里可以重新触发上传 |
| | | // 校验文件类型 |
| | | if ( |
| | | props.fileType && |
| | | Array.isArray(props.fileType) && |
| | | props.fileType.length > 0 |
| | | ) { |
| | | const fileName = file.name || ""; |
| | | const fileExtension = fileName |
| | | ? fileName.split(".").pop().toLowerCase() |
| | | : ""; |
| | | |
| | | // 根据文件类型确定期望的扩展名 |
| | | let expectedTypes = []; |
| | | if (file.type === "image") { |
| | | expectedTypes = ["jpg", "jpeg", "png", "gif", "webp"]; |
| | | } else if (file.type === "video") { |
| | | expectedTypes = ["mp4", "mov", "avi", "wmv"]; |
| | | } |
| | | |
| | | // 检查文件扩展名是否在允许的类型中 |
| | | if (fileExtension && expectedTypes.length > 0) { |
| | | const isAllowed = expectedTypes.some( |
| | | type => props.fileType.includes(type) && type === fileExtension |
| | | ); |
| | | |
| | | if (!isAllowed) { |
| | | uni.showToast({ |
| | | title: `文件格式不支持,请拍摄 ${expectedTypes.join("/")} 格式的文件`, |
| | | icon: "none", |
| | | }); |
| | | return false; |
| | | } |
| | | } |
| | | }); |
| | | } else { |
| | | uni.showToast({ |
| | | title: message, |
| | | icon: 'error' |
| | | }); |
| | | } |
| | | }; |
| | | |
| | | // 上传成功回调 |
| | | const handleUploadSuccess = (res, file) => { |
| | | if (res.code === 200 && res.data && Array.isArray(res.data) && res.data.length > 0) { |
| | | const uploadedFile = res.data[0]; |
| | | // 确保上传的文件数据完整,包含id |
| | | const fileData = { |
| | | ...file, |
| | | id: uploadedFile.id, // 添加服务器返回的id |
| | | url: uploadedFile.url || uploadedFile.downloadUrl, |
| | | bucketFilename: uploadedFile.bucketFilename || file.name, |
| | | downloadUrl: uploadedFile.downloadUrl || uploadedFile.url, |
| | | size: uploadedFile.size || file.size, |
| | | createTime: uploadedFile.createTime || new Date().getTime() |
| | | }; |
| | | |
| | | uploadList.value.push(fileData); |
| | | uploadedSuccessfully(); |
| | | } else { |
| | | number.value--; // 上传失败时减少计数 |
| | | handleUploadError(res.msg || '上传失败'); |
| | | } |
| | | }; |
| | | |
| | | // 删除文件 |
| | | const handleDelete = (index) => { |
| | | uni.showModal({ |
| | | title: '确认删除', |
| | | content: '确定要删除这个文件吗?', |
| | | success: (res) => { |
| | | if (res.confirm) { |
| | | fileList.value.splice(index, 1); |
| | | emit("update:modelValue", listToString(fileList.value)); |
| | | uni.showToast({ |
| | | title: '删除成功', |
| | | icon: 'success' |
| | | }); |
| | | } |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | // 上传结束处理 |
| | | const uploadedSuccessfully = () => { |
| | | if (number.value > 0 && uploadList.value.length === number.value) { |
| | | // 合并已存在的文件和刚上传的文件 |
| | | const existingFiles = fileList.value.filter((f) => f.url !== undefined); |
| | | fileList.value = [...existingFiles, ...uploadList.value]; |
| | | |
| | | // 重置状态 |
| | | uploadList.value = []; |
| | | number.value = 0; |
| | | |
| | | // 触发更新事件,传递完整的文件列表 |
| | | emit("update:modelValue", fileList.value); |
| | | } |
| | | }; |
| | | // 校验通过,开始上传 |
| | | uploadFile(file); |
| | | return true; |
| | | }; |
| | | |
| | | const listToString = (list, separator = ",") => { |
| | | const strs = list |
| | | .filter(item => item.url) |
| | | .map(item => item.url) |
| | | .join(separator); |
| | | return strs; |
| | | }; |
| | | // 上传失败处理 |
| | | const handleUploadError = (message = "上传文件失败", showRetry = true) => { |
| | | if (showRetry) { |
| | | uni.showModal({ |
| | | title: "上传失败", |
| | | content: message + ",是否重试?", |
| | | success: res => { |
| | | if (res.confirm) { |
| | | // 用户选择重试,这里可以重新触发上传 |
| | | } |
| | | }, |
| | | }); |
| | | } else { |
| | | uni.showToast({ |
| | | title: message, |
| | | icon: "error", |
| | | }); |
| | | } |
| | | }; |
| | | |
| | | // 上传成功回调 |
| | | const handleUploadSuccess = (res, file) => { |
| | | if ( |
| | | res.code === 200 && |
| | | res.data && |
| | | Array.isArray(res.data) && |
| | | res.data.length > 0 |
| | | ) { |
| | | const uploadedFile = res.data[0]; |
| | | // 确保上传的文件数据完整,包含id |
| | | const fileData = { |
| | | ...file, |
| | | id: uploadedFile.id, // 添加服务器返回的id |
| | | url: uploadedFile.url || uploadedFile.downloadUrl, |
| | | bucketFilename: uploadedFile.bucketFilename || file.name, |
| | | downloadUrl: uploadedFile.downloadUrl || uploadedFile.url, |
| | | size: uploadedFile.size || file.size, |
| | | createTime: uploadedFile.createTime || new Date().getTime(), |
| | | }; |
| | | |
| | | uploadList.value.push(fileData); |
| | | uploadedSuccessfully(); |
| | | } else { |
| | | number.value--; // 上传失败时减少计数 |
| | | handleUploadError(res.msg || "上传失败"); |
| | | } |
| | | }; |
| | | |
| | | // 删除文件 |
| | | const handleDelete = index => { |
| | | uni.showModal({ |
| | | title: "确认删除", |
| | | content: "确定要删除这个文件吗?", |
| | | success: res => { |
| | | if (res.confirm) { |
| | | fileList.value.splice(index, 1); |
| | | emit("update:modelValue", listToString(fileList.value)); |
| | | uni.showToast({ |
| | | title: "删除成功", |
| | | icon: "success", |
| | | }); |
| | | } |
| | | }, |
| | | }); |
| | | }; |
| | | |
| | | // 上传结束处理 |
| | | const uploadedSuccessfully = () => { |
| | | if (number.value > 0 && uploadList.value.length === number.value) { |
| | | // 合并已存在的文件和刚上传的文件 |
| | | const existingFiles = fileList.value.filter(f => f.url !== undefined); |
| | | fileList.value = [...existingFiles, ...uploadList.value]; |
| | | |
| | | // 重置状态 |
| | | uploadList.value = []; |
| | | number.value = 0; |
| | | |
| | | // 触发更新事件,传递完整的文件列表 |
| | | emit("update:modelValue", fileList.value); |
| | | } |
| | | }; |
| | | |
| | | const listToString = (list, separator = ",") => { |
| | | const strs = list |
| | | .filter(item => item.url) |
| | | .map(item => item.url) |
| | | .join(separator); |
| | | return strs; |
| | | }; |
| | | </script> |
| | | <style scoped lang="scss"> |
| | | .camera-upload { |
| | | width: 100%; |
| | | } |
| | | .camera-upload { |
| | | width: 100%; |
| | | } |
| | | |
| | | .camera-buttons { |
| | | margin-bottom: 15px; |
| | | |
| | | .button-row { |
| | | .camera-buttons { |
| | | margin-bottom: 15px; |
| | | |
| | | .button-row { |
| | | display: flex; |
| | | gap: 10px; |
| | | } |
| | | } |
| | | |
| | | .upload-tip { |
| | | font-size: 12px; |
| | | color: #909399; |
| | | margin-bottom: 15px; |
| | | text-align: center; |
| | | line-height: 1.5; |
| | | |
| | | .tip-text { |
| | | margin: 0 2px; |
| | | } |
| | | |
| | | .tip-highlight { |
| | | color: #f56c6c; |
| | | font-weight: bold; |
| | | } |
| | | } |
| | | |
| | | .media-list { |
| | | margin-top: 10px; |
| | | display: flex; |
| | | gap: 10px; |
| | | flex-wrap: wrap; |
| | | gap: 8px; |
| | | } |
| | | } |
| | | |
| | | .upload-tip { |
| | | font-size: 12px; |
| | | color: #909399; |
| | | margin-bottom: 15px; |
| | | text-align: center; |
| | | line-height: 1.5; |
| | | |
| | | .tip-text { |
| | | margin: 0 2px; |
| | | .media-item { |
| | | position: relative; |
| | | width: 80px; |
| | | height: 80px; |
| | | border-radius: 8px; |
| | | overflow: hidden; |
| | | background-color: #f5f5f5; |
| | | border: 2px solid #e9ecef; |
| | | transition: all 0.3s ease; |
| | | |
| | | &:hover { |
| | | border-color: #409eff; |
| | | transform: scale(1.02); |
| | | } |
| | | } |
| | | |
| | | .tip-highlight { |
| | | color: #f56c6c; |
| | | font-weight: bold; |
| | | } |
| | | } |
| | | |
| | | .media-list { |
| | | margin-top: 10px; |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | gap: 8px; |
| | | } |
| | | |
| | | .media-item { |
| | | position: relative; |
| | | width: 80px; |
| | | height: 80px; |
| | | border-radius: 8px; |
| | | overflow: hidden; |
| | | background-color: #f5f5f5; |
| | | border: 2px solid #e9ecef; |
| | | transition: all 0.3s ease; |
| | | |
| | | &:hover { |
| | | border-color: #409eff; |
| | | transform: scale(1.02); |
| | | } |
| | | } |
| | | |
| | | .media-preview { |
| | | position: relative; |
| | | width: 100%; |
| | | height: 100%; |
| | | cursor: pointer; |
| | | |
| | | .preview-image, .preview-video { |
| | | .media-preview { |
| | | position: relative; |
| | | width: 100%; |
| | | height: 100%; |
| | | object-fit: cover; |
| | | cursor: pointer; |
| | | |
| | | .preview-image, |
| | | .preview-video { |
| | | width: 100%; |
| | | height: 100%; |
| | | object-fit: cover; |
| | | } |
| | | |
| | | .media-type-icon { |
| | | position: absolute; |
| | | top: 4px; |
| | | right: 4px; |
| | | width: 20px; |
| | | height: 20px; |
| | | background-color: rgba(0, 0, 0, 0.6); |
| | | border-radius: 50%; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | } |
| | | } |
| | | |
| | | .media-type-icon { |
| | | |
| | | .media-actions { |
| | | position: absolute; |
| | | top: 4px; |
| | | right: 4px; |
| | | width: 20px; |
| | | height: 20px; |
| | | background-color: rgba(0, 0, 0, 0.6); |
| | | border-radius: 50%; |
| | | bottom: 0; |
| | | left: 0; |
| | | right: 0; |
| | | background: linear-gradient(transparent, rgba(0, 0, 0, 0.7)); |
| | | padding: 4px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | } |
| | | } |
| | | opacity: 0; |
| | | transition: opacity 0.3s ease; |
| | | |
| | | .media-actions { |
| | | position: absolute; |
| | | bottom: 0; |
| | | left: 0; |
| | | right: 0; |
| | | background: linear-gradient(transparent, rgba(0, 0, 0, 0.7)); |
| | | padding: 4px; |
| | | display: flex; |
| | | justify-content: center; |
| | | opacity: 0; |
| | | transition: opacity 0.3s ease; |
| | | |
| | | .media-item:hover & { |
| | | opacity: 1; |
| | | .media-item:hover & { |
| | | opacity: 1; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .upload-progress { |
| | | margin-top: 15px; |
| | | padding: 0 10px; |
| | | } |
| | | .upload-progress { |
| | | margin-top: 15px; |
| | | padding: 0 10px; |
| | | } |
| | | </style> |