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

612 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="ali-oss-uploader">
<view class="preview-container" :style="'width:'+width">
<!-- 预览区域 -->
<view v-if="accept == 'image/*'" class="preview-container" :style="'width:'+width">
<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"
>
<image
:src="file"
class="preview-image"
mode="aspectFill"
@click="handlePreview(index)"
/>
<view v-if="deletable" class="delete-icon" @click.stop="handleDelete(index)">
×
</view>
</view>
<!-- 上传按钮 -->
<view class="preview-item">
<view
class="upload-btn"
@click="handleUpload"
:style="btnStyle"
v-if="fileList.length < maxCount"
>
<slot name="button">
<text class="plus-icon">+</text>
</slot>
</view>
</view>
</view>
<!-- 视频预览区域 -->
<view class="preview-container" :style="'width:'+width" v-if="accept == 'video/*'">
<view
class="preview-item"
v-if="oneFile"
>
<!-- <video
:src="oneFile"
class="preview-image"
@click="handlePreview(index)"
/> -->
<image class="preview-image" :src="oneFile + '?x-oss-process=video/snapshot,t_0,f_jpg'" mode="aspectFill"></image>
<view v-if="deletable" class="delete-icon" @click.stop="handleDelete()">
×
</view>
</view>
<view class="preview-item">
<!-- 上传按钮 -->
<view
class="upload-btn"
@click="handleUpload"
:style="btnStyle"
v-if="!oneFile"
>
<slot name="button">
<text class="plus-icon">+</text>
</slot>
</view>
</view>
</view>
</view>
<!-- 提示文字 -->
<!-- <text v-if="tips" class="tips-text">{{ tips }}</text> -->
</view>
</template>
<script>
import request from '../../utils/request'
import permissionUtils from '../../utils/per'
import locationService from '../../utils/locationService';
export default {
name: 'AliOssUploader',
props: {
// 已上传文件列表
value: {
type: Array,
default: () => []
},
// 单个上传文件
fileString: {
type: String,
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
},
sortable: {
type: Boolean,
default: false
}
},
data() {
return {
fileList: [],
oneFile: '',
loading: false,
dragIndex: -1,
isDragging: false,
suppressPreview: false
}
},
watch: {
value: {
immediate: true,
handler(newVal) {
this.fileList = [...newVal]
}
},
fileString: {
immediate: true,
handler(newVal) {
this.oneFile = newVal
}
}
},
methods: {
// 显示权限说明弹窗
showPermissionDialog(title, content) {
return new Promise((resolve) => {
uni.showModal({
title,
content,
confirmText: '去开启',
success(res) {
resolve(res.confirm);
}
});
});
},
// 触发上传
async handleUpload() {
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) {
try {
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/*') {
const res = await uni.chooseVideo({
sourceType: [sourceType],
compressed: true,
maxDuration: 60
});
await this.uploadFile(res);
}
} catch (error) {
console.error('选择文件失败:', error);
}
},
// 上传文件到OSS
async uploadFile(file) {
// 检查文件大小
if (file.size > this.maxSize * 1024 * 1024) {
uni.showToast({
title: `文件大小不能超过${this.maxSize}MB`,
icon: 'none'
})
return Promise.reject(new Error('文件大小超出限制'))
}
let date = new Date().getTime()
const fileExt = this.getFileExt(file)
let artisan = this.type==1 ? 'yh' : this.type==2 ? 'syr' : this.type==3 ? 'sj' : '';
let name = `${this.userId}_${artisan}_${date}.${fileExt}`
console.log('name',name)
try {
// 1. 获取OSS上传凭证
const ossConfig = await this.getOssConfig()
// 2. 上传文件
const fileUrl = await this.uploadToOss(file, ossConfig, name)
// 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
this.$emit('input', this.oneFile)
this.$emit('change', this.oneFile)
this.$emit('success', newFile)
return newFile
}
} catch (error) {
console.error('上传失败:', error)
this.$emit('error', error)
throw error
}
},
// 获取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
}
},
getFileExt(file) {
const defaultExt = this.accept === 'video/*' ? 'mp4' : 'jpg'
const source = file && (file.name || file.tempFilePath || file.path || file.url || '')
const cleanPath = String(source).split('?')[0].split('#')[0]
const ext = cleanPath.includes('.') ? cleanPath.split('.').pop().toLowerCase() : ''
return /^[a-z0-9]+$/.test(ext) ? ext : defaultExt
},
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
},
// 上传到OSS
uploadToOss(file, ossConfig, name) {
return new Promise((resolve, reject) => {
const objectKey = this.buildObjectKey(ossConfig, name)
const fileUrl = this.buildFileUrl(ossConfig, name)
// #ifdef H5
const formData = new FormData()
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);
formData.append('key', objectKey);
// file必须为最后一个表单域除file以外的其他表单域无顺序要求。
formData.append('file', file.file || file.tempFile || file);
fetch(ossConfig.host, { method: 'POST', body: formData},).then((res) => {
if (res.ok) {
resolve(fileUrl)
} else {
reject(new Error(`上传失败: ${res.status}`))
}
}).catch((error)=>{
reject(error)
})
// #endif
// #ifdef APP-PLUS || MP-WEIXIN
let formData = {};
formData.name = name;
formData.policy = ossConfig.policy;
formData.OSSAccessKeyId = ossConfig.ossAccessKeyId;
formData.success_action_status = '200';
formData.signature = ossConfig.signature;
formData.key = objectKey;
formData.file = file;
let path;
if(this.accept == 'image/*') {
path = file.path
}else if(this.accept == 'video/*') {
// #ifdef H5
path = file.name
// #endif
// #ifdef APP-PLUS || MP-WEIXIN
path = file.tempFilePath
// #endif
}
// 使用 uni.uploadFile 上传文件(支持多平台)
uni.uploadFile({
url: ossConfig.host,
filePath: path, // 手机端文件路径
name: 'file',
formData: {
key: objectKey, // 文件名
policy: ossConfig.policy, // 后台获取超时时间
OSSAccessKeyId: ossConfig.ossAccessKeyId, // 后台获取临时ID
success_action_status: 200, // 让服务端返回200,不然默认会返回204
signature: ossConfig.signature // 后台获取签名
},
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)
}
});
// #endif
})
},
// 预览图片
handlePreview(index) {
if (this.isDragging || this.suppressPreview) return
uni.previewImage({
current: index,
urls: this.fileList
})
},
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)
},
// 删除文件
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/*') {
const deletedFile = this.oneFile
this.oneFile = ''
this.$emit('input', this.oneFile)
this.$emit('change', this.oneFile)
this.$emit('delete', deletedFile)
}
// }
// }
// })
}
}
}
</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;
grid-template-columns: repeat(3, 1fr);
justify-items: center;
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;
margin-top: 20rpx;
}
.image-preview-item.dragging {
opacity: 0.65;
transform: scale(0.96);
}
.preview-image {
width: 200rpx;
height: 200rpx;
border-radius: 8rpx;
}
.delete-icon {
position: absolute;
top: -10rpx;
right: -10rpx;
width: 40rpx;
height: 40rpx;
background-color: #ff4d4f;
color: white;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
font-size: 32rpx;
z-index: 1;
}
</style>