@@ -7,6 +7,7 @@ | |||||
:key="index" | :key="index" | ||||
class="bg-img" | class="bg-img" | ||||
> | > | ||||
<!-- {{item.id?CONFIG('webHost')+item.url:item.url}} --> | |||||
<image | <image | ||||
v-if="showfile()" | v-if="showfile()" | ||||
:src="item.id?CONFIG('webHost')+item.url:item.url" | :src="item.id?CONFIG('webHost')+item.url:item.url" | ||||
@@ -89,20 +90,6 @@ export default { | |||||
}, | }, | ||||
uploadImage(){ | 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)=>{ | return new Promise(async (reslove,reject)=>{ | ||||
let hasNotUpdatedList = this.imgList.every(item=>item.id) | let hasNotUpdatedList = this.imgList.every(item=>item.id) | ||||
@@ -232,6 +219,7 @@ export default { | |||||
// }, | // }, | ||||
}, | }, | ||||
created() { | created() { | ||||
console.log(123) | |||||
this.imgList = JSON.parse(JSON.stringify(this.value.map(item=>{ | this.imgList = JSON.parse(JSON.stringify(this.value.map(item=>{ | ||||
item.isUploaded = true | item.isUploaded = true | ||||
return item | return item | ||||
@@ -8,12 +8,12 @@ | |||||
class="bg-img" | class="bg-img" | ||||
> | > | ||||
<!-- {{API + '/user/img?data=' + (path.path ? path.path : path)}} --> | <!-- {{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" | 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> | <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"> | <view v-if="!readonly" @tap.stop="delImg(index)" class="cu-tag bg-red" style="width: 18px; height: 18px; font-size: 24px"> | ||||
<l-icon | <l-icon | ||||
@@ -23,15 +23,27 @@ | |||||
</view> | </view> | ||||
</view> | </view> | ||||
<view | |||||
v-if="!readonly && imgList.length < Number(number)" | |||||
@tap="chooseImg" | |||||
<view | |||||
v-show="!readonly && imgList.length < Number(number)&&isShow" | |||||
class="solids" | class="solids" | ||||
> | > | ||||
<!-- @tap="chooseImg" --> | |||||
<l-icon type="file" /> | <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> | </view> | ||||
</view> | </view> | ||||
</template> | </template> | ||||
<script> | <script> | ||||
@@ -46,67 +58,77 @@ export default { | |||||
data(){ | data(){ | ||||
return{ | return{ | ||||
isShow:false, | |||||
imgList:[], | imgList:[], | ||||
} | } | ||||
}, | }, | ||||
methods: { | 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) { | 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() { | showfile() { | ||||
return true; | 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 || [] | let newList = this.imgList || [] | ||||
if(uploadImageRes){ | if(uploadImageRes){ | ||||
newList = JSON.parse(JSON.stringify(newList)).concat(uploadImageRes); | newList = JSON.parse(JSON.stringify(newList)).concat(uploadImageRes); | ||||
} | } | ||||
this.imgList = newList | this.imgList = newList | ||||
this.$emit("update:value", 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)=>{ | return new Promise(async (reslove,reject)=>{ | ||||
this.LOADING('正在上传…'); | 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.HIDE_LOADING(); | ||||
this.$refs.lsjUpload.show() | |||||
if (data) { | if (data) { | ||||
// this.HTTP_GET('StuInfoFresh/upload', {fileId:data}) | // this.HTTP_GET('StuInfoFresh/upload', {fileId:data}) | ||||
reslove([{ | reslove([{ | ||||
id:data.F_Id, | id:data.F_Id, | ||||
name:data.F_FileName, | name:data.F_FileName, | ||||
url:data.F_FilePath | |||||
url:data.F_FilePath, | |||||
type:data.F_FileType, | |||||
}]) | }]) | ||||
}else{ | }else{ | ||||
reject('上传失败!') | 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(){ | ceshi(){ | ||||
function apiFn(params){ | function apiFn(params){ | ||||
@@ -169,7 +149,8 @@ export default { | |||||
this.imgList[params.index] = [{ | this.imgList[params.index] = [{ | ||||
id:data.F_Id, | id:data.F_Id, | ||||
name:data.F_FileName, | name:data.F_FileName, | ||||
url:data.F_FilePath | |||||
url:data.F_FilePath, | |||||
type:data.F_FileType | |||||
}] | }] | ||||
reslove(this.imgList[params.index]) | reslove(this.imgList[params.index]) | ||||
}else{ | }else{ | ||||
@@ -237,9 +218,24 @@ export default { | |||||
} | } | ||||
return true | return true | ||||
}, | }, | ||||
isImage(type){ | |||||
if(type&&type.length){ | |||||
return ["png","jpg"].includes(type.substring(type.length-3,type.length)) | |||||
}else{ | |||||
return false | |||||
} | |||||
}, | |||||
viewImg(item) { | 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() { | // previewFile() { | ||||
@@ -270,16 +266,17 @@ export default { | |||||
// }, | // }, | ||||
}, | }, | ||||
created() { | created() { | ||||
// id name | |||||
console.log(this.value) | |||||
this.imgList = JSON.parse(JSON.stringify(this.value.map(item=>{ | this.imgList = JSON.parse(JSON.stringify(this.value.map(item=>{ | ||||
return { | return { | ||||
id:item.F_Id, | id:item.F_Id, | ||||
name:item.F_FileName, | name:item.F_FileName, | ||||
url:item.F_FilePath | |||||
url:item.F_FilePath, | |||||
type:item.F_FileType | |||||
} | } | ||||
}))) | }))) | ||||
// [{url:"",id:""}] | |||||
this.$nextTick(()=>{ | |||||
this.isShow = true | |||||
}) | |||||
} | } | ||||
}; | }; | ||||
</script> | </script> | ||||
@@ -292,6 +289,7 @@ export default { | |||||
color: #606266; | color: #606266; | ||||
font-size: 13px; | font-size: 13px; | ||||
text-align: center; | text-align: center; | ||||
background-color: rgba(255,255,255,0.6); | |||||
text-overflow: ellipsis; | text-overflow: ellipsis; | ||||
overflow: hidden; | overflow: hidden; | ||||
@@ -27,8 +27,8 @@ export default { | |||||
"webHost":"http://192.168.10.85:8087/", | "webHost":"http://192.168.10.85:8087/", | ||||
// 开发环境下自动填充登录账号密码,与接口地址一一对应,只在开发环境下显示 | // 开发环境下自动填充登录账号密码,与接口地址一一对应,只在开发环境下显示 | ||||
"devAccount": [ | "devAccount": [ | ||||
// 20201130230 21364200000400266 老师 420528196310072253 学生 420528200606205026 | |||||
{ username: "420528200507261428", password: "www.qj.com" } | |||||
// 20201130230 21364200000400266 老师 420528196310072253 学生 420528200606205026 420528200507261428 | |||||
{ username: "system", password: "www.qj.com" } | |||||
], | ], | ||||
//是否分布式部署 指WebApi与Web不在一台服务器 | //是否分布式部署 指WebApi与Web不在一台服务器 | ||||
"isDistributed":true, | "isDistributed":true, | ||||
@@ -14,8 +14,6 @@ | |||||
:currentNode="currentNode" | :currentNode="currentNode" | ||||
:currentTask="currentTask" | :currentTask="currentTask" | ||||
/> | /> | ||||
</view> | </view> | ||||
<!-- Tab #2:流程图页 --> | <!-- Tab #2:流程图页 --> | ||||
<view v-if="ready && tab === 1" class="progress"><l-workflow-timeline :processList="processList" /></view> | <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 { | export default { | ||||
data() { | data() { | ||||
return { | return { | ||||
tab: 0, | tab: 0, | ||||
editMode: false, | editMode: false, | ||||
type: 'view', | type: 'view', | ||||
@@ -184,7 +184,7 @@ export default { | |||||
}, | }, | ||||
refreshComponent(){ | refreshComponent(){ | ||||
this.uploadVisiable = false | |||||
// this.uploadVisiable = false | |||||
this.$nextTick(()=>{ | this.$nextTick(()=>{ | ||||
this.uploadVisiable = true | 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颗小星星以作鼓励哈~ |