mrr.sj.front/components/ali-oss-uploader/ali-oss-uploader.vue

907 lines
23 KiB
Vue
Raw Normal View History

2026-03-24 11:45:13 +08:00
<template>
<view class="ali-oss-uploader">
<view class="preview-container" :style="'width:'+width">
<!-- 预览区域 -->
<view v-if="accept == 'image/*'" class="preview-container" :style="'width:'+width">
2026-06-11 20:48:30 +08:00
<view
v-for="(file, index) in fileList"
:key="file || index"
class="preview-item image-preview-item"
:class="{ dragging: dragIndex === index }"
@longpress="startDrag(index)"
@touchmove="handleDragMove"
@touchend="endDrag"
@touchcancel="endDrag"
2026-03-24 11:45:13 +08:00
>
<image
:src="file"
class="preview-image"
mode="aspectFill"
@click="handlePreview(index)"
/>
2026-06-12 13:59:52 +08:00
<image
v-if="deletable"
class="delete-icon"
src="https://mrrplus.oss-cn-beijing.aliyuncs.com/photo/system/6ab3fc63-3bad-403d-bbfc-9609a93c048a.png"
mode="aspectFit"
@click.stop="handleDelete(index)"
></image>
2026-03-24 11:45:13 +08:00
</view>
<!-- 上传按钮 -->
<view class="preview-item">
2026-06-11 20:48:30 +08:00
<view
2026-03-24 11:45:13 +08:00
class="upload-btn"
@click="handleUpload"
:style="btnStyle"
v-if="fileList.length < maxCount"
>
<slot name="button">
<text class="plus-icon">+</text>
</slot>
</view>
</view>
2026-06-11 20:48:30 +08:00
2026-03-24 11:45:13 +08:00
</view>
2026-06-11 20:48:30 +08:00
2026-03-24 11:45:13 +08:00
<!-- 视频预览区域 -->
<view class="preview-container" :style="'width:'+width" v-if="accept == 'video/*'">
2026-06-12 13:59:52 +08:00
<block v-if="showPreview">
<view
v-for="(file, index) in videoList"
:key="file || index"
class="preview-item"
2026-06-23 09:55:51 +08:00
@click="previewVideo(file)"
2026-06-12 13:59:52 +08:00
>
<!-- <video
:src="file"
class="preview-image"
@click="handlePreview(index)"
/> -->
<image class="preview-image" :src="getVideoPoster(file)" mode="aspectFill"></image>
2026-06-23 09:55:51 +08:00
<view class="video-play-mask">
<view class="video-play-circle">
<view class="video-play-icon"></view>
</view>
</view>
2026-06-12 13:59:52 +08:00
<image
v-if="deletable"
class="delete-icon"
src="https://mrrplus.oss-cn-beijing.aliyuncs.com/photo/system/6ab3fc63-3bad-403d-bbfc-9609a93c048a.png"
mode="aspectFit"
@click.stop="handleDelete(index)"
></image>
2026-03-24 11:45:13 +08:00
</view>
2026-06-12 13:59:52 +08:00
</block>
2026-03-24 11:45:13 +08:00
<view class="preview-item">
<!-- 上传按钮 -->
2026-06-11 20:48:30 +08:00
<view
2026-03-24 11:45:13 +08:00
class="upload-btn"
@click="handleUpload"
:style="btnStyle"
2026-06-12 13:59:52 +08:00
v-if="videoCount < maxCount"
2026-03-24 11:45:13 +08:00
>
<slot name="button">
<text class="plus-icon">+</text>
</slot>
</view>
</view>
</view>
</view>
2026-06-23 09:55:51 +08:00
<view v-if="showVideoPlayer" class="video-player-mask" @click="closeVideoPlayer">
<view class="video-player-box" @click.stop>
<video :src="previewVideoUrl" class="video-player" controls autoplay show-fullscreen-btn></video>
<view class="video-player-close" @click="closeVideoPlayer">
<text class="video-player-close-text">&times;</text>
</view>
</view>
</view>
2026-03-24 11:45:13 +08:00
<!-- 提示文字 -->
<!-- <text v-if="tips" class="tips-text">{{ tips }}</text> -->
2026-06-11 20:48:30 +08:00
2026-03-24 11:45:13 +08:00
</view>
</template>
<script>
import request from '../../utils/request'
import permissionUtils from '../../utils/per'
import locationService from '../../utils/locationService';
export default {
name: 'AliOssUploader',
2026-06-11 20:48:30 +08:00
2026-03-24 11:45:13 +08:00
props: {
// 已上传文件列表
value: {
2026-06-23 09:55:51 +08:00
type: [Array, String, Object],
2026-03-24 11:45:13 +08:00
default: () => []
},
// 单个上传文件
fileString: {
2026-06-23 09:55:51 +08:00
type: [String, Object, Array],
2026-03-24 11:45:13 +08:00
default: ''
},
// 按钮文字
buttonText: {
type: String,
default: '上传图片'
},
// 提示文字
tips: {
type: String,
default: ''
},
// 最大上传数量
maxCount: {
type: Number,
default: 3
},
// 单个文件大小限制(MB)
maxSize: {
type: Number,
default: 5
},
// 是否显示预览
showPreview: {
type: Boolean,
default: true
},
// 是否可删除
deletable: {
type: Boolean,
default: true
},
// 按钮自定义样式
btnStyle: {
type: Object,
default: () => ({})
},
// 文件类型
accept: {
type: String,
default: 'image/*'
},
// 当前用户id
userId: {
type: Number,
default: null
},
width: {
type: String,
default: ''
},
type: {
type: Number,
default: null
},
kind: {
type: Number,
default: 1
2026-06-11 20:48:30 +08:00
},
sortable: {
type: Boolean,
default: false
2026-03-24 11:45:13 +08:00
}
},
2026-06-11 20:48:30 +08:00
2026-03-24 11:45:13 +08:00
data() {
return {
fileList: [],
oneFile: '',
2026-06-12 13:59:52 +08:00
videoList: [],
2026-06-23 09:55:51 +08:00
loading: false,
2026-06-11 20:48:30 +08:00
dragIndex: -1,
isDragging: false,
2026-06-23 09:55:51 +08:00
suppressPreview: false,
showVideoPlayer: false,
previewVideoUrl: ''
2026-03-24 11:45:13 +08:00
}
},
2026-06-11 20:48:30 +08:00
2026-06-12 13:59:52 +08:00
computed: {
videoCount() {
return this.showPreview ? this.videoList.length : 0
}
},
2026-03-24 11:45:13 +08:00
watch: {
value: {
immediate: true,
handler(newVal) {
2026-06-23 09:55:51 +08:00
if (this.accept === 'video/*') {
const videoValue = this.normalizeVideoValue(newVal)
this.oneFile = videoValue
this.videoList = videoValue ? [videoValue] : []
return
}
this.fileList = Array.isArray(newVal) ? [...newVal] : []
2026-03-24 11:45:13 +08:00
}
},
fileString: {
immediate: true,
handler(newVal) {
2026-06-23 09:55:51 +08:00
const videoValue = this.normalizeVideoValue(newVal)
this.oneFile = videoValue
this.videoList = videoValue ? [videoValue] : []
2026-03-24 11:45:13 +08:00
}
}
},
2026-06-11 20:48:30 +08:00
2026-03-24 11:45:13 +08:00
methods: {
2026-06-23 09:55:51 +08:00
normalizeVideoValue(value) {
if (!value) return ''
if (typeof value === 'string') {
const str = value.trim()
if (!str) return ''
if ((str.startsWith('{') && str.endsWith('}')) || (str.startsWith('[') && str.endsWith(']'))) {
try {
return this.normalizeVideoValue(JSON.parse(str))
} catch (error) {
return str
}
}
return str
}
if (Array.isArray(value)) {
for (const item of value) {
const url = this.normalizeVideoValue(item)
if (url) return url
}
return ''
}
if (typeof value === 'object') {
const candidates = [value.url, value.path, value.filePath, value.tempFilePath, value.src, value.value]
for (const item of candidates) {
const url = this.normalizeVideoValue(item)
if (url) return url
}
return ''
}
return String(value)
},
2026-06-11 20:48:30 +08:00
2026-03-24 11:45:13 +08:00
// 显示权限说明弹窗
showPermissionDialog(title, content) {
return new Promise((resolve) => {
uni.showModal({
title,
content,
confirmText: '去开启',
success(res) {
resolve(res.confirm);
}
});
});
},
// 触发上传
async handleUpload() {
2026-03-25 13:29:04 +08:00
const systemInfo = uni.getSystemInfoSync();
const isIOS = systemInfo.platform === 'ios';
// 弹出操作菜单
const res = await new Promise((resolve, reject) => {
uni.showActionSheet({
itemList: ['拍照', '从相册选择'],
success: resolve,
fail: reject
});
}).catch(() => ({ tapIndex: -1 }));
if (res.tapIndex === -1) return;
// 拍照
if (res.tapIndex === 0) {
if (isIOS) {
this._openPicker('camera');
} else {
// 显示相机权限说明
this.$emit('ckeckisShowPer', true);
this.$emit('ckecknowQer', 'xj');
const { granted } = await permissionUtils.checkPermission('camera', '需要相机权限以拍照');
if (granted) {
this.$emit('ckeckisShowPer', false);
this._openPicker('camera');
return;
}
this.$emit('ckeckisShowPer', false);
const confirm = await this.showPermissionDialog('相机权限申请', '我们需要访问您的相机权限,以便您拍照上传');
if (!confirm) return;
const result = await permissionUtils.requestPermission('camera', '需要相机权限以拍照');
if (result) {
this._openPicker('camera');
} else {
locationService.openAppSettings();
}
}
}
// 从相册选择
else if (res.tapIndex === 1) {
if (isIOS) {
this._openPicker('album');
} else {
// 显示存储权限说明
this.$emit('ckeckisShowPer', true);
this.$emit('ckecknowQer', 'xc');
const { granted } = await permissionUtils.checkPermission('photo_library', '需要存储权限以选择照片');
if (granted) {
this.$emit('ckeckisShowPer', false);
this._openPicker('album');
return;
}
this.$emit('ckeckisShowPer', false);
const confirm = await this.showPermissionDialog('存储权限申请', '我们需要访问您的相册权限,以便您选择照片');
if (!confirm) return;
const result = await permissionUtils.requestPermission('photo_library', '需要存储权限以选择照片');
if (result) {
this._openPicker('album');
} else {
locationService.openAppSettings();
}
}
}
},
// 实际打开选择器(图片或视频)
async _openPicker(sourceType) {
2026-03-24 11:45:13 +08:00
try {
2026-03-25 13:29:04 +08:00
if (this.accept === 'image/*') {
const count = this.maxCount - this.fileList.length;
if (count <= 0) {
uni.showToast({ title: `最多上传${this.maxCount}个文件`, icon: 'none' });
return;
}
const res = await uni.chooseImage({
count: count,
sizeType: ['compressed'],
sourceType: [sourceType]
});
for (const file of res.tempFiles) {
await this.uploadFile(file);
}
} else if (this.accept === 'video/*') {
2026-06-12 13:59:52 +08:00
const count = this.maxCount - this.videoCount;
if (count <= 0) {
uni.showToast({ title: `最多上传${this.maxCount}个文件`, icon: 'none' });
return;
}
const files = await this.chooseVideoFiles(sourceType, count);
for (const file of files) {
await this.uploadFile(file);
}
2026-03-25 13:29:04 +08:00
}
2026-03-24 11:45:13 +08:00
} catch (error) {
2026-03-25 13:29:04 +08:00
console.error('选择文件失败:', error);
2026-03-24 11:45:13 +08:00
}
},
// 上传文件到OSS
async uploadFile(file) {
2026-06-23 09:55:51 +08:00
const uploadFile = await this.prepareUploadFile(file)
2026-03-24 11:45:13 +08:00
// 检查文件大小
2026-06-23 09:55:51 +08:00
if (uploadFile.size > this.maxSize * 1024 * 1024) {
2026-03-24 11:45:13 +08:00
uni.showToast({
title: `文件大小不能超过${this.maxSize}MB`,
icon: 'none'
})
return Promise.reject(new Error('文件大小超出限制'))
}
2026-06-12 13:59:52 +08:00
let date = `${new Date().getTime()}_${Math.floor(Math.random() * 100000)}`
2026-06-23 09:55:51 +08:00
const fileExt = this.getFileExt(uploadFile)
2026-06-11 20:48:30 +08:00
2026-03-24 11:45:13 +08:00
let artisan = this.type==1 ? 'yh' : this.type==2 ? 'syr' : this.type==3 ? 'sj' : '';
2026-06-11 20:48:30 +08:00
let name = `${this.userId}_${artisan}_${date}.${fileExt}`
2026-03-24 11:45:13 +08:00
console.log('name',name)
try {
// 1. 获取OSS上传凭证
const ossConfig = await this.getOssConfig()
// 2. 上传文件
2026-06-23 09:55:51 +08:00
const fileUrl = await this.uploadToOss(uploadFile, ossConfig, name)
2026-03-24 11:45:13 +08:00
// 3. 添加到文件列表
if(this.accept == 'image/*') {
const newFile = [`${fileUrl}`]
this.fileList = this.fileList.concat(newFile)
this.$emit('input', this.fileList)
this.$emit('change', this.fileList)
this.$emit('success', newFile)
return newFile
}else if(this.accept == 'video/*') {
const newFile = fileUrl
this.oneFile = fileUrl
2026-06-12 13:59:52 +08:00
if (this.showPreview) {
this.videoList = this.videoList.concat([newFile]).slice(0, this.maxCount)
}
const emitValue = this.maxCount > 1 && this.showPreview ? this.videoList : this.oneFile
this.$emit('input', emitValue)
this.$emit('change', emitValue)
2026-03-24 11:45:13 +08:00
this.$emit('success', newFile)
return newFile
}
2026-06-11 20:48:30 +08:00
2026-03-24 11:45:13 +08:00
} catch (error) {
console.error('上传失败:', error)
this.$emit('error', error)
throw error
}
},
2026-06-11 20:48:30 +08:00
2026-03-24 11:45:13 +08:00
// 获取OSS配置
async getOssConfig() {
try {
const res = await request.post('/user/getalioss',{type: this.type,kind: this.kind})
return res
} catch (error) {
console.error('获取OSS配置失败:', error)
throw error
}
},
2026-06-11 20:48:30 +08:00
getFileExt(file) {
const defaultExt = this.accept === 'video/*' ? 'mp4' : 'jpg'
2026-06-23 09:55:51 +08:00
const sources = this.accept === 'video/*'
? [file && file.tempFilePath, file && file.path, file && file.url, file && file.name]
: [file && file.path, file && file.tempFilePath, file && file.url, file && file.name]
for (const source of sources) {
const cleanPath = String(source || '').split('?')[0].split('#')[0]
const ext = cleanPath.includes('.') ? cleanPath.split('.').pop().toLowerCase() : ''
if (/^[a-z0-9]+$/.test(ext)) return ext
}
return defaultExt
},
getFilePath(file) {
if (!file) return ''
return file.tempFilePath || file.path || file.url || file.name || ''
},
getMimeType(file, name) {
const ext = this.getFileExt({ tempFilePath: name, path: name, name })
const mimeTypes = {
jpg: 'image/jpeg',
jpeg: 'image/jpeg',
png: 'image/png',
gif: 'image/gif',
webp: 'image/webp',
mp4: 'video/mp4',
m4v: 'video/x-m4v',
mov: 'video/quicktime',
qt: 'video/quicktime',
webm: 'video/webm',
mpeg: 'video/mpeg',
mpg: 'video/mpeg',
avi: 'video/x-msvideo',
'3gp': 'video/3gpp',
mkv: 'video/x-matroska',
}
return mimeTypes[ext] || (file && file.type) || ''
},
buildOssFormData(ossConfig, objectKey, mimeType) {
const formData = {
key: objectKey,
policy: ossConfig.policy,
OSSAccessKeyId: ossConfig.ossAccessKeyId,
success_action_status: '200',
signature: ossConfig.signature,
}
if (mimeType) {
formData['Content-Type'] = mimeType
formData['x-oss-meta-content-type'] = mimeType
}
return formData
},
async prepareUploadFile(file) {
if (this.accept !== 'video/*') return file
const filePath = this.getFilePath(file)
const fileExt = this.getFileExt(file)
if (!filePath || fileExt === 'mp4' || typeof uni.compressVideo !== 'function') {
return file
}
try {
const res = await uni.compressVideo({
src: filePath,
quality: 'medium'
})
const tempFilePath = res.tempFilePath || res.path
if (!tempFilePath) return file
return {
...file,
tempFilePath,
path: tempFilePath,
size: res.size || file.size,
name: this.replaceFileExt(file.name, 'mp4'),
type: 'video/mp4'
}
} catch (error) {
console.warn('视频压缩失败,使用原文件上传:', error)
return file
}
},
replaceFileExt(name, ext) {
if (!name) return ''
return String(name).replace(/\.[^.]+$/, `.${ext}`)
2026-06-11 20:48:30 +08:00
},
buildFileUrl(ossConfig, name) {
const host = String(ossConfig.host || '').replace(/\/+$/, '')
return `${host}/${this.buildObjectKey(ossConfig, name)}`
},
buildObjectKey(ossConfig, name) {
const dir = String(ossConfig.dir || '').replace(/^\/+/, '').replace(/\/+$/, '')
return dir ? `${dir}/${name}` : name
},
2026-06-12 13:59:52 +08:00
getVideoPoster(url) {
if (!url) return ''
if (String(url).indexOf('x-oss-process=') > -1) return url
const connector = String(url).indexOf('?') > -1 ? '&' : '?'
return `${url}${connector}x-oss-process=video/snapshot,t_2000,f_jpg,m_fast`
},
chooseVideoFiles(sourceType, count) {
2026-06-23 09:55:51 +08:00
if (count <= 1 && typeof uni.chooseVideo === 'function') {
return new Promise((resolve, reject) => {
uni.chooseVideo({
sourceType: [sourceType],
compressed: true,
maxDuration: 60,
success: (res) => resolve([{
...res,
tempFilePath: res.tempFilePath || res.path,
path: res.tempFilePath || res.path,
}]),
fail: reject,
});
});
}
2026-06-12 13:59:52 +08:00
if (uni.chooseMedia) {
return new Promise((resolve, reject) => {
uni.chooseMedia({
count,
mediaType: ['video'],
sourceType: [sourceType],
maxDuration: 60,
success: (res) => {
const files = (res.tempFiles || []).map((item) => ({
...item,
tempFilePath: item.tempFilePath || item.path,
path: item.tempFilePath || item.path,
}));
resolve(files);
},
fail: reject,
});
});
}
return new Promise((resolve, reject) => {
uni.chooseVideo({
sourceType: [sourceType],
compressed: true,
maxDuration: 60,
success: (res) => resolve([res]),
fail: reject,
});
});
},
2026-03-24 11:45:13 +08:00
// 上传到OSS
uploadToOss(file, ossConfig, name) {
return new Promise((resolve, reject) => {
2026-06-11 20:48:30 +08:00
const objectKey = this.buildObjectKey(ossConfig, name)
const fileUrl = this.buildFileUrl(ossConfig, name)
2026-06-23 09:55:51 +08:00
const mimeType = this.getMimeType(file, name)
let formData;
// #ifdef H5
formData = new FormData()
2026-03-24 11:45:13 +08:00
formData.append('name',name);
formData.append('policy', ossConfig.policy);
formData.append('OSSAccessKeyId', ossConfig.ossAccessKeyId);
formData.append('success_action_status', '200');
formData.append('signature', ossConfig.signature);
2026-06-11 20:48:30 +08:00
formData.append('key', objectKey);
2026-06-23 09:55:51 +08:00
if (mimeType) {
formData.append('Content-Type', mimeType);
formData.append('x-oss-meta-content-type', mimeType);
}
2026-03-24 11:45:13 +08:00
// file必须为最后一个表单域除file以外的其他表单域无顺序要求。
2026-06-11 20:48:30 +08:00
formData.append('file', file.file || file.tempFile || file);
2026-03-24 11:45:13 +08:00
fetch(ossConfig.host, { method: 'POST', body: formData},).then((res) => {
2026-06-11 20:48:30 +08:00
if (res.ok) {
resolve(fileUrl)
} else {
reject(new Error(`上传失败: ${res.status}`))
}
}).catch((error)=>{
reject(error)
2026-03-24 11:45:13 +08:00
})
// #endif
2026-06-11 20:48:30 +08:00
2026-06-23 09:55:51 +08:00
// #ifdef APP-PLUS || MP-WEIXIN
formData = this.buildOssFormData(ossConfig, objectKey, mimeType);
formData.name = name;
2026-03-24 11:45:13 +08:00
let path;
if(this.accept == 'image/*') {
2026-06-23 09:55:51 +08:00
path = this.getFilePath(file)
2026-03-24 11:45:13 +08:00
}else if(this.accept == 'video/*') {
// #ifdef H5
2026-06-23 09:55:51 +08:00
path = this.getFilePath(file)
2026-03-24 11:45:13 +08:00
// #endif
// #ifdef APP-PLUS || MP-WEIXIN
2026-06-23 09:55:51 +08:00
path = this.getFilePath(file)
2026-03-24 11:45:13 +08:00
// #endif
}
// 使用 uni.uploadFile 上传文件(支持多平台)
uni.uploadFile({
url: ossConfig.host,
filePath: path, // 手机端文件路径
name: 'file',
2026-06-23 09:55:51 +08:00
formData,
2026-06-11 20:48:30 +08:00
success: (res) => {
const statusCode = Number(res.statusCode)
if (statusCode >= 200 && statusCode < 300) {
resolve(fileUrl)
} else {
reject(new Error(`上传失败: ${statusCode} ${res.data || ''}`))
}
},
fail(error) {
reject(error)
2026-03-24 11:45:13 +08:00
}
});
// #endif
})
2026-06-11 20:48:30 +08:00
2026-03-24 11:45:13 +08:00
},
2026-06-11 20:48:30 +08:00
2026-03-24 11:45:13 +08:00
// 预览图片
handlePreview(index) {
2026-06-11 20:48:30 +08:00
if (this.isDragging || this.suppressPreview) return
2026-03-24 11:45:13 +08:00
uni.previewImage({
current: index,
2026-06-11 20:48:30 +08:00
urls: this.fileList
2026-03-24 11:45:13 +08:00
})
},
2026-06-11 20:48:30 +08:00
2026-06-23 09:55:51 +08:00
previewVideo(url) {
if (!url) return
this.previewVideoUrl = url
this.showVideoPlayer = true
},
closeVideoPlayer() {
this.showVideoPlayer = false
this.previewVideoUrl = ''
},
2026-06-11 20:48:30 +08:00
startDrag(index) {
if (!this.sortable || this.accept !== 'image/*' || this.fileList.length < 2) return
this.dragIndex = index
this.isDragging = true
},
handleDragMove(event) {
if (!this.isDragging || this.dragIndex < 0) return
if (event.preventDefault) event.preventDefault()
if (event.stopPropagation) event.stopPropagation()
const touch = event.touches && event.touches[0]
if (!touch) return
uni.createSelectorQuery()
.in(this)
.selectAll('.image-preview-item')
.boundingClientRect((rects) => {
if (!Array.isArray(rects) || !rects.length) return
const targetIndex = rects.findIndex((rect) => {
return touch.clientX >= rect.left &&
touch.clientX <= rect.right &&
touch.clientY >= rect.top &&
touch.clientY <= rect.bottom
})
if (targetIndex < 0 || targetIndex === this.dragIndex || targetIndex >= this.fileList.length) return
const nextList = this.fileList.slice()
const moved = nextList.splice(this.dragIndex, 1)[0]
nextList.splice(targetIndex, 0, moved)
this.fileList = nextList
this.dragIndex = targetIndex
this.$emit('input', this.fileList)
this.$emit('change', this.fileList)
})
.exec()
},
endDrag() {
if (!this.isDragging) return
this.dragIndex = -1
this.suppressPreview = true
setTimeout(() => {
this.isDragging = false
this.suppressPreview = false
}, 200)
},
2026-03-24 11:45:13 +08:00
// 删除文件
handleDelete(index) {
// uni.showModal({
// title: '提示',
// content: '确定删除该文件吗?',
// success: (res) => {
// if (res.confirm) {
if(this.accept == 'image/*') {
const deletedFile = this.fileList[index]
this.fileList.splice(index, 1)
this.$emit('input', this.fileList)
this.$emit('change', this.fileList)
this.$emit('delete', deletedFile, index)
}else if(this.accept == 'video/*') {
2026-06-12 13:59:52 +08:00
const deleteIndex = index === undefined ? 0 : index
const deletedFile = this.videoList[deleteIndex]
this.videoList.splice(deleteIndex, 1)
this.oneFile = this.videoList[0] || ''
const emitValue = this.maxCount > 1 ? this.videoList : this.oneFile
this.$emit('input', emitValue)
this.$emit('change', emitValue)
this.$emit('delete', deletedFile, deleteIndex)
2026-03-24 11:45:13 +08:00
}
2026-06-11 20:48:30 +08:00
2026-03-24 11:45:13 +08:00
// }
// }
// })
}
}
}
</script>
<style scoped>
/* .ali-oss-uploader {
display: grid;
grid-template-columns: repeat(3, 1fr);
justify-items: center;
align-items: center;
margin-top: 20rpx;
position: relative;
} */
.preview-container {
display: grid;
2026-06-12 13:59:52 +08:00
grid-template-columns: repeat(3, 200rpx);
column-gap: 18rpx;
row-gap: 20rpx;
justify-content: flex-start;
justify-items: stretch;
2026-03-24 11:45:13 +08:00
align-items: center;
position: relative;
}
.upload-btn {
width: 200rpx;
height: 200rpx;
border: 2rpx dashed #ddd;
border-radius: 8rpx;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
box-sizing: border-box;
/* margin-top: 20rpx; */
}
.plus-icon {
font-size: 60rpx;
color: #999;
line-height: 1;
}
.btn-text {
font-size: 24rpx;
color: #666;
margin-top: 10rpx;
}
.tips-text {
font-size: 24rpx;
color: #999;
margin-top: 10rpx;
}
.preview-item {
position: relative;
width: 200rpx;
height: 200rpx;
}
2026-06-11 20:48:30 +08:00
.image-preview-item.dragging {
opacity: 0.65;
transform: scale(0.96);
}
2026-03-24 11:45:13 +08:00
.preview-image {
width: 200rpx;
height: 200rpx;
border-radius: 8rpx;
}
2026-06-23 09:55:51 +08:00
.video-play-mask {
position: absolute;
left: 0;
top: 0;
width: 200rpx;
height: 200rpx;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(0, 0, 0, 0.18);
border-radius: 8rpx;
}
.video-play-circle {
width: 60rpx;
height: 60rpx;
border-radius: 50%;
background-color: rgba(0, 0, 0, 0.55);
display: flex;
align-items: center;
justify-content: center;
}
.video-play-icon {
width: 0;
height: 0;
border-top: 14rpx solid transparent;
border-bottom: 14rpx solid transparent;
border-left: 22rpx solid #fff;
margin-left: 6rpx;
}
2026-03-24 11:45:13 +08:00
.delete-icon {
position: absolute;
top: -10rpx;
right: -10rpx;
2026-06-12 13:59:52 +08:00
width: 42rpx;
height: 42rpx;
z-index: 5;
2026-03-24 11:45:13 +08:00
}
2026-06-23 09:55:51 +08:00
.video-player-mask {
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
z-index: 9999;
background-color: rgba(0, 0, 0, 0.75);
display: flex;
align-items: center;
justify-content: center;
}
.video-player-box {
position: relative;
width: 690rpx;
background-color: #000;
border-radius: 8rpx;
overflow: hidden;
}
.video-player {
width: 690rpx;
height: 388rpx;
background-color: #000;
}
.video-player-close {
position: absolute;
top: 12rpx;
right: 12rpx;
width: 52rpx;
height: 52rpx;
border-radius: 50%;
background-color: rgba(0, 0, 0, 0.55);
display: flex;
align-items: center;
justify-content: center;
}
.video-player-close-text {
color: #fff;
font-size: 42rpx;
line-height: 52rpx;
}
2026-06-11 20:48:30 +08:00
</style>