@@ -7,6 +7,7 @@ | |||
:key="index" | |||
class="bg-img" | |||
> | |||
<!-- {{item.id?CONFIG('webHost')+item.url:item.url}} --> | |||
<image | |||
v-if="showfile()" | |||
:src="item.id?CONFIG('webHost')+item.url:item.url" | |||
@@ -89,20 +90,6 @@ export default { | |||
}, | |||
uploadImage(){ | |||
// let array = | |||
// function apiFn(item){ | |||
// return this.HTTP_POST('StuInfoFresh/savePhoto', postData, '图片上传失败!').then((data) => { | |||
// if (data) { | |||
// reslove([{ | |||
// url:data.Url, | |||
// id:data.AnnexesFileId | |||
// }]) | |||
// }else{ | |||
// reject('图片上传失败!') | |||
// } | |||
// }) | |||
// } | |||
// return this.promiseAllLimit(5,array,apiFn) | |||
// 单图上传 | |||
return new Promise(async (reslove,reject)=>{ | |||
let hasNotUpdatedList = this.imgList.every(item=>item.id) | |||
@@ -232,6 +219,7 @@ export default { | |||
// }, | |||
}, | |||
created() { | |||
console.log(123) | |||
this.imgList = JSON.parse(JSON.stringify(this.value.map(item=>{ | |||
item.isUploaded = true | |||
return item | |||
@@ -8,12 +8,12 @@ | |||
class="bg-img" | |||
> | |||
<!-- {{API + '/user/img?data=' + (path.path ? path.path : path)}} --> | |||
<!-- <image | |||
v-if="showfile()" | |||
:src="API + item.url" | |||
<image | |||
v-if="showfile()&&isImage(item.type)" | |||
:src="CONFIG('webHost') + item.url" | |||
mode="aspectFill" | |||
></image> --> | |||
<l-icon v-if="showfile()" type="text" /> | |||
></image> | |||
<l-icon v-if="showfile()&&!isImage(item.type)" type="text" /> | |||
<text class="file-name">{{item.name}}</text> | |||
<view v-if="!readonly" @tap.stop="delImg(index)" class="cu-tag bg-red" style="width: 18px; height: 18px; font-size: 24px"> | |||
<l-icon | |||
@@ -23,15 +23,27 @@ | |||
</view> | |||
</view> | |||
<view | |||
v-if="!readonly && imgList.length < Number(number)" | |||
@tap="chooseImg" | |||
<view | |||
v-show="!readonly && imgList.length < Number(number)&&isShow" | |||
class="solids" | |||
> | |||
<!-- @tap="chooseImg" --> | |||
<l-icon type="file" /> | |||
<lsj-upload | |||
ref="lsjUpload" | |||
height="80px" | |||
width="100%" | |||
:size="20" | |||
:option="{}" | |||
:count="1" | |||
@change="chooseChange" | |||
style="opacity: 0;" | |||
></lsj-upload> | |||
</view> | |||
</view> | |||
</view> | |||
</template> | |||
<script> | |||
@@ -46,67 +58,77 @@ export default { | |||
data(){ | |||
return{ | |||
isShow:false, | |||
imgList:[], | |||
} | |||
}, | |||
methods: { | |||
chooseChange(files){ | |||
let array = Array.from(files); | |||
if(array.length){ | |||
this.$refs.lsjUpload.clear() | |||
} | |||
let tempFilePaths = [],tempFiles=[]; | |||
array.forEach(item=>{ | |||
tempFilePaths.push(item[1].path) | |||
tempFiles.push(item[1].file) | |||
}) | |||
this.chooseChangeback(tempFilePaths,tempFiles) | |||
}, | |||
delImg(index) { | |||
this.LOADING('正在删除…'); | |||
const newList = JSON.parse(JSON.stringify(this.imgList)); | |||
this.HTTP_POST('StuInfoFresh/deleteFiles', {id:this.imgList[index].id},"文件删除失败").then((data) => { | |||
this.HIDE_LOADING(); | |||
if (data) { | |||
newList.splice(index, 1); | |||
this.imgList = newList | |||
this.$emit("update:value", newList); | |||
this.$emit("input", newList); | |||
this.$emit("change"); | |||
this.$emit("del"); | |||
} | |||
}) | |||
this.LOADING('正在删除…'); | |||
const newList = JSON.parse(JSON.stringify(this.imgList)); | |||
this.HTTP_POST('StuInfoFresh/deleteFiles', {id:this.imgList[index].id},"文件删除失败").then((data) => { | |||
this.HIDE_LOADING(); | |||
if (data) { | |||
newList.splice(index, 1); | |||
this.imgList = newList | |||
this.$emit("update:value", newList); | |||
this.$emit("input", newList); | |||
this.$emit("change"); | |||
this.$emit("del"); | |||
} | |||
}) | |||
}, | |||
showfile() { | |||
return true; | |||
}, | |||
chooseImg() { | |||
uni.chooseFile({ | |||
// count: (Number(this.number) - this.imgList.length), | |||
count: 1, | |||
type:"all", | |||
success: async (res) => { | |||
let {tempFilePaths,tempFiles} = res | |||
if(!this.validate(tempFiles))return | |||
let uploadImageRes = await this.uploadImage(tempFilePaths[0]) | |||
async chooseChangeback(tempFilePaths,tempFiles) { | |||
// let {tempFilePaths,tempFiles} = res | |||
// if(!this.validate(tempFiles))return | |||
let uploadImageRes = await this.uploadImage(tempFilePaths[0],tempFiles[0]?tempFiles[0].name:"") | |||
let newList = this.imgList || [] | |||
if(uploadImageRes){ | |||
newList = JSON.parse(JSON.stringify(newList)).concat(uploadImageRes); | |||
} | |||
this.imgList = newList | |||
this.$emit("update:value", newList); | |||
this.$emit("input", newList); | |||
this.$emit("change",newList); | |||
this.$emit("add"); | |||
}, | |||
}); | |||
}, | |||
this.$emit("input", newList); | |||
this.$emit("change",newList); | |||
this.$emit("add"); | |||
}, | |||
uploadImage(url){ | |||
uploadImage(url,name){ | |||
if(!url)return | |||
// 文件上传 | |||
return new Promise(async (reslove,reject)=>{ | |||
this.LOADING('正在上传…'); | |||
this.HTTP_UPLOAD2('StuInfoFresh/upload', url||this.imgList[0].url,{folderId:this.folderId}).then((data) => { | |||
let params = name?{folderId:this.folderId,name}:{folderId:this.folderId} | |||
this.HTTP_UPLOAD2('StuInfoFresh/upload', url,params).then((data) => { | |||
this.HIDE_LOADING(); | |||
this.$refs.lsjUpload.show() | |||
if (data) { | |||
// this.HTTP_GET('StuInfoFresh/upload', {fileId:data}) | |||
reslove([{ | |||
id:data.F_Id, | |||
name:data.F_FileName, | |||
url:data.F_FilePath | |||
url:data.F_FilePath, | |||
type:data.F_FileType, | |||
}]) | |||
}else{ | |||
reject('上传失败!') | |||
@@ -115,48 +137,6 @@ export default { | |||
}) | |||
}, | |||
// 图片文件转为 base64 | |||
getPictureBase64(file) { | |||
return new Promise((resolve, reject) => { | |||
const reader = new FileReader(); | |||
reader.readAsDataURL(file); | |||
reader.onload = () => resolve(reader.result); | |||
reader.onerror = (error) => reject(error); | |||
}); | |||
}, | |||
base64ToBlob (base64) { | |||
const parts = base64.split(";base64,"); | |||
const contentType = parts[0].split(":")[1]; | |||
const raw = window.atob(parts[1]); | |||
const rawLength = raw.length; | |||
const uInt8Array = new Uint8Array(rawLength); | |||
for (let i = 0; i < rawLength; i += 1) { | |||
uInt8Array[i] = raw.charCodeAt(i); | |||
} | |||
return new Blob([uInt8Array], { type: contentType }); | |||
}, | |||
imgToBase64(url){ | |||
return new Promise((resolve,reject)=>{ | |||
var canvas = document.createElement('canvas'), | |||
ctx = canvas.getContext('2d'), | |||
img = new Image; | |||
img.crossOrigin = 'Anonymous'; | |||
img.onload = function () { | |||
canvas.height = img.height; | |||
canvas.width = img.width; | |||
ctx.drawImage(img, 0, 0); | |||
var dataURL = canvas.toDataURL('image/png'); | |||
canvas = null; | |||
resolve(dataURL) | |||
}; | |||
img.src = url; | |||
}) | |||
}, | |||
ceshi(){ | |||
function apiFn(params){ | |||
@@ -169,7 +149,8 @@ export default { | |||
this.imgList[params.index] = [{ | |||
id:data.F_Id, | |||
name:data.F_FileName, | |||
url:data.F_FilePath | |||
url:data.F_FilePath, | |||
type:data.F_FileType | |||
}] | |||
reslove(this.imgList[params.index]) | |||
}else{ | |||
@@ -237,9 +218,24 @@ export default { | |||
} | |||
return true | |||
}, | |||
isImage(type){ | |||
if(type&&type.length){ | |||
return ["png","jpg"].includes(type.substring(type.length-3,type.length)) | |||
}else{ | |||
return false | |||
} | |||
}, | |||
viewImg(item) { | |||
window.location.href = this.CONFIG("webHost")+item.url | |||
if(!this.isImage(item.type)){ | |||
window.location.href = this.CONFIG("webHost")+item.url | |||
}else{ | |||
uni.previewImage({ | |||
urls: [this.CONFIG('webHost')+item.url], | |||
current: this.CONFIG('webHost')+item.url | |||
}); | |||
} | |||
}, | |||
// previewFile() { | |||
@@ -270,16 +266,17 @@ export default { | |||
// }, | |||
}, | |||
created() { | |||
// id name | |||
console.log(this.value) | |||
this.imgList = JSON.parse(JSON.stringify(this.value.map(item=>{ | |||
return { | |||
id:item.F_Id, | |||
name:item.F_FileName, | |||
url:item.F_FilePath | |||
url:item.F_FilePath, | |||
type:item.F_FileType | |||
} | |||
}))) | |||
// [{url:"",id:""}] | |||
this.$nextTick(()=>{ | |||
this.isShow = true | |||
}) | |||
} | |||
}; | |||
</script> | |||
@@ -292,6 +289,7 @@ export default { | |||
color: #606266; | |||
font-size: 13px; | |||
text-align: center; | |||
background-color: rgba(255,255,255,0.6); | |||
text-overflow: ellipsis; | |||
overflow: hidden; | |||
@@ -27,8 +27,8 @@ export default { | |||
"webHost":"http://192.168.10.85:8087/", | |||
// 开发环境下自动填充登录账号密码,与接口地址一一对应,只在开发环境下显示 | |||
"devAccount": [ | |||
// 20201130230 21364200000400266 老师 420528196310072253 学生 420528200606205026 | |||
{ username: "420528200507261428", password: "www.qj.com" } | |||
// 20201130230 21364200000400266 老师 420528196310072253 学生 420528200606205026 420528200507261428 | |||
{ username: "system", password: "www.qj.com" } | |||
], | |||
//是否分布式部署 指WebApi与Web不在一台服务器 | |||
"isDistributed":true, | |||
@@ -14,8 +14,6 @@ | |||
:currentNode="currentNode" | |||
:currentTask="currentTask" | |||
/> | |||
</view> | |||
<!-- Tab #2:流程图页 --> | |||
<view v-if="ready && tab === 1" class="progress"><l-workflow-timeline :processList="processList" /></view> | |||
@@ -30,7 +28,6 @@ import workflowFormMixins from '../workflow.js' | |||
export default { | |||
data() { | |||
return { | |||
tab: 0, | |||
editMode: false, | |||
type: 'view', | |||
@@ -184,7 +184,7 @@ export default { | |||
}, | |||
refreshComponent(){ | |||
this.uploadVisiable = false | |||
// this.uploadVisiable = false | |||
this.$nextTick(()=>{ | |||
this.uploadVisiable = true | |||
}) | |||
@@ -0,0 +1,92 @@ | |||
## 2.1.9(2022-07-13) | |||
[修复] app端选择文件后初始化设置的文件列表被清空问题 | |||
## 2.1.8(2022-07-13) | |||
[新增] ref方法初始化文件列表,用于已提交后再次编辑时需带入已上传文件:setFiles(files),可传入数组或Map对象,传入格式请与组件选择返回格式保持一致,且name为必须属性。 | |||
## 2.1.7(2022-07-12) | |||
修复ios端偶现创建webview初始化参数未生效的问题 | |||
## 2.1.6(2022-07-11) | |||
[修复]:修复上个版本更新导致nvue窗口组件不能选择文件的问题; | |||
[新增]: | |||
1.应群友建议(填写禁止格式太多)格式限制formats由原来填写禁止选择的格式改为填写允许被选择的格式; | |||
2.应群友建议(增加上传结束回调事件),上传结束回调事件@uploadEnd | |||
3.如能帮到你请留下你的免费好评,组件使用过程中有问题可以加QQ群交流,至于Map对象怎么使用这类前端基础问题请自行百度 | |||
## 2.1.5(2022-07-01) | |||
app端组件销毁时添加自动销毁webview功能,避免v-if销毁组件的情况控件还能被点击的问题 | |||
## 2.1.4(2022-07-01) | |||
修复小程序端回显问题 | |||
## 2.1.3(2022-06-30) | |||
回调事件返回参数新增path字段(文件临时地址),用于回显 | |||
## 2.1.2(2022-06-16) | |||
修复APP端Tabbar窗口无法选择文件的问题 | |||
## 2.1.1(2022-06-16) | |||
优化: | |||
1.组件优化为允许在v-if中使用; | |||
2.允许option直接在data赋值,不再强制在onRead中初始化; | |||
## 2.1.0(2022-06-13) | |||
h5 pc端更改为单次可多选 | |||
## 2.0.9(2022-06-10) | |||
更新演示内容,部分同学不知道怎么获取服务端返回的数据 | |||
## 2.0.8(2022-06-09) | |||
优化动态更新上传参数函数,具体查看下方说明:动态更新参数演示 | |||
## 2.0.7(2022-06-07) | |||
新增wxFileType属性,用于小程序端选择附件时可选文件类型 | |||
## 2.0.6(2022-06-07) | |||
修复小程序端真机选择文件提示失败的问题 | |||
## 2.0.5(2022-06-02) | |||
优化小程序端调用hide()后未阻止触发文件选择问题 | |||
## 2.0.4(2022-06-01) | |||
优化APP端选择器初始定位 | |||
## 2.0.3(2022-05-31) | |||
修复nvue窗口选择文件报错问题 | |||
## 2.0.2(2022-05-20) | |||
修复ios端opiton设置过早未传入webview导致不自动上传问题 | |||
## 2.0.1(2022-05-19) | |||
修复APP端子窗口点击选择文件不响应问题 | |||
## 2.0.0(2022-05-18) | |||
此次组件更新至2.0版本,与1.0版本使用上略有差异,已使用1.0的同学请自行斟酌是否需要升级! | |||
部分差异: | |||
一、 2.0新增异步触发上传功能; | |||
二、2.0新增文件批量上传功能; | |||
三、2.0优化option,剔除属性,只保留上传接口所需字段,且允许异步更改option的值; | |||
四、组件增加size(文件大小限制)、count(文件个数限制)、formats(文件后缀限制)、accept(文件类型限制)、instantly(是否立即自动上传)、debug(日志打印)等属性; | |||
五、回调事件取消input事件、callback事件,新增change事件和progress事件; | |||
六、ref事件新增upload事件、clear事件; | |||
七、优化组件代码,show和hide函数改为显示隐藏,不再重复开关webview; | |||
## 1.2.3(2022-03-22) | |||
修复Demo里传入待完善功能[手动上传属性manual=true]导致不自动上传的问题,手动提交上传待下个版本更新 | |||
## 1.2.2(2022-02-21) | |||
修复上版本APP优化导致H5和小程序端不自动初始化的问题,此次更新仅修复此问题。异步提交功能下个版本更新~ | |||
## 1.2.1(2022-01-25) | |||
QQ1群已满,已开放2群:469580165 | |||
## 1.2.0(2021-12-09) | |||
优化APP端页面中DOM重排后每次需要重新定位的问题 | |||
## 1.1.1(2021-12-09) | |||
优化,与上版本使用方式有改变,请检查后确认是否需要更新,create更名为show, close更名为hide,取消初始化时手动create, 传参方式改为props=>option | |||
## 1.1.0(2021-12-09) | |||
新增refresh方法,用于DOM发生重排时重新定位控件(APP端) | |||
## 1.0.9(2021-07-15) | |||
修复上传进度未同步渲染,直接返回100%的BUG | |||
## 1.0.8(2021-07-12) | |||
修复H5端传入height和width未生效的bug | |||
## 1.0.7(2021-07-07) | |||
修复h5和小程序端上传完成callback未返回fileName字段问题 | |||
## 1.0.6(2021-07-07) | |||
修复h5端提示信息debug | |||
## 1.0.5(2021-06-29) | |||
感谢小伙伴找出bug,上传成功回调success未置为true,已修复 | |||
## 1.0.4(2021-06-28) | |||
新增兼容APP,H5,小程序手动关闭控件,关闭后不再弹出文件选择框,需要重新create再次开启 | |||
## 1.0.3(2021-06-28) | |||
close增加条件编译,除app端外不需要close | |||
## 1.0.2(2021-06-28) | |||
1.修复页面滚动位置后再create控件导致控件位置不正确的问题; | |||
2.修复nvue无法create控件; | |||
3.示例项目新增nvue使用案例; | |||
## 1.0.1(2021-06-28) | |||
因为有的朋友不清楚app端切换tab时应该怎么处理webview,现重新上传一版示例项目,需要做tab切换的朋友可以导入示例项目查看 | |||
## 1.0.0(2021-06-25) | |||
此插件为l-file插件中上传功能改版,更新内容为: | |||
1. 按钮内嵌入页面,不再强制固定底部,可跟随页面滚动 | |||
2.无需再单独弹框点击上传,减去中间层 | |||
3.通过slot自定义按钮样式 |
@@ -0,0 +1,392 @@ | |||
export class LsjFile { | |||
constructor(data) { | |||
this.dom = null; | |||
// files.type = waiting(等待上传)|| loading(上传中)|| success(成功) || fail(失败) | |||
this.files = new Map(); | |||
this.debug = data.debug || false; | |||
this.id = data.id; | |||
this.width = data.width; | |||
this.height = data.height; | |||
this.option = data.option; | |||
this.instantly = data.instantly; | |||
this.prohibited = data.prohibited; | |||
this.onchange = data.onchange; | |||
this.onprogress = data.onprogress; | |||
this.uploadHandle = this._uploadHandle; | |||
// #ifdef MP-WEIXIN | |||
this.uploadHandle = this._uploadHandleWX; | |||
// #endif | |||
} | |||
/** | |||
* 创建File节点 | |||
* @param {string}path webview地址 | |||
*/ | |||
create(path) { | |||
if (!this.dom) { | |||
// #ifdef H5 | |||
let dom = document.createElement('input'); | |||
dom.type = 'file' | |||
dom.value = '' | |||
dom.style.height = this.height | |||
dom.style.width = this.width | |||
dom.style.position = 'absolute' | |||
dom.style.top = 0 | |||
dom.style.left = 0 | |||
dom.style.right = 0 | |||
dom.style.bottom = 0 | |||
dom.style.opacity = 0 | |||
dom.style.zIndex = 999 | |||
dom.accept = this.prohibited.accept; | |||
if (this.prohibited.count > 1) { | |||
dom.multiple = 'multiple'; | |||
} | |||
dom.onchange = event => { | |||
for (let file of event.target.files) { | |||
this.addFile(file); | |||
} | |||
this.dom.value = ''; | |||
}; | |||
this.dom = dom; | |||
// #endif | |||
// #ifdef APP-PLUS | |||
let styles = { | |||
top: '-500px', | |||
left: 0, | |||
width: '1px', | |||
height: '1px', | |||
background: 'transparent' | |||
}; | |||
let extras = { | |||
debug: this.debug, | |||
instantly: this.instantly, | |||
prohibited: this.prohibited, | |||
} | |||
this.dom = plus.webview.create(path, this.id, styles,extras); | |||
this.setData(this.option); | |||
this._overrideUrlLoading(); | |||
// #endif | |||
return this.dom; | |||
} | |||
} | |||
copyObject(obj) { | |||
if (typeof obj !== "undefined") { | |||
return JSON.parse(JSON.stringify(obj)); | |||
} else { | |||
return obj; | |||
} | |||
} | |||
/** | |||
* 自动根据字符串路径设置对象中的值 支持.和[] | |||
* @param {Object} dataObj 数据源 | |||
* @param {String} name 支持a.b 和 a[b] | |||
* @param {String} value 值 | |||
* setValue(dataObj, name, value); | |||
*/ | |||
setValue(dataObj, name, value) { | |||
// 通过正则表达式 查找路径数据 | |||
let dataValue; | |||
if (typeof value === "object") { | |||
dataValue = this.copyObject(value); | |||
} else { | |||
dataValue = value; | |||
} | |||
let regExp = new RegExp("([\\w$]+)|\\[(:\\d)\\]", "g"); | |||
const patten = name.match(regExp); | |||
// 遍历路径 逐级查找 最后一级用于直接赋值 | |||
for (let i = 0; i < patten.length - 1; i++) { | |||
let keyName = patten[i]; | |||
if (typeof dataObj[keyName] !== "object") dataObj[keyName] = {}; | |||
dataObj = dataObj[keyName]; | |||
} | |||
// 最后一级 | |||
dataObj[patten[patten.length - 1]] = dataValue; | |||
this.debug&&console.log('参数更新后',JSON.stringify(this.option)); | |||
} | |||
/** | |||
* 设置上传参数 | |||
* @param {object|string}name 上传参数,支持a.b 和 a[b] | |||
*/ | |||
setData() { | |||
let [name,value = ''] = arguments; | |||
if (typeof name === 'object') { | |||
Object.assign(this.option,name); | |||
} | |||
else { | |||
this.setValue(this.option,name,value); | |||
} | |||
this.debug&&console.log(JSON.stringify(this.option)); | |||
// #ifdef APP-PLUS | |||
this.dom.evalJS(`vm.setData('${JSON.stringify(this.option)}')`); | |||
// #endif | |||
} | |||
/** | |||
* 上传 | |||
* @param {string}name 文件名称 | |||
*/ | |||
async upload(name='') { | |||
if (!this.option.url) { | |||
throw Error('未设置上传地址'); | |||
} | |||
// #ifndef APP-PLUS | |||
if (name && this.files.has(name)) { | |||
await this.uploadHandle(this.files.get(name)); | |||
} | |||
else { | |||
for (let item of this.files.values()) { | |||
if (item.type === 'waiting' || item.type === 'fail') { | |||
await this.uploadHandle(item); | |||
} | |||
} | |||
} | |||
// #endif | |||
// #ifdef APP-PLUS | |||
this.dom&&this.dom.evalJS(`vm.upload('${name}')`); | |||
// #endif | |||
} | |||
// 选择文件change | |||
addFile(file) { | |||
let name = file.name; | |||
this.debug&&console.log('文件名称',name,'大小',file.size); | |||
if (file) { | |||
// 限制文件格式 | |||
let path = ''; | |||
let suffix = name.substring(name.lastIndexOf(".")+1).toLowerCase(); | |||
let formats = this.prohibited.formats.toLowerCase(); | |||
if (formats&&!formats.includes(suffix)) { | |||
this.toast(`不支持上传${suffix.toUpperCase()}格式文件`); | |||
return false; | |||
} | |||
// 限制文件大小 | |||
if (file.size > 1024 * 1024 * Math.abs(this.prohibited.size)) { | |||
this.toast(`附件大小请勿超过${this.prohibited.size}M`) | |||
return false; | |||
} | |||
// #ifndef MP-WEIXIN | |||
path = URL.createObjectURL(file); | |||
// #endif | |||
// #ifdef MP-WEIXIN | |||
path = file.path; | |||
// #endif | |||
this.files.set(file.name,{file,path,name: file.name,size: file.size,progress: 0,type: 'waiting'}); | |||
// #ifndef MP-WEIXIN | |||
this.onchange(this.files); | |||
this.instantly&&this.upload(); | |||
// #endif | |||
// #ifdef MP-WEIXIN | |||
return true; | |||
// #endif | |||
} | |||
} | |||
/** | |||
* 移除文件 | |||
* @param {string}name 不传name默认移除所有文件,传入name移除指定name的文件 | |||
*/ | |||
clear(name='') { | |||
// #ifdef APP-PLUS | |||
this.dom&&this.dom.evalJS(`vm.clear('${name}')`); | |||
// #endif | |||
if (!name) { | |||
this.files.clear(); | |||
} | |||
else { | |||
this.files.delete(name); | |||
} | |||
return this.onchange(this.files); | |||
} | |||
/** | |||
* 提示框 | |||
* @param {string}msg 轻提示内容 | |||
*/ | |||
toast(msg) { | |||
uni.showToast({ | |||
title: msg, | |||
icon: 'none' | |||
}); | |||
} | |||
/** | |||
* 微信小程序选择文件 | |||
* @param {number}count 可选择文件数量 | |||
*/ | |||
chooseMessageFile(type,count) { | |||
wx.chooseMessageFile({ | |||
count: count, | |||
type: type, | |||
success: ({ tempFiles }) => { | |||
for (let file of tempFiles) { | |||
let next = this.addFile(file); | |||
if (!next) {return} | |||
} | |||
this.onchange(this.files); | |||
this.instantly&&this.upload(); | |||
}, | |||
fail: () => { | |||
this.toast(`打开失败`); | |||
} | |||
}) | |||
} | |||
_overrideUrlLoading() { | |||
this.dom.overrideUrlLoading({ mode: 'reject' }, e => { | |||
let {retype,item,files,end} = this._getRequest( | |||
e.url | |||
); | |||
let _this = this; | |||
switch (retype) { | |||
case 'updateOption': | |||
this.dom.evalJS(`vm.setData('${JSON.stringify(_this.option)}')`); | |||
break | |||
case 'change': | |||
try { | |||
_this.files = new Map([..._this.files,...JSON.parse(unescape(files))]); | |||
} catch (e) { | |||
return console.error('出错了,请检查代码') | |||
} | |||
_this.onchange(_this.files); | |||
break | |||
case 'progress': | |||
try { | |||
item = JSON.parse(unescape(item)); | |||
} catch (e) { | |||
return console.error('出错了,请检查代码') | |||
} | |||
_this._changeFilesItem(item,end); | |||
break | |||
default: | |||
break | |||
} | |||
}) | |||
} | |||
_getRequest(url) { | |||
let theRequest = new Object() | |||
let index = url.indexOf('?') | |||
if (index != -1) { | |||
let str = url.substring(index + 1) | |||
let strs = str.split('&') | |||
for (let i = 0; i < strs.length; i++) { | |||
theRequest[strs[i].split('=')[0]] = unescape(strs[i].split('=')[1]) | |||
} | |||
} | |||
return theRequest | |||
} | |||
_changeFilesItem(item,end=false) { | |||
this.debug&&console.log('onprogress',JSON.stringify(item)); | |||
this.onprogress(item,end); | |||
this.files.set(item.name,item); | |||
} | |||
_uploadHandle(item) { | |||
item.type = 'loading'; | |||
delete item.responseText; | |||
return new Promise((resolve,reject)=>{ | |||
this.debug&&console.log('option',JSON.stringify(this.option)); | |||
let {url,name,method='POST',header,formData} = this.option; | |||
let form = new FormData(); | |||
for (let keys in formData) { | |||
form.append(keys, formData[keys]) | |||
} | |||
form.append(name, item.file); | |||
let xmlRequest = new XMLHttpRequest(); | |||
xmlRequest.open(method, url, true); | |||
for (let keys in header) { | |||
xmlRequest.setRequestHeader(keys, header[keys]) | |||
} | |||
xmlRequest.upload.addEventListener( | |||
'progress', | |||
event => { | |||
if (event.lengthComputable) { | |||
let progress = Math.ceil((event.loaded * 100) / event.total) | |||
if (progress <= 100) { | |||
item.progress = progress; | |||
this._changeFilesItem(item); | |||
} | |||
} | |||
}, | |||
false | |||
); | |||
xmlRequest.ontimeout = () => { | |||
console.error('请求超时') | |||
item.type = 'fail'; | |||
this._changeFilesItem(item,true); | |||
return resolve(false); | |||
} | |||
xmlRequest.onreadystatechange = ev => { | |||
if (xmlRequest.readyState == 4) { | |||
if (xmlRequest.status == 200) { | |||
this.debug&&console.log('上传完成:' + xmlRequest.responseText) | |||
item['responseText'] = xmlRequest.responseText; | |||
item.type = 'success'; | |||
this._changeFilesItem(item,true); | |||
return resolve(true); | |||
} else if (xmlRequest.status == 0) { | |||
console.error('status = 0 :请检查请求头Content-Type与服务端是否匹配,服务端已正确开启跨域,并且nginx未拦截阻止请求') | |||
} | |||
console.error('--ERROR--:status = ' + xmlRequest.status) | |||
item.type = 'fail'; | |||
this._changeFilesItem(item,true); | |||
return resolve(false); | |||
} | |||
} | |||
xmlRequest.send(form) | |||
}); | |||
} | |||
_uploadHandleWX(item) { | |||
item.type = 'loading'; | |||
delete item.responseText; | |||
return new Promise((resolve,reject)=>{ | |||
this.debug&&console.log('option',JSON.stringify(this.option)); | |||
let form = {filePath: item.file.path,...this.option }; | |||
form['fail'] = ({ errMsg = '' }) => { | |||
console.error('--ERROR--:' + errMsg) | |||
item.type = 'fail'; | |||
this._changeFilesItem(item,true); | |||
return resolve(false); | |||
} | |||
form['success'] = res => { | |||
if (res.statusCode == 200) { | |||
this.debug&&console.log('上传完成,微信端返回不一定是字符串,根据接口返回格式判断是否需要JSON.parse:' + res.data) | |||
item['responseText'] = res.data; | |||
item.type = 'success'; | |||
this._changeFilesItem(item,true); | |||
return resolve(true); | |||
} | |||
item.type = 'fail'; | |||
this._changeFilesItem(item,true); | |||
return resolve(false); | |||
} | |||
let xmlRequest = uni.uploadFile(form); | |||
xmlRequest.onProgressUpdate(({ progress = 0 }) => { | |||
if (progress <= 100) { | |||
item.progress = progress; | |||
this._changeFilesItem(item); | |||
} | |||
}) | |||
}); | |||
} | |||
} |
@@ -0,0 +1,313 @@ | |||
<template> | |||
<view class="lsj-file" :style="[getStyles]"> | |||
<view ref="lsj" class="hFile" :style="[getStyles]" @click="onClick"> | |||
<slot><view class="defview" :style="[getStyles]">附件上传</view></slot> | |||
</view> | |||
</view> | |||
</template> | |||
<script> | |||
// 查看文档:https://ext.dcloud.net.cn/plugin?id=5459 | |||
import {LsjFile} from './LsjFile.js' | |||
export default { | |||
name: 'Lsj-upload', | |||
props: { | |||
// 打印日志 | |||
debug: {type: Boolean,default: false}, | |||
// 自动上传 | |||
instantly: {type: Boolean,default: false}, | |||
// 上传接口参数设置 | |||
option: {type: Object,default: ()=>{}}, | |||
// 文件大小上限 | |||
size: { type: Number, default: 10 }, | |||
// 文件选择个数上限,超出后不触发点击 | |||
count: { type: Number, default: 9 }, | |||
// 允许上传的文件格式(多个以逗号隔开) | |||
formats: { type: String, default:''}, | |||
// input file选择限制 | |||
accept: {type: String,default: ''}, | |||
// 微信选择文件类型 | |||
//all=从所有文件选择, | |||
//video=只能选择视频文件, | |||
//image=只能选择图片文件, | |||
//file=可以选择除了图片和视频之外的其它的文件 | |||
wxFileType: { type: String, default: 'all' }, | |||
// webviewID需唯一,不同窗口也不要同Id | |||
childId: { type: String, default: 'lsjUpload' }, | |||
// 文件选择触发面宽度 | |||
width: { type: String, default: '100%' }, | |||
// 文件选择触发面高度 | |||
height: { type: String, default: '80rpx' }, | |||
// top,left,bottom,right仅position=absolute时才需要传入 | |||
top: { type: [String, Number], default: '' }, | |||
left: { type: [String, Number], default: '' }, | |||
bottom: { type: [String, Number], default: '' }, | |||
right: { type: [String, Number], default: '' }, | |||
// nvue不支持跟随窗口滚动 | |||
position: { | |||
type: String, | |||
// #ifdef APP-NVUE | |||
default: 'absolute', | |||
// #endif | |||
// #ifndef APP-NVUE | |||
default: 'static', | |||
// #endif | |||
}, | |||
}, | |||
data() { | |||
return { | |||
} | |||
}, | |||
watch: { | |||
option(v) { | |||
// #ifdef APP-PLUS | |||
this.lsjFile&&this.show(); | |||
// #endif | |||
} | |||
}, | |||
updated() { | |||
// #ifdef APP-PLUS | |||
if (this.isShow) { | |||
this.lsjFile&&this.show(); | |||
} | |||
// #endif | |||
}, | |||
computed: { | |||
getStyles() { | |||
let styles = { | |||
width: this.width, | |||
height: this.height | |||
} | |||
if (this.position == 'absolute') { | |||
styles['top'] = this.top | |||
styles['bottom'] = this.bottom | |||
styles['left'] = this.left | |||
styles['right'] = this.right | |||
styles['position'] = 'fixed' | |||
} | |||
return styles | |||
} | |||
}, | |||
mounted() { | |||
this.$nextTick(()=>{ | |||
this._size = 0; | |||
this.lsjFile = new LsjFile({ | |||
debug: this.debug, | |||
id: this.childId, | |||
width: this.width, | |||
height: this.height, | |||
option: this.option, | |||
instantly: this.instantly, | |||
// 限制条件 | |||
prohibited: { | |||
// 大小 | |||
size: this.size, | |||
// 允许上传的格式 | |||
formats: this.formats, | |||
// 限制选择的格式 | |||
accept: this.accept, | |||
count: this.count | |||
}, | |||
onchange: this.onchange, | |||
onprogress: this.onprogress, | |||
}); | |||
this.create(); | |||
}) | |||
}, | |||
beforeDestroy() { | |||
// #ifdef APP-PLUS | |||
this.lsjFile.dom.close(); | |||
// #endif | |||
}, | |||
methods: { | |||
setFiles(array) { | |||
if (array instanceof Map) { | |||
for (let [key, item] of array) { | |||
item['progress'] = 100; | |||
item['type'] = 'success'; | |||
this.lsjFile.files.set(key,item); | |||
} | |||
} | |||
else if (Array.isArray(array)) { | |||
array.forEach(item=>{ | |||
if (item.name) { | |||
item['progress'] = 100; | |||
item['type'] = 'success'; | |||
this.lsjFile.files.set(item.name,item); | |||
} | |||
}); | |||
} | |||
this.onchange(this.lsjFile.files); | |||
}, | |||
setData() { | |||
this.lsjFile&&this.lsjFile.setData(...arguments); | |||
}, | |||
getDomStyles(callback) { | |||
// #ifndef APP-NVUE | |||
let view = uni | |||
.createSelectorQuery() | |||
.in(this) | |||
.select('.lsj-file') | |||
view.fields( | |||
{ | |||
size: true, | |||
rect: true | |||
}, | |||
({ height, width, top, left, right, bottom }) => { | |||
uni.createSelectorQuery() | |||
.selectViewport() | |||
.scrollOffset(({ scrollTop }) => { | |||
return callback({ | |||
top: parseInt(top) + parseInt(scrollTop) + 'px', | |||
left: parseInt(left) + 'px', | |||
width: parseInt(width) + 'px', | |||
height: parseInt(height) + 'px' | |||
}) | |||
}) | |||
.exec() | |||
} | |||
).exec() | |||
// #endif | |||
// #ifdef APP-NVUE | |||
const dom = weex.requireModule('dom') | |||
dom.getComponentRect(this.$refs.lsj, ({ size: { height, width, top, left, right, bottom } }) => { | |||
return callback({ | |||
top: parseInt(top) + 'px', | |||
left: parseInt(left) + 'px', | |||
width: parseInt(width) + 'px', | |||
height: parseInt(height) + 'px', | |||
right: parseInt(right) + 'px', | |||
bottom: parseInt(bottom) + 'px' | |||
}) | |||
}) | |||
// #endif | |||
}, | |||
show() { | |||
this.isShow = true; | |||
// #ifdef APP-PLUS | |||
this.lsjFile&&this.getDomStyles(styles => { | |||
this.lsjFile.dom.setStyle(styles) | |||
}); | |||
// #endif | |||
// #ifdef H5 | |||
this.lsjFile.dom.style.display = 'inline' | |||
// #endif | |||
}, | |||
hide() { | |||
this.isShow = false; | |||
// #ifdef APP-PLUS | |||
this.lsjFile&&this.lsjFile.dom.setStyle({ | |||
top: '-500px', | |||
left:'0px', | |||
width: '1px', | |||
height: '1px', | |||
}); | |||
// #endif | |||
// #ifdef H5 | |||
this.lsjFile.dom.style.display = 'none' | |||
// #endif | |||
}, | |||
/** | |||
* 手动提交上传 | |||
* @param {string}name 文件名称,不传则上传所有type等于waiting和fail的文件 | |||
*/ | |||
upload(name) { | |||
this.lsjFile&&this.lsjFile.upload(name); | |||
}, | |||
/** | |||
* @returns {Map} 已选择的文件Map集 | |||
*/ | |||
onchange(files) { | |||
this.$emit('change',files); | |||
this._size = files.size; | |||
return files.size >= this.count ? this.hide() : this.show(); | |||
}, | |||
/** | |||
* @returns {object} 当前上传中的对象 | |||
*/ | |||
onprogress(item,end=false) { | |||
this.$emit('progress',item); | |||
if (end) { | |||
setTimeout(()=>{ | |||
this.$emit('uploadEnd',item); | |||
},0); | |||
} | |||
}, | |||
/** | |||
* 移除组件内缓存的某条数据 | |||
* @param {string}name 文件名称,不指定默认清除所有文件 | |||
*/ | |||
clear(name) { | |||
this.lsjFile.clear(name); | |||
}, | |||
// 创建选择器 | |||
create() { | |||
// 若iOS端服务端处理不了跨域就将hybrid目录内的html放到服务端去,并将此处path改成服务器上的地址 | |||
let path = '/uni_modules/lsj-upload/hybrid/html/uploadFile.html'; | |||
let dom = this.lsjFile.create(path); | |||
// #ifdef H5 | |||
this.$refs.lsj.$el.appendChild(dom); | |||
// #endif | |||
// #ifndef APP-PLUS | |||
this.show(); | |||
// #endif | |||
// #ifdef APP-PLUS | |||
dom.setStyle({position: this.position}); | |||
dom.loadURL(path); | |||
setTimeout(()=>{ | |||
// #ifdef APP-NVUE | |||
plus.webview.currentWebview().append(dom); | |||
// #endif | |||
// #ifndef APP-NVUE | |||
this.$root.$scope.$getAppWebview().append(dom); | |||
// #endif | |||
this.show(); | |||
},300) | |||
// #endif | |||
}, | |||
// 点击选择附件 | |||
onClick() { | |||
if (this._size >= this.count) { | |||
this.toast(`只允许上传${this.count}个文件`); | |||
return; | |||
} | |||
// #ifdef MP-WEIXIN | |||
if (!this.isShow) {return;} | |||
let count = this.count - this._size; | |||
this.lsjFile.chooseMessageFile(this.wxFileType,count); | |||
// #endif | |||
}, | |||
toast(msg) { | |||
uni.showToast({ | |||
title: msg, | |||
icon: 'none' | |||
}); | |||
} | |||
} | |||
} | |||
</script> | |||
<style scoped> | |||
.lsj-file { | |||
display: inline-block; | |||
} | |||
.defview { | |||
background-color: #007aff; | |||
color: #fff; | |||
border-radius: 10rpx; | |||
display: flex; | |||
align-items: center; | |||
justify-content: center; | |||
font-size: 28rpx; | |||
} | |||
.hFile { | |||
position: relative; | |||
overflow: hidden; | |||
} | |||
</style> |
@@ -0,0 +1,179 @@ | |||
<!DOCTYPE html> | |||
<html lang="zh-cn"> | |||
<head> | |||
<meta charset="UTF-8"> | |||
<title class="title">[文件管理器]</title> | |||
<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" /> | |||
<style type="text/css"> | |||
.content {background: transparent;} | |||
.btn {position: relative;top: 0;left: 0;bottom: 0;right: 0;} | |||
.btn .file {position: fixed;z-index: 93;left: 0;right: 0;top: 0;bottom: 0;width: 100%;opacity: 0;} | |||
</style> | |||
</head> | |||
<body> | |||
<div id="content" class="content"> | |||
<div class="btn"> | |||
<input @change="onChange" :accept="accept" ref="file" class="file" type="file" /> | |||
</div> | |||
</div> | |||
<script type="text/javascript" src="js/vue.min.js"></script> | |||
<script type="text/javascript"> | |||
let _this; | |||
var vm = new Vue({ | |||
el: '#content', | |||
data: { | |||
accept: '', | |||
}, | |||
mounted() { | |||
console.log('加载webview'); | |||
_this = this; | |||
this.files = new Map(); | |||
document.addEventListener('plusready', (e)=>{ | |||
let {debug,instantly,prohibited} = plus.webview.currentWebview(); | |||
this.debug = debug; | |||
this.instantly = instantly; | |||
this.prohibited = prohibited; | |||
this.accept = prohibited.accept; | |||
location.href = 'callback?retype=updateOption'; | |||
}, false); | |||
}, | |||
methods: { | |||
toast(msg) { | |||
plus.nativeUI.toast(msg); | |||
}, | |||
clear(name) { | |||
if (!name) { | |||
this.files.clear(); | |||
return; | |||
} | |||
this.files.delete(name); | |||
}, | |||
setData(option='{}') { | |||
this.debug&&console.log('更新参数:'+option); | |||
try{ | |||
_this.option = JSON.parse(option); | |||
}catch(e){ | |||
console.error('参数设置错误') | |||
} | |||
}, | |||
async upload(name=''){ | |||
if (name && this.files.has(name)) { | |||
await this.createUpload(this.files.get(name)); | |||
} | |||
else { | |||
for (let item of this.files.values()) { | |||
if (item.type === 'waiting' || item.type === 'fail') { | |||
await this.createUpload(item); | |||
} | |||
} | |||
} | |||
}, | |||
onChange(e) { | |||
let fileDom = this.$refs.file; | |||
let file = fileDom.files[0]; | |||
let name = file.name; | |||
fileDom.value = ''; | |||
this.debug&&console.log('文件名称',name,'大小',file.size); | |||
if (file) { | |||
// 限制文件格式 | |||
let suffix = name.substring(name.lastIndexOf(".")+1).toLowerCase(); | |||
let formats = this.prohibited.formats.toLowerCase(); | |||
if (formats&&!formats.includes(suffix)) { | |||
this.toast(`不支持上传${suffix.toUpperCase()}格式文件`); | |||
return; | |||
} | |||
// 限制文件大小 | |||
if (file.size > 1024 * 1024 * Math.abs(this.prohibited.size)) { | |||
this.toast(`附件大小请勿超过${this.prohibited.size}M`) | |||
return; | |||
} | |||
let path = URL.createObjectURL(file); | |||
this.files.set(file.name,{file,path,name: file.name,size: file.size,progress: 0,type: 'waiting'}); | |||
this.callChange(); | |||
this.instantly&&this.upload(); | |||
} | |||
}, | |||
/** | |||
* @returns {Map} 已选择的文件Map集 | |||
*/ | |||
callChange() { | |||
location.href = 'callback?retype=change&files=' + escape(JSON.stringify([...this.files])); | |||
}, | |||
/** | |||
* @returns {object} 正在处理的当前对象 | |||
*/ | |||
changeFilesItem(item,end='') { | |||
this.files.set(item.name,item); | |||
location.href = 'callback?retype=progress&end='+ end +'&item=' + escape(JSON.stringify(item)); | |||
}, | |||
createUpload(item) { | |||
this.debug&&console.log('准备上传,option=:'+JSON.stringify(this.option)); | |||
item.type = 'loading'; | |||
delete item.responseText; | |||
return new Promise((resolve,reject)=>{ | |||
let {url,name,method='POST',header={},formData={}} = this.option; | |||
let form = new FormData(); | |||
for (let keys in formData) { | |||
form.append(keys, formData[keys]) | |||
} | |||
form.append(name, item.file); | |||
let xmlRequest = new XMLHttpRequest(); | |||
xmlRequest.open(method, url, true); | |||
for (let keys in header) { | |||
xmlRequest.setRequestHeader(keys, header[keys]) | |||
} | |||
xmlRequest.upload.addEventListener( | |||
'progress', | |||
event => { | |||
if (event.lengthComputable) { | |||
let progress = Math.ceil((event.loaded * 100) / event.total) | |||
if (progress <= 100) { | |||
item.progress = progress; | |||
this.changeFilesItem(item); | |||
} | |||
} | |||
}, | |||
false | |||
); | |||
xmlRequest.ontimeout = () => { | |||
console.error('请求超时') | |||
item.type = 'fail'; | |||
this.changeFilesItem(item,true); | |||
return resolve(false); | |||
} | |||
xmlRequest.onreadystatechange = ev => { | |||
if (xmlRequest.readyState == 4) { | |||
if (xmlRequest.status == 200) { | |||
this.debug && console.log('上传完成:' + xmlRequest.responseText) | |||
item['responseText'] = xmlRequest.responseText; | |||
item.type = 'success'; | |||
this.changeFilesItem(item,true); | |||
return resolve(true); | |||
} else if (xmlRequest.status == 0) { | |||
console.error('status = 0 :请检查请求头Content-Type与服务端是否匹配,服务端已正确开启跨域,并且nginx未拦截阻止请求') | |||
} | |||
console.error('--ERROR--:status = ' + xmlRequest.status) | |||
item.type = 'fail'; | |||
this.changeFilesItem(item,true); | |||
return resolve(false); | |||
} | |||
} | |||
xmlRequest.send(form) | |||
}); | |||
} | |||
} | |||
}); | |||
</script> | |||
</body> | |||
</html> |
@@ -0,0 +1,82 @@ | |||
{ | |||
"id": "lsj-upload", | |||
"displayName": "全文件上传选择非原生2.0版", | |||
"version": "2.1.9", | |||
"description": "文件选择上传-支持APP-H5网页-微信小程序", | |||
"keywords": [ | |||
"附件", | |||
"file", | |||
"upload", | |||
"上传", | |||
"文件管理器" | |||
], | |||
"repository": "", | |||
"engines": { | |||
"HBuilderX": "^3.2.0" | |||
}, | |||
"dcloudext": { | |||
"category": [ | |||
"前端组件", | |||
"通用组件" | |||
], | |||
"sale": { | |||
"regular": { | |||
"price": "0.00" | |||
}, | |||
"sourcecode": { | |||
"price": "0.00" | |||
} | |||
}, | |||
"contact": { | |||
"qq": "" | |||
}, | |||
"declaration": { | |||
"ads": "无", | |||
"data": "无", | |||
"permissions": "无" | |||
}, | |||
"npmurl": "" | |||
}, | |||
"uni_modules": { | |||
"platforms": { | |||
"cloud": { | |||
"tcb": "y", | |||
"aliyun": "y" | |||
}, | |||
"client": { | |||
"App": { | |||
"app-vue": "y", | |||
"app-nvue": "y" | |||
}, | |||
"H5-mobile": { | |||
"Safari": "y", | |||
"Android Browser": "y", | |||
"微信浏览器(Android)": "y", | |||
"QQ浏览器(Android)": "y" | |||
}, | |||
"H5-pc": { | |||
"Chrome": "y", | |||
"IE": "y", | |||
"Edge": "y", | |||
"Firefox": "y", | |||
"Safari": "y" | |||
}, | |||
"小程序": { | |||
"微信": "y", | |||
"阿里": "u", | |||
"百度": "u", | |||
"字节跳动": "u", | |||
"QQ": "u" | |||
}, | |||
"快应用": { | |||
"华为": "y", | |||
"联盟": "y" | |||
}, | |||
"Vue": { | |||
"vue2": "y", | |||
"vue3": "y" | |||
} | |||
} | |||
} | |||
} | |||
} |
@@ -0,0 +1,330 @@ | |||
# lsj-upload | |||
### 插件地址:https://ext.dcloud.net.cn/plugin?id=5459 | |||
### 不清楚使用方式可点击右侧导入示例项目运行完整示例 | |||
### 此次更新2.0与1.0使用方式略有差异,已使用1.0的同学自行斟酌是否更新到2.0版本!!! | |||
使用插件有任何问题欢迎加入QQ讨论群: | |||
- 群1:701468256(已满) | |||
- 群2:469580165 | |||
若能帮到你请高抬贵手点亮5颗星~ | |||
------ | |||
## 重要提示 | |||
### 组件是窗口级滚动,不要在scroll-view内使用!! | |||
### 组件是窗口级滚动,不要在scroll-view内使用!! | |||
### 组件是窗口级滚动,不要在scroll-view内使用!! | |||
### 控件的height高度应与slot自定义内容高度保持一致 | |||
### nvue窗口只能使用固定模式position=absolute | |||
### show() 当DOM重排后在this.$nextTick内调用show(),控件定位会更加准确 | |||
### hide() APP端webview层级比view高,如不希望触发点击时,应调用hide隐藏控件,反之调用show | |||
### 若iOS端跨域服务端同学实在配置不好,可把hybrid下html目录放到服务器去,同源则不存在跨域问题。 | |||
### 小程序端因hybrid不能使用本地HTML,所以插件提供的是从微信消息列表拉取文件并选择,请知悉。 | |||
------ | |||
## 使用说明 | |||
| 属性 | 是否必填 | 值类型 | 默认值 | 说明 | | |||
| --------- | -------- | -----: | --: | :------------:| | |||
| width | 否 | String |100% | 容器宽度 | | |||
| height | 是 | String |80rpx | 容器高度 | | |||
| debug | 否 | Boolean |false | 打印调试日志 | | |||
| option | 是 | Object |- | [文件上传接口相关参数](#p1)| | |||
| instantly | 否 | Boolean |false | true=自动上传 | | |||
| count | 否 | Number |10 | 附件选择上限(个)| | |||
| size | 否 | Number |10 | 附件大小上限(M)| | |||
| wxFileType | 否 | String |all | 微信小程序文件选择器格式限制(all=从所有文件选择,video=只能选择视频文件,image=只能选择图片文件,file=可以选择除了图片和视频之外的其它的文件)| | |||
| accept | 否 | String |- | 文件选择器input file格式限制(部分机型不兼容,建议使用formats)| | |||
| formats | 否 | String |- | 限制允许上传的格式,空串=不限制,默认为空,多个格式以逗号隔开,例如png,jpg,pdf| | |||
| childId | 否 | String |lsjUpload| 控件的id(仅APP有效,应用内每个控件命名一个唯一Id,不同窗口也不要同名Id)| | |||
| position | 否 | String |static | 控件的定位模式(static=控件随页面滚动;absolute=控件在页面中绝对定位,不随窗口内容滚动)| | |||
| top,left,right,bottom | 否 | [Number,String] |0 | 设置控件绝对位置,position=absolute时有效| | |||
| @change | 否 | Function |Map | 选择文件触发,返回所有已选择文件Map集合| | |||
| @progress | 否 | Function |Object | 上传过程中发生状态变化的文件对象,需通过set更新至Map集合| | |||
| @uploadEnd| 否 | Function |Object | 上传结束回调,返回参数与progress一致| | |||
## <a id="p1">option说明</a> | |||
|参数 | 是否必填 | 说明| | |||
|---- | ---- | :--: | | |||
|url | 是 | 上传接口地址| | |||
|name| 否 |上传接口文件key,默认为file| | |||
|header| 否 |上传接口请求头| | |||
|formData| 否 |上传接口额外参数| | |||
## ref调用 | |||
|作用 | 方法名| 传入参数| 说明| | |||
|---- | --------- | -------- | :--: | | |||
|显示控件| show|-| 控件显示状态下可触发点击| | |||
|隐藏控件| hide|-| 控件隐藏状态下不触发点击| | |||
|动态设置文件列表| setFiles|[Array,Map] files| 传入格式请与组件选择返回格式保持一致,且name为必须属性,可查看下方演示| | |||
|动态更新参数| setData|[String] name,[any] value| name支持a.b 和 a[b],可查看下方演示| | |||
|移除选择的文件| clear|[String] name| 不传参数清空所有文件,传入文件name时删除该name的文件| | |||
|手动上传| upload|[String] name| 不传参数默认依次上传所有type=waiting的文件,传入文件name时不关心type是否为waiting,单独上传指定name的文件| | |||
## progress返回对象字段说明 | |||
|字段 | 说明| | |||
|---- | :--: | | |||
|file | 文件对象| | |||
|name |文件名称| | |||
|size |文件大小| | |||
|type |文件上传状态:waiting(等待上传)、loading(上传中)、success(成功) 、fail(失败)| | |||
|responseText|上传成功后服务端返回数据(仅type为success时存在)| | |||
## 以下演示为vue窗口使用方式,nvue使用区别是必须传入控件绝对位置如top,bottom,left,right,且position只能为absolute,如不清楚可点击右侧导入示例项目有详细演示代码。 | |||
### vue: | |||
``` javascript | |||
<lsj-upload | |||
ref="lsjUpload" | |||
childId="upload1" | |||
:width="width" | |||
:height="height" | |||
:option="option" | |||
:size="size" | |||
:formats="formats" | |||
:debug="debug" | |||
:instantly="instantly" | |||
@progress="onprogress" | |||
@change="onChange"> | |||
<view class="btn" :style="{width: width,height: height}">选择附件</view> | |||
</lsj-upload> | |||
<view class="padding"> | |||
<view>已选择文件列表:</view> | |||
<!-- #ifndef MP-WEIXIN --> | |||
<view v-for="(item,index) in files.values()" :key="index"> | |||
<image style="width: 100rpx;height: 100rpx;" :src="item.path" mode="widthFix"></image> | |||
<text>{{item.path}}</text> | |||
<text>{{item.name}}</text> | |||
<text style="margin-left: 10rpx;">大小:{{item.size}}</text> | |||
<text style="margin-left: 10rpx;">状态:{{item.type}}</text> | |||
<text style="margin-left: 10rpx;">进度:{{item.progress}}</text> | |||
<!-- <text style="margin-left: 10rpx;" v-if="item.responseText">服务端返回演示:{{item.responseText.code}}</text> --> | |||
<text @click="clear(item.name)" style="margin-left: 10rpx;padding: 0 10rpx;border: 1rpx solid #007AFF;">删除</text> | |||
</view> | |||
<!-- #endif --> | |||
<!-- #ifdef MP-WEIXIN --> | |||
<view v-for="(item,index) in wxFiles" :key="index"> | |||
<text>{{item.name}}</text> | |||
<text style="margin-left: 10rpx;">大小:{{item.size}}</text> | |||
<text style="margin-left: 10rpx;">状态:{{item.type}}</text> | |||
<text style="margin-left: 10rpx;">进度:{{item.progress}}</text> | |||
<view> | |||
<button>删除</button> | |||
</view> | |||
</view> | |||
<!-- #endif --> | |||
</view> | |||
``` | |||
--- | |||
* 函数说明 | |||
``` javascript | |||
export default { | |||
data() { | |||
return { | |||
// 上传接口参数 | |||
option: { | |||
// 上传服务器地址,此地址需要替换为你的接口地址 | |||
url: 'http://hlapi.j56.com/dropbox/document/upload', | |||
// 上传附件的key | |||
name: 'file', | |||
// 根据你接口需求自定义请求头 | |||
header: { | |||
'Authorization': 'bearer eyJhbGciOiJSUzI1NiIsI', | |||
'uid': '27682', | |||
'client': 'app', | |||
'accountid': 'DP', | |||
}, | |||
// 根据你接口需求自定义body参数 | |||
formData: { | |||
// 'orderId': 1000 | |||
} | |||
}, | |||
// 选择文件后是否立即自动上传,true=选择后立即上传 | |||
instantly: false, | |||
// 必传宽高且宽高应与slot宽高保持一致 | |||
width: '180rpx', | |||
height: '180rpx', | |||
// 限制允许选择的格式,空串=不限制,默认为空 | |||
formats: 'png,jpg,mp4', | |||
// 文件上传大小限制 | |||
size: 10, | |||
// 文件回显列表 | |||
files: new Map(), | |||
// 微信小程序Map对象for循环不显示,所以转成普通数组,不要问为什么,我也不知道 | |||
wxFiles: [], | |||
// 是否打印日志 | |||
debug: true, | |||
// 演示用 | |||
tabIndex: 0, | |||
list:[], | |||
} | |||
}, | |||
onReady() { | |||
setTimeout(()=>{ | |||
console.log('----演示动态更新参数-----'); | |||
this.$refs.lsjUpload.setData('formData.orderId','动态设置的参数'); | |||
console.log('以下注释内容为-动态更新参数更多演示,放开后可查看演示效果'); | |||
// 修改option对象的name属性 | |||
// this.$refs.lsjUpload.setData('name','myFile'); | |||
// 修改option对象的formData内的属性 | |||
// this.$refs.lsjUpload.setData('formData.appid','1111'); | |||
// 替换option对象的formData | |||
// this.$refs.lsjUpload.setData('formData',{appid:'222'}); | |||
// option对象的formData新增属性 | |||
// this.$refs.lsjUpload.setData('formData.newkey','新插入到formData的属性'); | |||
// ---------演示初始化值,用于已提交后再次编辑时需带入已上传文件------- | |||
// 方式1=传入数组 | |||
let files1 = [{ | |||
name: '1.png' | |||
}, | |||
{ | |||
name: '2.png', | |||
}]; | |||
// 方式2=传入Map对象 | |||
let files2 = new Map(); | |||
files2.set('1.png',{name: '1.png'}) | |||
// 设置初始files列表 | |||
this.$refs.lsjUpload.setFiles(files1); | |||
},2000) | |||
}, | |||
methods: { | |||
// 某文件上传结束回调(成功失败都回调) | |||
onuploadEnd(item) { | |||
console.log(`${item.name}已上传结束,上传状态=${item.type}`); | |||
// 更新当前状态变化的文件 | |||
this.files.set(item.name,item); | |||
// 演示上传完成后取服务端数据 | |||
if (item['responseText']) { | |||
console.log('演示服务器返回的字符串JSON转对象'); | |||
this.files.get(item.name).responseText = JSON.parse(item.responseText); | |||
} | |||
// 微信小程序Map对象for循环不显示,所以转成普通数组,不要问为什么,我也不知道 | |||
// #ifdef MP-WEIXIN | |||
this.wxFiles = [...this.files.values()]; | |||
// #endif | |||
// 强制更新视图 | |||
this.$forceUpdate(); | |||
// ---可删除--演示判断是否所有文件均已上传成功 | |||
let isAll = [...this.files.values()].find(item=>item.type!=='success'); | |||
if (!isAll) { | |||
console.log('已全部上传完毕'); | |||
} | |||
else { | |||
console.log(isAll.name+'待上传'); | |||
} | |||
}, | |||
// 上传进度回调 | |||
onprogress(item) { | |||
// 更新当前状态变化的文件 | |||
this.files.set(item.name,item); | |||
console.log('打印对象',JSON.stringify(this.files.get(item.name))); | |||
// 微信小程序Map对象for循环不显示,所以转成普通数组,不要问为什么,我也不知道 | |||
// #ifdef MP-WEIXIN | |||
this.wxFiles = [...this.files.values()]; | |||
// #endif | |||
// 强制更新视图 | |||
this.$forceUpdate(); | |||
}, | |||
// 文件选择回调 | |||
onChange(files) { | |||
// 更新选择的文件 | |||
this.files = files; | |||
// 强制更新视图 | |||
this.$forceUpdate(); | |||
// 微信小程序Map对象for循环不显示,所以转成普通数组,不要问为什么,我也不知道 | |||
// #ifdef MP-WEIXIN | |||
this.wxFiles = [...this.files.values()]; | |||
// #endif | |||
}, | |||
// 手动上传 | |||
upload() { | |||
// name=指定文件名,不指定则上传所有type等于waiting和fail的文件 | |||
this.$refs.lsjUpload.upload(); | |||
}, | |||
// 移除某个文件 | |||
clear(name) { | |||
// name=指定文件名,不传name默认移除所有文件 | |||
this.$refs.lsjUpload.clear(name); | |||
}, | |||
/** | |||
* 以下为演示 | |||
*/ | |||
// DOM重排演示,重排后组件内部updated默认会触发show方法,若特殊情况未能触发updated也可以手动调用一次show() | |||
// 什么是DOM重排?自行百度去~ | |||
add() { | |||
this.list.push('DOM重排测试'); | |||
}, | |||
// 切换视图演示,APP端因为是webview,层级比view高, | |||
// 此时若不希望点击触发选择文件,需要手动调用hide() | |||
// 手动调用hide后,需要调用show()才能恢复触发面 | |||
onTab(tabIndex) { | |||
this.tabIndex = tabIndex; | |||
if (tabIndex == 0 ) { | |||
this.$nextTick(()=>{ | |||
this.$refs.lsjUpload.show(); | |||
}) | |||
} | |||
else { | |||
this.$refs.lsjUpload.hide(); | |||
} | |||
}, | |||
// 打开nvue窗口 | |||
open() { | |||
uni.navigateTo({ | |||
url: '/pages/nvue-demo/nvue-demo' | |||
}); | |||
} | |||
} | |||
} | |||
``` | |||
## 温馨提示 | |||
* 文件上传 | |||
0. 如说明表达还不够清楚,不清楚怎么使用可导入完整示例项目运行体验和查看 | |||
1. APP端请优先联调Android,上传成功后再运行iOS端,如iOS返回status=0则需要后端开启允许跨域; | |||
2. header的Content-Type类型需要与服务端要求一致,否则收不到附件(服务端若没有明文规定则可不写,使用默认匹配) | |||
3. 服务端不清楚怎么配置跨域可加群咨询,具体百度~ | |||
4. 欢迎加入QQ讨论群:701468256(已满) | |||
5. 欢迎加入QQ讨论群:469580165 | |||
6. 欢迎加入QQ讨论群:469580165 | |||
7. 若能帮到你还请点亮5颗小星星以作鼓励哈~ | |||
8. 若能帮到你还请点亮5颗小星星以作鼓励哈~ | |||
9. 若能帮到你还请点亮5颗小星星以作鼓励哈~ |