13 changed files with 2633 additions and 88 deletions
Split View
Diff Options
-
25apis/add-paper.js
-
12pages.json
-
643pages/add-paper/index.vue
-
111pages/mall/index.vue
-
172pages/store-settings/index.vue
-
61uni_modules/uni-file-picker/changelog.md
-
224uni_modules/uni-file-picker/components/uni-file-picker/choose-and-upload-file.js
-
650uni_modules/uni-file-picker/components/uni-file-picker/uni-file-picker.vue
-
325uni_modules/uni-file-picker/components/uni-file-picker/upload-file.vue
-
292uni_modules/uni-file-picker/components/uni-file-picker/upload-image.vue
-
109uni_modules/uni-file-picker/components/uni-file-picker/utils.js
-
86uni_modules/uni-file-picker/package.json
-
11uni_modules/uni-file-picker/readme.md
@ -0,0 +1,172 @@ |
|||
<template> |
|||
<view class="store-setting"> |
|||
<view> |
|||
<uni-nav-bar left-icon="back" @clickLeft="back" statusBar fixed title=""> |
|||
<view class="store-setting-title">店铺设置</view> |
|||
<view slot="left"></view> |
|||
<view slot="right"></view> |
|||
</uni-nav-bar> |
|||
</view> |
|||
<view class="store-setting-main"> |
|||
<view class="store-setting-name"><text>店铺名称</text></view> |
|||
<view class=""> |
|||
<qn-easyinput |
|||
class="store-setting-textArea" |
|||
:maxlength="100" |
|||
:styles="{ disableColor: '#F7F8FA' }" |
|||
v-model="storeName" |
|||
:inputBorder="false" |
|||
type="textarea" |
|||
placeholder="请输入店铺名称" |
|||
></qn-easyinput> |
|||
</view> |
|||
<view class="store-log"> |
|||
<view class="tore-setting-name"><text>店铺logo</text></view> |
|||
<view class="store-log store-image-flex"> |
|||
<uni-file-picker |
|||
class="stotr-image-width" |
|||
v-model="imageValue" |
|||
file-mediatype="image" |
|||
mode="grid" |
|||
file-extname="png,jpg" |
|||
:limit="1" |
|||
:image-styles="imageStyles" |
|||
@progress="progress" |
|||
@success="success" |
|||
@fail="fail" |
|||
@select="select" |
|||
@delete="deleteImage" |
|||
/> |
|||
<view class="store-image-text ">建议尺寸:200*200像素,尺寸不匹配时,图片将被压缩或拉伸以铺满画面</view> |
|||
</view> |
|||
</view> |
|||
<view class="store-log"> |
|||
<view class="tore-setting-name"><text>背景图片</text></view> |
|||
<view class="store-log store-image-flex"> |
|||
<uni-file-picker class="stotr-image-widtht" :limit="1" fileMediatype="image" :image-styles="imageStylest" /> |
|||
<view class="store-image-text">建议尺寸:750*370像素,尺寸不匹配时,图片将被压缩或拉伸以铺满画面</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
<view class=""> |
|||
<view class="upload-btn"><button type="primary" class="btn-class">保存设置</button></view> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import { back, go2 } from '@/utils/hook.js' |
|||
export default { |
|||
data() { |
|||
return { |
|||
storeName: '', |
|||
imageValue: [], |
|||
imageStyles: { |
|||
width: 100, |
|||
height: 100, |
|||
border: { |
|||
color: '#f1f1f1', |
|||
width: 1, |
|||
style: 'dashed', |
|||
radius: '4rpx' |
|||
} |
|||
}, |
|||
imageStylest: { |
|||
width: 183, |
|||
height: 94, |
|||
border: { |
|||
color: '#f1f1f1', |
|||
width: 1, |
|||
style: 'dashed', |
|||
radius: '4rpx' |
|||
} |
|||
} |
|||
} |
|||
}, |
|||
methods: { |
|||
back, |
|||
// 上传进度 |
|||
progress() {}, |
|||
// 上传成功回调 |
|||
success(item) { |
|||
console.log(item) |
|||
}, |
|||
// 上传失败回调 |
|||
fail(item) { |
|||
console.log(item) |
|||
}, |
|||
// 图片选择事件 |
|||
select(item) { |
|||
console.log(item) |
|||
console.log(this.imageValue) |
|||
this.imageValue.push(item.tempFiles[0]) |
|||
}, |
|||
// 删除图片 |
|||
deleteImage(item) { |
|||
console.log('deleteImage', item) |
|||
for (let i = 0; i < this.imageValue.length; i++) { |
|||
if (this.imageValue[i].uuid === item.tempFile.uuid) { |
|||
this.imageValue.splice(i, 1) |
|||
} |
|||
} |
|||
console.log(this.imageValue) |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss"> |
|||
.store-setting { |
|||
background-color: #FFFFFF; |
|||
.store-setting-title { |
|||
width: 100%; |
|||
font-size: 36rpx; |
|||
color: #000000; |
|||
letter-spacing: 0; |
|||
text-align: center; |
|||
font-weight: 500; |
|||
} |
|||
.store-setting-main { |
|||
padding: 20rpx 32rpx; |
|||
background-color: #ffffff; |
|||
} |
|||
.store-setting-name { |
|||
// font-size: 28rpx; |
|||
color: #000000; |
|||
letter-spacing: 0; |
|||
font-weight: 500; |
|||
} |
|||
.store-setting-textArea { |
|||
background: #f7f8fa; |
|||
border-radius: 20rpx; |
|||
padding: 10rpx; |
|||
margin-top: 20rpx; |
|||
} |
|||
.store-log { |
|||
margin-top: 32rpx; |
|||
} |
|||
.store-image-flex{ |
|||
display: flex; |
|||
} |
|||
.store-image-text { |
|||
font-size: 24rpx; |
|||
color: #888888; |
|||
letter-spacing: 0; |
|||
font-weight: 400; |
|||
line-height: 40rpx; |
|||
margin-left: 10rpx; |
|||
} |
|||
.stotr-image-widtht{ |
|||
width: 100%; |
|||
} |
|||
.stotr-image-width{ |
|||
width: 50%; |
|||
} |
|||
.upload-btn{ |
|||
position: absolute; |
|||
width: 92%; |
|||
bottom: 32rpx; |
|||
left: 32rpx; |
|||
} |
|||
} |
|||
</style> |
|||
@ -0,0 +1,61 @@ |
|||
## 1.0.1(2021-11-23) |
|||
- 修复 参数为对象的情况下,url在某些情况显示错误的bug |
|||
## 1.0.0(2021-11-19) |
|||
- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) |
|||
- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-file-picker](https://uniapp.dcloud.io/component/uniui/uni-file-picker) |
|||
## 0.2.16(2021-11-08) |
|||
- 修复 传入空对象 ,显示错误的Bug |
|||
## 0.2.15(2021-08-30) |
|||
- 修复 return-type="object" 时且存在v-model时,无法删除文件的Bug |
|||
## 0.2.14(2021-08-23) |
|||
- 新增 参数中返回 fileID 字段 |
|||
## 0.2.13(2021-08-23) |
|||
- 修复 腾讯云传入fileID 不能回显的bug |
|||
- 修复 选择图片后,不能放大的问题 |
|||
## 0.2.12(2021-08-17) |
|||
- 修复 由于 0.2.11 版本引起的不能回显图片的Bug |
|||
## 0.2.11(2021-08-16) |
|||
- 新增 clearFiles(index) 方法,可以手动删除指定文件 |
|||
- 修复 v-model 值设为 null 报错的Bug |
|||
## 0.2.10(2021-08-13) |
|||
- 修复 return-type="object" 时,无法删除文件的Bug |
|||
## 0.2.9(2021-08-03) |
|||
- 修复 auto-upload 属性失效的Bug |
|||
## 0.2.8(2021-07-31) |
|||
- 修复 fileExtname属性不指定值报错的Bug |
|||
## 0.2.7(2021-07-31) |
|||
- 修复 在某种场景下图片不回显的Bug |
|||
## 0.2.6(2021-07-30) |
|||
- 修复 return-type为object下,返回值不正确的Bug |
|||
## 0.2.5(2021-07-30) |
|||
- 修复(重要) H5 平台下如果和uni-forms组件一同使用导致页面卡死的问题 |
|||
## 0.2.3(2021-07-28) |
|||
- 优化 调整示例代码 |
|||
## 0.2.2(2021-07-27) |
|||
- 修复 vue3 下赋值错误的Bug |
|||
- 优化 h5平台下上传文件导致页面卡死的问题 |
|||
## 0.2.0(2021-07-13) |
|||
- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) |
|||
## 0.1.1(2021-07-02) |
|||
- 修复 sourceType 缺少默认值导致 ios 无法选择文件 |
|||
## 0.1.0(2021-06-30) |
|||
- 优化 解耦与uniCloud的强绑定关系 ,如不绑定服务空间,默认autoUpload为false且不可更改 |
|||
## 0.0.11(2021-06-30) |
|||
- 修复 由 0.0.10 版本引发的 returnType 属性失效的问题 |
|||
## 0.0.10(2021-06-29) |
|||
- 优化 文件上传后进度条消失时机 |
|||
## 0.0.9(2021-06-29) |
|||
- 修复 在uni-forms 中,删除文件 ,获取的值不对的Bug |
|||
## 0.0.8(2021-06-15) |
|||
- 修复 删除文件时无法触发 v-model 的Bug |
|||
## 0.0.7(2021-05-12) |
|||
- 新增 组件示例地址 |
|||
## 0.0.6(2021-04-09) |
|||
- 修复 选择的文件非 file-extname 字段指定的扩展名报错的Bug |
|||
## 0.0.5(2021-04-09) |
|||
- 优化 更新组件示例 |
|||
## 0.0.4(2021-04-09) |
|||
- 优化 file-extname 字段支持字符串写法,多个扩展名需要用逗号分隔 |
|||
## 0.0.3(2021-02-05) |
|||
- 调整为uni_modules目录规范 |
|||
- 修复 微信小程序不指定 fileExtname 属性选择失败的Bug |
|||
@ -0,0 +1,224 @@ |
|||
'use strict'; |
|||
|
|||
const ERR_MSG_OK = 'chooseAndUploadFile:ok'; |
|||
const ERR_MSG_FAIL = 'chooseAndUploadFile:fail'; |
|||
|
|||
function chooseImage(opts) { |
|||
const { |
|||
count, |
|||
sizeType = ['original', 'compressed'], |
|||
sourceType = ['album', 'camera'], |
|||
extension |
|||
} = opts |
|||
return new Promise((resolve, reject) => { |
|||
uni.chooseImage({ |
|||
count, |
|||
sizeType, |
|||
sourceType, |
|||
extension, |
|||
success(res) { |
|||
resolve(normalizeChooseAndUploadFileRes(res, 'image')); |
|||
}, |
|||
fail(res) { |
|||
reject({ |
|||
errMsg: res.errMsg.replace('chooseImage:fail', ERR_MSG_FAIL), |
|||
}); |
|||
}, |
|||
}); |
|||
}); |
|||
} |
|||
|
|||
function chooseVideo(opts) { |
|||
const { |
|||
camera, |
|||
compressed, |
|||
maxDuration, |
|||
sourceType = ['album', 'camera'], |
|||
extension |
|||
} = opts; |
|||
return new Promise((resolve, reject) => { |
|||
uni.chooseVideo({ |
|||
camera, |
|||
compressed, |
|||
maxDuration, |
|||
sourceType, |
|||
extension, |
|||
success(res) { |
|||
const { |
|||
tempFilePath, |
|||
duration, |
|||
size, |
|||
height, |
|||
width |
|||
} = res; |
|||
resolve(normalizeChooseAndUploadFileRes({ |
|||
errMsg: 'chooseVideo:ok', |
|||
tempFilePaths: [tempFilePath], |
|||
tempFiles: [ |
|||
{ |
|||
name: (res.tempFile && res.tempFile.name) || '', |
|||
path: tempFilePath, |
|||
size, |
|||
type: (res.tempFile && res.tempFile.type) || '', |
|||
width, |
|||
height, |
|||
duration, |
|||
fileType: 'video', |
|||
cloudPath: '', |
|||
}, ], |
|||
}, 'video')); |
|||
}, |
|||
fail(res) { |
|||
reject({ |
|||
errMsg: res.errMsg.replace('chooseVideo:fail', ERR_MSG_FAIL), |
|||
}); |
|||
}, |
|||
}); |
|||
}); |
|||
} |
|||
|
|||
function chooseAll(opts) { |
|||
const { |
|||
count, |
|||
extension |
|||
} = opts; |
|||
return new Promise((resolve, reject) => { |
|||
let chooseFile = uni.chooseFile; |
|||
if (typeof wx !== 'undefined' && |
|||
typeof wx.chooseMessageFile === 'function') { |
|||
chooseFile = wx.chooseMessageFile; |
|||
} |
|||
if (typeof chooseFile !== 'function') { |
|||
return reject({ |
|||
errMsg: ERR_MSG_FAIL + ' 请指定 type 类型,该平台仅支持选择 image 或 video。', |
|||
}); |
|||
} |
|||
chooseFile({ |
|||
type: 'all', |
|||
count, |
|||
extension, |
|||
success(res) { |
|||
resolve(normalizeChooseAndUploadFileRes(res)); |
|||
}, |
|||
fail(res) { |
|||
reject({ |
|||
errMsg: res.errMsg.replace('chooseFile:fail', ERR_MSG_FAIL), |
|||
}); |
|||
}, |
|||
}); |
|||
}); |
|||
} |
|||
|
|||
function normalizeChooseAndUploadFileRes(res, fileType) { |
|||
res.tempFiles.forEach((item, index) => { |
|||
if (!item.name) { |
|||
item.name = item.path.substring(item.path.lastIndexOf('/') + 1); |
|||
} |
|||
if (fileType) { |
|||
item.fileType = fileType; |
|||
} |
|||
item.cloudPath = |
|||
Date.now() + '_' + index + item.name.substring(item.name.lastIndexOf('.')); |
|||
}); |
|||
if (!res.tempFilePaths) { |
|||
res.tempFilePaths = res.tempFiles.map((file) => file.path); |
|||
} |
|||
return res; |
|||
} |
|||
|
|||
function uploadCloudFiles(files, max = 5, onUploadProgress) { |
|||
files = JSON.parse(JSON.stringify(files)) |
|||
const len = files.length |
|||
let count = 0 |
|||
let self = this |
|||
return new Promise(resolve => { |
|||
while (count < max) { |
|||
next() |
|||
} |
|||
|
|||
function next() { |
|||
let cur = count++ |
|||
if (cur >= len) { |
|||
!files.find(item => !item.url && !item.errMsg) && resolve(files) |
|||
return |
|||
} |
|||
const fileItem = files[cur] |
|||
const index = self.files.findIndex(v => v.uuid === fileItem.uuid) |
|||
fileItem.url = '' |
|||
delete fileItem.errMsg |
|||
|
|||
uniCloud |
|||
.uploadFile({ |
|||
filePath: fileItem.path, |
|||
cloudPath: fileItem.cloudPath, |
|||
fileType: fileItem.fileType, |
|||
onUploadProgress: res => { |
|||
res.index = index |
|||
onUploadProgress && onUploadProgress(res) |
|||
} |
|||
}) |
|||
.then(res => { |
|||
fileItem.url = res.fileID |
|||
fileItem.index = index |
|||
if (cur < len) { |
|||
next() |
|||
} |
|||
}) |
|||
.catch(res => { |
|||
fileItem.errMsg = res.errMsg || res.message |
|||
fileItem.index = index |
|||
if (cur < len) { |
|||
next() |
|||
} |
|||
}) |
|||
} |
|||
}) |
|||
} |
|||
|
|||
|
|||
|
|||
|
|||
|
|||
function uploadFiles(choosePromise, { |
|||
onChooseFile, |
|||
onUploadProgress |
|||
}) { |
|||
return choosePromise |
|||
.then((res) => { |
|||
if (onChooseFile) { |
|||
const customChooseRes = onChooseFile(res); |
|||
if (typeof customChooseRes !== 'undefined') { |
|||
return Promise.resolve(customChooseRes).then((chooseRes) => typeof chooseRes === 'undefined' ? |
|||
res : chooseRes); |
|||
} |
|||
} |
|||
return res; |
|||
}) |
|||
.then((res) => { |
|||
if (res === false) { |
|||
return { |
|||
errMsg: ERR_MSG_OK, |
|||
tempFilePaths: [], |
|||
tempFiles: [], |
|||
}; |
|||
} |
|||
return res |
|||
}) |
|||
} |
|||
|
|||
function chooseAndUploadFile(opts = { |
|||
type: 'all' |
|||
}) { |
|||
if (opts.type === 'image') { |
|||
return uploadFiles(chooseImage(opts), opts); |
|||
} |
|||
else if (opts.type === 'video') { |
|||
return uploadFiles(chooseVideo(opts), opts); |
|||
} |
|||
return uploadFiles(chooseAll(opts), opts); |
|||
} |
|||
|
|||
export { |
|||
chooseAndUploadFile, |
|||
uploadCloudFiles |
|||
}; |
|||
@ -0,0 +1,650 @@ |
|||
<template> |
|||
<view class="uni-file-picker"> |
|||
<view v-if="title" class="uni-file-picker__header"> |
|||
<text class="file-title">{{ title }}</text> |
|||
<text class="file-count">{{ filesList.length }}/{{ limitLength }}</text> |
|||
</view> |
|||
<upload-image v-if="fileMediatype === 'image' && showType === 'grid'" :readonly="readonly" |
|||
:image-styles="imageStyles" :files-list="filesList" :limit="limitLength" :disablePreview="disablePreview" |
|||
:delIcon="delIcon" @uploadFiles="uploadFiles" @choose="choose" @delFile="delFile"> |
|||
<slot> |
|||
<view class="is-add"> |
|||
<view class="icon-add"></view> |
|||
<view class="icon-add rotate"></view> |
|||
</view> |
|||
</slot> |
|||
</upload-image> |
|||
<upload-file v-if="fileMediatype !== 'image' || showType !== 'grid'" :readonly="readonly" |
|||
:list-styles="listStyles" :files-list="filesList" :showType="showType" :delIcon="delIcon" |
|||
@uploadFiles="uploadFiles" @choose="choose" @delFile="delFile"> |
|||
<slot><button type="primary" size="mini">选择文件</button></slot> |
|||
</upload-file> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import { |
|||
chooseAndUploadFile, |
|||
uploadCloudFiles |
|||
} from './choose-and-upload-file.js' |
|||
import { |
|||
get_file_ext, |
|||
get_extname, |
|||
get_files_and_is_max, |
|||
get_file_info, |
|||
get_file_data |
|||
} from './utils.js' |
|||
import uploadImage from './upload-image.vue' |
|||
import uploadFile from './upload-file.vue' |
|||
let fileInput = null |
|||
/** |
|||
* FilePicker 文件选择上传 |
|||
* @description 文件选择上传组件,可以选择图片、视频等任意文件并上传到当前绑定的服务空间 |
|||
* @tutorial https://ext.dcloud.net.cn/plugin?id=4079 |
|||
* @property {Object|Array} value 组件数据,通常用来回显 ,类型由return-type属性决定 |
|||
* @property {Boolean} disabled = [true|false] 组件禁用 |
|||
* @value true 禁用 |
|||
* @value false 取消禁用 |
|||
* @property {Boolean} readonly = [true|false] 组件只读,不可选择,不显示进度,不显示删除按钮 |
|||
* @value true 只读 |
|||
* @value false 取消只读 |
|||
* @property {String} return-type = [array|object] 限制 value 格式,当为 object 时 ,组件只能单选,且会覆盖 |
|||
* @value array 规定 value 属性的类型为数组 |
|||
* @value object 规定 value 属性的类型为对象 |
|||
* @property {Boolean} disable-preview = [true|false] 禁用图片预览,仅 mode:grid 时生效 |
|||
* @value true 禁用图片预览 |
|||
* @value false 取消禁用图片预览 |
|||
* @property {Boolean} del-icon = [true|false] 是否显示删除按钮 |
|||
* @value true 显示删除按钮 |
|||
* @value false 不显示删除按钮 |
|||
* @property {Boolean} auto-upload = [true|false] 是否自动上传,值为true则只触发@select,可自行上传 |
|||
* @value true 自动上传 |
|||
* @value false 取消自动上传 |
|||
* @property {Number|String} limit 最大选择个数 ,h5 会自动忽略多选的部分 |
|||
* @property {String} title 组件标题,右侧显示上传计数 |
|||
* @property {String} mode = [list|grid] 选择文件后的文件列表样式 |
|||
* @value list 列表显示 |
|||
* @value grid 宫格显示 |
|||
* @property {String} file-mediatype = [image|video|all] 选择文件类型 |
|||
* @value image 只选择图片 |
|||
* @value video 只选择视频 |
|||
* @value all 选择所有文件 |
|||
* @property {Array} file-extname 选择文件后缀,根据 file-mediatype 属性而不同 |
|||
* @property {Object} list-style mode:list 时的样式 |
|||
* @property {Object} image-styles 选择文件后缀,根据 file-mediatype 属性而不同 |
|||
* @event {Function} select 选择文件后触发 |
|||
* @event {Function} progress 文件上传时触发 |
|||
* @event {Function} success 上传成功触发 |
|||
* @event {Function} fail 上传失败触发 |
|||
* @event {Function} delete 文件从列表移除时触发 |
|||
*/ |
|||
export default { |
|||
name: 'uniFilePicker', |
|||
components: { |
|||
uploadImage, |
|||
uploadFile |
|||
}, |
|||
emits: ['select', 'success', 'fail', 'progress', 'delete', 'update:modelValue', 'input'], |
|||
props: { |
|||
// #ifdef VUE3 |
|||
modelValue: { |
|||
type: [Array, Object], |
|||
default () { |
|||
return [] |
|||
} |
|||
}, |
|||
// #endif |
|||
|
|||
// #ifndef VUE3 |
|||
value: { |
|||
type: [Array, Object], |
|||
default () { |
|||
return [] |
|||
} |
|||
}, |
|||
// #endif |
|||
|
|||
disabled: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
disablePreview: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
delIcon: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 自动上传 |
|||
autoUpload: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 最大选择个数 ,h5只能限制单选或是多选 |
|||
limit: { |
|||
type: [Number, String], |
|||
default: 9 |
|||
}, |
|||
// 列表样式 grid | list | list-card |
|||
mode: { |
|||
type: String, |
|||
default: 'grid' |
|||
}, |
|||
// 选择文件类型 image/video/all |
|||
fileMediatype: { |
|||
type: String, |
|||
default: 'image' |
|||
}, |
|||
// 文件类型筛选 |
|||
fileExtname: { |
|||
type: [Array, String], |
|||
default () { |
|||
return [] |
|||
} |
|||
}, |
|||
title: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
listStyles: { |
|||
type: Object, |
|||
default () { |
|||
return { |
|||
// 是否显示边框 |
|||
border: true, |
|||
// 是否显示分隔线 |
|||
dividline: true, |
|||
// 线条样式 |
|||
borderStyle: {} |
|||
} |
|||
} |
|||
}, |
|||
imageStyles: { |
|||
type: Object, |
|||
default () { |
|||
return { |
|||
width: 'auto', |
|||
height: 'auto' |
|||
} |
|||
} |
|||
}, |
|||
readonly: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
returnType: { |
|||
type: String, |
|||
default: 'array' |
|||
}, |
|||
sizeType: { |
|||
type: Array, |
|||
default () { |
|||
return ['original', 'compressed'] |
|||
} |
|||
} |
|||
}, |
|||
data() { |
|||
return { |
|||
files: [], |
|||
localValue: [] |
|||
} |
|||
}, |
|||
watch: { |
|||
// #ifndef VUE3 |
|||
value: { |
|||
handler(newVal, oldVal) { |
|||
this.setValue(newVal, oldVal) |
|||
}, |
|||
immediate: true |
|||
}, |
|||
// #endif |
|||
// #ifdef VUE3 |
|||
modelValue: { |
|||
handler(newVal, oldVal) { |
|||
this.setValue(newVal, oldVal) |
|||
}, |
|||
immediate: true |
|||
}, |
|||
// #endif |
|||
}, |
|||
computed: { |
|||
filesList() { |
|||
let files = [] |
|||
this.files.forEach(v => { |
|||
files.push(v) |
|||
}) |
|||
return files |
|||
}, |
|||
showType() { |
|||
if (this.fileMediatype === 'image') { |
|||
return this.mode |
|||
} |
|||
return 'list' |
|||
}, |
|||
limitLength() { |
|||
if (this.returnType === 'object') { |
|||
return 1 |
|||
} |
|||
if (!this.limit) { |
|||
return 1 |
|||
} |
|||
if (this.limit >= 9) { |
|||
return 9 |
|||
} |
|||
return this.limit |
|||
} |
|||
}, |
|||
created() { |
|||
// TODO 兼容不开通服务空间的情况 |
|||
if (!(uniCloud.config && uniCloud.config.provider)) { |
|||
this.noSpace = true |
|||
uniCloud.chooseAndUploadFile = chooseAndUploadFile |
|||
} |
|||
this.form = this.getForm('uniForms') |
|||
this.formItem = this.getForm('uniFormsItem') |
|||
if (this.form && this.formItem) { |
|||
if (this.formItem.name) { |
|||
this.rename = this.formItem.name |
|||
this.form.inputChildrens.push(this) |
|||
} |
|||
} |
|||
}, |
|||
methods: { |
|||
/** |
|||
* 公开用户使用,清空文件 |
|||
* @param {Object} index |
|||
*/ |
|||
clearFiles(index) { |
|||
if (index !== 0 && !index) { |
|||
this.files = [] |
|||
this.$nextTick(() => { |
|||
this.setEmit() |
|||
}) |
|||
} else { |
|||
this.files.splice(index, 1) |
|||
} |
|||
this.$nextTick(() => { |
|||
this.setEmit() |
|||
}) |
|||
}, |
|||
/** |
|||
* 公开用户使用,继续上传 |
|||
*/ |
|||
upload() { |
|||
let files = [] |
|||
this.files.forEach((v, index) => { |
|||
if (v.status === 'ready' || v.status === 'error') { |
|||
files.push(Object.assign({}, v)) |
|||
} |
|||
}) |
|||
this.uploadFiles(files) |
|||
}, |
|||
async setValue(newVal, oldVal) { |
|||
const newData = async (v) => { |
|||
const reg = /cloud:\/\/([\w.]+\/?)\S*/ |
|||
let url = '' |
|||
if(v.fileID){ |
|||
url = v.fileID |
|||
}else{ |
|||
url = v.url |
|||
} |
|||
if (reg.test(url)) { |
|||
v.fileID = url |
|||
v.url = await this.getTempFileURL(url) |
|||
} |
|||
if(v.url) v.path = v.url |
|||
return v |
|||
} |
|||
if (this.returnType === 'object') { |
|||
if (newVal) { |
|||
await newData(newVal) |
|||
} else { |
|||
newVal = {} |
|||
} |
|||
} else { |
|||
if (!newVal) newVal = [] |
|||
for(let i =0 ;i < newVal.length ;i++){ |
|||
let v = newVal[i] |
|||
await newData(v) |
|||
} |
|||
} |
|||
this.localValue = newVal |
|||
if (this.form && this.formItem &&!this.is_reset) { |
|||
this.is_reset = false |
|||
this.formItem.setValue(this.localValue) |
|||
} |
|||
let filesData = Object.keys(newVal).length > 0 ? newVal : []; |
|||
this.files = [].concat(filesData) |
|||
}, |
|||
|
|||
/** |
|||
* 选择文件 |
|||
*/ |
|||
choose() { |
|||
|
|||
if (this.disabled) return |
|||
if (this.files.length >= Number(this.limitLength) && this.showType !== 'grid' && this.returnType === |
|||
'array') { |
|||
uni.showToast({ |
|||
title: `您最多选择 ${this.limitLength} 个文件`, |
|||
icon: 'none' |
|||
}) |
|||
return |
|||
} |
|||
this.chooseFiles() |
|||
}, |
|||
|
|||
/** |
|||
* 选择文件并上传 |
|||
*/ |
|||
chooseFiles() { |
|||
const _extname = get_extname(this.fileExtname) |
|||
// 获取后缀 |
|||
uniCloud |
|||
.chooseAndUploadFile({ |
|||
type: this.fileMediatype, |
|||
compressed: false, |
|||
sizeType: this.sizeType, |
|||
// TODO 如果为空,video 有问题 |
|||
extension: _extname.length > 0 ? _extname : undefined, |
|||
count: this.limitLength - this.files.length, //默认9 |
|||
onChooseFile: this.chooseFileCallback, |
|||
onUploadProgress: progressEvent => { |
|||
this.setProgress(progressEvent, progressEvent.index) |
|||
} |
|||
}) |
|||
.then(result => { |
|||
this.setSuccessAndError(result.tempFiles) |
|||
}) |
|||
.catch(err => { |
|||
console.log('选择失败', err) |
|||
}) |
|||
}, |
|||
|
|||
/** |
|||
* 选择文件回调 |
|||
* @param {Object} res |
|||
*/ |
|||
async chooseFileCallback(res) { |
|||
const _extname = get_extname(this.fileExtname) |
|||
const is_one = (Number(this.limitLength) === 1 && |
|||
this.disablePreview && |
|||
!this.disabled) || |
|||
this.returnType === 'object' |
|||
// 如果这有一个文件 ,需要清空本地缓存数据 |
|||
if (is_one) { |
|||
this.files = [] |
|||
} |
|||
|
|||
let { |
|||
filePaths, |
|||
files |
|||
} = get_files_and_is_max(res, _extname) |
|||
if (!(_extname && _extname.length > 0)) { |
|||
filePaths = res.tempFilePaths |
|||
files = res.tempFiles |
|||
} |
|||
|
|||
let currentData = [] |
|||
for (let i = 0; i < files.length; i++) { |
|||
if (this.limitLength - this.files.length <= 0) break |
|||
files[i].uuid = Date.now() |
|||
let filedata = await get_file_data(files[i], this.fileMediatype) |
|||
filedata.progress = 0 |
|||
filedata.status = 'ready' |
|||
this.files.push(filedata) |
|||
currentData.push({ |
|||
...filedata, |
|||
file: files[i] |
|||
}) |
|||
} |
|||
this.$emit('select', { |
|||
tempFiles: currentData, |
|||
tempFilePaths: filePaths |
|||
}) |
|||
res.tempFiles = files |
|||
// 停止自动上传 |
|||
if (!this.autoUpload || this.noSpace) { |
|||
res.tempFiles = [] |
|||
} |
|||
}, |
|||
|
|||
/** |
|||
* 批传 |
|||
* @param {Object} e |
|||
*/ |
|||
uploadFiles(files) { |
|||
files = [].concat(files) |
|||
uploadCloudFiles.call(this, files, 5, res => { |
|||
this.setProgress(res, res.index, true) |
|||
}) |
|||
.then(result => { |
|||
this.setSuccessAndError(result) |
|||
}) |
|||
.catch(err => { |
|||
console.log(err) |
|||
}) |
|||
}, |
|||
|
|||
/** |
|||
* 成功或失败 |
|||
*/ |
|||
async setSuccessAndError(res, fn) { |
|||
let successData = [] |
|||
let errorData = [] |
|||
let tempFilePath = [] |
|||
let errorTempFilePath = [] |
|||
for (let i = 0; i < res.length; i++) { |
|||
const item = res[i] |
|||
const index = item.uuid ? this.files.findIndex(p => p.uuid === item.uuid) : item.index |
|||
|
|||
if (index === -1 || !this.files) break |
|||
if (item.errMsg === 'request:fail') { |
|||
this.files[index].url = item.path |
|||
this.files[index].status = 'error' |
|||
this.files[index].errMsg = item.errMsg |
|||
// this.files[index].progress = -1 |
|||
errorData.push(this.files[index]) |
|||
errorTempFilePath.push(this.files[index].url) |
|||
} else { |
|||
this.files[index].errMsg = '' |
|||
this.files[index].fileID = item.url |
|||
const reg = /cloud:\/\/([\w.]+\/?)\S*/ |
|||
if (reg.test(item.url)) { |
|||
this.files[index].url = await this.getTempFileURL(item.url) |
|||
}else{ |
|||
this.files[index].url = item.url |
|||
} |
|||
|
|||
this.files[index].status = 'success' |
|||
this.files[index].progress += 1 |
|||
successData.push(this.files[index]) |
|||
tempFilePath.push(this.files[index].fileID) |
|||
} |
|||
} |
|||
|
|||
if (successData.length > 0) { |
|||
this.setEmit() |
|||
// 状态改变返回 |
|||
this.$emit('success', { |
|||
tempFiles: this.backObject(successData), |
|||
tempFilePaths: tempFilePath |
|||
}) |
|||
} |
|||
|
|||
if (errorData.length > 0) { |
|||
this.$emit('fail', { |
|||
tempFiles: this.backObject(errorData), |
|||
tempFilePaths: errorTempFilePath |
|||
}) |
|||
} |
|||
}, |
|||
|
|||
/** |
|||
* 获取进度 |
|||
* @param {Object} progressEvent |
|||
* @param {Object} index |
|||
* @param {Object} type |
|||
*/ |
|||
setProgress(progressEvent, index, type) { |
|||
const fileLenth = this.files.length |
|||
const percentNum = (index / fileLenth) * 100 |
|||
const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total) |
|||
let idx = index |
|||
if (!type) { |
|||
idx = this.files.findIndex(p => p.uuid === progressEvent.tempFile.uuid) |
|||
} |
|||
if (idx === -1 || !this.files[idx]) return |
|||
// fix by mehaotian 100 就会消失,-1 是为了让进度条消失 |
|||
this.files[idx].progress = percentCompleted - 1 |
|||
// 上传中 |
|||
this.$emit('progress', { |
|||
index: idx, |
|||
progress: parseInt(percentCompleted), |
|||
tempFile: this.files[idx] |
|||
}) |
|||
}, |
|||
|
|||
/** |
|||
* 删除文件 |
|||
* @param {Object} index |
|||
*/ |
|||
delFile(index) { |
|||
this.$emit('delete', { |
|||
tempFile: this.files[index], |
|||
tempFilePath: this.files[index].url |
|||
}) |
|||
this.files.splice(index, 1) |
|||
this.$nextTick(() => { |
|||
this.setEmit() |
|||
}) |
|||
}, |
|||
|
|||
/** |
|||
* 获取文件名和后缀 |
|||
* @param {Object} name |
|||
*/ |
|||
getFileExt(name) { |
|||
const last_len = name.lastIndexOf('.') |
|||
const len = name.length |
|||
return { |
|||
name: name.substring(0, last_len), |
|||
ext: name.substring(last_len + 1, len) |
|||
} |
|||
}, |
|||
|
|||
/** |
|||
* 处理返回事件 |
|||
*/ |
|||
setEmit() { |
|||
let data = [] |
|||
if (this.returnType === 'object') { |
|||
data = this.backObject(this.files)[0] |
|||
this.localValue = data?data:null |
|||
} else { |
|||
data = this.backObject(this.files) |
|||
if (!this.localValue) { |
|||
this.localValue = [] |
|||
} |
|||
this.localValue = [...data] |
|||
} |
|||
// #ifdef VUE3 |
|||
this.$emit('update:modelValue', this.localValue) |
|||
// #endif |
|||
// #ifndef VUE3 |
|||
this.$emit('input', this.localValue) |
|||
// #endif |
|||
}, |
|||
|
|||
/** |
|||
* 处理返回参数 |
|||
* @param {Object} files |
|||
*/ |
|||
backObject(files) { |
|||
let newFilesData = [] |
|||
files.forEach(v => { |
|||
newFilesData.push({ |
|||
extname: v.extname, |
|||
fileType: v.fileType, |
|||
image: v.image, |
|||
name: v.name, |
|||
path: v.path, |
|||
size: v.size, |
|||
fileID:v.fileID, |
|||
url: v.url |
|||
}) |
|||
}) |
|||
return newFilesData |
|||
}, |
|||
async getTempFileURL(fileList) { |
|||
fileList = { |
|||
fileList: [].concat(fileList) |
|||
} |
|||
const urls = await uniCloud.getTempFileURL(fileList) |
|||
return urls.fileList[0].tempFileURL || '' |
|||
}, |
|||
/** |
|||
* 获取父元素实例 |
|||
*/ |
|||
getForm(name = 'uniForms') { |
|||
let parent = this.$parent; |
|||
let parentName = parent.$options.name; |
|||
while (parentName !== name) { |
|||
parent = parent.$parent; |
|||
if (!parent) return false; |
|||
parentName = parent.$options.name; |
|||
} |
|||
return parent; |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style> |
|||
.uni-file-picker { |
|||
/* #ifndef APP-NVUE */ |
|||
box-sizing: border-box; |
|||
overflow: hidden; |
|||
/* #endif */ |
|||
} |
|||
|
|||
.uni-file-picker__header { |
|||
padding-top: 5px; |
|||
padding-bottom: 10px; |
|||
/* #ifndef APP-NVUE */ |
|||
display: flex; |
|||
/* #endif */ |
|||
justify-content: space-between; |
|||
} |
|||
|
|||
.file-title { |
|||
font-size: 14px; |
|||
color: #333; |
|||
} |
|||
|
|||
.file-count { |
|||
font-size: 14px; |
|||
color: #999; |
|||
} |
|||
|
|||
.is-add { |
|||
/* #ifndef APP-NVUE */ |
|||
display: flex; |
|||
/* #endif */ |
|||
align-items: center; |
|||
justify-content: center; |
|||
} |
|||
|
|||
.icon-add { |
|||
width: 50px; |
|||
height: 5px; |
|||
background-color: #f1f1f1; |
|||
border-radius: 2px; |
|||
} |
|||
|
|||
.rotate { |
|||
position: absolute; |
|||
transform: rotate(90deg); |
|||
} |
|||
</style> |
|||
@ -0,0 +1,325 @@ |
|||
<template> |
|||
<view class="uni-file-picker__files"> |
|||
<view v-if="!readonly" class="files-button" @click="choose"> |
|||
<slot></slot> |
|||
</view> |
|||
<!-- :class="{'is-text-box':showType === 'list'}" --> |
|||
<view v-if="list.length > 0" class="uni-file-picker__lists is-text-box" :style="borderStyle"> |
|||
<!-- ,'is-list-card':showType === 'list-card' --> |
|||
|
|||
<view class="uni-file-picker__lists-box" v-for="(item ,index) in list" :key="index" :class="{ |
|||
'files-border':index !== 0 && styles.dividline}" |
|||
:style="index !== 0 && styles.dividline &&borderLineStyle"> |
|||
<view class="uni-file-picker__item"> |
|||
<!-- :class="{'is-text-image':showType === 'list'}" --> |
|||
<!-- <view class="files__image is-text-image"> |
|||
<image class="header-image" :src="item.logo" mode="aspectFit"></image> |
|||
</view> --> |
|||
<view class="files__name">{{item.name}}</view> |
|||
<view v-if="delIcon&&!readonly" class="icon-del-box icon-files" @click="delFile(index)"> |
|||
<view class="icon-del icon-files"></view> |
|||
<view class="icon-del rotate"></view> |
|||
</view> |
|||
</view> |
|||
<view v-if="(item.progress && item.progress !== 100) ||item.progress===0 " class="file-picker__progress"> |
|||
<progress class="file-picker__progress-item" :percent="item.progress === -1?0:item.progress" stroke-width="4" |
|||
:backgroundColor="item.errMsg?'#ff5a5f':'#EBEBEB'" /> |
|||
</view> |
|||
<view v-if="item.status === 'error'" class="file-picker__mask" @click.stop="uploadFiles(item,index)"> |
|||
点击重试 |
|||
</view> |
|||
</view> |
|||
|
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
name: "uploadFile", |
|||
emits:['uploadFiles','choose','delFile'], |
|||
props: { |
|||
filesList: { |
|||
type: Array, |
|||
default () { |
|||
return [] |
|||
} |
|||
}, |
|||
delIcon: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
limit: { |
|||
type: [Number, String], |
|||
default: 9 |
|||
}, |
|||
showType: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
listStyles: { |
|||
type: Object, |
|||
default () { |
|||
return { |
|||
// 是否显示边框 |
|||
border: true, |
|||
// 是否显示分隔线 |
|||
dividline: true, |
|||
// 线条样式 |
|||
borderStyle: {} |
|||
} |
|||
} |
|||
}, |
|||
readonly:{ |
|||
type:Boolean, |
|||
default:false |
|||
} |
|||
}, |
|||
computed: { |
|||
list() { |
|||
let files = [] |
|||
this.filesList.forEach(v => { |
|||
files.push(v) |
|||
}) |
|||
return files |
|||
}, |
|||
styles() { |
|||
let styles = { |
|||
border: true, |
|||
dividline: true, |
|||
'border-style': {} |
|||
} |
|||
return Object.assign(styles, this.listStyles) |
|||
}, |
|||
borderStyle() { |
|||
let { |
|||
borderStyle, |
|||
border |
|||
} = this.styles |
|||
let obj = {} |
|||
if (!border) { |
|||
obj.border = 'none' |
|||
} else { |
|||
let width = (borderStyle && borderStyle.width) || 1 |
|||
width = this.value2px(width) |
|||
let radius = (borderStyle && borderStyle.radius) || 5 |
|||
radius = this.value2px(radius) |
|||
obj = { |
|||
'border-width': width, |
|||
'border-style': (borderStyle && borderStyle.style) || 'solid', |
|||
'border-color': (borderStyle && borderStyle.color) || '#eee', |
|||
'border-radius': radius |
|||
} |
|||
} |
|||
let classles = '' |
|||
for (let i in obj) { |
|||
classles += `${i}:${obj[i]};` |
|||
} |
|||
return classles |
|||
}, |
|||
borderLineStyle() { |
|||
let obj = {} |
|||
let { |
|||
borderStyle |
|||
} = this.styles |
|||
if (borderStyle && borderStyle.color) { |
|||
obj['border-color'] = borderStyle.color |
|||
} |
|||
if (borderStyle && borderStyle.width) { |
|||
let width = borderStyle && borderStyle.width || 1 |
|||
let style = borderStyle && borderStyle.style || 0 |
|||
if (typeof width === 'number') { |
|||
width += 'px' |
|||
} else { |
|||
width = width.indexOf('px') ? width : width + 'px' |
|||
} |
|||
obj['border-width'] = width |
|||
|
|||
if (typeof style === 'number') { |
|||
style += 'px' |
|||
} else { |
|||
style = style.indexOf('px') ? style : style + 'px' |
|||
} |
|||
obj['border-top-style'] = style |
|||
} |
|||
let classles = '' |
|||
for (let i in obj) { |
|||
classles += `${i}:${obj[i]};` |
|||
} |
|||
return classles |
|||
} |
|||
}, |
|||
|
|||
methods: { |
|||
uploadFiles(item, index) { |
|||
this.$emit("uploadFiles", { |
|||
item, |
|||
index |
|||
}) |
|||
}, |
|||
choose() { |
|||
this.$emit("choose") |
|||
}, |
|||
delFile(index) { |
|||
this.$emit('delFile', index) |
|||
}, |
|||
value2px(value) { |
|||
if (typeof value === 'number') { |
|||
value += 'px' |
|||
} else { |
|||
value = value.indexOf('px') !== -1 ? value : value + 'px' |
|||
} |
|||
return value |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss"> |
|||
.uni-file-picker__files { |
|||
/* #ifndef APP-NVUE */ |
|||
display: flex; |
|||
/* #endif */ |
|||
flex-direction: column; |
|||
justify-content: flex-start; |
|||
} |
|||
|
|||
.files-button { |
|||
// border: 1px red solid; |
|||
} |
|||
|
|||
.uni-file-picker__lists { |
|||
position: relative; |
|||
margin-top: 5px; |
|||
overflow: hidden; |
|||
} |
|||
|
|||
.file-picker__mask { |
|||
/* #ifndef APP-NVUE */ |
|||
display: flex; |
|||
/* #endif */ |
|||
justify-content: center; |
|||
align-items: center; |
|||
position: absolute; |
|||
right: 0; |
|||
top: 0; |
|||
bottom: 0; |
|||
left: 0; |
|||
color: #fff; |
|||
font-size: 14px; |
|||
background-color: rgba(0, 0, 0, 0.4); |
|||
} |
|||
|
|||
.uni-file-picker__lists-box { |
|||
position: relative; |
|||
} |
|||
|
|||
.uni-file-picker__item { |
|||
/* #ifndef APP-NVUE */ |
|||
display: flex; |
|||
/* #endif */ |
|||
align-items: center; |
|||
padding: 8px 10px; |
|||
padding-right: 5px; |
|||
padding-left: 10px; |
|||
} |
|||
|
|||
.files-border { |
|||
border-top: 1px #eee solid; |
|||
} |
|||
|
|||
.files__name { |
|||
flex: 1; |
|||
font-size: 14px; |
|||
color: #666; |
|||
margin-right: 25px; |
|||
/* #ifndef APP-NVUE */ |
|||
word-break: break-all; |
|||
word-wrap: break-word; |
|||
/* #endif */ |
|||
} |
|||
|
|||
.icon-files { |
|||
/* #ifndef APP-NVUE */ |
|||
position: static; |
|||
background-color: initial; |
|||
/* #endif */ |
|||
} |
|||
|
|||
// .icon-files .icon-del { |
|||
// background-color: #333; |
|||
// width: 12px; |
|||
// height: 1px; |
|||
// } |
|||
|
|||
|
|||
.is-list-card { |
|||
border: 1px #eee solid; |
|||
margin-bottom: 5px; |
|||
border-radius: 5px; |
|||
box-shadow: 0 0 2px 0px rgba(0, 0, 0, 0.1); |
|||
padding: 5px; |
|||
} |
|||
|
|||
.files__image { |
|||
width: 40px; |
|||
height: 40px; |
|||
margin-right: 10px; |
|||
} |
|||
|
|||
.header-image { |
|||
width: 100%; |
|||
height: 100%; |
|||
} |
|||
|
|||
.is-text-box { |
|||
border: 1px #eee solid; |
|||
border-radius: 5px; |
|||
} |
|||
|
|||
.is-text-image { |
|||
width: 25px; |
|||
height: 25px; |
|||
margin-left: 5px; |
|||
} |
|||
|
|||
.rotate { |
|||
position: absolute; |
|||
transform: rotate(90deg); |
|||
} |
|||
|
|||
.icon-del-box { |
|||
/* #ifndef APP-NVUE */ |
|||
display: flex; |
|||
margin: auto 0; |
|||
/* #endif */ |
|||
align-items: center; |
|||
justify-content: center; |
|||
position: absolute; |
|||
top: 0px; |
|||
bottom: 0; |
|||
right: 5px; |
|||
height: 26px; |
|||
width: 26px; |
|||
// border-radius: 50%; |
|||
// background-color: rgba(0, 0, 0, 0.5); |
|||
z-index: 2; |
|||
transform: rotate(-45deg); |
|||
} |
|||
|
|||
.icon-del { |
|||
width: 15px; |
|||
height: 1px; |
|||
background-color: #333; |
|||
// border-radius: 1px; |
|||
} |
|||
|
|||
/* #ifdef H5 */ |
|||
@media all and (min-width: 768px) { |
|||
.uni-file-picker__files { |
|||
max-width: 375px; |
|||
} |
|||
} |
|||
|
|||
/* #endif */ |
|||
</style> |
|||
@ -0,0 +1,292 @@ |
|||
<template> |
|||
<view class="uni-file-picker__container"> |
|||
<view class="file-picker__box" v-for="(item,index) in filesList" :key="index" :style="boxStyle"> |
|||
<view class="file-picker__box-content" :style="borderStyle"> |
|||
<image class="file-image" :src="item.url" mode="aspectFill" @click.stop="prviewImage(item,index)"></image> |
|||
<view v-if="delIcon && !readonly" class="icon-del-box" @click.stop="delFile(index)"> |
|||
<view class="icon-del"></view> |
|||
<view class="icon-del rotate"></view> |
|||
</view> |
|||
<view v-if="(item.progress && item.progress !== 100) ||item.progress===0 " class="file-picker__progress"> |
|||
<progress class="file-picker__progress-item" :percent="item.progress === -1?0:item.progress" stroke-width="4" |
|||
:backgroundColor="item.errMsg?'#ff5a5f':'#EBEBEB'" /> |
|||
</view> |
|||
<view v-if="item.errMsg" class="file-picker__mask" @click.stop="uploadFiles(item,index)"> |
|||
点击重试 |
|||
</view> |
|||
</view> |
|||
</view> |
|||
<view v-if="filesList.length < limit && !readonly" class="file-picker__box" :style="boxStyle"> |
|||
<view class="file-picker__box-content is-add" :style="borderStyle" @click="choose"> |
|||
<slot> |
|||
<view class="icon-add"></view> |
|||
<view class="icon-add rotate"></view> |
|||
</slot> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
name: "uploadImage", |
|||
emits:['uploadFiles','choose','delFile'], |
|||
props: { |
|||
filesList: { |
|||
type: Array, |
|||
default () { |
|||
return [] |
|||
} |
|||
}, |
|||
disabled:{ |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
disablePreview: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
limit: { |
|||
type: [Number, String], |
|||
default: 9 |
|||
}, |
|||
imageStyles: { |
|||
type: Object, |
|||
default () { |
|||
return { |
|||
width: 'auto', |
|||
height: 'auto', |
|||
border: {} |
|||
} |
|||
} |
|||
}, |
|||
delIcon: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
readonly:{ |
|||
type:Boolean, |
|||
default:false |
|||
} |
|||
}, |
|||
computed: { |
|||
styles() { |
|||
let styles = { |
|||
width: 'auto', |
|||
height: 'auto', |
|||
border: {} |
|||
} |
|||
return Object.assign(styles, this.imageStyles) |
|||
}, |
|||
boxStyle() { |
|||
const { |
|||
width = 'auto', |
|||
height = 'auto' |
|||
} = this.styles |
|||
let obj = {} |
|||
if (height === 'auto') { |
|||
if (width !== 'auto') { |
|||
obj.height = this.value2px(width) |
|||
obj['padding-top'] = 0 |
|||
} else { |
|||
obj.height = 0 |
|||
} |
|||
} else { |
|||
obj.height = this.value2px(height) |
|||
obj['padding-top'] = 0 |
|||
} |
|||
|
|||
if (width === 'auto') { |
|||
if (height !== 'auto') { |
|||
obj.width = this.value2px(height) |
|||
} else { |
|||
obj.width = '33.3%' |
|||
} |
|||
} else { |
|||
obj.width = this.value2px(width) |
|||
} |
|||
|
|||
let classles = '' |
|||
for(let i in obj){ |
|||
classles+= `${i}:${obj[i]};` |
|||
} |
|||
return classles |
|||
}, |
|||
borderStyle() { |
|||
let { |
|||
border |
|||
} = this.styles |
|||
let obj = {} |
|||
const widthDefaultValue = 1 |
|||
const radiusDefaultValue = 3 |
|||
if (typeof border === 'boolean') { |
|||
obj.border = border ? '1px #eee solid' : 'none' |
|||
} else { |
|||
let width = (border && border.width) || widthDefaultValue |
|||
width = this.value2px(width) |
|||
let radius = (border && border.radius) || radiusDefaultValue |
|||
radius = this.value2px(radius) |
|||
obj = { |
|||
'border-width': width, |
|||
'border-style': (border && border.style) || 'solid', |
|||
'border-color': (border && border.color) || '#eee', |
|||
'border-radius': radius |
|||
} |
|||
} |
|||
let classles = '' |
|||
for(let i in obj){ |
|||
classles+= `${i}:${obj[i]};` |
|||
} |
|||
return classles |
|||
} |
|||
}, |
|||
methods: { |
|||
uploadFiles(item, index) { |
|||
this.$emit("uploadFiles", item) |
|||
}, |
|||
choose() { |
|||
this.$emit("choose") |
|||
}, |
|||
delFile(index) { |
|||
this.$emit('delFile', index) |
|||
}, |
|||
prviewImage(img, index) { |
|||
let urls = [] |
|||
if(Number(this.limit) === 1&&this.disablePreview&&!this.disabled){ |
|||
this.$emit("choose") |
|||
} |
|||
if(this.disablePreview) return |
|||
this.filesList.forEach(i => { |
|||
urls.push(i.url) |
|||
}) |
|||
|
|||
uni.previewImage({ |
|||
urls: urls, |
|||
current: index |
|||
}); |
|||
}, |
|||
value2px(value) { |
|||
if (typeof value === 'number') { |
|||
value += 'px' |
|||
} else { |
|||
if (value.indexOf('%') === -1) { |
|||
value = value.indexOf('px') !== -1 ? value : value + 'px' |
|||
} |
|||
} |
|||
return value |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss"> |
|||
.uni-file-picker__container { |
|||
/* #ifndef APP-NVUE */ |
|||
display: flex; |
|||
box-sizing: border-box; |
|||
/* #endif */ |
|||
flex-wrap: wrap; |
|||
margin: -5px; |
|||
} |
|||
|
|||
.file-picker__box { |
|||
position: relative; |
|||
// flex: 0 0 33.3%; |
|||
width: 33.3%; |
|||
height: 0; |
|||
padding-top: 33.33%; |
|||
/* #ifndef APP-NVUE */ |
|||
box-sizing: border-box; |
|||
/* #endif */ |
|||
} |
|||
|
|||
.file-picker__box-content { |
|||
position: absolute; |
|||
top: 0; |
|||
right: 0; |
|||
bottom: 0; |
|||
left: 0; |
|||
margin: 5px; |
|||
border: 1px #eee solid; |
|||
border-radius: 5px; |
|||
overflow: hidden; |
|||
} |
|||
|
|||
.file-picker__progress { |
|||
position: absolute; |
|||
bottom: 0; |
|||
left: 0; |
|||
right: 0; |
|||
/* border: 1px red solid; */ |
|||
z-index: 2; |
|||
} |
|||
|
|||
.file-picker__progress-item { |
|||
width: 100%; |
|||
} |
|||
|
|||
.file-picker__mask { |
|||
/* #ifndef APP-NVUE */ |
|||
display: flex; |
|||
/* #endif */ |
|||
justify-content: center; |
|||
align-items: center; |
|||
position: absolute; |
|||
right: 0; |
|||
top: 0; |
|||
bottom: 0; |
|||
left: 0; |
|||
color: #fff; |
|||
font-size: 12px; |
|||
background-color: rgba(0, 0, 0, 0.4); |
|||
} |
|||
|
|||
.file-image { |
|||
width: 100%; |
|||
height: 100%; |
|||
} |
|||
|
|||
.is-add { |
|||
/* #ifndef APP-NVUE */ |
|||
display: flex; |
|||
/* #endif */ |
|||
align-items: center; |
|||
justify-content: center; |
|||
} |
|||
|
|||
.icon-add { |
|||
width: 50px; |
|||
height: 5px; |
|||
background-color: #f1f1f1; |
|||
border-radius: 2px; |
|||
} |
|||
|
|||
.rotate { |
|||
position: absolute; |
|||
transform: rotate(90deg); |
|||
} |
|||
|
|||
.icon-del-box { |
|||
/* #ifndef APP-NVUE */ |
|||
display: flex; |
|||
/* #endif */ |
|||
align-items: center; |
|||
justify-content: center; |
|||
position: absolute; |
|||
top: 3px; |
|||
right: 3px; |
|||
height: 26px; |
|||
width: 26px; |
|||
border-radius: 50%; |
|||
background-color: rgba(0, 0, 0, 0.5); |
|||
z-index: 2; |
|||
transform: rotate(-45deg); |
|||
} |
|||
|
|||
.icon-del { |
|||
width: 15px; |
|||
height: 2px; |
|||
background-color: #fff; |
|||
border-radius: 2px; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,109 @@ |
|||
/** |
|||
* 获取文件名和后缀 |
|||
* @param {String} name |
|||
*/ |
|||
export const get_file_ext = (name) => { |
|||
const last_len = name.lastIndexOf('.') |
|||
const len = name.length |
|||
return { |
|||
name: name.substring(0, last_len), |
|||
ext: name.substring(last_len + 1, len) |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 获取扩展名 |
|||
* @param {Array} fileExtname |
|||
*/ |
|||
export const get_extname = (fileExtname) => { |
|||
if (!Array.isArray(fileExtname)) { |
|||
let extname = fileExtname.replace(/(\[|\])/g, '') |
|||
return extname.split(',') |
|||
} else { |
|||
return fileExtname |
|||
} |
|||
return [] |
|||
} |
|||
|
|||
/** |
|||
* 获取文件和检测是否可选 |
|||
*/ |
|||
export const get_files_and_is_max = (res, _extname) => { |
|||
let filePaths = [] |
|||
let files = [] |
|||
if(!_extname || _extname.length === 0){ |
|||
return { |
|||
filePaths, |
|||
files |
|||
} |
|||
} |
|||
res.tempFiles.forEach(v => { |
|||
let fileFullName = get_file_ext(v.name) |
|||
const extname = fileFullName.ext.toLowerCase() |
|||
if (_extname.indexOf(extname) !== -1) { |
|||
files.push(v) |
|||
filePaths.push(v.path) |
|||
} |
|||
}) |
|||
if (files.length !== res.tempFiles.length) { |
|||
uni.showToast({ |
|||
title: `当前选择了${res.tempFiles.length}个文件 ,${res.tempFiles.length - files.length} 个文件格式不正确`, |
|||
icon: 'none', |
|||
duration: 5000 |
|||
}) |
|||
} |
|||
|
|||
return { |
|||
filePaths, |
|||
files |
|||
} |
|||
} |
|||
|
|||
|
|||
/** |
|||
* 获取图片信息 |
|||
* @param {Object} filepath |
|||
*/ |
|||
export const get_file_info = (filepath) => { |
|||
return new Promise((resolve, reject) => { |
|||
uni.getImageInfo({ |
|||
src: filepath, |
|||
success(res) { |
|||
resolve(res) |
|||
}, |
|||
fail(err) { |
|||
reject(err) |
|||
} |
|||
}) |
|||
}) |
|||
} |
|||
/** |
|||
* 获取封装数据 |
|||
*/ |
|||
export const get_file_data = async (files, type = 'image') => { |
|||
// 最终需要上传数据库的数据
|
|||
let fileFullName = get_file_ext(files.name) |
|||
const extname = fileFullName.ext.toLowerCase() |
|||
let filedata = { |
|||
name: files.name, |
|||
uuid: files.uuid, |
|||
extname: extname || '', |
|||
cloudPath: files.cloudPath, |
|||
fileType: files.fileType, |
|||
url: files.path || files.path, |
|||
size: files.size, //单位是字节
|
|||
image: {}, |
|||
path: files.path, |
|||
video: {} |
|||
} |
|||
if (type === 'image') { |
|||
const imageinfo = await get_file_info(files.path) |
|||
delete filedata.video |
|||
filedata.image.width = imageinfo.width |
|||
filedata.image.height = imageinfo.height |
|||
filedata.image.location = imageinfo.path |
|||
} else { |
|||
delete filedata.image |
|||
} |
|||
return filedata |
|||
} |
|||
@ -0,0 +1,86 @@ |
|||
{ |
|||
"id": "uni-file-picker", |
|||
"displayName": "uni-file-picker 文件选择上传", |
|||
"version": "1.0.1", |
|||
"description": "文件选择上传组件,可以选择图片、视频等任意文件并上传到当前绑定的服务空间", |
|||
"keywords": [ |
|||
"uni-ui", |
|||
"uniui", |
|||
"图片上传", |
|||
"文件上传" |
|||
], |
|||
"repository": "https://github.com/dcloudio/uni-ui", |
|||
"engines": { |
|||
"HBuilderX": "" |
|||
}, |
|||
"directories": { |
|||
"example": "../../temps/example_temps" |
|||
}, |
|||
"dcloudext": { |
|||
"category": [ |
|||
"前端组件", |
|||
"通用组件" |
|||
], |
|||
"sale": { |
|||
"regular": { |
|||
"price": "0.00" |
|||
}, |
|||
"sourcecode": { |
|||
"price": "0.00" |
|||
} |
|||
}, |
|||
"contact": { |
|||
"qq": "" |
|||
}, |
|||
"declaration": { |
|||
"ads": "无", |
|||
"data": "无", |
|||
"permissions": "无" |
|||
}, |
|||
"npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" |
|||
}, |
|||
"uni_modules": { |
|||
"dependencies": ["uni-scss"], |
|||
"encrypt": [], |
|||
"platforms": { |
|||
"cloud": { |
|||
"tcb": "y", |
|||
"aliyun": "y" |
|||
}, |
|||
"client": { |
|||
"App": { |
|||
"app-vue": "y", |
|||
"app-nvue": "n" |
|||
}, |
|||
"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", |
|||
"阿里": "y", |
|||
"百度": "y", |
|||
"字节跳动": "y", |
|||
"QQ": "y" |
|||
}, |
|||
"快应用": { |
|||
"华为": "u", |
|||
"联盟": "u" |
|||
}, |
|||
"Vue": { |
|||
"vue2": "y", |
|||
"vue3": "y" |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
|
|||
## FilePicker 文件选择上传 |
|||
|
|||
> **组件名:uni-file-picker** |
|||
> 代码块: `uFilePicker` |
|||
|
|||
|
|||
文件选择上传组件,可以选择图片、视频等任意文件并上传到当前绑定的服务空间 |
|||
|
|||
### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-file-picker) |
|||
#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 |
|||
Write
Preview
Loading…
Cancel
Save