Преглед изворни кода

feat: app支持上传文件

xsx пре 1 година
родитељ
комит
00ef46a229

+ 2 - 0
im-platform/src/main/java/com/bx/implatform/controller/FileController.java

@@ -8,6 +8,7 @@ import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.CrossOrigin;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RestController;
 import org.springframework.web.multipart.MultipartFile;
@@ -26,6 +27,7 @@ public class FileController {
         return ResultUtils.success(fileService.uploadImage(file));
     }
 
+    @CrossOrigin
     @ApiOperation(value = "上传文件", notes = "上传文件,上传后返回文件url")
     @PostMapping("/file/upload")
     public Result<String> uploadFile(MultipartFile file) {

+ 4 - 1
im-platform/src/main/java/com/bx/implatform/exception/GlobalExceptionHandler.java

@@ -29,7 +29,10 @@ public class GlobalExceptionHandler {
     public Result handleException(Exception e) {
         if (e instanceof GlobalException) {
             GlobalException ex = (GlobalException) e;
-            log.error("全局异常捕获:msg:{},log:{},{}", ex.getMessage(), e);
+            // token过期是正常情况,不打印
+            if(!ex.getCode().equals(ResultCode.INVALID_TOKEN.getCode())){
+                log.error("全局异常捕获:msg:{},log:{},{}", ex.getMessage(), e);
+            }
             return ResultUtils.error(ex.getCode(), ex.getMessage());
         } else if (e instanceof UndeclaredThrowableException) {
             GlobalException ex = (GlobalException) e.getCause();

+ 4 - 3
im-platform/src/main/java/com/bx/implatform/interceptor/AuthInterceptor.java

@@ -36,14 +36,15 @@ public class AuthInterceptor implements HandlerInterceptor {
             log.error("未登陆,url:{}", request.getRequestURI());
             throw new GlobalException(ResultCode.NO_LOGIN);
         }
+        String strJson = JwtUtil.getInfo(token);
+        UserSession userSession = JSON.parseObject(strJson, UserSession.class);
         //验证 token
         if (!JwtUtil.checkSign(token, jwtProperties.getAccessTokenSecret())) {
-            log.error("token已失效,url:{}", request.getRequestURI());
+            log.error("token已失效,用户:{}", userSession.getUserName());
+            log.error("token:{}", token);
             throw new GlobalException(ResultCode.INVALID_TOKEN);
         }
         // 存放session
-        String strJson = JwtUtil.getInfo(token);
-        UserSession userSession = JSON.parseObject(strJson, UserSession.class);
         request.setAttribute("session", userSession);
         return true;
     }

+ 2 - 1
im-platform/src/main/java/com/bx/implatform/util/MinioUtil.java

@@ -87,8 +87,9 @@ public class MinioUtil {
         }
         String objectName = DateTimeUtils.getFormatDate(new Date(), DateTimeUtils.PARTDATEFORMAT) + "/" + fileName;
         try {
+            InputStream stream = new ByteArrayInputStream(file.getBytes());
             PutObjectArgs objectArgs = PutObjectArgs.builder().bucket(bucketName).object(path + "/" + objectName)
-                    .stream(file.getInputStream(), file.getSize(), -1).contentType(file.getContentType()).build();
+                .stream(stream, file.getSize(), -1).contentType(file.getContentType()).build();
             //文件名称相同会覆盖
             minioClient.putObject(objectArgs);
         } catch (Exception e) {

+ 1 - 8
im-ui/src/api/httpRequest.js

@@ -7,10 +7,7 @@ import {
 const http = axios.create({
 	baseURL: process.env.VUE_APP_BASE_API,
 	timeout: 1000 * 30,
-	withCredentials: true,
-	headers: {
-		'Content-Type': 'application/json; charset=utf-8'
-	}
+	withCredentials: true
 })
 
 /**
@@ -53,10 +50,6 @@ http.interceptors.response.use(async response => {
 		// 保存token
 		sessionStorage.setItem("accessToken", data.accessToken);
 		sessionStorage.setItem("refreshToken", data.refreshToken);
-		// 这里需要把headers清掉,否则请求时会报错,原因暂不详...
-		if(typeof response.config.data != 'object'){
-			response.config.headers=undefined;
-		}
 		// 重新发送刚才的请求
 		return http(response.config)
 	} else {

+ 6 - 2
im-ui/src/components/chat/ChatBox.vue

@@ -336,6 +336,7 @@
 				}
 				let msgInfo = {
 					id: 0,
+					tmpId: this.generateId(),
 					fileId: file.uid,
 					sendId: this.mine.id,
 					content: JSON.stringify(data),
@@ -391,6 +392,7 @@
 				}
 				let msgInfo = {
 					id: 0,
+					tmpId: this.generateId(),
 					sendId: this.mine.id,
 					content: JSON.stringify(data),
 					sendTime: new Date().getTime(),
@@ -738,7 +740,6 @@
 				});
 			},
 			refreshPlaceHolder() {
-				console.log("placeholder")
 				if (this.isReceipt) {
 					this.placeholder = "【回执消息】"
 				} else if (this.$refs.editBox && this.$refs.editBox.innerHTML) {
@@ -746,7 +747,10 @@
 				} else {
 					this.placeholder = "聊点什么吧~";
 				}
-
+			},
+			generateId(){
+				// 生成临时id
+				return String(new Date().getTime()) + String(Math.floor(Math.random() * 1000));
 			}
 		},
 		computed: {

+ 1 - 3
im-ui/src/components/common/FileUpload.vue

@@ -1,6 +1,6 @@
 <template>
 	<el-upload :action="'#'" :http-request="onFileUpload" :accept="fileTypes==null?'':fileTypes.join(',')" :show-file-list="false"
-		:disabled="disabled" :before-upload="beforeUpload">
+		:disabled="disabled" :before-upload="beforeUpload" :multiple="true">
 		<slot></slot>
 	</el-upload>
 </template>
@@ -49,7 +49,6 @@
 						background: 'rgba(0, 0, 0, 0.7)'
 					});
 				}
-
 				let formData = new FormData()
 				formData.append('file', file.file)
 				this.$http({
@@ -82,7 +81,6 @@
 					this.$message.error(`文件大小不能超过 ${this.fileSizeStr}!`);
 					return false;
 				}
-
 				this.$emit("before", file);
 				return true;
 			}

+ 3 - 3
im-ui/src/store/chatStore.js

@@ -318,9 +318,9 @@ export default {
 				if (msgInfo.id && chat.messages[idx].id == msgInfo.id) {
 					return chat.messages[idx];
 				}
-				// 正在发送中的消息可能没有id,通过发送时间判断
-				if (msgInfo.selfSend && chat.messages[idx].selfSend &&
-					chat.messages[idx].sendTime == msgInfo.sendTime) {
+				// 正在发送中的消息可能没有id,只有tmpId
+				if (msgInfo.tmpId && chat.messages[idx].tmpId &&
+					chat.messages[idx].tmpId == msgInfo.tmpId) {
 					return chat.messages[idx];
 				}
 			}

+ 3 - 2
im-uniapp/App.vue

@@ -1,6 +1,7 @@
 <script>
 	import store from './store';
 	import http from './common/request';
+	import * as msgType from './common/messageType';
 	import * as enums from './common/enums';
 	import * as wsApi from './common/wssocket';
 	import UNI_APP from '@/.env.js'
@@ -109,7 +110,7 @@
 			},
 			insertPrivateMessage(friend, msg) {
 				// 单人视频信令
-				if (this.$msgType.isRtcPrivate(msg.type)) {
+				if (msgType.isRtcPrivate(msg.type)) {
 					// #ifdef MP-WEIXIN
 						// 小程序不支持音视频
 						return;
@@ -186,7 +187,7 @@
 			},
 			insertGroupMessage(group, msg) {
 				// 群视频信令
-				if (this.$msgType.isRtcGroup(msg.type)) {
+				if (msgType.isRtcGroup(msg.type)) {
 					// #ifdef MP-WEIXIN
 						// 小程序不支持音视频
 						return;

+ 75 - 54
im-uniapp/components/file-upload/file-upload.vue

@@ -1,25 +1,33 @@
 <template>
-	<view @click="selectAndUpload()">
-		<slot></slot>
+	<view>
+		<lsj-upload ref="lsjUpload" :height="'100%'" :option="option" @uploadEnd="onUploadEnd" @change="onChange"
+			:size="maxSize" :instantly="true">
+			<slot></slot>
+		</lsj-upload>
 	</view>
 </template>
 
 <script>
 	import UNI_APP from '@/.env.js';
-	
+
 	export default {
 		name: "file-upload",
 		data() {
 			return {
-				uploadHeaders: {
-					"accessToken": uni.getStorageSync('loginInfo').accessToken
+				fileMap: new Map(),
+				option: {
+					url: UNI_APP.BASE_URL + '/file/upload',
+					name: 'file',
+					header: {
+						accessToken: uni.getStorageSync('loginInfo').accessToken
+					}
 				}
 			}
 		},
 		props: {
 			maxSize: {
 				type: Number,
-				default: 10*1024*1024
+				default: 10
 			},
 			onBefore: {
 				type: Function,
@@ -35,61 +43,74 @@
 			}
 		},
 		methods: {
-			selectAndUpload() {
-				console.log(uni.chooseFile)
-				console.log(uni.chooseMessageFile)
-				let chooseFile = uni.chooseFile || uni.chooseMessageFile;
-				chooseFile({
-					success: (res) => {
-						res.tempFiles.forEach((file) => {
-							// 校验大小
-							if (this.maxSize && file.size > this.maxSize) {
-								this.$message.error(`文件大小不能超过 ${this.fileSizeStr}!`);
-								this.$emit("fail", file);
-								return;
-							}
-
-							if (!this.onBefore || this.onBefore(file)) {
-								// 调用上传图片的接口
-								this.uploadFile(file);
-							}
-						})
+			onUploadEnd(item) {
+				let file = this.fileMap.get(item.path);
+				if (item.type == 'fail') {
+					this.onError(file)
+					return;
+				}
+				let res = JSON.parse(item.responseText);
+				if (res.code == 200) {
+					// 上传成功
+					this.onOk(file, res);
+				} else if (res.code == 401) {
+					// token已过期,重新获取token
+					this.refreshToken().then((res) => {
+						let newToken = res.data.accessToken;
+						this.option.header.accessToken = newToken;
+						this.$refs.lsjUpload.setData(this.option);
+						// 重新上传
+						this.$refs.lsjUpload.upload(file.name);
+					}).catch(() => {
+						this.onError(file, res);
+					})
+				} else {
+					// 上传失败
+					this.onError(file, res);
+				}
+			},
+			onChange(files) {
+				if (!files.size) {
+					return;
+				}
+				files.forEach((file, name) => {
+					if(!this.fileMap.has(file.path)){
+						this.onBefore && this.onBefore(file)
+						this.fileMap.set(file.path, file);
+						console.log(file)
 					}
 				})
 			},
-			uploadFile(file) {
-				uni.uploadFile({
-					url: UNI_APP.BASE_URL + '/file/upload',
-					header: {
-						accessToken: uni.getStorageSync("loginInfo").accessToken
-					},
-					filePath: file.path, // 要上传文件资源的路径
-					name: 'file',
-					success: (res) => {
-						let data = JSON.parse(res.data);
-						if(data.code != 200){
-							this.onError && this.onError(file, data);
-						}else{
-							this.onSuccess && this.onSuccess(file, data);
+			onOk(file, res) {
+				this.fileMap.delete(file.path);
+				this.$refs.lsjUpload.clear(file.name);
+				this.onSuccess && this.onSuccess(file, res);
+			},
+			onFailed(file, res) {
+				this.fileMap.delete(file.path);
+				this.$refs.lsjUpload.clear(file.name);
+				this.onError && this.onError(file, res);
+			},
+			refreshToken() {
+				return new Promise((resolve, reject) => {
+					let loginInfo = uni.getStorageSync('loginInfo')
+					uni.request({
+						method: 'PUT',
+						url: UNI_APP.BASE_URL + '/refreshToken',
+						header: {
+							refreshToken: loginInfo.refreshToken
+						},
+						success: (res) => {
+							resolve(res.data);
+						},
+						fail: (res) => {
+							reject(res);
 						}
-					},
-					fail: (err) => {
-						this.onError && this.onError(file, err);
-					}
+					})
 				})
 			}
-		},
-		computed: {
-			fileSizeStr() {
-				if (this.maxSize > 1024 * 1024) {
-					return Math.round(this.maxSize / 1024 / 1024) + "M";
-				}
-				if (this.maxSize > 1024) {
-					return Math.round(this.maxSize / 1024) + "KB";
-				}
-				return this.maxSize + "B";
-			}
 		}
+
 	}
 </script>
 

+ 13 - 9
im-uniapp/pages/chat/chat-box.vue

@@ -61,8 +61,7 @@
 					</image-upload>
 					<view class="tool-name">拍摄</view>
 				</view>
-				<!-- #ifndef APP-PLUS -->
-				<!-- APP 暂时不支持选择文件 -->
+				
 				<view class="chat-tools-item">
 					<file-upload :onBefore="onUploadFileBefore" :onSuccess="onUploadFileSuccess"
 						:onError="onUploadFileFail">
@@ -70,7 +69,7 @@
 					</file-upload>
 					<view class="tool-name">文件</view>
 				</view>
-				<!-- #endif -->
+		
 				<view class="chat-tools-item" @click="onRecorderInput()">
 					<view class="tool-icon iconfont icon-microphone"></view>
 					<view class="tool-name">语音消息</view>
@@ -331,7 +330,7 @@
 				}
 			},
 			scrollToBottom() {
-				let size = this.chat.messages.length;
+				let size = this.messageSize;
 				if (size > 0) {
 					this.scrollToMsgIdx(size - 1);
 				}
@@ -391,6 +390,7 @@
 				}
 				let msgInfo = {
 					id: 0,
+					tmpId: this.generateId(),
 					fileId: file.uid,
 					sendId: this.mine.id,
 					content: JSON.stringify(data),
@@ -441,6 +441,7 @@
 				}
 				let msgInfo = {
 					id: 0,
+					tmpId: this.generateId(),
 					sendId: this.mine.id,
 					content: JSON.stringify(data),
 					sendTime: new Date().getTime(),
@@ -537,7 +538,6 @@
 						}
 					},
 					fail(e) {
-						console.log(e);
 						uni.showToast({
 							title: "文件下载失败",
 							icon: "none"
@@ -550,7 +550,6 @@
 					console.log("消息已滚动到顶部")
 					return;
 				}
-
 				//  #ifndef H5
 				// 防止滚动条定格在顶部,不能一直往上滚
 				this.scrollToMsgIdx(this.showMinIdx);
@@ -591,7 +590,6 @@
 				});
 			},
 			readedMessage() {
-				console.log("readedMessage")
 				if (this.unreadCount == 0) {
 					return;
 				}
@@ -642,6 +640,10 @@
 				let info = uni.getSystemInfoSync()
 				let px = info.windowWidth * rpx / 750;
 				return Math.floor(rpx);
+			},
+			generateId(){
+				// 生成临时id
+				return String(new Date().getTime()) + String(Math.floor(Math.random() * 1000));
 			}
 		},
 		computed: {
@@ -669,6 +671,9 @@
 				return this.chat.messages.length;
 			},
 			unreadCount() {
+				if (!this.chat || !this.chat.unreadCount) {
+					return 0;
+				}
 				return this.chat.unreadCount;
 			},
 			atUserItems() {
@@ -693,7 +698,6 @@
 			messageSize: function(newSize, oldSize) {
 				// 接收到消息时滚动到底部
 				if (newSize > oldSize) {
-					console.log("messageSize",newSize,oldSize)
 					let pages = getCurrentPages();
 					let curPage = pages[pages.length-1].route;
 					if(curPage == "pages/chat/chat-box"){
@@ -716,7 +720,7 @@
 			// 聊天数据
 			this.chat = this.$store.state.chatStore.chats[options.chatIdx];
 			// 初始状态只显示20条消息
-			let size = this.chat.messages.length;
+			let size = this.messageSize;
 			this.showMinIdx = size > 20 ? size - 20 : 0;
 			// 消息已读
 			this.readedMessage()

+ 4 - 3
im-uniapp/store/chatStore.js

@@ -380,9 +380,10 @@ export default {
 				if (msgInfo.id && chat.messages[idx].id == msgInfo.id) {
 					return chat.messages[idx];
 				}
-				// 正在发送中的消息可能没有id,通过发送时间判断
-				if (msgInfo.selfSend && chat.messages[idx].selfSend &&
-					chat.messages[idx].sendTime == msgInfo.sendTime) {
+				// 正在发送中的消息可能没有id,只有tmpId
+				if (msgInfo.tmpId && chat.messages[idx].tmpId &&
+					chat.messages[idx].tmpId == msgInfo.tmpId) {
+						console.log("chat.messages[idx].tmpId == msgInfo.tmpId")
 					return chat.messages[idx];
 				}
 			}

+ 120 - 0
im-uniapp/uni_modules/lsj-upload/changelog.md

@@ -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自定义按钮样式

+ 414 - 0
im-uniapp/uni_modules/lsj-upload/components/lsj-upload/LsjFile.js

@@ -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);
+				}
+			})
+		});
+	}
+}

+ 338 - 0
im-uniapp/uni_modules/lsj-upload/components/lsj-upload/lsj-upload.vue

@@ -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>

Разлика између датотеке није приказан због своје велике величине
+ 5 - 0
im-uniapp/uni_modules/lsj-upload/hybrid/html/js/vue.min.js


+ 213 - 0
im-uniapp/uni_modules/lsj-upload/hybrid/html/uploadFile.html

@@ -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>

+ 80 - 0
im-uniapp/uni_modules/lsj-upload/package.json

@@ -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"
+                }
+            }
+        }
+    }
+}

Неке датотеке нису приказане због велике количине промена