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)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<view v-if="deletable" class="delete-icon" @click.stop="handleDelete(index)">
|
|
|
|
|
|
×
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</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-11 20:48:30 +08:00
|
|
|
|
<view
|
2026-03-24 11:45:13 +08:00
|
|
|
|
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">
|
|
|
|
|
|
<!-- 上传按钮 -->
|
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="!oneFile"
|
|
|
|
|
|
>
|
|
|
|
|
|
<slot name="button">
|
|
|
|
|
|
<text class="plus-icon">+</text>
|
|
|
|
|
|
</slot>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<!-- 提示文字 -->
|
|
|
|
|
|
<!-- <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: {
|
|
|
|
|
|
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
|
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-11 20:48:30 +08:00
|
|
|
|
loading: false,
|
|
|
|
|
|
dragIndex: -1,
|
|
|
|
|
|
isDragging: false,
|
|
|
|
|
|
suppressPreview: 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
|
|
|
|
watch: {
|
|
|
|
|
|
value: {
|
|
|
|
|
|
immediate: true,
|
|
|
|
|
|
handler(newVal) {
|
|
|
|
|
|
this.fileList = [...newVal]
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
fileString: {
|
|
|
|
|
|
immediate: true,
|
|
|
|
|
|
handler(newVal) {
|
|
|
|
|
|
this.oneFile = newVal
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
2026-06-11 20:48:30 +08:00
|
|
|
|
|
2026-03-24 11:45:13 +08:00
|
|
|
|
methods: {
|
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/*') {
|
|
|
|
|
|
const res = await uni.chooseVideo({
|
|
|
|
|
|
sourceType: [sourceType],
|
|
|
|
|
|
compressed: true,
|
|
|
|
|
|
maxDuration: 60
|
|
|
|
|
|
});
|
|
|
|
|
|
await this.uploadFile(res);
|
|
|
|
|
|
}
|
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) {
|
|
|
|
|
|
// 检查文件大小
|
|
|
|
|
|
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()
|
2026-06-11 20:48:30 +08:00
|
|
|
|
const fileExt = this.getFileExt(file)
|
|
|
|
|
|
|
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-11 20:48:30 +08:00
|
|
|
|
const fileUrl = await this.uploadToOss(file, 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
|
|
|
|
|
|
this.$emit('input', this.oneFile)
|
|
|
|
|
|
this.$emit('change', this.oneFile)
|
|
|
|
|
|
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'
|
|
|
|
|
|
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
|
|
|
|
|
|
},
|
|
|
|
|
|
|
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-03-24 11:45:13 +08:00
|
|
|
|
// #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);
|
2026-06-11 20:48:30 +08:00
|
|
|
|
formData.append('key', objectKey);
|
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-03-24 11:45:13 +08:00
|
|
|
|
// #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;
|
2026-06-11 20:48:30 +08:00
|
|
|
|
formData.key = objectKey;
|
2026-03-24 11:45:13 +08:00
|
|
|
|
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: {
|
2026-06-11 20:48:30 +08:00
|
|
|
|
key: objectKey, // 文件名
|
2026-03-24 11:45:13 +08:00
|
|
|
|
policy: ossConfig.policy, // 后台获取超时时间
|
|
|
|
|
|
OSSAccessKeyId: ossConfig.ossAccessKeyId, // 后台获取临时ID
|
|
|
|
|
|
success_action_status: 200, // 让服务端返回200,不然,默认会返回204
|
|
|
|
|
|
signature: ossConfig.signature // 后台获取签名
|
|
|
|
|
|
},
|
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
|
|
|
|
|
|
|
|
|
|
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/*') {
|
|
|
|
|
|
const deletedFile = this.oneFile
|
|
|
|
|
|
this.oneFile = ''
|
|
|
|
|
|
this.$emit('input', this.oneFile)
|
|
|
|
|
|
this.$emit('change', this.oneFile)
|
|
|
|
|
|
this.$emit('delete', deletedFile)
|
|
|
|
|
|
}
|
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;
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.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;
|
|
|
|
|
|
}
|
2026-06-11 20:48:30 +08:00
|
|
|
|
</style>
|