Commit 7f9d0d62 by pangchong

feat: 文件上传完善

parent d94311a5
......@@ -23,6 +23,7 @@ declare module 'vue' {
GlobalPopup: typeof import('./src/mocp/components/global-popup/global-popup.vue')['default']
GlobalTabs: typeof import('./src/mocp/components/global-tabs/global-tabs.vue')['default']
GlobalUpload: typeof import('./src/mocp/components/global-upload/global-upload.vue')['default']
GlobalUploadImage: typeof import('./src/mocp/components/global-upload-image/global-upload-image.vue')['default']
// 自定义组件
CustomScore: typeof import('./src/mocp/components/widget/custom-score.vue')['default']
CustomTabbar: typeof import('./src/mocp/components/widget/custom-tabbar.vue')['default']
......
......@@ -12,6 +12,7 @@
show-refresher-update-time
:hide-empty-view="!isDataList"
@query="query"
@scroll="scroll"
:paging-style="getPagingStyle"
>
<template #empty v-if="refresherEnabled || loadingMoreEnabled">
......@@ -53,7 +54,8 @@
</z-paging>
</template>
<script setup>
import { computed, ref } from 'vue'
import { debounce } from 'lodash'
import { computed, nextTick, ref } from 'vue'
const dataList = ref([])
const es = defineEmits(['query', 'handleLeftClick', 'handleRightClick', 'handleFooterClick'])
......@@ -191,6 +193,14 @@ const getStyle = computed(() => {
}
return style
})
//监听滚动
const scroll = debounce(() => {
// #ifdef APP
nextTick(() => {
uni.$emit('$upload-show', {})
})
// #endif
})
const complete = (data) => {
paging.value?.complete(data || true)
}
......
<template>
<!-- 全局图片下载 -->
<up-upload
:width="width + 'rpx'"
:height="height + 'rpx'"
:fileList="getFileList"
@afterRead="afterRead"
@delete="deletePic"
multiple
:maxCount="10"
:previewFullImage="true"
>
<slot>
<view class="upload-button" :style="getUploadButtonSize">
<up-icon :name="uploadIcon" size="24" color="rgba(0, 0, 0, 0.4)"></up-icon>
</view>
</slot>
</up-upload>
</template>
<script setup>
import { computed, ref, watch } from 'vue'
import { cloneDeep } from 'lodash'
import { upload } from 'mocp/utils/http'
const es = defineEmits(['update:modelValue', 'change'])
const ps = defineProps({
fileList: {
type: Array,
default: () => []
},
width: {
type: [String, Number],
default: '155'
},
height: {
type: [String, Number],
default: '155'
},
url: {
type: String,
default: '/resource/uploadFile'
},
uploadIcon: {
type: String,
default: 'plus'
},
mapFieldName: {
type: String,
default: ''
},
mapFieldSplit: {
type: String,
default: ','
}
})
const getUploadButtonSize = computed(() => {
return {
width: ps.width + 'rpx',
height: ps.height + 'rpx'
}
})
const fileList = ref(cloneDeep(ps.fileList))
const getFileList = computed(() => {
return fileList.value.map((item) => {
return { ...item, url: item.fileUrl }
})
})
// 删除图片
const deletePic = (event) => {
fileList.value.splice(event.index, 1)
}
// 新增图片
const afterRead = async (event) => {
// 当设置 mutiple 为 true 时, file 为数组格式,否则为对象格式
let lists = [].concat(event.file)
let fileListLen = fileList.value.length
lists.map((item) => {
fileList.value.push({
...item,
status: 'uploading',
message: '上传中'
})
})
for (let i = 0; i < lists.length; i++) {
try {
const res = await upload({ url: ps.url, filePath: lists[i].url, name: 'file' })
if (res.code == 200) {
fileList.value.splice(fileListLen, 1, {
...res.data,
status: 'success',
message: ''
})
} else {
fileList.value.splice(fileListLen, 1)
uni.$mocpMessage.showToast(res.message || '请求错误')
}
} catch (error) {
fileList.value.splice(fileListLen, 1)
uni.$mocpMessage.showToast('上传失败')
}
fileListLen++
}
}
watch(
fileList,
(value) => {
let files = cloneDeep(value)
.filter((item) => {
return item.status != 'uploading'
})
.filter((item) => {
return delete item.status
})
.filter((item) => {
return delete item.message
})
if (ps.mapFieldName) {
files = files.map((v) => v[ps.mapFieldName]).join(ps.mapFieldSplit)
}
es('update:modelValue', files)
es('change', files)
},
{ deep: true, immediate: true }
)
</script>
<style lang="scss" scoped>
.upload-button {
display: flex;
align-items: center;
justify-content: center;
background: #f3f3f3;
}
</style>
<template>
<!-- 全局图片下载 -->
<up-upload
:width="width + 'rpx'"
:height="height + 'rpx'"
:fileList="getFileList"
@afterRead="afterRead"
@delete="deletePic"
multiple
:maxCount="10"
:previewFullImage="true"
>
<slot>
<view class="upload-button" :style="getUploadButtonSize">
<up-icon :name="uploadIcon" size="24" color="rgba(0, 0, 0, 0.4)"></up-icon>
<!-- 全局文件下载 -->
<view class="upload">
<view class="upload-list">
<view class="upload-item" v-for="(item, index) in getFileList" :key="item.id">
<view class="upload-item-left">
<view class="label">
<template v-if="item.fileType == 'image'">
<image src="/static/mocp/image/common/image.png" mode="widthFix" />
</template>
<template v-else-if="item.fileType == 'video'">
<image src="/static/mocp/image/common/video.png" mode="widthFix" />
</template>
<template v-else>
<image src="/static/mocp/image/common/pdf.png" mode="widthFix" />
</template>
<view class="u-line-1">{{ item.fileName }}</view>
</view>
<view class="tool" @tap="previewFile(item.fileUrl)"><up-icon name="eye" size="18" color="#4E5969"></up-icon></view>
</view>
<view class="upload-item-right" @tap="deleteFile(index)"><global-icon icon="delete-01" color="#F53F3F"></global-icon></view>
</view>
</slot>
</up-upload>
</view>
<view class="upload-button" v-if="isLoading">
<up-loading-icon text="上传中" textSize="14"></up-loading-icon>
</view>
<lsj-upload
v-else
width="640rpx"
height="88rpx"
:option="getOptions"
:size="30"
:instantly="true"
:distinct="true"
@uploadEnd="onuploadEnd"
@change="change"
>
<view class="upload-button">
<up-icon name="plus" size="18" color="#4E5969"></up-icon>
<text class="txt">上传文件</text>
</view>
</lsj-upload>
<up-modal
:width="250"
content="确认删除吗?删除后不可恢复!"
:show="show"
showCancelButton
closeOnClickOverlay
@confirm="confirm"
@cancel="show = false"
@close="show = false"
></up-modal>
</view>
</template>
<script setup>
import { computed, ref, watch } from 'vue'
import { cloneDeep } from 'lodash'
import { upload } from 'mocp/utils/http'
import { httpInterceptor } from 'mocp/utils/http'
const es = defineEmits(['update:modelValue', 'change'])
const ps = defineProps({
fileList: {
type: Array,
default: () => []
},
width: {
type: [String, Number],
default: '155'
},
height: {
type: [String, Number],
default: '155'
default: () => {
return []
}
},
url: {
type: String,
default: '/resource/uploadFile'
},
uploadIcon: {
type: String,
default: 'plus'
},
mapFieldName: {
type: String,
default: ''
......@@ -54,69 +79,65 @@ const ps = defineProps({
default: ','
}
})
const getUploadButtonSize = computed(() => {
return {
width: ps.width + 'rpx',
height: ps.height + 'rpx'
}
})
const fileList = ref(cloneDeep(ps.fileList))
const getFileList = computed(() => {
return fileList.value.map((item) => {
return { ...item, url: item.fileUrl }
})
})
// 删除图片
const deletePic = (event) => {
fileList.value.splice(event.index, 1)
const getOptions = computed(() => {
return httpInterceptor({ url: ps.url, name: 'file' })
})
// 删除文件
const show = ref(false)
const index = ref(-1)
const deleteFile = (_index) => {
show.value = true
index.value = _index
}
// 新增图片
const afterRead = async (event) => {
// 当设置 mutiple 为 true 时, file 为数组格式,否则为对象格式
let lists = [].concat(event.file)
let fileListLen = fileList.value.length
lists.map((item) => {
fileList.value.push({
...item,
status: 'uploading',
message: '上传中'
})
//确认删除
const confirm = () => {
show.value = false
fileList.value.splice(index.value, 1)
}
//预览图片
const previewFile = (fileUrl) => {
uni.downloadFile({
url: fileUrl,
success: function (res) {
var filePath = res.tempFilePath
uni.openDocument({
filePath: filePath,
showMenu: true,
success: function (res) {
console.log('打开文档成功')
}
})
}
})
for (let i = 0; i < lists.length; i++) {
try {
const res = await upload({ url: ps.url, filePath: lists[i].url, name: 'file' })
if (res.code == 200) {
fileList.value.splice(fileListLen, 1, {
...res.data,
status: 'success',
message: ''
})
} else {
fileList.value.splice(fileListLen, 1)
uni.$mocpMessage.showToast(res.message || '请求错误')
}
} catch (error) {
fileList.value.splice(fileListLen, 1)
uni.$mocpMessage.showToast('上传失败')
}
const isLoading = ref(false)
const onuploadEnd = (e) => {
if (e.type == 'success') {
const res = JSON.parse(e.responseText)
if (res.code == 200) {
fileList.value.push(res.data)
} else {
uni.$mocpMessage.showToast(res.message || '请求错误')
}
fileListLen++
} else {
uni.$mocpMessage.showToast('上传失败')
}
}
isLoading.value = false
}
const change = (e) => {
isLoading.value = true
}
watch(
fileList,
(value) => {
let files = cloneDeep(value)
.filter((item) => {
return item.status != 'uploading'
})
.filter((item) => {
return delete item.status
})
.filter((item) => {
return delete item.message
})
if (ps.mapFieldName) {
files = files.map((v) => v[ps.mapFieldName]).join(ps.mapFieldSplit)
}
......@@ -127,10 +148,49 @@ watch(
)
</script>
<style lang="scss" scoped>
.upload-button {
display: flex;
align-items: center;
justify-content: center;
background: #f3f3f3;
.upload {
&-button {
display: flex;
align-items: center;
justify-content: center;
background: #f3f3f3;
width: 640rpx;
height: 88rpx;
.txt {
color: $mocp-text-5;
}
}
&-item {
height: 72rpx;
line-height: 72rpx;
margin-bottom: 16rpx;
display: flex;
align-items: center;
justify-content: space-between;
&-left {
display: flex;
align-items: center;
width: 640rpx;
background: #f3f3f3;
box-sizing: border-box;
padding: 0 20rpx 0 24rpx;
.label {
flex: auto;
display: flex;
align-items: center;
margin-right: 10rpx;
color: $mocp-text-5;
image {
width: 32rpx;
min-width: 32rpx;
margin-right: 20rpx;
}
.name {
flex: auto;
word-break: break-all;
}
}
}
}
}
</style>
......@@ -41,6 +41,16 @@ const useAppraisalRecordStore = defineStore('appraisalRecord', {
}
},
// 配置持久化
persist: false
persist: {
// 调整为兼容多端的API
storage: {
setItem(key, value) {
uni.setStorageSync(key, value)
},
getItem(key) {
return uni.getStorageSync(key)
}
}
}
})
export default useAppraisalRecordStore
......@@ -101,6 +101,16 @@ const useAssignWorkStore = defineStore('assignWork', {
}
},
// 配置持久化
persist: false
persist: {
// 调整为兼容多端的API
storage: {
setItem(key, value) {
uni.setStorageSync(key, value)
},
getItem(key) {
return uni.getStorageSync(key)
}
}
}
})
export default useAssignWorkStore
......@@ -31,7 +31,7 @@ class ServiceLoading {
}
const serviceLoading = new ServiceLoading()
//请求拦截器
const httpInterceptor = (options) => {
export const httpInterceptor = (options) => {
const userStore = useUserStore()
//responseType
if (options.config?.arraybuffer) {
......
......@@ -54,10 +54,11 @@
text-align: center;
font-size: 28rpx;
color: $mocp-text-5;
margin-top: 36rpx;
margin-top: 28rpx;
}
&-txt {
font-weight: bold;
margin-top: 8rpx;
}
&-line {
width: 2rpx;
......
......@@ -55,6 +55,24 @@
<ul class="icon_lists dib-box">
<li class="dib">
<span class="icon iconfont">&#xe6df;</span>
<div class="name">jpg</div>
<div class="code-name">&amp;#xe6df;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe6e0;</span>
<div class="name">pdf</div>
<div class="code-name">&amp;#xe6e0;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe6e1;</span>
<div class="name">video</div>
<div class="code-name">&amp;#xe6e1;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe6de;</span>
<div class="name">delete-01</div>
<div class="code-name">&amp;#xe6de;</div>
......@@ -138,9 +156,9 @@
<pre><code class="language-css"
>@font-face {
font-family: 'iconfont';
src: url('iconfont.woff2?t=1716962655386') format('woff2'),
url('iconfont.woff?t=1716962655386') format('woff'),
url('iconfont.ttf?t=1716962655386') format('truetype');
src: url('iconfont.woff2?t=1717745202331') format('woff2'),
url('iconfont.woff?t=1717745202331') format('woff'),
url('iconfont.ttf?t=1717745202331') format('truetype');
}
</code></pre>
<h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
......@@ -167,6 +185,33 @@
<ul class="icon_lists dib-box">
<li class="dib">
<span class="icon iconfont icon-jpg"></span>
<div class="name">
jpg
</div>
<div class="code-name">.icon-jpg
</div>
</li>
<li class="dib">
<span class="icon iconfont icon-pdf"></span>
<div class="name">
pdf
</div>
<div class="code-name">.icon-pdf
</div>
</li>
<li class="dib">
<span class="icon iconfont icon-video"></span>
<div class="name">
video
</div>
<div class="code-name">.icon-video
</div>
</li>
<li class="dib">
<span class="icon iconfont icon-delete-01"></span>
<div class="name">
delete-01
......@@ -294,6 +339,30 @@
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-jpg"></use>
</svg>
<div class="name">jpg</div>
<div class="code-name">#icon-jpg</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-pdf"></use>
</svg>
<div class="name">pdf</div>
<div class="code-name">#icon-pdf</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-video"></use>
</svg>
<div class="name">video</div>
<div class="code-name">#icon-video</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-delete-01"></use>
</svg>
<div class="name">delete-01</div>
......
@font-face {
font-family: "iconfont"; /* Project id 4550048 */
src: url('iconfont.woff2?t=1716962655386') format('woff2'),
url('iconfont.woff?t=1716962655386') format('woff'),
url('iconfont.ttf?t=1716962655386') format('truetype');
src: url('iconfont.woff2?t=1717745202331') format('woff2'),
url('iconfont.woff?t=1717745202331') format('woff'),
url('iconfont.ttf?t=1717745202331') format('truetype');
}
.iconfont {
......@@ -13,6 +13,18 @@
-moz-osx-font-smoothing: grayscale;
}
.icon-jpg:before {
content: "\e6df";
}
.icon-pdf:before {
content: "\e6e0";
}
.icon-video:before {
content: "\e6e1";
}
.icon-delete-01:before {
content: "\e6de";
}
......
......@@ -6,6 +6,27 @@
"description": "",
"glyphs": [
{
"icon_id": "40650590",
"name": "jpg",
"font_class": "jpg",
"unicode": "e6df",
"unicode_decimal": 59103
},
{
"icon_id": "40650589",
"name": "pdf",
"font_class": "pdf",
"unicode": "e6e0",
"unicode_decimal": 59104
},
{
"icon_id": "40650588",
"name": "video",
"font_class": "video",
"unicode": "e6e1",
"unicode_decimal": 59105
},
{
"icon_id": "40524214",
"name": "delete-01",
"font_class": "delete-01",
......
## 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自定义按钮样式
<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>
<!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;
}
if (!this.prohibited.distinct) {
let homonymIndex = [...this.files.keys()].findLastIndex(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}`;
}
}
// 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>
{
"id": "lsj-upload",
"displayName": "全文件上传选择非原生2.0版",
"version": "2.3.1",
"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"
}
}
}
}
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment