init
This commit is contained in:
218
uni_modules/nutui-uni/components/uploader/index.scss
Normal file
218
uni_modules/nutui-uni/components/uploader/index.scss
Normal file
@@ -0,0 +1,218 @@
|
||||
@import '../progress/index';
|
||||
@import '../button/index';
|
||||
|
||||
.nut-theme-dark {
|
||||
.nut-uploader {
|
||||
&__upload {
|
||||
background: $dark-background;
|
||||
}
|
||||
}
|
||||
|
||||
.nut-uploader__preview-list {
|
||||
color: $dark-color;
|
||||
background: $dark-background2;
|
||||
}
|
||||
|
||||
.close {
|
||||
color: $dark-color !important;
|
||||
}
|
||||
}
|
||||
|
||||
.nut-uploader {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
&__slot {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&__upload {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: $uploader-picture-width;
|
||||
height: $uploader-picture-height;
|
||||
background: $uploader-background;
|
||||
|
||||
|
||||
}
|
||||
|
||||
&__input {
|
||||
position: absolute !important;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
overflow: hidden;
|
||||
cursor: pointer !important;
|
||||
opacity: 0;
|
||||
|
||||
&:disabled,
|
||||
&.disabled {
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
}
|
||||
|
||||
&__preview {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 10px;
|
||||
margin-bottom: 10px;
|
||||
box-shadow: 0 2px 10px 0 rgb(0 0 0 / 10%);
|
||||
|
||||
&__progress {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
/* #ifdef H5 */
|
||||
|
||||
z-index: 99;
|
||||
|
||||
/* #endif */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgb(0 0 0 / 60%);
|
||||
|
||||
|
||||
|
||||
|
||||
&__msg {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
&.list {
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
margin-right: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&-list {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
|
||||
.nut-uploader__preview-img__file__name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
padding: 2px 4px;
|
||||
|
||||
.file-name__tips {
|
||||
padding: 0 20px;
|
||||
margin-left: 4px;
|
||||
|
||||
@include oneline-ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.nut-uploader__preview-img__file__del {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 6px;
|
||||
}
|
||||
|
||||
.nut-uploader__preview-img__file__link {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 6px;
|
||||
}
|
||||
|
||||
.nut-progress {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
|
||||
.nut-progress-outer {
|
||||
height: 2px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-img {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: $uploader-picture-width;
|
||||
height: $uploader-picture-height;
|
||||
border-radius: 6px;
|
||||
|
||||
.close {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 1;
|
||||
color: rgb(0 0 0 / 60%);
|
||||
transform: translate(50%, -50%);
|
||||
}
|
||||
|
||||
.tips {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 0;
|
||||
font-size: 12px;
|
||||
color: $white;
|
||||
text-align: center;
|
||||
background: rgb(0 0 0 / 54%);
|
||||
transition: height 0.3s;
|
||||
|
||||
@include oneline-ellipsis;
|
||||
}
|
||||
|
||||
&__c {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
&__file {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transition: all 0.3s;
|
||||
|
||||
&__name {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 10px;
|
||||
overflow: hidden;
|
||||
font-size: 12px;
|
||||
color: $text-color;
|
||||
|
||||
&.error {
|
||||
color: red !important;
|
||||
}
|
||||
|
||||
&.success {
|
||||
color: #1890ff !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
3
uni_modules/nutui-uni/components/uploader/index.ts
Normal file
3
uni_modules/nutui-uni/components/uploader/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './type'
|
||||
export * from './uploader'
|
||||
export * from './use-uploader'
|
||||
25
uni_modules/nutui-uni/components/uploader/type.ts
Normal file
25
uni_modules/nutui-uni/components/uploader/type.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
export type SizeType = 'original' | 'compressed'
|
||||
export type SourceType = 'album' | 'camera'
|
||||
export type AcceptType = 'image' | 'media' | 'video' | 'all'
|
||||
export type MediaType = 'image' | 'video' | 'mix'
|
||||
export type FileType = 'image' | 'video' | 'audio'
|
||||
export type FileItemStatus = 'ready' | 'uploading' | 'success' | 'error'
|
||||
export interface FileItem {
|
||||
status: FileItemStatus
|
||||
|
||||
message: string
|
||||
|
||||
uid?: string
|
||||
|
||||
name: string
|
||||
|
||||
url?: string
|
||||
|
||||
type?: FileType
|
||||
|
||||
path?: string
|
||||
|
||||
percentage?: string | number
|
||||
|
||||
formData?: any
|
||||
}
|
||||
169
uni_modules/nutui-uni/components/uploader/uploader.ts
Normal file
169
uni_modules/nutui-uni/components/uploader/uploader.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
import type { ExtractPropTypes, PropType } from 'vue'
|
||||
import type { Interceptor } from '../_utils'
|
||||
import {
|
||||
commonProps,
|
||||
makeArrayProp,
|
||||
makeNumericProp,
|
||||
makeStringProp,
|
||||
nullableBooleanProp,
|
||||
truthProp,
|
||||
} from '../_utils'
|
||||
import type { AcceptType, FileItem, MediaType, SizeType, SourceType } from './type'
|
||||
import type { UploadOptions } from './use-uploader'
|
||||
|
||||
export const uploaderProps = {
|
||||
...commonProps,
|
||||
/**
|
||||
* @description 发到后台的文件参数名
|
||||
* - 类型为 `string`
|
||||
* - 默认值为 `'file'`
|
||||
*/
|
||||
name: makeStringProp('file'),
|
||||
/**
|
||||
* @description 上传服务器的接口地址
|
||||
*/
|
||||
url: String,
|
||||
/**
|
||||
* @description 选择文件的来源
|
||||
* - 类型为 `Array<SourceType>`
|
||||
* - 默认值为 `['album', 'camera']`
|
||||
*/
|
||||
sourceType: makeArrayProp<SourceType>(['album', 'camera']),
|
||||
/**
|
||||
* @description 选择文件类型
|
||||
* - 类型为 `Array<MediaType>`
|
||||
* - 默认值为 `['image', 'video', 'mix']`
|
||||
*/
|
||||
mediaType: makeArrayProp<MediaType>(['image', 'video', 'mix']),
|
||||
/**
|
||||
* @description 超时时间,单位为毫秒
|
||||
* - 类型为 `number | string`
|
||||
* - 默认值为 `1000 * 30`
|
||||
*/
|
||||
timeout: makeNumericProp(1000 * 30),
|
||||
/**
|
||||
* @description 默认已经上传的文件列表
|
||||
*/
|
||||
fileList: makeArrayProp<any>([]),
|
||||
/**
|
||||
* @description 是否上传成功后展示预览图
|
||||
* - 类型为 `boolean`
|
||||
* - 默认值为 `true`
|
||||
*/
|
||||
isPreview: truthProp,
|
||||
/**
|
||||
* @description 上传列表的内建样式,支持两种基础样式 `picture`、`list`
|
||||
* - 类型为 `string`
|
||||
* - 默认值为 `'picture'`
|
||||
*/
|
||||
listType: makeStringProp<'picture' | 'list'>('picture'),
|
||||
/**
|
||||
* @description 是否展示删除按钮
|
||||
*/
|
||||
isDeletable: truthProp,
|
||||
/**
|
||||
* @description 上传请求的 http method
|
||||
* - 类型为 `string`
|
||||
* - 默认值为 `'post'`
|
||||
*/
|
||||
method: makeStringProp('post'),
|
||||
capture: Boolean,
|
||||
/**
|
||||
* @description 可以设定最大上传文件的大小(字节)
|
||||
*/
|
||||
maximize: makeNumericProp(Number.MAX_VALUE),
|
||||
/**
|
||||
* @description 最多可以选择的文件个数,微信基础库2.25.0前,最多可支持9个文件,2.25.0及以后最多可支持20个文件
|
||||
*/
|
||||
maximum: makeNumericProp(9),
|
||||
accept: makeStringProp<AcceptType>('image'),
|
||||
/**
|
||||
* @description 设置上传的请求头部
|
||||
*/
|
||||
headers: { type: Object, default: {} },
|
||||
/**
|
||||
* @description 附加上传的信息 formData
|
||||
*/
|
||||
data: { type: Object, default: {} },
|
||||
/**
|
||||
* @description 接口响应的成功状态(status)值
|
||||
*/
|
||||
xhrState: makeNumericProp(200),
|
||||
/**
|
||||
* @description 是否支持文件多选
|
||||
*/
|
||||
multiple: truthProp,
|
||||
/**
|
||||
* @description 是否禁用文件上传
|
||||
*/
|
||||
disabled: nullableBooleanProp,
|
||||
/**
|
||||
* @description 是否在选取文件后立即进行上传,false 时需要手动执行ref的`submit`方法进行上传
|
||||
*/
|
||||
autoUpload: truthProp,
|
||||
/**
|
||||
* @description 执行 `uni.uploadFile` 上传时,自定义方法
|
||||
*/
|
||||
beforeUpload: {
|
||||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
/**
|
||||
* @description 除文件时的回调,返回值为 false 时不移除。支持返回一个 Promise 对象,Promise 对象 resolve(false) 或 reject 时不移除
|
||||
*/
|
||||
beforeDelete: {
|
||||
type: Function as PropType<Interceptor>,
|
||||
default: null,
|
||||
},
|
||||
/**
|
||||
* @description 当accept为video时生效,是否压缩视频,默认为true(默认 true )
|
||||
* - compressed: { type: Boolean, default: true },
|
||||
* - 当accept为video时生效,拍摄视频最长拍摄时间,单位秒(默认 60 )
|
||||
*/
|
||||
maxDuration: makeNumericProp(60),
|
||||
/**
|
||||
* @description 所选的图片的尺寸, 可选值为original compressed
|
||||
*/
|
||||
sizeType: makeArrayProp<SizeType>(['compressed', 'original']),
|
||||
/**
|
||||
* 当accept为video时生效,可选值为back或front
|
||||
*/
|
||||
camera: makeStringProp<'back' | 'front' | undefined>('back'),
|
||||
/**
|
||||
* @description 预览图片的 mode 属性
|
||||
*/
|
||||
mode: makeStringProp<'aspectFit' | 'scaleToFill' | 'aspectFill' | 'widthFix' | 'heightFix' | 'top' | 'bottom' | 'center' | 'left' | 'right' | 'top left' | 'top right' | 'bottom left' | 'bottom right'>('aspectFit'),
|
||||
}
|
||||
|
||||
export type UploaderProps = ExtractPropTypes<typeof uploaderProps>
|
||||
|
||||
export const uploaderEmits = {
|
||||
'start': (option: UploadOptions) => option instanceof Object,
|
||||
'progress': (val: { event: any, option: UploadOptions, percentage: string | number }) => val instanceof Object,
|
||||
'update:fileList': (val: FileItem[]) => val instanceof Object,
|
||||
'oversize': (val: any[]) => val instanceof Object,
|
||||
'success': (val: any) => val instanceof Object,
|
||||
'failure': (err: any) => err instanceof Object,
|
||||
'change': (val: { fileList: FileItem[], event?: any }) => val instanceof Object,
|
||||
'delete': (val: { file: FileItem, fileList: FileItem[], index: number }) => val instanceof Object,
|
||||
'fileItemClick': (val: any) => val instanceof Object,
|
||||
}
|
||||
|
||||
export type UploaderEmits = typeof uploaderEmits
|
||||
|
||||
export interface UploaderInst {
|
||||
/**
|
||||
* @description 手动上传模式,执行上传操作
|
||||
*/
|
||||
submit: () => void
|
||||
/**
|
||||
* @description 调用选择文件的方法,效果等同于点击 nut-uploader 组件
|
||||
*/
|
||||
chooseImage: () => void
|
||||
/**
|
||||
* @description 清空已选择的文件队列(该方法一般配合在手动模式上传时使用)
|
||||
* @param index
|
||||
* @returns
|
||||
*/
|
||||
clearUploadQueue: (index: number) => void
|
||||
}
|
||||
313
uni_modules/nutui-uni/components/uploader/uploader.vue
Normal file
313
uni_modules/nutui-uni/components/uploader/uploader.vue
Normal file
@@ -0,0 +1,313 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, defineComponent, reactive, ref, toRef, watch } from 'vue'
|
||||
import { PREFIX } from '../_constants'
|
||||
import { getMainClass } from '../_utils'
|
||||
import { useTranslate } from '../../locale'
|
||||
import NutButton from '../button/button.vue'
|
||||
import { useFormDisabled } from '../form/form'
|
||||
import NutIcon from '../icon/icon.vue'
|
||||
import NutProgress from '../progress/progress.vue'
|
||||
import type { FileItem } from './type'
|
||||
import { uploaderEmits, uploaderProps } from './uploader'
|
||||
import type { ChooseFile, OnProgressUpdateResult, UploadFileSuccessCallbackResult, UploadOptions } from './use-uploader'
|
||||
import { chooseFile, createUploader } from './use-uploader'
|
||||
|
||||
const props = defineProps(uploaderProps)
|
||||
const emit = defineEmits(uploaderEmits)
|
||||
defineExpose({ submit, chooseImage, clearUploadQueue })
|
||||
const fileList = ref(props.fileList as Array<FileItem>)
|
||||
const uploadQueue = ref<Promise<any>[]>([])
|
||||
const disabled = useFormDisabled(toRef(props, 'disabled'))
|
||||
|
||||
watch(
|
||||
() => props.fileList,
|
||||
() => {
|
||||
fileList.value = props.fileList
|
||||
},
|
||||
)
|
||||
|
||||
function fileItemClick(fileItem: FileItem) {
|
||||
emit('fileItemClick', { fileItem })
|
||||
}
|
||||
|
||||
function executeUpload(fileItem: FileItem, index: number) {
|
||||
const { type, url, formData } = fileItem
|
||||
|
||||
const uploadOption: UploadOptions = {
|
||||
url: props.url ? props.url : '',
|
||||
filePath: url,
|
||||
name: props.name,
|
||||
fileType: type,
|
||||
header: props.headers,
|
||||
timeout: +props?.timeout,
|
||||
xhrState: +props.xhrState,
|
||||
formData,
|
||||
file: fileItem as any,
|
||||
}
|
||||
|
||||
uploadOption.onStart = (option: UploadOptions) => {
|
||||
fileItem.status = 'ready'
|
||||
fileItem.message = translate('readyUpload')
|
||||
clearUploadQueue(index)
|
||||
emit('start', option)
|
||||
}
|
||||
uploadOption.onProgress = (event: OnProgressUpdateResult, option: UploadOptions) => {
|
||||
fileItem.status = 'uploading'
|
||||
fileItem.message = translate('uploading')
|
||||
|
||||
fileItem.percentage = event?.progress
|
||||
emit('progress', { event, option, percentage: fileItem.percentage })
|
||||
}
|
||||
|
||||
uploadOption.onSuccess = (data: UploadFileSuccessCallbackResult, option: UploadOptions) => {
|
||||
fileItem.status = 'success'
|
||||
fileItem.message = translate('success')
|
||||
emit('success', {
|
||||
data,
|
||||
responseText: data,
|
||||
option,
|
||||
fileItem,
|
||||
})
|
||||
emit('update:fileList', fileList.value)
|
||||
}
|
||||
uploadOption.onFailure = (data, option: UploadOptions) => {
|
||||
fileItem.status = 'error'
|
||||
fileItem.message = translate('error')
|
||||
emit('failure', {
|
||||
data,
|
||||
responseText: data,
|
||||
option,
|
||||
fileItem,
|
||||
})
|
||||
}
|
||||
|
||||
const task = createUploader(uploadOption)
|
||||
if (props.beforeUpload) {
|
||||
props.beforeUpload(uni.uploadFile, uploadOption)
|
||||
}
|
||||
|
||||
else if (props.autoUpload) {
|
||||
task.upload()
|
||||
}
|
||||
else {
|
||||
uploadQueue.value.push(
|
||||
new Promise((resolve, reject) => {
|
||||
resolve(task)
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function clearUploadQueue(index = -1) {
|
||||
if (index > -1) {
|
||||
uploadQueue.value.splice(index, 1)
|
||||
}
|
||||
else {
|
||||
uploadQueue.value = []
|
||||
fileList.value.splice(0, fileList.value.length)
|
||||
}
|
||||
}
|
||||
function submit() {
|
||||
Promise.all(uploadQueue.value).then((res) => {
|
||||
res.forEach(i => i.upload())
|
||||
})
|
||||
}
|
||||
|
||||
function readFile(files: ChooseFile[]) {
|
||||
files.forEach((file, index: number) => {
|
||||
let fileType = file.type
|
||||
const filepath = (file.tempFilePath || file.path || file.url) as string
|
||||
const fileItem = reactive({} as FileItem)
|
||||
|
||||
if (file.fileType) {
|
||||
fileType = file.fileType
|
||||
}
|
||||
else {
|
||||
const imgReg = /\.(png|jpeg|jpg|webp|gif)$/i
|
||||
if (!fileType && (imgReg.test(filepath) || filepath.includes('data:image')))
|
||||
fileType = 'image'
|
||||
}
|
||||
fileItem.uid = new Date().getTime().toString() + Math.random().toString(36).substring(2, 9)
|
||||
fileItem.path = filepath
|
||||
fileItem.name = file.name || filepath
|
||||
fileItem.status = 'ready'
|
||||
fileItem.message = translate('waitingUpload')
|
||||
fileItem.type = fileType!
|
||||
fileItem.formData = props.data
|
||||
if (props.isPreview)
|
||||
fileItem.url = fileType === 'video' ? file.url : filepath
|
||||
|
||||
fileList.value.push(fileItem)
|
||||
executeUpload(fileItem, index)
|
||||
})
|
||||
}
|
||||
|
||||
function filterFiles(files: ChooseFile[]) {
|
||||
const maximum = (props.maximum as number) * 1
|
||||
const maximize = (props.maximize as number) * 1
|
||||
const oversizes = new Array<ChooseFile>()
|
||||
files = files.filter((file: ChooseFile) => {
|
||||
if (file.size > maximize) {
|
||||
oversizes.push(file)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
if (oversizes.length)
|
||||
emit('oversize', oversizes)
|
||||
|
||||
const currentFileLength = files.length + fileList.value.length
|
||||
if (currentFileLength > maximum)
|
||||
files.splice(files.length - (currentFileLength - maximum))
|
||||
|
||||
return files
|
||||
}
|
||||
|
||||
async function onDelete(file: FileItem, index: number) {
|
||||
clearUploadQueue(index)
|
||||
if (props.beforeDelete == null || await props.beforeDelete(file, fileList)) {
|
||||
fileList.value.splice(index, 1)
|
||||
emit('delete', {
|
||||
file,
|
||||
fileList: fileList.value,
|
||||
index,
|
||||
})
|
||||
}
|
||||
else {
|
||||
// console.log('用户阻止了删除!');
|
||||
}
|
||||
}
|
||||
|
||||
function chooseImage(event: InputEvent) {
|
||||
if (disabled.value)
|
||||
return
|
||||
|
||||
const maximum = (props.maximum as number) * 1
|
||||
|
||||
chooseFile({
|
||||
accept: props.accept,
|
||||
multiple: props.multiple,
|
||||
capture: props.capture,
|
||||
maxDuration: +props.maxDuration,
|
||||
sizeType: props.sizeType,
|
||||
camera: props.camera,
|
||||
maxCount: maximum - fileList.value.length,
|
||||
}, props, fileList.value).then((files) => {
|
||||
const filteredFiles: ChooseFile[] = filterFiles(
|
||||
new Array<ChooseFile>().slice.call(files),
|
||||
)
|
||||
|
||||
readFile(filteredFiles)
|
||||
|
||||
emit('change', { fileList: fileList.value, event })
|
||||
})
|
||||
}
|
||||
|
||||
const classes = computed(() => {
|
||||
return getMainClass(props, componentName)
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
const componentName = `${PREFIX}-uploader`
|
||||
const { translate } = useTranslate(componentName)
|
||||
export default defineComponent({
|
||||
name: componentName,
|
||||
options: {
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
styleIsolation: 'shared',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view :class="classes" :style="customStyle">
|
||||
<view v-if="$slots.default" class="nut-uploader__slot">
|
||||
<slot />
|
||||
<template v-if="Number(maximum) - fileList.length">
|
||||
<NutButton custom-class="nut-uploader__input" @click="(chooseImage as any)" />
|
||||
</template>
|
||||
</view>
|
||||
|
||||
<view
|
||||
v-for="(item, index) in fileList"
|
||||
:key="item.uid"
|
||||
class="nut-uploader__preview"
|
||||
:class="[listType]"
|
||||
>
|
||||
<view v-if="listType === 'picture' && !$slots.default" class="nut-uploader__preview-img">
|
||||
<view v-if="item.status !== 'success'" class="nut-uploader__preview__progress">
|
||||
<template v-if="item.status !== 'ready'">
|
||||
<NutIcon v-if="item.status === 'error'" name="failure" custom-color="#fff" />
|
||||
<NutIcon v-else name="loading" custom-color="#fff" />
|
||||
</template>
|
||||
<view class="nut-uploader__preview__progress__msg">
|
||||
{{ item.message }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="isDeletable && !disabled" class="close" @click="onDelete(item, index)">
|
||||
<slot name="deleteIcon">
|
||||
<NutIcon name="failure" />
|
||||
</slot>
|
||||
</view>
|
||||
|
||||
<image
|
||||
v-if="(item?.type?.includes('image') || item?.type?.includes('video')) && item.url"
|
||||
class="nut-uploader__preview-img__c"
|
||||
:mode="mode"
|
||||
:src="item.url"
|
||||
@click="fileItemClick(item)"
|
||||
/>
|
||||
<view v-else class="nut-uploader__preview-img__file">
|
||||
<view class="nut-uploader__preview-img__file__name" @click="fileItemClick(item)">
|
||||
<view class="file-name__tips">
|
||||
{{ item.name }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="tips">
|
||||
{{ item.name }}
|
||||
</view>
|
||||
</view>
|
||||
<view v-else-if="listType === 'list'" class="nut-uploader__preview-list">
|
||||
<view class="nut-uploader__preview-img__file__name" :class="[item.status]" @click="fileItemClick(item)">
|
||||
<NutIcon name="link" custom-class="nut-uploader__preview-img__file__link" />
|
||||
<view class="file-name__tips">
|
||||
{{ item.name }}
|
||||
</view>
|
||||
<NutIcon
|
||||
v-if="isDeletable && !disabled"
|
||||
name="del"
|
||||
custom-color="#808080"
|
||||
custom-class="nut-uploader__preview-img__file__del"
|
||||
@click="onDelete(item, index)"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<NutProgress
|
||||
v-if="item.status === 'uploading'"
|
||||
size="small"
|
||||
:percentage="item.percentage"
|
||||
stroke-color="linear-gradient(270deg, rgba(18,126,255,1) 0%,rgba(32,147,255,1) 32.815625%,rgba(13,242,204,1) 100%)"
|
||||
:show-text="false"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
v-if="listType === 'picture' && !$slots.default && Number(maximum) - fileList.length"
|
||||
class="nut-uploader__upload"
|
||||
:class="[listType]"
|
||||
>
|
||||
<slot name="uploadIcon">
|
||||
<NutIcon name="photograph" custom-color="#808080" />
|
||||
</slot>
|
||||
<NutButton custom-class="nut-uploader__input" :class="{ disabled }" @click="(chooseImage as any)" />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import './index';
|
||||
</style>
|
||||
240
uni_modules/nutui-uni/components/uploader/use-uploader.ts
Normal file
240
uni_modules/nutui-uni/components/uploader/use-uploader.ts
Normal file
@@ -0,0 +1,240 @@
|
||||
import { omit } from '../_utils'
|
||||
import type { AcceptType, FileType, SizeType } from './type'
|
||||
import type { UploaderProps } from './uploader'
|
||||
|
||||
interface UniChooseFileSuccessCallbackResult {
|
||||
/**
|
||||
* 本地文件路径列表
|
||||
*/
|
||||
tempFilePaths?: string[]
|
||||
/**
|
||||
* 本地文件列表,每一项是一个 File 对象
|
||||
*/
|
||||
tempFiles: ({
|
||||
path: string
|
||||
size: number
|
||||
name: string
|
||||
type: string
|
||||
} & File)[]
|
||||
}
|
||||
|
||||
interface UniChooseImageSuccessCallbackResult extends UniChooseFileSuccessCallbackResult {
|
||||
|
||||
}
|
||||
|
||||
interface UniChooseVideoSuccessCallbackResult {
|
||||
/**
|
||||
* 本地文件路径
|
||||
*/
|
||||
tempFilePath?: string
|
||||
/**
|
||||
* 本地文件,一个 File 对象
|
||||
*/
|
||||
tempFile: ({
|
||||
path: string
|
||||
size: number
|
||||
name: string
|
||||
type: string
|
||||
} & File)
|
||||
/**
|
||||
* 选定视频的时间长度,单位为s
|
||||
*/
|
||||
duration: number
|
||||
/**
|
||||
* 选定视频的数据量大小
|
||||
*/
|
||||
size: number
|
||||
/**
|
||||
* 返回选定视频的高
|
||||
*/
|
||||
height: number
|
||||
/**
|
||||
* 返回选定视频的宽
|
||||
*/
|
||||
width: number
|
||||
/**
|
||||
* 包含扩展名的文件名称
|
||||
*/
|
||||
name: string
|
||||
}
|
||||
|
||||
export interface ChooseFile {
|
||||
size: number
|
||||
type?: FileType
|
||||
fileType?: FileType
|
||||
originalFileObj?: any
|
||||
tempFilePath?: string
|
||||
thumbTempFilePath?: string
|
||||
path?: string
|
||||
name: string
|
||||
thumb?: string
|
||||
url?: string
|
||||
[k: string]: unknown
|
||||
}
|
||||
|
||||
function omitProps<T>(obj: T, keys: string[]) {
|
||||
if (!['[object Object]', '[object File]'].includes(Object.prototype.toString.call(obj)))
|
||||
return {}
|
||||
|
||||
return omit(obj as unknown as Record<string, unknown>, keys)
|
||||
}
|
||||
|
||||
function formatImage(res: UniChooseImageSuccessCallbackResult): ChooseFile[] {
|
||||
return res.tempFiles.map(item => ({
|
||||
...omitProps(item, ['path']),
|
||||
type: 'image',
|
||||
url: item.path,
|
||||
thumb: item.path,
|
||||
size: item.size,
|
||||
name: item.name || 'image',
|
||||
}))
|
||||
}
|
||||
|
||||
function formatVideo(res: UniChooseVideoSuccessCallbackResult): ChooseFile[] {
|
||||
return [{
|
||||
...omitProps(res.tempFile, ['path']),
|
||||
type: 'video',
|
||||
url: res.tempFilePath,
|
||||
thumb: res.tempFilePath,
|
||||
size: res.tempFile.size,
|
||||
name: res.tempFile.name || 'video',
|
||||
}]
|
||||
}
|
||||
|
||||
function formatMedia(res: UniApp.ChooseMediaSuccessCallbackResult & { name?: string }): ChooseFile[] {
|
||||
return res.tempFiles.map(item => ({
|
||||
...omitProps(item, ['fileType', 'thumbTempFilePath', 'tempFilePath']),
|
||||
type: res.type as FileType,
|
||||
url: item.tempFilePath,
|
||||
thumb: res.type === 'video' ? item.thumbTempFilePath : item.tempFilePath,
|
||||
size: item.size,
|
||||
name: res?.name || 'media',
|
||||
}))
|
||||
}
|
||||
|
||||
export interface ChooseFileOptions {
|
||||
accept: AcceptType
|
||||
multiple: boolean
|
||||
capture: boolean
|
||||
maxDuration: number
|
||||
sizeType: SizeType[]
|
||||
camera?: 'back' | 'front'
|
||||
maxCount: number
|
||||
}
|
||||
|
||||
export function chooseFile({
|
||||
accept,
|
||||
multiple,
|
||||
maxDuration,
|
||||
sizeType,
|
||||
camera,
|
||||
}: ChooseFileOptions, props: UploaderProps, fileList: any[]): Promise<ChooseFile[] | ChooseFile> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// chooseMedia 目前只支持微信小程序原生,其余端全部使用 chooseImage API
|
||||
// #ifdef MP-WEIXIN
|
||||
|
||||
uni.chooseMedia({
|
||||
/** 最多可以选择的文件个数 */
|
||||
count: multiple ? Number(props.maximum) * 1 - fileList.length : 1,
|
||||
/** 文件类型 */
|
||||
mediaType: props.mediaType,
|
||||
/** 图片和视频选择的来源 */
|
||||
sourceType: props.sourceType,
|
||||
/** 拍摄视频最长拍摄时间,单位秒。时间范围为 3s 至 30s 之间 */
|
||||
maxDuration,
|
||||
/** 仅对 mediaType 为 image 时有效,是否压缩所选文件 */
|
||||
sizeType,
|
||||
/** 仅在 sourceType 为 camera 时生效,使用前置或后置摄像头 */
|
||||
camera,
|
||||
/** 接口调用失败的回调函数 */
|
||||
fail: reject,
|
||||
/** 接口调用成功的回调函数 */
|
||||
success: res => resolve(formatMedia(res)),
|
||||
})
|
||||
// #endif
|
||||
|
||||
// #ifndef MP-WEIXIN
|
||||
if (accept === 'image') {
|
||||
uni.chooseImage({
|
||||
// 选择数量
|
||||
count: props.multiple ? (props.maximum as number) * 1 - props.fileList.length : 1,
|
||||
// 可以指定是原图还是压缩图,默认二者都有
|
||||
sizeType,
|
||||
sourceType: props.sourceType,
|
||||
success: (res) => {
|
||||
resolve(formatImage(res as UniChooseFileSuccessCallbackResult))
|
||||
},
|
||||
fail: reject,
|
||||
})
|
||||
}
|
||||
else if (accept === 'video') {
|
||||
uni.chooseVideo({
|
||||
sourceType: props.sourceType,
|
||||
success: (res) => {
|
||||
resolve(formatVideo(res as UniChooseVideoSuccessCallbackResult))
|
||||
},
|
||||
fail: reject,
|
||||
})
|
||||
}
|
||||
else if (accept === 'all') {
|
||||
uni.chooseFile({
|
||||
type: 'all',
|
||||
// 选择数量
|
||||
count: props.multiple ? (props.maximum as number) * 1 - props.fileList.length : 1,
|
||||
// 可以指定是原图还是压缩图,默认二者都有
|
||||
sizeType,
|
||||
sourceType: props.sourceType,
|
||||
success: (res) => {
|
||||
resolve(formatImage(res as UniChooseFileSuccessCallbackResult))
|
||||
},
|
||||
fail: reject,
|
||||
})
|
||||
}
|
||||
|
||||
// #endif
|
||||
})
|
||||
}
|
||||
|
||||
export type OnProgressUpdateResult = UniApp.OnProgressUpdateResult
|
||||
export type UploadFileSuccessCallbackResult = UniApp.UploadFileSuccessCallbackResult
|
||||
export type GeneralCallbackResult = UniApp.GeneralCallbackResult
|
||||
|
||||
export type UploadOptions = UniNamespace.UploadFileOption & {
|
||||
xhrState?: number
|
||||
onStart?: (option: UploadOptions) => void
|
||||
onProgress?: (result: UniApp.OnProgressUpdateResult, option: UploadOptions) => void
|
||||
onSuccess?: (result: UniApp.UploadFileSuccessCallbackResult, option: UploadOptions) => void
|
||||
onFailure?: (result: UniApp.GeneralCallbackResult | UniApp.UploadFileSuccessCallbackResult, option: UploadOptions) => void
|
||||
}
|
||||
|
||||
export function createUploader(options: UploadOptions) {
|
||||
const upload = () => {
|
||||
const uploadTask = uni.uploadFile({
|
||||
url: options.url,
|
||||
fileType: options.fileType,
|
||||
file: options.file,
|
||||
filePath: options.filePath,
|
||||
name: options.name,
|
||||
header: options.header,
|
||||
timeout: options.timeout,
|
||||
formData: options.formData,
|
||||
success: (result) => {
|
||||
if (options.xhrState === result.statusCode)
|
||||
options.onSuccess?.(result, options)
|
||||
else
|
||||
options.onFailure?.(result, options)
|
||||
},
|
||||
fail: (result) => {
|
||||
options.onFailure?.(result, options)
|
||||
},
|
||||
})
|
||||
|
||||
options.onStart?.(options)
|
||||
|
||||
uploadTask.onProgressUpdate((event) => {
|
||||
options.onProgress?.(event, options)
|
||||
})
|
||||
}
|
||||
|
||||
return { upload }
|
||||
}
|
||||
Reference in New Issue
Block a user