Browse Source

feat: app支持上传文件

master
xsx 2 years ago
parent
commit
00ef46a229
  1. 2
      im-platform/src/main/java/com/bx/implatform/controller/FileController.java
  2. 3
      im-platform/src/main/java/com/bx/implatform/exception/GlobalExceptionHandler.java
  3. 7
      im-platform/src/main/java/com/bx/implatform/interceptor/AuthInterceptor.java
  4. 3
      im-platform/src/main/java/com/bx/implatform/util/MinioUtil.java
  5. 9
      im-ui/src/api/httpRequest.js
  6. 8
      im-ui/src/components/chat/ChatBox.vue
  7. 4
      im-ui/src/components/common/FileUpload.vue
  8. 6
      im-ui/src/store/chatStore.js
  9. 5
      im-uniapp/App.vue
  10. 111
      im-uniapp/components/file-upload/file-upload.vue
  11. 22
      im-uniapp/pages/chat/chat-box.vue
  12. 7
      im-uniapp/store/chatStore.js
  13. 120
      im-uniapp/uni_modules/lsj-upload/changelog.md
  14. 414
      im-uniapp/uni_modules/lsj-upload/components/lsj-upload/LsjFile.js
  15. 338
      im-uniapp/uni_modules/lsj-upload/components/lsj-upload/lsj-upload.vue
  16. 8
      im-uniapp/uni_modules/lsj-upload/hybrid/html/js/vue.min.js
  17. 213
      im-uniapp/uni_modules/lsj-upload/hybrid/html/uploadFile.html
  18. 80
      im-uniapp/uni_modules/lsj-upload/package.json

2
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) {

3
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;
// 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();

7
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;
}

3
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) {

9
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 {

8
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: {

4
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;
}

6
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];
}
}

5
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;

111
im-uniapp/components/file-upload/file-upload.vue

@ -1,6 +1,9 @@
<template>
<view @click="selectAndUpload()">
<view>
<lsj-upload ref="lsjUpload" :height="'100%'" :option="option" @uploadEnd="onUploadEnd" @change="onChange"
:size="maxSize" :instantly="true">
<slot></slot>
</lsj-upload>
</view>
</template>
@ -11,15 +14,20 @@
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);
onUploadEnd(item) {
let file = this.fileMap.get(item.path);
if (item.type == 'fail') {
this.onError(file)
return;
}
if (!this.onBefore || this.onBefore(file)) {
//
this.uploadFile(file);
}
let res = JSON.parse(item.responseText);
if (res.code == 200) {
//
this.onOk(file, res);
} else if (res.code == 401) {
// tokentoken
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',
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: {
accessToken: uni.getStorageSync("loginInfo").accessToken
refreshToken: loginInfo.refreshToken
},
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);
}
resolve(res.data);
},
fail: (err) => {
this.onError && this.onError(file, err);
fail: (res) => {
reject(res);
}
})
}
},
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>

22
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()

7
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
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
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
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' },
// webviewIDId
childId: { type: String, default: 'lsjUpload' },
//
width: { type: String, default: '100%' },
//
height: { type: String, default: '80rpx' },
// top,left,bottom,rightposition=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() {
// iOShybridhtmlpath
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>

8
im-uniapp/uni_modules/lsj-upload/hybrid/html/js/vue.min.js

File diff suppressed because one or more lines are too long

213
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
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"
}
}
}
}
}
Loading…
Cancel
Save