Files
2026-02-23 16:31:39 +08:00

600 lines
14 KiB
Vue
Raw Permalink 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="upload">
<view
v-for="(item, index) in uploadFiles"
class="upload__file"
:class="'upload__file--' + item.status"
:style="{
width: addUnit(width),
height: addUnit(height),
}"
@tap="onPreview(index, item)"
>
<slot :item="item" :index="index">
<image
:style="{
width: addUnit(width),
height: addUnit(height),
}"
:src="item.url"
:mode="imageMode"
/>
<view
class="upload__file--mask"
v-if="['uploading', 'fail'].includes(item.status)"
>
<text v-if="item.status == 'uploading'">{{ item.percentage }}%</text>
<icon
v-if="item.status == 'fail'"
type="warn"
color="red"
size="20"
/>
</view>
</slot>
<icon
class="upload__file--remove"
type="clear"
color="#263240"
size="20"
@tap.stop="handleRemove(item)"
/>
</view>
<view
:style="{
width: addUnit(width),
height: addUnit(height),
}"
class="upload__btn"
hover-class="upload__btn--hover"
hover-stay-time="150"
v-if="showUploadBtn"
@tap="chooseImage"
>
<slot>
<view class="icon"></view>
</slot>
</view>
</view>
</template>
<script>
function noop() {}
function uuid() {
let d = new Date().getTime();
let uuid = "xxxxxxxxxxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
let r = (d + Math.random() * 16) % 16 | 0;
d = Math.floor(d / 16);
return (c == "x" ? r : (r & 0x3) | 0x8).toString(16);
});
return uuid;
}
export default {
name: "hjb-upload",
props: {
fileList: {
type: Array,
default: () => [],
},
// 后端地址
action: {
type: String,
default: "",
},
// 是否开启图片多选
multiple: {
type: Boolean,
default: false,
},
sourceType: {
type: Array,
default: () => ["album", "camera"],
},
// 所选的图片的尺寸, 可选值为original compressed
sizeType: {
type: Array,
default: () => ["original", "compressed"],
},
// 预览上传的图片时的裁剪模式和image组件mode属性一致
imageMode: {
type: String,
default: "aspectFill",
},
// 头部信息
headers: {
type: Object,
default: () => ({}),
},
// 额外携带的参数
formData: {
type: Object,
default: () => ({}),
},
// 上传的文件字段名
name: {
type: String,
default: "file",
},
// 文件大小限制单位为byte
maxSize: {
type: [String, Number],
default: Number.MAX_VALUE,
},
// 最大上传数量 多图的时候才有效
limit: {
type: [String, Number],
default: 10,
},
// 是否自动上传
autoUpload: {
type: Boolean,
default: true,
},
// 是否显示toast消息提示
showTips: {
type: Boolean,
default: true,
},
// 内部预览图片区域和选择图片按钮的区域宽度
width: {
type: [String, Number],
default: 120,
},
// 内部预览图片区域和选择图片按钮的区域高度
height: {
type: [String, Number],
default: 120,
},
// 允许上传的图片后缀
fileType: {
type: Array,
default() {
// 支付宝小程序真机选择图片的后缀为"image"
// https://opendocs.alipay.com/mini/api/media-image
return [];
},
},
// 是否启用
disabled: {
type: Boolean,
default: false,
},
// 是否启用预览
enablePreview: {
type: Boolean,
default: true,
},
beforeUpload: Function,
beforeRemove: Function,
onRemove: {
type: Function,
default: noop,
},
onChange: {
type: Function,
default: noop,
},
onSuccess: {
type: Function,
default: noop,
},
onProgress: {
type: Function,
default: noop,
},
onError: {
type: Function,
default: noop,
},
httpRequest: {
type: Function,
default: uni.uploadFile,
},
},
data() {
return {
uploadFiles: [],
uploading: false,
reqs: {},
};
},
computed: {
showUploadBtn() {
if (this.multiple) {
return this.limit > this.uploadFiles.length;
}
return this.uploadFiles.length < 1;
},
},
watch: {
fileList: {
immediate: true,
handler(files) {
this.uploadFiles = files.map((item) => {
item.uid = item.uid || uuid();
item.status = item.status || "success";
return item;
});
},
},
},
methods: {
// 清除列表
clear() {
this.uploadFiles = [];
},
// 该方法供用户通过ref调用手动上传
upload() {
if (this.disabled) return;
this.uploadFiles
.filter((file) => file.status === "ready")
.forEach((item) => {
this.uploadFile(item);
});
},
// 图片预览
onPreview(index, file) {
if (!this.enablePreview) return;
const images = this.uploadFiles.map((item) => item.url || item.path);
uni.previewImage({
current: index,
urls: images,
success: () => {
this.$emit("preview", file, index);
},
fail: () => {
uni.showToast({
title: "Preview image failed",
icon: "none",
});
},
});
},
// 选择图片
chooseImage() {
if (this.disabled) return;
const {
limit,
multiple,
maxSize,
uploadFiles,
sourceType,
sizeType,
autoUpload,
} = this;
const maxCount = limit - uploadFiles.length;
uni.chooseImage({
count: multiple ? maxCount : 1,
sourceType,
sizeType,
type: "image",
success: (res) => {
const files = res.tempFiles;
if (limit && uploadFiles.length + files.length > limit) {
this.showToast("The maximum number of files allowed is exceeded");
return;
}
files.forEach((val, index) => {
// 检查文件后缀是否允许如果不在this.fileType内就会返回false
if (!this.checkFileExt(val)) return;
// 如果是非多选index大于等于1或者超出最大限制数量时不处理
if (!multiple && index >= 1) return;
if (val.size > maxSize) {
this.showToast("Exceeds allowable file size");
return;
}
val.uid = uuid();
let file = {
status: "ready",
size: val.size,
percentage: 0,
uid: val.uid,
raw: val,
url: val.path,
};
this.uploadFiles.push(file);
this.onChange(file, this.uploadFiles);
if (autoUpload) this.uploadFile(file);
});
// 每次图片选择完,抛出一个事件,并将当前内部选择的图片数组抛出去
this.$emit("choose-complete", this.uploadFiles);
},
fail: (e) => {
this.$emit("choose-fail", e);
},
});
},
getFile(rawFile) {
let fileList = this.uploadFiles;
let target;
fileList.every((item) => {
target = rawFile.uid === item.uid ? item : null;
return !target;
});
return target;
},
// 对失败的图片重新上传
retry() {
uni.showLoading({
title: "重新上传",
});
this.uploadFiles
.filter((item) => item.ststus == "fail")
.forEach((item) => {
this.uploadFile(item);
});
},
// 上传图片
uploadFile(rawFile) {
// 检查上传地址
if (!this.action) {
this.showToast("Please configure the upload address", true);
return;
}
if (!this.beforeUpload) {
this.post(rawFile);
return;
}
const before = this.beforeUpload(rawFile);
if (before && before.then) {
console.log("async before");
before.then(
(file) => {
this.post(file);
},
() => {
this.handleRemove(null, rawFile);
}
);
} else if (before !== false) {
this.post(rawFile);
} else {
this.handleRemove(null, rawFile);
}
},
post(rawFile) {
const { uid } = rawFile;
const options = {
header: this.headers,
filePath: rawFile.url,
formData: this.formData,
name: this.name,
url: this.action,
success: (res) => {
// 判断是否json字符串将其转为json格式
let data = this.jsonString(res.data)
? JSON.parse(res.data)
: res.data;
if (![200, 201, 204].includes(res.statusCode)) {
this.uploadError(data, rawFile);
} else {
this.uploadSuccess(data, rawFile);
}
delete this.reqs[uid];
},
fail: (err) => {
this.uploadError(err, rawFile);
delete this.reqs[uid];
},
};
var uploadTask = this.httpRequest(options);
this.reqs[uid] = uploadTask;
uploadTask.onProgressUpdate((res) => {
this.uploadProgress(res, rawFile);
});
},
// 上传中
uploadProgress(ev, rawFile) {
const file = this.getFile(rawFile);
this.onProgress(ev, file, this.uploadFiles);
file.status = "uploading";
file.percentage = ev.progress || 0;
},
// 上传成功
uploadSuccess(res, rawFile) {
const file = this.getFile(rawFile);
const fileList = this.uploadFiles;
file.status = "success";
file.response = res;
file.name = res.name;
file.url = res.url;
this.onSuccess(res, file, this.uploadFiles);
this.onChange(file, this.uploadFiles);
},
// 上传失败
uploadError(err, rawFile) {
const file = this.getFile(rawFile);
const fileList = this.uploadFiles;
file.status = "fail";
fileList.splice(fileList.indexOf(file), 1);
this.onError(err, file, this.uploadFiles);
this.onChange(file, this.uploadFiles);
},
// 删除
handleRemove(file, raw) {
if (raw) {
file = this.getFile(raw);
}
let doRemove = () => {
this.abort(file);
let fileList = this.uploadFiles;
fileList.splice(fileList.indexOf(file), 1);
this.onRemove(file, fileList);
};
if (!this.beforeRemove) {
doRemove();
} else if (typeof this.beforeRemove === "function") {
const before = this.beforeRemove(file, this.uploadFiles);
if (before && before.then) {
before.then(() => {
doRemove();
}, noop);
} else if (before !== false) {
doRemove();
}
}
},
// 中断上传任务
abort(file) {
const { reqs } = this;
if (file) {
let uid = file;
if (file.uid) uid = file.uid;
if (reqs[uid]) {
reqs[uid].abort();
}
} else {
Object.keys(reqs).forEach((uid) => {
if (reqs[uid]) reqs[uid].abort();
delete reqs[uid];
});
}
},
// 判断文件后缀是否允许
checkFileExt(file) {
if (this.fileType.length == 0) {
return true;
}
// 检查是否在允许的后缀中
let noArrowExt = false;
// 获取后缀名
let fileExt = "";
const reg = /.+\./;
// 如果是H5需要从name中判断
// #ifdef H5
fileExt = file.name.replace(reg, "").toLowerCase();
// #endif
// 非H5需要从path中读取后缀
// #ifndef H5
fileExt = file.path.replace(reg, "").toLowerCase();
// #endif
// 使用数组的some方法只要符合limitType中的一个就返回true
noArrowExt = this.fileType.some((ext) => {
// 转为小写
return ext.toLowerCase() === fileExt;
});
if (!noArrowExt)
this.showToast(
`You are not allowed to select files in ${fileExt} format`
);
return noArrowExt;
},
// 提示用户消息
showToast(message, force = false) {
if (this.showTips || force) {
uni.showToast({
title: message,
icon: "none",
});
}
},
// 添加单位如果有rpx%px等单位结尾或者值为auto直接返回否则加上rpx单位结尾
addUnit(value = "auto", unit = "rpx") {
let reg = /^(?:-?\d+|-?\d{1,3}(?:,\d{3})+)?(?:\.\d+)?$/;
value = String(value);
// 用uView内置验证规则中的number判断是否为数值
return reg.test(value) ? `${value}${unit}` : value;
},
/**
* 是否json字符串
*/
jsonString(value) {
if (typeof value == "string") {
try {
var obj = JSON.parse(value);
if (typeof obj == "object" && obj) {
return true;
} else {
return false;
}
} catch (e) {
return false;
}
}
return false;
},
},
};
</script>
<style lang="scss" scoped>
.upload {
display: flex;
flex-wrap: wrap;
align-items: center;
&__file,
&__btn {
border-radius: 4rpx;
margin: 0 30rpx 30rpx 0;
}
&__file {
position: relative;
image {
border-radius: 4rpx;
}
&--mask {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: rgba(37, 38, 45, 0.4);
border-radius: 4rpx;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
}
&--remove {
position: absolute;
right: 0;
top: 0;
transform: translate(50%, -50%);
border: 3rpx solid #fff;
border-radius: 50%;
background: #fff;
}
}
&__btn {
display: flex;
align-items: center;
justify-content: center;
border: 2rpx solid #e0e0e0;
&--hover {
background: rgba(0, 0, 0, 0.04);
}
.icon {
position: relative;
&::before,
&::after {
content: "";
position: absolute;
top: 50%;
left: 50%;
width: 32rpx;
height: 3rpx;
transform: translate(-50%, -50%);
background-color: #b0b0b0;
}
&::after {
transform: translate(-50%, -50%) rotate(90deg);
}
}
}
}
</style>