committed by
Gitee
22 changed files with 1305 additions and 111 deletions
@ -0,0 +1,120 @@ |
|||
## 2.3.2(2024-06-13) |
|||
问题修复:2.3.1版本引起的部分设备不支持findLastIndex问题 |
|||
## 2.3.1(2024-05-20) |
|||
修复:文件不去重时返回文件列表name与组件内置列表key不一致问题。 |
|||
## 2.3.0(2024-05-20) |
|||
优化:1:增加属性distinct【选择文件是否去重】、2:对show/hide函数增加uni.$emit事件监听,若页面存在多个上传组件时,可通过uni.$emit控制所有上传组件webview透明层是否显示。 |
|||
## 2.2.9(2023-06-01) |
|||
优化:将是否多选与count字段解绑(原逻辑是count>1为允许多选),改为新增multiple属性控制是否多选。 |
|||
## 2.2.8(2023-06-01) |
|||
修复上版本提交时accept测试值未删除导致h5端只能选择图片的问题。 |
|||
## 2.2.7(2023-05-06) |
|||
应群友建议,当instantly为true时,触发change事件后延迟1000毫秒再自动上传,方便动态修改参数,其实个人还是建议想在change事件动态设置参数的伙伴将instantly设置为false,修改参数后手动调用upload() |
|||
## 2.2.6(2023-02-09) |
|||
修复多个文件同时选择时返回多次change回调的问题 |
|||
## 2.2.5(2022-12-27) |
|||
1.修复多选文件时未能正常校验数量的问题; |
|||
2.app端与H5端支持单选或多选文件,通过count数量控制,超过1开启多选。 |
|||
## 2.2.4(2022-12-27) |
|||
1.修复多选文件时未能正常校验数量的问题; |
|||
2.app端修复多选只取到第一个文件的问题。 |
|||
## 2.2.3(2022-12-06) |
|||
修复手动调用show()导致count失效的问题 |
|||
## 2.2.2(2022-12-01) |
|||
Vue3自行修改兼容 |
|||
## 2.2.1(2022-10-19) |
|||
修复childId警告提示 |
|||
## 2.2.0(2022-10-10) |
|||
更新app端webview窗口参数clidId,默认值添加时间戳保证唯一性 |
|||
## 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,414 @@ |
|||
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.multiple) { |
|||
dom.multiple = 'multiple'; |
|||
} |
|||
dom.onchange = event => { |
|||
for (let file of event.target.files) { |
|||
if (this.files.size >= this.prohibited.count) { |
|||
this.toast(`只允许上传${this.prohibited.count}个文件`); |
|||
this.dom.value = ''; |
|||
break; |
|||
} |
|||
this.addFile(file); |
|||
} |
|||
|
|||
this._uploadAfter(); |
|||
|
|||
this.dom.value = ''; |
|||
}; |
|||
this.dom = dom; |
|||
// #endif
|
|||
|
|||
// #ifdef APP-PLUS
|
|||
let styles = { |
|||
top: '-200px', |
|||
left: 0, |
|||
width: '1px', |
|||
height: '200px', |
|||
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; |
|||
} |
|||
} |
|||
|
|||
|
|||
/** |
|||
* 设置上传参数 |
|||
* @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,isCallChange) { |
|||
|
|||
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(); |
|||
|
|||
// #ifndef MP-WEIXIN
|
|||
path = URL.createObjectURL(file); |
|||
// #endif
|
|||
// #ifdef MP-WEIXIN
|
|||
path = file.path; |
|||
// #endif
|
|||
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; |
|||
} |
|||
|
|||
try{ |
|||
if (!this.prohibited.distinct) { |
|||
let homonymIndex = [...this.files.keys()].findIndex(item=>{ |
|||
return (item.substring(0,item.lastIndexOf("("))||item.substring(0,item.lastIndexOf("."))) == name.substring(0,name.lastIndexOf(".")) && |
|||
item.substring(item.lastIndexOf(".")+1).toLowerCase() === suffix; |
|||
}) |
|||
if (homonymIndex > -1) { |
|||
name = `${name.substring(0,name.lastIndexOf("."))}(${homonymIndex+1}).${suffix}`; |
|||
} |
|||
} |
|||
}catch(e){ |
|||
//TODO handle the exception
|
|||
} |
|||
|
|||
this.files.set(name,{file,path,name: name,size: file.size,progress: 0,type: 'waiting'}); |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 移除文件 |
|||
* @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) { |
|||
this.addFile(file); |
|||
} |
|||
this._uploadAfter(); |
|||
}, |
|||
fail: () => { |
|||
this.toast(`打开失败`); |
|||
} |
|||
}) |
|||
} |
|||
|
|||
_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)); |
|||
} |
|||
|
|||
_uploadAfter() { |
|||
this.onchange(this.files); |
|||
setTimeout(()=>{ |
|||
this.instantly&&this.upload(); |
|||
},1000) |
|||
} |
|||
|
|||
_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,338 @@ |
|||
<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}, |
|||
// 是否去重文件(同名文件覆盖) |
|||
distinct: {type: Boolean,default: false}, |
|||
// 自动上传 |
|||
instantly: {type: Boolean,default: false}, |
|||
// 上传接口参数设置 |
|||
option: {type: Object,default: ()=>{}}, |
|||
// 文件大小上限 |
|||
size: { type: Number, default: 10 }, |
|||
// 文件选择个数上限,超出后不触发点击 |
|||
count: { type: Number, default: 9 }, |
|||
// 是否允许多选文件 |
|||
multiple: {type:Boolean, default: true}, |
|||
// 允许上传的文件格式(多个以逗号隔开) |
|||
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 { |
|||
|
|||
} |
|||
}, |
|||
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 |
|||
} |
|||
}, |
|||
watch: { |
|||
option(v) { |
|||
// #ifdef APP-PLUS |
|||
this.lsjFile&&this.show(); |
|||
// #endif |
|||
} |
|||
}, |
|||
updated() { |
|||
// #ifdef APP-PLUS |
|||
if (this.isShow) { |
|||
this.lsjFile&&this.show(); |
|||
} |
|||
// #endif |
|||
}, |
|||
created() { |
|||
uni.$on('$upload-show',this.emitShow); |
|||
uni.$on('$upload-hide',this.hide); |
|||
}, |
|||
beforeDestroy() { |
|||
uni.$off('$upload-show',this.emitShow); |
|||
uni.$off('$upload-hide',this.hide); |
|||
// #ifdef APP-PLUS |
|||
this.lsjFile.dom.close(); |
|||
// #endif |
|||
}, |
|||
mounted() { |
|||
let pages = getCurrentPages(); |
|||
this.myRoute = pages[pages.length - 1].route; |
|||
this._size = 0; |
|||
let WEBID = 'lsj_' + this.childId + new Date().getTime(); |
|||
this.lsjFile = new LsjFile({ |
|||
id: WEBID, |
|||
debug: this.debug, |
|||
width: this.width, |
|||
height: this.height, |
|||
option: this.option, |
|||
instantly: this.instantly, |
|||
// 限制条件 |
|||
prohibited: { |
|||
// 是否去重 |
|||
distinct: this.distinct, |
|||
// 大小 |
|||
size: this.size, |
|||
// 允许上传的格式 |
|||
formats: this.formats, |
|||
// 限制选择的格式 |
|||
accept: this.accept, |
|||
count: this.count, |
|||
// 是否多选 |
|||
multiple: this.multiple, |
|||
}, |
|||
onchange: this.onchange, |
|||
onprogress: this.onprogress, |
|||
}); |
|||
this.create(); |
|||
}, |
|||
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 |
|||
}, |
|||
emitShow() { |
|||
let pages = getCurrentPages(); |
|||
let route = pages[pages.length - 1].route; |
|||
if (route === this.myRoute) { |
|||
return this.show(); |
|||
} |
|||
}, |
|||
show() { |
|||
this.debug&&console.log('触发show函数'); |
|||
if (this._size && (this._size >= this.count)) { |
|||
return; |
|||
} |
|||
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.debug&&console.log('触发hide函数'); |
|||
this.isShow = false; |
|||
// #ifdef APP-PLUS |
|||
this.lsjFile&&this.lsjFile.dom.setStyle({ |
|||
top: '-100px', |
|||
left:'0px', |
|||
width: '1px', |
|||
height: '100px', |
|||
}); |
|||
// #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> |
|||
File diff suppressed because one or more lines are too long
@ -0,0 +1,213 @@ |
|||
<!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 :multiple="multiple" @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: '', |
|||
multiple: true, |
|||
}, |
|||
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; |
|||
if (prohibited.multiple === 'false') { |
|||
prohibited.multiple = false; |
|||
} |
|||
this.multiple = prohibited.multiple; |
|||
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; |
|||
for (let file of fileDom.files) { |
|||
if (this.files.size >= this.prohibited.count) { |
|||
this.toast(`只允许上传${this.prohibited.count}个文件`); |
|||
fileDom.value = ''; |
|||
break; |
|||
} |
|||
this.addFile(file); |
|||
} |
|||
this.uploadAfter(); |
|||
fileDom.value = ''; |
|||
}, |
|||
addFile(file) { |
|||
if (file) { |
|||
let name = file.name; |
|||
this.debug&&console.log('文件名称',name,'大小',file.size); |
|||
// 限制文件格式 |
|||
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; |
|||
} |
|||
try{ |
|||
if (!this.prohibited.distinct) { |
|||
let homonymIndex = [...this.files.keys()].findIndex(item=>{ |
|||
return (item.substring(0,item.lastIndexOf("("))||item.substring(0,item.lastIndexOf("."))) == name.substring(0,name.lastIndexOf(".")) && |
|||
item.substring(item.lastIndexOf(".")+1).toLowerCase() === suffix; |
|||
}) |
|||
if (homonymIndex > -1) { |
|||
name = `${name.substring(0,name.lastIndexOf("."))}(${homonymIndex+1}).${suffix}`; |
|||
} |
|||
} |
|||
}catch(e){ |
|||
//TODO handle the exception |
|||
} |
|||
|
|||
// let itemBlob = new Blob([file]); |
|||
// let path = URL.createObjectURL(itemBlob); |
|||
let path = URL.createObjectURL(file); |
|||
this.files.set(name,{file,path,name: name,size: file.size,progress: 0,type: 'waiting'}); |
|||
} |
|||
}, |
|||
/** |
|||
* @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)); |
|||
}, |
|||
uploadAfter() { |
|||
this.callChange(); |
|||
setTimeout(()=>{ |
|||
this.instantly&&this.upload(); |
|||
},1000) |
|||
}, |
|||
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) { |
|||
this.debug && console.log('接口是否支持跨域',xmlRequest.withCredentials); |
|||
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,80 @@ |
|||
{ |
|||
"id": "lsj-upload", |
|||
"displayName": "全文件上传选择非原生2.0版", |
|||
"version": "2.3.2", |
|||
"description": "文件选择上传-支持APP-H5网页-微信小程序", |
|||
"keywords": [ |
|||
"附件", |
|||
"file", |
|||
"upload", |
|||
"上传", |
|||
"文件管理器" |
|||
], |
|||
"repository": "", |
|||
"engines": { |
|||
"HBuilderX": "^3.4.9" |
|||
}, |
|||
"dcloudext": { |
|||
"sale": { |
|||
"regular": { |
|||
"price": "0.00" |
|||
}, |
|||
"sourcecode": { |
|||
"price": "0.00" |
|||
} |
|||
}, |
|||
"contact": { |
|||
"qq": "" |
|||
}, |
|||
"declaration": { |
|||
"ads": "无", |
|||
"data": "无", |
|||
"permissions": "相机/相册读取" |
|||
}, |
|||
"npmurl": "", |
|||
"type": "component-vue" |
|||
}, |
|||
"uni_modules": { |
|||
"platforms": { |
|||
"cloud": { |
|||
"tcb": "y", |
|||
"aliyun": "y", |
|||
"alipay": "n" |
|||
}, |
|||
"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" |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue