mirror of
https://github.com/tusen-ai/naive-ui.git
synced 2025-01-24 12:45:18 +08:00
feat(upload): add list type (#785)
* feat(upload): add `list-type`, `show-preview-button`, `on-preview` and `preview-file` props, thumbail style and some tests (#393) * docs(upload): add thumbail api (#393) * docs(upload): add CHANGELOG (#393) * docs(upload): add fix CHANGELOG (#393) * fix(upload): fix husky file * Update .husky/pre-commit * Update src/upload/demos/enUS/picture-style.demo.md Co-authored-by: 07akioni <07akioni2@gmail.com> * Update src/upload/demos/zhCN/index.demo-entry.md Co-authored-by: 07akioni <07akioni2@gmail.com> * Update src/upload/src/Upload.tsx Co-authored-by: 07akioni <07akioni2@gmail.com> * Update src/upload/src/styles/index.cssr.ts Co-authored-by: 07akioni <07akioni2@gmail.com> * feat(upload): update variable name and fix style (#393) * docs(upload): add preview-file prop api and fix CHANGELOG (#393) * feat(upload): fix component attribute (#393) * feat(upload): change create thumnail url and fix bug * feat(upload): change code location (#393) * docs(upload): change demo and CHANGELOG (#393) * feat(upload): fix husky(#393) * Update src/upload/src/UploadFile.tsx Co-authored-by: 07akioni <07akioni2@gmail.com> * Update src/upload/demos/zhCN/picture-card-style.demo.md Co-authored-by: 07akioni <07akioni2@gmail.com> * Update src/upload/demos/zhCN/picture-style.demo.md Co-authored-by: 07akioni <07akioni2@gmail.com> * feat(upload): fix preview animation error, preview ref and picture style (#393) * feat(upload): add image group to the file list (#393) Co-authored-by: kev1nzh <kev1nzh@app-ark.com> Co-authored-by: 07akioni <07akioni2@gmail.com>
This commit is contained in:
parent
c791d5a96c
commit
da1646570d
@ -106,6 +106,7 @@
|
||||
- `n-button` add `text-color` prop.
|
||||
- `n-form` export `FormValidationError` type.
|
||||
- `n-popconfirm` support not show action components, closes [#770](https://github.com/TuSimple/naive-ui/issues/770).
|
||||
- `n-upload` add `list-type`, `show-preview-button`, `on-preview` and `create-thumbnail-url` prop.
|
||||
|
||||
### Fixes
|
||||
|
||||
|
@ -106,6 +106,7 @@
|
||||
- `n-button` 新增 `text-color` 属性
|
||||
- `n-form` 导出 `FormValidationError` 类型
|
||||
- `n-popconfirm` 支持不显示操作组件,关闭 [#770](https://github.com/TuSimple/naive-ui/issues/770)
|
||||
- `n-upload` 增加 `list-type`、 `show-preview-button`、 `on-preview` 和 `create-thumbnail-url` 属性
|
||||
|
||||
### Fixes
|
||||
|
||||
|
21
src/_internal/icons/File.tsx
Normal file
21
src/_internal/icons/File.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import { h, defineComponent } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'File',
|
||||
render () {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||
<g
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M14 3v4a1 1 0 0 0 1 1h4"></path>
|
||||
<path d="M17 21H7a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7l5 5v11a2 2 0 0 1-2 2z"></path>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
})
|
23
src/_internal/icons/Photo.tsx
Normal file
23
src/_internal/icons/Photo.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import { h, defineComponent } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Photo',
|
||||
render () {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||
<g
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M15 8h.01"></path>
|
||||
<rect x="4" y="4" width="16" height="16" rx="3"></rect>
|
||||
<path d="M4 15l4-4a3 5 0 0 1 3 0l5 5"></path>
|
||||
<path d="M14 14l1-1a3 5 0 0 1 3 0l2 2"></path>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
})
|
@ -36,3 +36,5 @@ export { default as RotateClockwiseIcon } from './RotateClockwise'
|
||||
export { default as RotateCounterclockwiseIcon } from './RotateCounterclockwise'
|
||||
export { default as ZoomInIcon } from './ZoomIn'
|
||||
export { default as ZoomOutIcon } from './ZoomOut'
|
||||
export { default as FileIcon } from './File'
|
||||
export { default as PhotoIcon } from './Photo'
|
||||
|
@ -24,6 +24,9 @@ interface imgProps {
|
||||
usemap?: string
|
||||
width?: number
|
||||
}
|
||||
export interface ImageInst {
|
||||
handleClick: () => void
|
||||
}
|
||||
|
||||
const imageProps = {
|
||||
alt: String,
|
||||
|
@ -12,6 +12,8 @@ controlled
|
||||
on-finish
|
||||
default-files
|
||||
before-upload
|
||||
picture-card-style
|
||||
picture-style
|
||||
```
|
||||
|
||||
## Props
|
||||
@ -29,6 +31,7 @@ before-upload
|
||||
| file-list-style | `Object` | `undefined` | The style of file list area |
|
||||
| file-list | `Array<UploadFile>` | `undefined` | The file list of component. If set, the component will work in controlled manner. |
|
||||
| headers | `Object \| ({ file: UploadFile }) => Object` | `undefined` | The additional HTTP Headers of request. |
|
||||
| list-type | `string` | `'text'` | Built-in styles for file lists, `text`, `picture` and `picture-card`. |
|
||||
| method | `string` | `'POST'` | The method of HTTP request. |
|
||||
| multiple | `boolean` | `false` | If multiple files selection supported. |
|
||||
| name | `string` | `'file'` | The field name of file in form data. |
|
||||
@ -36,12 +39,15 @@ before-upload
|
||||
| show-remove-button | `boolean` | `true` | Whether to show remove button (at file finished status). Click on remove button will fire `on-remove` callback. |
|
||||
| show-retry-button | `boolean` | `true` | Whether to show retry button (at file error status). |
|
||||
| show-file-list | `boolean` | `true` | Whether to show file list. |
|
||||
| show-preview-button | `boolean` | `true` | Whether to show the preview button (shown when `list-type` is `picture-card`). |
|
||||
| with-credentials | `boolean` | `false` | If cookie attached. |
|
||||
| on-change | `(options: { file: UploadFile, fileList: Array<UploadFile>, event?: Event }) => void` | `() => {}` | The callback of status change of the component. Any file status change would fire the callback. |
|
||||
| on-update:file-list | `(fileList: UploadFile[]) => void` | `undefined` | Callback function triggered on fileList changes. |
|
||||
| on-finish | `(options: { file: UploadFile, event: Event }) => UploadFile \| void` | `({ file }) => file` | The callback of file upload finish. You can modify the UploadFile or retun a new UploadFile. |
|
||||
| on-remove | `(options: { file: UploadFile, fileList: Array<UploadFile> }) => boolean \| Promise<boolean> \| any` | `() => true` | The callback of file removal. Return false, promise resolve false or promise reject will cancel this removal. |
|
||||
| on-before-upload | `(options: { file: UploadFile, fileList: Array<UploadFile> }) => (Promise<boolean \| void> \| boolean \| void)` | `true` | Callback before file is uploaded, return false or a Promise that resolve false or reject will cancel this upload. |
|
||||
| on-preview | `(file: FileInfo) => void` | `undefined` | Callback functions for clicking on file links or preview buttons. |
|
||||
| create-thumbnail-url | `(file: File) => Promise<thumbnailUrl: string>` | `undefined` | Customize file thumbnails. |
|
||||
|
||||
### UploadFile Type
|
||||
|
||||
|
119
src/upload/demos/enUS/picture-card-style.demo.md
Normal file
119
src/upload/demos/enUS/picture-card-style.demo.md
Normal file
@ -0,0 +1,119 @@
|
||||
# Pictures Wall
|
||||
|
||||
`list-type = 'picture-card'`
|
||||
|
||||
The preview in the photo wall will call the internal component by default, you can also use `on-preview` to customize the method of showing uploaded files.
|
||||
|
||||
```html
|
||||
<n-upload
|
||||
action="http://www.mocky.io/v2/5e4bafc63100007100d8b70f"
|
||||
:default-file-list="fileList"
|
||||
list-type="picture-card"
|
||||
>
|
||||
<div :style="style">
|
||||
<n-icon size="30">
|
||||
<Ios-add />
|
||||
</n-icon>
|
||||
<span>Upload</span>
|
||||
</div>
|
||||
</n-upload>
|
||||
<n-divider />
|
||||
<n-upload
|
||||
action="http://www.mocky.io/v2/5e4bafc63100007100d8b70f"
|
||||
:default-file-list="previewFileList"
|
||||
list-type="picture-card"
|
||||
@preview="handlePreview"
|
||||
>
|
||||
<div :style="style">
|
||||
<n-icon size="30">
|
||||
<Ios-add />
|
||||
</n-icon>
|
||||
<span>Upload</span>
|
||||
</div>
|
||||
</n-upload>
|
||||
|
||||
<n-modal
|
||||
v-model:show="showModal"
|
||||
preset="card"
|
||||
:style="modalStyle"
|
||||
title="Coooooool Photo"
|
||||
size="huge"
|
||||
:bordered="false"
|
||||
>
|
||||
<img :src="previewImageUrl" :style="imgStyle" />
|
||||
</n-modal>
|
||||
```
|
||||
|
||||
```js
|
||||
import { IosAdd } from '@vicons/ionicons4'
|
||||
export default {
|
||||
components: {
|
||||
IosAdd
|
||||
},
|
||||
methods: {
|
||||
handlePreview (file) {
|
||||
const { url, thumbUrl } = file
|
||||
this.previewImageUrl = url || thumbUrl
|
||||
this.showModal = true
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
style: {
|
||||
display: 'flex',
|
||||
'justify-content': 'center',
|
||||
'align-items': 'center',
|
||||
height: '100%',
|
||||
'flex-direction': 'column'
|
||||
},
|
||||
imgStyle: {
|
||||
width: '100%'
|
||||
},
|
||||
modalStyle: {
|
||||
width: '600px'
|
||||
},
|
||||
showModal: false,
|
||||
previewImageUrl: '',
|
||||
fileList: [
|
||||
{
|
||||
id: 'a',
|
||||
name: 'I am a regular file with errors.png',
|
||||
status: 'error'
|
||||
},
|
||||
{
|
||||
id: 'b',
|
||||
name: 'I am a regular file.doc',
|
||||
status: 'finished',
|
||||
type: 'text/plain'
|
||||
},
|
||||
{
|
||||
id: 'c',
|
||||
name: 'I am a regular file with url.png',
|
||||
status: 'finished',
|
||||
url: 'https://gw.alipayobjects.com/zos/antfincdn/aPkFc8Sj7n/method-draw-image.svg'
|
||||
},
|
||||
{
|
||||
id: 'd',
|
||||
name: 'I am uploading a normal file.doc',
|
||||
status: 'uploading',
|
||||
percentage: 99
|
||||
}
|
||||
],
|
||||
previewFileList: [
|
||||
{
|
||||
id: 'react',
|
||||
name: 'I am react.png',
|
||||
status: 'finished',
|
||||
url: 'https://gw.alipayobjects.com/zos/antfincdn/aPkFc8Sj7n/method-draw-image.svg'
|
||||
},
|
||||
{
|
||||
id: 'vue',
|
||||
name: 'I am vue.png',
|
||||
status: 'finished',
|
||||
url: 'https://cn.vuejs.org/images/logo.svg'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
61
src/upload/demos/enUS/picture-style.demo.md
Normal file
61
src/upload/demos/enUS/picture-style.demo.md
Normal file
@ -0,0 +1,61 @@
|
||||
# Thumbnail File List
|
||||
|
||||
`list-type = 'picture'`
|
||||
|
||||
You can use `preview-file` to customize the thumbnails of the file.
|
||||
|
||||
```html
|
||||
<n-upload
|
||||
action="http://www.mocky.io/v2/5e4bafc63100007100d8b70f"
|
||||
:default-file-list="fileList"
|
||||
list-type="picture"
|
||||
:createThumbnailUrl="createThumbnailUrl"
|
||||
>
|
||||
<n-button>Upload</n-button>
|
||||
</n-upload>
|
||||
```
|
||||
|
||||
```js
|
||||
import { defineComponent, ref } from 'vue'
|
||||
import { useMessage } from 'naive-ui'
|
||||
|
||||
export default defineComponent({
|
||||
setup () {
|
||||
const message = useMessage()
|
||||
const fileListRef = ref([
|
||||
{
|
||||
id: 'a',
|
||||
name: 'I am a regular file with errors.png',
|
||||
status: 'error'
|
||||
},
|
||||
{
|
||||
id: 'b',
|
||||
name: 'I am a regular file.doc',
|
||||
status: 'finished',
|
||||
type: 'text/plain'
|
||||
},
|
||||
{
|
||||
id: 'c',
|
||||
name: 'I am a regular file with url.png',
|
||||
status: 'finished',
|
||||
url: 'https://gw.alipayobjects.com/zos/antfincdn/aPkFc8Sj7n/method-draw-image.svg'
|
||||
},
|
||||
{
|
||||
id: 'd',
|
||||
name: 'I am uploading a normal file.doc',
|
||||
status: 'uploading',
|
||||
percentage: 99
|
||||
}
|
||||
])
|
||||
return {
|
||||
fileList: fileListRef,
|
||||
createThumbnailUrl (file) {
|
||||
message.info(
|
||||
'previewFile changes the thumbnail image of the uploaded file so that it looks all Vue.'
|
||||
)
|
||||
return 'https://cn.vuejs.org/images/logo.svg'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
@ -12,6 +12,8 @@ controlled
|
||||
on-finish
|
||||
default-files
|
||||
before-upload
|
||||
picture-card-style
|
||||
picture-style
|
||||
```
|
||||
|
||||
## Props
|
||||
@ -29,6 +31,7 @@ before-upload
|
||||
| file-list-style | `Object` | `undefined` | 文件列表区域的样式 |
|
||||
| file-list | `Array<UploadFile>` | `undefined` | 文件列表,如果传入组件会处于受控状态 |
|
||||
| headers | `Object \| ({ file: UploadFile }) => Object` | `undefined` | HTTP 请求需要附加的 Headers |
|
||||
| list-type | `string` | `'text'` | 文件列表的内建样式,`text`、`picture` 和 `picture-card` |
|
||||
| method | `string` | `'POST'` | HTTP 请求的方法 |
|
||||
| multiple | `boolean` | `false` | 是否支持多个文件 |
|
||||
| name | `string` | `'file'` | 文件在提交表单中的字段名 |
|
||||
@ -36,11 +39,14 @@ before-upload
|
||||
| show-remove-button | `boolean` | `true` | 是否显示删除按钮(在 finished 的时候展示),点击删除按钮会触发 `on-remove` 回调 |
|
||||
| show-retry-button | `boolean` | `true` | 是否显示重新上传按钮(在 error 时展示) |
|
||||
| show-file-list | `boolean` | `true` | 是否显示文件列表 |
|
||||
| show-preview-button | `boolean` | `true` | 是否显示预览按钮(在 `list-type` 为 `picture-card` 时展示) |
|
||||
| with-credentials | `boolean` | `false` | 是否携带 Cookie |
|
||||
| on-change | `(options: { file: UploadFile, fileList: Array<UploadFile>, event?: Event }) => void` | `() => {}` | 组件状态变化的回调,组件的任何文件状态变化都会触发回调 |
|
||||
| on-finish | `(options: { file: UploadFile, event: Event }) => UploadFile \| void` | `({ file }) => file` | 文件上传结束的回调,可以修改传入的 UploadFile 或者返回一个新的 UploadFile |
|
||||
| on-update:file-list | `(fileList: UploadFile[]) => void` | `undefined` | 当 file-list 改变时触发的回调函数 |
|
||||
| on-before-upload | `(options: { file: UploadFile, fileList: UploadFile[] }) => (Promise<boolean \| void> \| boolean \| void)` | `undefined` | 文件上传之前的回调,返回 `false`、`Promise resolve false`、`Promise rejected` 时会取消本次上传 |
|
||||
| on-preview | `(file: FileInfo) => void` | `undefined` | 点击文件链接或预览按钮的回调函数 |
|
||||
| create-thumbnail-url | `(file: File) => Promise<thumbnailUrl: string>` | `undefined` | 自定义文件缩略图 |
|
||||
|
||||
### UploadFile Type
|
||||
|
||||
@ -71,6 +77,6 @@ before-upload
|
||||
|
||||
### Upload Dragger Slots
|
||||
|
||||
| 名称 | 参数 | 说明 |
|
||||
| ------- | ---- | ------------------------------------ |
|
||||
| 名称 | 参数 | 说明 |
|
||||
| ------- | ---- | --------------------------------------------- |
|
||||
| default | `()` | 上传拖动器的内容,使用可参考[拖拽上传](#drag) |
|
||||
|
119
src/upload/demos/zhCN/picture-card-style.demo.md
Normal file
119
src/upload/demos/zhCN/picture-card-style.demo.md
Normal file
@ -0,0 +1,119 @@
|
||||
# 照片墙
|
||||
|
||||
`list-type = 'picture-card'`
|
||||
|
||||
照片墙中的预览会默认调用内部组件,你也可以使用 `on-preview` 自定义展示上传文件的方法
|
||||
|
||||
```html
|
||||
<n-upload
|
||||
action="http://www.mocky.io/v2/5e4bafc63100007100d8b70f"
|
||||
:default-file-list="fileList"
|
||||
list-type="picture-card"
|
||||
>
|
||||
<div :style="style">
|
||||
<n-icon size="30">
|
||||
<Ios-add />
|
||||
</n-icon>
|
||||
<span>Upload</span>
|
||||
</div>
|
||||
</n-upload>
|
||||
<n-divider />
|
||||
<n-upload
|
||||
action="http://www.mocky.io/v2/5e4bafc63100007100d8b70f"
|
||||
:default-file-list="previewFileList"
|
||||
list-type="picture-card"
|
||||
@preview="handlePreview"
|
||||
>
|
||||
<div :style="style">
|
||||
<n-icon size="30">
|
||||
<Ios-add />
|
||||
</n-icon>
|
||||
<span>Upload</span>
|
||||
</div>
|
||||
</n-upload>
|
||||
|
||||
<n-modal
|
||||
v-model:show="showModal"
|
||||
preset="card"
|
||||
:style="modalStyle"
|
||||
title="这是一张超酷的图片"
|
||||
size="huge"
|
||||
:bordered="false"
|
||||
>
|
||||
<img :src="previewImageUrl" :style="imgStyle" />
|
||||
</n-modal>
|
||||
```
|
||||
|
||||
```js
|
||||
import { IosAdd } from '@vicons/ionicons4'
|
||||
export default {
|
||||
components: {
|
||||
IosAdd
|
||||
},
|
||||
methods: {
|
||||
handlePreview (file) {
|
||||
const { url, thumbUrl } = file
|
||||
this.previewImageUrl = url || thumbUrl
|
||||
this.showModal = true
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
style: {
|
||||
display: 'flex',
|
||||
'justify-content': 'center',
|
||||
'align-items': 'center',
|
||||
height: '100%',
|
||||
'flex-direction': 'column'
|
||||
},
|
||||
imgStyle: {
|
||||
width: '100%'
|
||||
},
|
||||
modalStyle: {
|
||||
width: '600px'
|
||||
},
|
||||
showModal: false,
|
||||
previewImageUrl: '',
|
||||
fileList: [
|
||||
{
|
||||
id: 'a',
|
||||
name: '我是上传出错的普通文件.png',
|
||||
status: 'error'
|
||||
},
|
||||
{
|
||||
id: 'b',
|
||||
name: '我是普通文本.doc',
|
||||
status: 'finished',
|
||||
type: 'text/plain'
|
||||
},
|
||||
{
|
||||
id: 'c',
|
||||
name: '我是自带url的图片.png',
|
||||
status: 'finished',
|
||||
url: 'https://gw.alipayobjects.com/zos/antfincdn/aPkFc8Sj7n/method-draw-image.svg'
|
||||
},
|
||||
{
|
||||
id: 'd',
|
||||
name: '我是上传进度99%的文本.doc',
|
||||
status: 'uploading',
|
||||
percentage: 99
|
||||
}
|
||||
],
|
||||
previewFileList: [
|
||||
{
|
||||
id: 'react',
|
||||
name: '我是react.png',
|
||||
status: 'finished',
|
||||
url: 'https://gw.alipayobjects.com/zos/antfincdn/aPkFc8Sj7n/method-draw-image.svg'
|
||||
},
|
||||
{
|
||||
id: 'vue',
|
||||
name: '我是vue.png',
|
||||
status: 'finished',
|
||||
url: 'https://cn.vuejs.org/images/logo.svg'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
59
src/upload/demos/zhCN/picture-style.demo.md
Normal file
59
src/upload/demos/zhCN/picture-style.demo.md
Normal file
@ -0,0 +1,59 @@
|
||||
# 缩略图文件列表
|
||||
|
||||
`list-type = 'picture'`
|
||||
|
||||
你可以使用 `preview-file`自定义文件的缩略图
|
||||
|
||||
```html
|
||||
<n-upload
|
||||
action="http://www.mocky.io/v2/5e4bafc63100007100d8b70f"
|
||||
:default-file-list="fileList"
|
||||
list-type="picture"
|
||||
:create-thumbnail-url="createThumbnailUrl"
|
||||
>
|
||||
<n-button>上传文件</n-button>
|
||||
</n-upload>
|
||||
```
|
||||
|
||||
```js
|
||||
import { defineComponent, ref } from 'vue'
|
||||
import { useMessage } from 'naive-ui'
|
||||
|
||||
export default defineComponent({
|
||||
setup () {
|
||||
const message = useMessage()
|
||||
const fileListRef = ref([
|
||||
{
|
||||
id: 'a',
|
||||
name: '我是上传出错的普通文件.png',
|
||||
status: 'error'
|
||||
},
|
||||
{
|
||||
id: 'b',
|
||||
name: '我是普通文本.doc',
|
||||
status: 'finished',
|
||||
type: 'text/plain'
|
||||
},
|
||||
{
|
||||
id: 'c',
|
||||
name: '我是自带url的图片.png',
|
||||
status: 'finished',
|
||||
url: 'https://gw.alipayobjects.com/zos/antfincdn/aPkFc8Sj7n/method-draw-image.svg'
|
||||
},
|
||||
{
|
||||
id: 'd',
|
||||
name: '我是上传进度99%的文本.doc',
|
||||
status: 'uploading',
|
||||
percentage: 99
|
||||
}
|
||||
])
|
||||
return {
|
||||
fileList: fileListRef,
|
||||
createThumbnailUrl (file) {
|
||||
message.info('previewFile改变了上传文件的缩略图,让它看起来都是Vue。')
|
||||
return 'https://cn.vuejs.org/images/logo.svg'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
@ -34,10 +34,14 @@ import {
|
||||
OnChange,
|
||||
uploadInjectionKey,
|
||||
OnUpdateFileList,
|
||||
OnBeforeUpload
|
||||
OnBeforeUpload,
|
||||
listType,
|
||||
OnPreview,
|
||||
CreateThumbnailUrl
|
||||
} from './interface'
|
||||
import { useMergedState } from 'vooks'
|
||||
import { uploadDraggerKey } from './UploadDragger'
|
||||
import { NImageGroup } from '../../image'
|
||||
|
||||
/**
|
||||
* fils status ['pending', 'uploading', 'finished', 'removed', 'error']
|
||||
@ -240,9 +244,50 @@ const uploadProps = {
|
||||
showRetryButton: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
showPreivewButton: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
listType: {
|
||||
type: String as PropType<listType>,
|
||||
default: 'text'
|
||||
},
|
||||
onPreview: Function as PropType<OnPreview>,
|
||||
createThumbnailUrl: Function as PropType<CreateThumbnailUrl>
|
||||
} as const
|
||||
|
||||
const isImageFileType = (type: string): boolean => type.includes('image/')
|
||||
|
||||
const extname = (url: string = ''): string => {
|
||||
const temp = url.split('/')
|
||||
const filename = temp[temp.length - 1]
|
||||
const filenameWithoutSuffix = filename.split(/#|\?/)[0]
|
||||
return (/\.[^./\\]*$/.exec(filenameWithoutSuffix) || [''])[0]
|
||||
}
|
||||
|
||||
const isImageUrl = (file: FileInfo): boolean => {
|
||||
if (file.type && !file.thumbnailUrl) {
|
||||
return isImageFileType(file.type)
|
||||
}
|
||||
const url: string = file.thumbnailUrl || file.url || ''
|
||||
const extension = extname(url)
|
||||
if (
|
||||
/^data:image\//.test(url) ||
|
||||
/(webp|svg|png|gif|jpg|jpeg|jfif|bmp|dpg|ico)$/i.test(extension)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
if (/^data:/.test(url)) {
|
||||
return false
|
||||
}
|
||||
if (extension) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export type UploadProps = ExtractPublicPropTypes<typeof uploadProps>
|
||||
|
||||
export default defineComponent({
|
||||
@ -316,7 +361,6 @@ export default defineComponent({
|
||||
function handleFileAddition (files: FileList | null, e?: Event): void {
|
||||
if (!files || files.length === 0) return
|
||||
const { onBeforeUpload } = props
|
||||
|
||||
const filesAsArray = props.multiple ? Array.from(files) : [files[0]]
|
||||
void Promise.all(
|
||||
filesAsArray.map(async (file) => {
|
||||
@ -326,7 +370,8 @@ export default defineComponent({
|
||||
status: 'pending',
|
||||
percentage: 0,
|
||||
file: file,
|
||||
url: null
|
||||
url: null,
|
||||
type: file.type
|
||||
}
|
||||
if (
|
||||
!onBeforeUpload ||
|
||||
@ -417,6 +462,41 @@ export default defineComponent({
|
||||
warn('upload', 'File has no corresponding id in current file list.')
|
||||
}
|
||||
}
|
||||
async function getFileThumbnail (file: FileInfo): Promise<string> {
|
||||
const { createThumbnailUrl } = props
|
||||
|
||||
return createThumbnailUrl
|
||||
? await createThumbnailUrl(file.file as File)
|
||||
: await previewImage(file.file as File)
|
||||
}
|
||||
|
||||
async function previewImage (file: File): Promise<string> {
|
||||
return await new Promise((resolve) => {
|
||||
if (!file.type || !isImageFileType(file.type)) {
|
||||
resolve('')
|
||||
return
|
||||
}
|
||||
|
||||
const img = new Image()
|
||||
img.onload = () => {
|
||||
const { width, height } = img
|
||||
const canvas = document.createElement('canvas')
|
||||
const ctx = canvas.getContext('2d')
|
||||
canvas.width = width
|
||||
canvas.height = height
|
||||
canvas.style.cssText = `position: fixed; left: 0; top: 0; width: ${width}px; height: ${height}px; z-index: 9999; display: none;`
|
||||
document.body.appendChild(canvas)
|
||||
|
||||
ctx?.drawImage(img, 0, 0, width, height)
|
||||
const dataURL = canvas.toDataURL()
|
||||
document.body.removeChild(canvas)
|
||||
|
||||
resolve(dataURL)
|
||||
}
|
||||
img.src = window.URL.createObjectURL(file)
|
||||
})
|
||||
}
|
||||
|
||||
provide(uploadInjectionKey, {
|
||||
mergedClsPrefixRef,
|
||||
mergedThemeRef: themeRef,
|
||||
@ -430,7 +510,11 @@ export default defineComponent({
|
||||
mergedFileListRef: mergedFileListRef,
|
||||
XhrMap,
|
||||
submit,
|
||||
doChange
|
||||
doChange,
|
||||
isImageUrl,
|
||||
showPreivewButtonRef: toRef(props, 'showPreivewButton'),
|
||||
onPreviewRef: toRef(props, 'onPreview'),
|
||||
getFileThumbnail
|
||||
})
|
||||
return {
|
||||
mergedClsPrefix: mergedClsPrefixRef,
|
||||
@ -464,7 +548,8 @@ export default defineComponent({
|
||||
itemDisabledOpacity,
|
||||
lineHeight,
|
||||
borderRadius,
|
||||
fontSize
|
||||
fontSize,
|
||||
itemIconErrorColor
|
||||
}
|
||||
} = themeRef.value
|
||||
return {
|
||||
@ -481,7 +566,8 @@ export default defineComponent({
|
||||
'--item-text-color': itemTextColor,
|
||||
'--item-text-color-error': itemTextColorError,
|
||||
'--item-text-color-success': itemTextColorSuccess,
|
||||
'--line-height': lineHeight
|
||||
'--line-height': lineHeight,
|
||||
'--item-icon-error-color': itemIconErrorColor
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -495,6 +581,38 @@ export default defineComponent({
|
||||
draggerInsideRef.value = true
|
||||
}
|
||||
}
|
||||
const uploadTrigger = (
|
||||
<div
|
||||
class={[
|
||||
`${mergedClsPrefix}-upload__trigger`,
|
||||
this.listType === 'picture-card' &&
|
||||
`${mergedClsPrefix}-upload__trigger--picture-card`
|
||||
]}
|
||||
onClick={this.handleTriggerClick}
|
||||
onDrop={this.handleTriggerDrop}
|
||||
onDragover={this.handleTriggerDragOver}
|
||||
onDragenter={this.handleTriggerDragEnter}
|
||||
onDragleave={this.handleTriggerDragLeave}
|
||||
>
|
||||
{this.$slots}
|
||||
</div>
|
||||
)
|
||||
const uploadFileList = (
|
||||
<NFadeInExpandTransition group>
|
||||
{{
|
||||
default: () =>
|
||||
this.mergedFileList.map((file) => (
|
||||
<NUploadFile
|
||||
clsPrefix={mergedClsPrefix}
|
||||
key={file.id}
|
||||
file={file}
|
||||
listType={this.listType}
|
||||
/>
|
||||
))
|
||||
}}
|
||||
</NFadeInExpandTransition>
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
class={[
|
||||
@ -516,33 +634,22 @@ export default defineComponent({
|
||||
multiple={this.multiple}
|
||||
onChange={this.handleFileInputChange}
|
||||
/>
|
||||
<div
|
||||
class={`${mergedClsPrefix}-upload__trigger`}
|
||||
onClick={this.handleTriggerClick}
|
||||
onDrop={this.handleTriggerDrop}
|
||||
onDragover={this.handleTriggerDragOver}
|
||||
onDragenter={this.handleTriggerDragEnter}
|
||||
onDragleave={this.handleTriggerDragLeave}
|
||||
>
|
||||
{this.$slots}
|
||||
</div>
|
||||
{this.listType !== 'picture-card' && uploadTrigger}
|
||||
{this.showFileList && (
|
||||
<div
|
||||
class={`${mergedClsPrefix}-upload-file-list`}
|
||||
style={this.fileListStyle}
|
||||
>
|
||||
<NFadeInExpandTransition group>
|
||||
{{
|
||||
default: () =>
|
||||
this.mergedFileList.map((file) => (
|
||||
<NUploadFile
|
||||
clsPrefix={mergedClsPrefix}
|
||||
key={file.id}
|
||||
file={file}
|
||||
/>
|
||||
))
|
||||
}}
|
||||
</NFadeInExpandTransition>
|
||||
{this.listType === 'picture-card' ? (
|
||||
<NImageGroup>
|
||||
{{
|
||||
default: () => uploadFileList
|
||||
}}
|
||||
</NImageGroup>
|
||||
) : (
|
||||
uploadFileList
|
||||
)}
|
||||
{this.listType === 'picture-card' && uploadTrigger}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
@ -1,16 +1,30 @@
|
||||
import { h, defineComponent, PropType, computed, inject } from 'vue'
|
||||
import {
|
||||
h,
|
||||
defineComponent,
|
||||
PropType,
|
||||
computed,
|
||||
inject,
|
||||
ref,
|
||||
watchEffect,
|
||||
VNode
|
||||
} from 'vue'
|
||||
import {
|
||||
CancelIcon,
|
||||
TrashIcon,
|
||||
AttachIcon,
|
||||
RetryIcon,
|
||||
DownloadIcon
|
||||
DownloadIcon,
|
||||
FileIcon,
|
||||
PhotoIcon,
|
||||
EyeIcon
|
||||
} from '../../_internal/icons'
|
||||
import { NButton } from '../../button'
|
||||
import { NIconSwitchTransition, NBaseIcon } from '../../_internal'
|
||||
import { NIconSwitchTransition, NBaseIcon, NBaseLoading } from '../../_internal'
|
||||
import { warn } from '../../_utils'
|
||||
import NUploadProgress from './UploadProgress'
|
||||
import { FileInfo, uploadInjectionKey } from './interface'
|
||||
import { FileInfo, listType, uploadInjectionKey } from './interface'
|
||||
import { NImage } from '../../image'
|
||||
import { ImageInst } from '../../image/src/Image'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'UploadFile',
|
||||
@ -22,11 +36,19 @@ export default defineComponent({
|
||||
file: {
|
||||
type: Object as PropType<FileInfo>,
|
||||
required: true
|
||||
},
|
||||
listType: {
|
||||
type: String as PropType<listType>,
|
||||
default: 'text'
|
||||
}
|
||||
},
|
||||
setup (props) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const NUpload = inject(uploadInjectionKey)!
|
||||
|
||||
const imageRef = ref<ImageInst | null>(null)
|
||||
const thumbnailUrl = ref<string>('')
|
||||
|
||||
const progressStatusRef = computed(() => {
|
||||
const { file } = props
|
||||
if (file.status === 'finished') return 'success'
|
||||
@ -62,6 +84,18 @@ export default defineComponent({
|
||||
const { file } = props
|
||||
return ['error'].includes(file.status)
|
||||
})
|
||||
const showPreivewButtonRef = computed(() => {
|
||||
if (!NUpload.showPreivewButtonRef.value) return false
|
||||
const {
|
||||
file: { status, url },
|
||||
listType
|
||||
} = props
|
||||
return (
|
||||
['finished'].includes(status) &&
|
||||
(url || thumbnailUrl.value) &&
|
||||
listType === 'picture-card'
|
||||
)
|
||||
})
|
||||
function handleRetryClick (): void {
|
||||
NUpload.submit(props.file.id)
|
||||
}
|
||||
@ -121,6 +155,46 @@ export default defineComponent({
|
||||
XHR?.abort()
|
||||
handleRemove(Object.assign({}, file))
|
||||
}
|
||||
function isImageUrl (file: FileInfo): boolean {
|
||||
return NUpload.isImageUrl(file)
|
||||
}
|
||||
function handlePreviewClick (e: MouseEvent): void {
|
||||
const {
|
||||
onPreviewRef: { value: onPreview }
|
||||
} = NUpload
|
||||
|
||||
if (onPreview) {
|
||||
e.preventDefault()
|
||||
onPreview(props.file)
|
||||
} else if (props.listType === 'picture-card') {
|
||||
const { value } = imageRef
|
||||
if (!value) return
|
||||
value.handleClick()
|
||||
}
|
||||
}
|
||||
|
||||
const getFileThumbnail = async (): Promise<void> => {
|
||||
if (props.listType !== 'picture' && props.listType !== 'picture-card') {
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
typeof document === 'undefined' ||
|
||||
typeof window === 'undefined' ||
|
||||
!window.FileReader ||
|
||||
!window.File ||
|
||||
!(props.file.file instanceof File)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
thumbnailUrl.value = await NUpload.getFileThumbnail(props.file)
|
||||
}
|
||||
|
||||
watchEffect(() => {
|
||||
void getFileThumbnail()
|
||||
})
|
||||
|
||||
return {
|
||||
mergedTheme: NUpload.mergedThemeRef,
|
||||
progressStatus: progressStatusRef,
|
||||
@ -133,30 +207,149 @@ export default defineComponent({
|
||||
showRetryButton: showRetryButtonRef,
|
||||
handleRemoveOrCancelClick,
|
||||
handleDownloadClick,
|
||||
handleRetryClick
|
||||
handleRetryClick,
|
||||
isImageUrl,
|
||||
showPreivewButton: showPreivewButtonRef,
|
||||
handlePreviewClick,
|
||||
thumbnailUrl,
|
||||
imageRef
|
||||
}
|
||||
},
|
||||
render () {
|
||||
const { clsPrefix, mergedTheme } = this
|
||||
|
||||
const thumbnailNameClass = [`${clsPrefix}-upload-file-info-thumbnail__name`]
|
||||
const fileIcon = (icon: VNode): VNode => (
|
||||
<span class={`${clsPrefix}-upload-file-info-thumbnail__image`}>
|
||||
<NBaseIcon clsPrefix={clsPrefix}>{{ default: () => icon }}</NBaseIcon>
|
||||
</span>
|
||||
)
|
||||
// if there is text list type, show file icon
|
||||
let icon = (
|
||||
<NBaseIcon clsPrefix={clsPrefix}>
|
||||
{{ default: () => <AttachIcon /> }}
|
||||
</NBaseIcon>
|
||||
)
|
||||
|
||||
if (this.listType === 'picture' || this.listType === 'picture-card') {
|
||||
if (this.file.status === 'uploading') {
|
||||
icon =
|
||||
this.listType === 'picture-card' ? (
|
||||
<span>Upload..</span>
|
||||
) : (
|
||||
<NBaseLoading
|
||||
key="loading"
|
||||
clsPrefix={clsPrefix}
|
||||
class={`${clsPrefix}-upload-file-info-thumbnail__spin`}
|
||||
strokeWidth={20}
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
icon = !this.isImageUrl(this.file) ? (
|
||||
fileIcon(<FileIcon />)
|
||||
) : (this.file.url || this.thumbnailUrl) &&
|
||||
this.file.status !== 'error' ? (
|
||||
<a
|
||||
rel="noopener noreferer"
|
||||
target="_blank"
|
||||
href={this.file.url || undefined}
|
||||
class={`${clsPrefix}-upload-file-info-thumbnail__image`}
|
||||
onClick={(e) => this.handlePreviewClick(e)}
|
||||
>
|
||||
{this.listType === 'picture-card' ? (
|
||||
<NImage
|
||||
src={this.thumbnailUrl || this.file.url || undefined}
|
||||
alt={this.file.name}
|
||||
ref="imageRef"
|
||||
></NImage>
|
||||
) : (
|
||||
<img
|
||||
src={this.thumbnailUrl || this.file.url || undefined}
|
||||
alt={this.file.name}
|
||||
/>
|
||||
)}
|
||||
</a>
|
||||
) : (
|
||||
fileIcon(<PhotoIcon />)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<a
|
||||
ref="noopener noreferer"
|
||||
target="_blank"
|
||||
href={this.file.url || undefined}
|
||||
<div
|
||||
class={[
|
||||
`${clsPrefix}-upload-file`,
|
||||
`${clsPrefix}-upload-file--${this.progressStatus}-status`,
|
||||
this.file.url && `${clsPrefix}-upload-file--with-url`
|
||||
this.file.url &&
|
||||
this.file.status !== 'error' &&
|
||||
this.listType !== 'picture-card' &&
|
||||
`${clsPrefix}-upload-file--with-url`,
|
||||
`${clsPrefix}-upload-file--${this.listType}-type`
|
||||
]}
|
||||
>
|
||||
<div class={`${clsPrefix}-upload-file-info`}>
|
||||
<div class={`${clsPrefix}-upload-file-info__name`}>
|
||||
<NBaseIcon clsPrefix={clsPrefix}>
|
||||
{{ default: () => <AttachIcon /> }}
|
||||
</NBaseIcon>
|
||||
{this.file.name}
|
||||
<div
|
||||
class={
|
||||
this.listType === 'picture' || this.listType === 'picture-card'
|
||||
? `${clsPrefix}-upload-file-info-thumbnail`
|
||||
: `${clsPrefix}-upload-file-info__name`
|
||||
}
|
||||
>
|
||||
{icon}
|
||||
{(this.listType !== 'picture' &&
|
||||
this.listType !== 'picture-card') ||
|
||||
(this.file.url && this.file.status !== 'error') ? (
|
||||
<a
|
||||
rel="noopener noreferer"
|
||||
target="_blank"
|
||||
href={this.file.url || undefined}
|
||||
class={[
|
||||
...thumbnailNameClass,
|
||||
`${clsPrefix}-upload-file-info-thumbnail__hide`
|
||||
]}
|
||||
onClick={(e) => this.handlePreviewClick(e)}
|
||||
>
|
||||
{this.file.name}
|
||||
</a>
|
||||
) : (
|
||||
<span
|
||||
class={[
|
||||
...thumbnailNameClass,
|
||||
(this.file.url ||
|
||||
this.thumbnailUrl ||
|
||||
this.file.status === 'uploading') &&
|
||||
`${clsPrefix}-upload-file-info-thumbnail__hide`
|
||||
]}
|
||||
onClick={(e) => this.handlePreviewClick(e)}
|
||||
>
|
||||
{this.file.name}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div class={`${clsPrefix}-upload-file-info__action`}>
|
||||
<div
|
||||
class={[
|
||||
`${clsPrefix}-upload-file-info__action`,
|
||||
`${clsPrefix}-upload-file-info__action--${this.listType}-type`
|
||||
]}
|
||||
>
|
||||
{this.showPreivewButton ? (
|
||||
<NButton
|
||||
key="preview"
|
||||
text
|
||||
type={this.buttonType}
|
||||
onClick={this.handlePreviewClick}
|
||||
theme={mergedTheme.peers.Button}
|
||||
themeOverrides={mergedTheme.peerOverrides.Button}
|
||||
>
|
||||
{{
|
||||
icon: () => (
|
||||
<NBaseIcon clsPrefix={clsPrefix}>
|
||||
{{ default: () => <EyeIcon /> }}
|
||||
</NBaseIcon>
|
||||
)
|
||||
}}
|
||||
</NButton>
|
||||
) : null}
|
||||
{(this.showRemoveButton || this.showCancelButton) &&
|
||||
!this.disabled && (
|
||||
<NButton
|
||||
@ -230,7 +423,7 @@ export default defineComponent({
|
||||
percentage={this.file.percentage || 0}
|
||||
status={this.progressStatus}
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
@ -8,7 +8,9 @@ export interface FileInfo {
|
||||
url: string | null
|
||||
percentage: number
|
||||
status: 'pending' | 'uploading' | 'finished' | 'removed' | 'error'
|
||||
file: File | null
|
||||
file: File | null | Blob
|
||||
thumbnailUrl?: string
|
||||
type?: string
|
||||
}
|
||||
|
||||
export type FuncOrRecordOrUndef =
|
||||
@ -65,6 +67,10 @@ export interface UploadInjection {
|
||||
XhrMap: Map<string, XMLHttpRequest>
|
||||
submit: (fileId?: string) => void
|
||||
doChange: DoChange
|
||||
isImageUrl: (file: FileInfo) => boolean
|
||||
showPreivewButtonRef: Ref<boolean>
|
||||
onPreviewRef: Ref<OnPreview | undefined>
|
||||
getFileThumbnail: (file: FileInfo) => Promise<string>
|
||||
}
|
||||
|
||||
export const uploadInjectionKey: InjectionKey<UploadInjection> =
|
||||
@ -85,3 +91,9 @@ export type OnBeforeUpload = (data: {
|
||||
file: FileInfo
|
||||
fileList: FileInfo[]
|
||||
}) => Promise<unknown>
|
||||
|
||||
export type listType = 'text' | 'picture' | 'picture-card'
|
||||
|
||||
export type OnPreview = (file: FileInfo) => void
|
||||
|
||||
export type CreateThumbnailUrl = (file: File) => Promise<string>
|
||||
|
@ -11,7 +11,26 @@ export default cB('upload', [
|
||||
`),
|
||||
cE('trigger', `
|
||||
display: inline-block;
|
||||
`),
|
||||
`, [
|
||||
cM('picture-card', `
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 104px;
|
||||
height: 104px;
|
||||
margin: 0 8px 8px 0;
|
||||
vertical-align: top;
|
||||
padding: 8px;
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
transition: border-color .3s var(--bezier), background-color .3s var(--bezier);
|
||||
background-color: var(--dragger-color);
|
||||
border: var(--dragger-border);
|
||||
`, [
|
||||
c('&:hover', `
|
||||
border: var(--dragger-border-hover);
|
||||
`)
|
||||
])
|
||||
]),
|
||||
cM('dragger-inside', [
|
||||
cE('trigger', `
|
||||
display: block;
|
||||
@ -65,25 +84,208 @@ export default cB('upload', [
|
||||
`)
|
||||
])
|
||||
]),
|
||||
cM('picture-type', `
|
||||
height: 66px;
|
||||
padding: 8px;
|
||||
border: 1px solid;
|
||||
border-color: var(--border-color);
|
||||
border-radius: var(--border-radius);
|
||||
margin-top: 8px;
|
||||
text-decoration: underline;
|
||||
text-decoration-color: #0000;
|
||||
`, [
|
||||
cB('upload-file-info', `
|
||||
padding-top: 0px;
|
||||
padding-bottom: 0px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
`, [
|
||||
cB('upload-file-info-thumbnail', `
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
min-width:0;
|
||||
flex:1;
|
||||
`,
|
||||
[
|
||||
cE('name', `
|
||||
flex: auto;
|
||||
margin-left: 8px;
|
||||
padding: 0 8px;
|
||||
line-height: 48px;
|
||||
height: 100%;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
text-decoration: underline;
|
||||
text-decoration-color: #0000;
|
||||
font-size: var(--font-size);
|
||||
transition:
|
||||
color .3s var(--bezier),
|
||||
text-decoration-color .3s var(--bezier);
|
||||
color: var(--item-text-color);
|
||||
`),
|
||||
cE('spin', `
|
||||
color: var(--loading-color);
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 26px;
|
||||
`),
|
||||
cE('image', `
|
||||
opacity: .8;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-content: center;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
`, [
|
||||
c('img', `
|
||||
width: 100%;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
`),
|
||||
c('i', `
|
||||
font-size: 34px;
|
||||
`)
|
||||
])
|
||||
])
|
||||
])
|
||||
]),
|
||||
cM('picture-card-type', `
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 104px;
|
||||
height: 104px;
|
||||
margin: 0 8px 8px 0;
|
||||
vertical-align: top;
|
||||
padding: 8px;
|
||||
border: 1px solid;
|
||||
border-color: var(--border-color);
|
||||
border-radius: var(--border-radius);
|
||||
`, [
|
||||
cB('upload-file-info', `
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
`, [
|
||||
cB('upload-file-info-thumbnail', `
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`, [
|
||||
cE('image', `
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`, [
|
||||
c('img', `
|
||||
width: 100%;
|
||||
`),
|
||||
c('i', `
|
||||
font-size: 34px;
|
||||
`)
|
||||
]),
|
||||
cE('name', `
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
bottom: 10px;
|
||||
color: var(--item-text-color);
|
||||
font-size: var(--font-size);
|
||||
text-align: center;
|
||||
height: 24px;
|
||||
`),
|
||||
cE('hide', 'display: none;')
|
||||
])
|
||||
]),
|
||||
c('&::before', `
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
width: calc(100% - 8px);
|
||||
height: calc(100% - 8px);
|
||||
background-color: rgb(243, 243, 245, 0.9);
|
||||
opacity: 0;
|
||||
transition: opacity .2s var(--bezier);
|
||||
content: "";
|
||||
left: 4px;
|
||||
top: 4px;
|
||||
`),
|
||||
c('&:hover', `
|
||||
background-color: initial !important;
|
||||
`, [
|
||||
c('&::before', 'opacity: 1;')
|
||||
])
|
||||
]),
|
||||
cM('error-status', [
|
||||
c('&:hover', `
|
||||
background-color: var(--item-color-hover-error);
|
||||
`),
|
||||
cB('upload-file-info', [
|
||||
cE('name', `
|
||||
color: var(--item-text-color-error);
|
||||
`)
|
||||
cE('name', [
|
||||
c('a', `
|
||||
color: var(--item-text-color-error);
|
||||
`)
|
||||
]),
|
||||
cB('upload-file-info-thumbnail', [
|
||||
cE('name', `
|
||||
color: var(--item-text-color-error);
|
||||
`),
|
||||
cE('image', [
|
||||
c('i', `
|
||||
color: var(--item-icon-error-color);
|
||||
`)
|
||||
])
|
||||
])
|
||||
]),
|
||||
cM('picture-card-type, picture-type', `
|
||||
border-color: var(--item-icon-error-color);
|
||||
`)
|
||||
]),
|
||||
cM('success-status', [
|
||||
cB('upload-file-info', [
|
||||
cB('upload-file-info-thumbnail', [
|
||||
cE('image', [
|
||||
c('i', `
|
||||
color: #2080f0;
|
||||
`)
|
||||
])
|
||||
])
|
||||
])
|
||||
]),
|
||||
cM('with-url', `
|
||||
cursor: pointer;
|
||||
`, [
|
||||
cB('upload-file-info', [
|
||||
cE('name', `
|
||||
text-decoration: underline;
|
||||
color: var(--item-text-color-success);
|
||||
text-decoration-color: var(--item-text-color-success);
|
||||
`)
|
||||
cE('name', [
|
||||
c('a', `
|
||||
text-decoration: underline;
|
||||
color: var(--item-text-color-success);
|
||||
text-decoration-color: var(--item-text-color-success);
|
||||
`)
|
||||
]),
|
||||
cB('upload-file-info-thumbnail', [
|
||||
cE('name', `
|
||||
text-decoration: underline;
|
||||
color: var(--item-text-color-success);
|
||||
text-decoration-color: var(--item-text-color-success);
|
||||
`)
|
||||
])
|
||||
])
|
||||
]),
|
||||
cB('upload-file-info', `
|
||||
@ -114,21 +316,41 @@ export default cB('upload', [
|
||||
createIconSwitchTransition()
|
||||
])
|
||||
])
|
||||
])
|
||||
]),
|
||||
cM('picture-type', `
|
||||
position: relative;
|
||||
max-width: 80px;
|
||||
width: auto;
|
||||
`),
|
||||
cM('picture-card-type', `
|
||||
z-index: 2;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
top: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`)
|
||||
]),
|
||||
cE('name', `
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
text-decoration: underline;
|
||||
text-decoration-color: #0000;
|
||||
font-size: var(--font-size);
|
||||
transition:
|
||||
color .3s var(--bezier),
|
||||
text-decoration-color .3s var(--bezier);
|
||||
color: var(--item-text-color);
|
||||
`, [
|
||||
c('a', `
|
||||
text-decoration: underline;
|
||||
text-decoration-color: #0000;
|
||||
font-size: var(--font-size);
|
||||
transition:
|
||||
color .3s var(--bezier),
|
||||
text-decoration-color .3s var(--bezier);
|
||||
color: var(--item-text-color);
|
||||
`),
|
||||
cB('base-icon', `
|
||||
font-size: 18px;
|
||||
margin-right: 2px;
|
||||
@ -136,6 +358,15 @@ export default cB('upload', [
|
||||
color: var(--item-icon-color);
|
||||
`)
|
||||
])
|
||||
]),
|
||||
cM('info-status, pending-status, uploading', [
|
||||
cM('picture-card-type', `
|
||||
transition:
|
||||
border-color .3s var(--bezier),
|
||||
background-color .3s var(--bezier);
|
||||
background-color: var(--dragger-color);
|
||||
border: var(--dragger-border);
|
||||
`)
|
||||
])
|
||||
])
|
||||
]),
|
||||
@ -153,6 +384,9 @@ export default cB('upload', [
|
||||
`),
|
||||
cB('upload-dragger', `
|
||||
cursor: not-allowed;
|
||||
`),
|
||||
cE('picture-card', `
|
||||
cursor: not-allowed;
|
||||
`)
|
||||
]),
|
||||
cM('drag-over', [
|
||||
|
@ -35,7 +35,8 @@ export const self = (vars: ThemeCommonVars) => {
|
||||
itemTextColorError: errorColor,
|
||||
itemTextColorSuccess: successColor,
|
||||
itemIconColor: iconColor,
|
||||
itemDisabledOpacity: opacityDisabled
|
||||
itemDisabledOpacity: opacityDisabled,
|
||||
itemIconErrorColor: errorColor
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,14 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { NUpload } from '../index'
|
||||
import { sleep } from 'seemly'
|
||||
|
||||
const getMockFile = (element: Element, files: File[]): void => {
|
||||
Object.defineProperty(element, 'files', {
|
||||
get () {
|
||||
return files
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
describe('n-upload', () => {
|
||||
it('should work with import on demand', () => {
|
||||
@ -23,4 +32,155 @@ describe('n-upload', () => {
|
||||
await wrapper.setProps({ disabled: true })
|
||||
expect(wrapper.find('.n-upload').classes()).toContain('n-upload--disabled')
|
||||
})
|
||||
|
||||
it('should work with `on-before-upload` prop', async () => {
|
||||
const onBeforeUpload = jest.fn(async () => true)
|
||||
const onChange = jest.fn()
|
||||
const wrapper = mount(NUpload, {
|
||||
props: {
|
||||
onBeforeUpload,
|
||||
onChange
|
||||
}
|
||||
})
|
||||
const input = wrapper.find('input')
|
||||
const fileList = [new File(['index'], 'file.txt')]
|
||||
|
||||
getMockFile(input.element, fileList)
|
||||
await input.trigger('change')
|
||||
|
||||
expect(onBeforeUpload).toHaveBeenCalled()
|
||||
expect(onChange).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should work with `list-type` prop', async () => {
|
||||
const wrapper = mount(NUpload, {
|
||||
props: {
|
||||
listType: 'text',
|
||||
action: 'http://www.mocky.io/v2/5e4bafc63100007100d8b70f'
|
||||
}
|
||||
})
|
||||
const input = wrapper.find('input')
|
||||
const fileList = [new File(['index'], 'file.txt')]
|
||||
|
||||
getMockFile(input.element, fileList)
|
||||
await input.trigger('change')
|
||||
|
||||
expect(wrapper.findAll('.n-upload-file--text-type').length).toBe(1)
|
||||
|
||||
await wrapper.setProps({
|
||||
listType: 'picture'
|
||||
})
|
||||
expect(wrapper.findAll('.n-upload-file--picture-type').length).toBe(1)
|
||||
|
||||
await wrapper.setProps({
|
||||
listType: 'picture-card'
|
||||
})
|
||||
expect(wrapper.findAll('.n-upload-file--picture-card-type').length).toBe(1)
|
||||
})
|
||||
|
||||
it('should work with `create-thumbnail-url` prop', async () => {
|
||||
const createThumbnailUrl = async (): Promise<string> => '/testThumbUrl.png'
|
||||
const wrapper = mount(NUpload, {
|
||||
props: {
|
||||
listType: 'picture',
|
||||
createThumbnailUrl
|
||||
}
|
||||
})
|
||||
const input = wrapper.find('input')
|
||||
const fileList = [new File(['index'], 'file.txt')]
|
||||
|
||||
getMockFile(input.element, fileList)
|
||||
await input.trigger('change')
|
||||
await sleep(1000)
|
||||
expect(
|
||||
wrapper.find('.n-upload-file-info-thumbnail__image img').attributes('src')
|
||||
).toEqual('/testThumbUrl.png')
|
||||
})
|
||||
|
||||
it('should work with `on-preview` prop', async () => {
|
||||
const onPreview = jest.fn()
|
||||
const wrapper = mount(NUpload, {
|
||||
props: {
|
||||
defaultFileList: [
|
||||
{
|
||||
name: 'test.png',
|
||||
url: '/testUrl.png',
|
||||
status: 'finished',
|
||||
id: 'test',
|
||||
percentage: 100,
|
||||
file: null
|
||||
}
|
||||
],
|
||||
onPreview
|
||||
}
|
||||
})
|
||||
const urlName = wrapper.findAll('.n-upload-file-info-thumbnail__name')[0]
|
||||
await urlName.trigger('click')
|
||||
|
||||
expect(onPreview).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should work with `show-remove-button` and `on-remove` prop', async () => {
|
||||
const onRemove = jest.fn()
|
||||
const wrapper = mount(NUpload, {
|
||||
props: {
|
||||
defaultFileList: [
|
||||
{
|
||||
name: 'test.png',
|
||||
url: '/testUrl.png',
|
||||
status: 'finished',
|
||||
id: 'test',
|
||||
percentage: 100,
|
||||
file: null
|
||||
}
|
||||
],
|
||||
onRemove,
|
||||
showRemoveButton: false
|
||||
}
|
||||
})
|
||||
let button = wrapper.find('.n-button--default-type')
|
||||
expect(button.exists()).not.toBe(true)
|
||||
|
||||
await wrapper.setProps({
|
||||
showRemoveButton: true
|
||||
})
|
||||
|
||||
button = wrapper.find('.n-button--default-type')
|
||||
expect(button.exists()).toBe(true)
|
||||
|
||||
await button.trigger('click')
|
||||
expect(onRemove).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should work with `show-cancel-button` and `on-remove` prop', async () => {
|
||||
const onRemove = jest.fn()
|
||||
const wrapper = mount(NUpload, {
|
||||
props: {
|
||||
defaultFileList: [
|
||||
{
|
||||
name: 'test.png',
|
||||
url: '/testUrl.png',
|
||||
status: 'error',
|
||||
id: 'test',
|
||||
percentage: 0,
|
||||
file: null
|
||||
}
|
||||
],
|
||||
onRemove,
|
||||
showCancelButton: false
|
||||
}
|
||||
})
|
||||
let button = wrapper.findAll('.n-button--error-type')
|
||||
expect(button.length).toEqual(1)
|
||||
|
||||
await wrapper.setProps({
|
||||
showCancelButton: true
|
||||
})
|
||||
|
||||
button = wrapper.findAll('.n-button--error-type')
|
||||
expect(button.length).toEqual(2)
|
||||
|
||||
await button[0].trigger('click')
|
||||
expect(onRemove).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user