first commit
This commit is contained in:
52
components/hjb-upload/README.md
Normal file
52
components/hjb-upload/README.md
Normal file
@@ -0,0 +1,52 @@
|
||||
### 使用说明
|
||||
|
||||
拷贝该组件到 components 目录下之后
|
||||
在 script 中引用组件
|
||||
|
||||
```js
|
||||
import hjbUpload from "@/components/hjb-upload/hjb-upload.vue";
|
||||
export default {
|
||||
components: { hjbUpload },
|
||||
};
|
||||
```
|
||||
|
||||
在 template 中使用组件:
|
||||
|
||||
```html
|
||||
<hjb-upload
|
||||
ref="upload"
|
||||
name="file"
|
||||
action="/upload"
|
||||
:headers="headers"
|
||||
:limit="10"
|
||||
:file-list="fileList"
|
||||
:on-change="uploadChange"
|
||||
>
|
||||
<!-- 自定义渲染可选 -->
|
||||
<image slot-scope="{item,index}" :src="item.url" />
|
||||
</hjb-upload>
|
||||
```
|
||||
|
||||
### 组件参数说明
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
| ------------- | ------------------------------------------------------------------------------------------------------- | ---------------------------------- | ------ |
|
||||
| action | 必选参数,上传的地址 | string | — |
|
||||
| headers | 设置上传的请求头部 | object | — |
|
||||
| multiple | 是否支持多选文件 | boolean | false |
|
||||
| formData | 上传时附带的额外参数 | object | — |
|
||||
| name | 上传的文件字段名 | string | file |
|
||||
| fileType | 接受上传的文件类型,例如: ['png'] | array | [] |
|
||||
| on-preview | 点击文件列表中已上传的文件时的钩子 | function(file) | — |
|
||||
| on-remove | 文件列表移除文件时的钩子 | function(file, fileList) | — |
|
||||
| on-success | 文件上传成功时的钩子 | function(response, file, fileList) | — |
|
||||
| on-error | 文件上传失败时的钩子 | function(err, file, fileList) | — |
|
||||
| on-progress | 文件上传时的钩子 | function(event, file, fileList) | — |
|
||||
| on-change | 文件状态改变时的钩子,添加文件、上传成功和上传失败时都会被调用 | function(file, fileList) | — |
|
||||
| before-upload | 上传文件之前的钩子,参数为上传的文件,若返回 false 或者返回 Promise 且被 reject,则停止上传。 | function(file) | — |
|
||||
| before-remove | 删除文件之前的钩子,参数为上传的文件和文件列表,若返回 false 或者返回 Promise 且被 reject,则停止删除。 | function(file, fileList) | — |
|
||||
| auto-upload | 是否在选取文件后立即进行上传 | boolean | true |
|
||||
| file-list | 上传的文件列表, 例如: [{name: 'food.jpg', url: 'https://xxx.cdn.com/xxx.jpg'}] | array | [] |
|
||||
| http-request | 覆盖默认的上传行为,可以自定义上传的实现 | function | — |
|
||||
| disabled | 是否禁用 | boolean | false |
|
||||
| limit | 最大允许上传个数 | number | 10 |
|
||||
599
components/hjb-upload/hjb-upload.vue
Normal file
599
components/hjb-upload/hjb-upload.vue
Normal file
@@ -0,0 +1,599 @@
|
||||
<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>
|
||||
4
components/hjb-upload/package.json
Normal file
4
components/hjb-upload/package.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "hjb-upload",
|
||||
"version": "1.0.3"
|
||||
}
|
||||
Reference in New Issue
Block a user