diff --git a/packages/varlet-vue2-ui/src/popup/Popup.jsx b/packages/varlet-vue2-ui/src/popup/Popup.jsx index d8a0fcb..d4a4740 100644 --- a/packages/varlet-vue2-ui/src/popup/Popup.jsx +++ b/packages/varlet-vue2-ui/src/popup/Popup.jsx @@ -56,11 +56,12 @@ export default defineComponent({ }, renderContent() { + console.log(this.$attrs) return (
{this.slots('default')}
diff --git a/packages/varlet-vue2-ui/src/uploader/Uploader.vue b/packages/varlet-vue2-ui/src/uploader/Uploader.vue new file mode 100644 index 0000000..495c121 --- /dev/null +++ b/packages/varlet-vue2-ui/src/uploader/Uploader.vue @@ -0,0 +1,332 @@ + + + + + diff --git a/packages/varlet-vue2-ui/src/uploader/__tests__/__snapshots__/index.spec.js.snap b/packages/varlet-vue2-ui/src/uploader/__tests__/__snapshots__/index.spec.js.snap new file mode 100644 index 0000000..1ce6f5a --- /dev/null +++ b/packages/varlet-vue2-ui/src/uploader/__tests__/__snapshots__/index.spec.js.snap @@ -0,0 +1,334 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`test uploader disabled 1`] = ` +"
+
+
+
+ + + + +
+
+ +
+
+
" +`; + +exports[`test uploader disabled 2`] = ` +"
+
+
+
+ cat.png +
+
+ +
+
+
+
+ + + + +
+
+ +
+
+
" +`; + +exports[`test uploader example 1`] = ` +"
+
基本使用
+
+
+
+
+ + + + +
+
+ +
+
+
+
文件预览
+
+
+
+
+ https://varlet.gitee.io/varlet-ui/cat.jpg +
+
+
+
+
+
+ https://www.runoob.com/try/demo_source/mov_bbb.mp4 +
+
+
+
+
+
+ + + + +
+
+ +
+
+
+
上传状态
+
+
+
+
+ https://varlet.gitee.io/varlet-ui/cat.jpg +
+
+
+
+
+
+ https://varlet.gitee.io/varlet-ui/cat.jpg +
+
+
+
+
+
+ https://varlet.gitee.io/varlet-ui/cat.jpg +
+
+
+
+
+
+ + + + +
+
+ +
+
+
+
文件数量限制
+
+
+
+
+ +
+
+
0 / 1
+
+
+ +
+
+ +
+
+
+
文件大小限制
+
+
+
+
+ + + + +
+
+ +
+
+
+
上传预处理
+
+
+
+
+ + + + +
+
+ +
+
+
+
禁用
+
+
+
+
+ + + + +
+
+ +
+
+
+
只读
+
+
+
+
+ + + + +
+
+ +
+
+
+
删除前处理
+
+
+
+
+ https://varlet.gitee.io/varlet-ui/cat.jpg +
+
+
+
+
+
+ + + + +
+
+ +
+
+
+
自定义上传样式
+
+
+
+
+ + + + +
+
+ +
+
+
+
字段校验
+
+
+
+
+ https://varlet.gitee.io/varlet-ui/cat.jpg +
+
+
+
+
+
+ + + + +
+
+ +
+
+
+
+
" +`; + +exports[`test uploader length over maxlength in multiple mode 1`] = ` +"
+
+
+
+ +
+
+
0 / 1
+
+
+ +
+
+ +
+
+
" +`; + +exports[`test uploader validation 1`] = ` +"
+
+
+
+ +
+
您至少上传一个
+
+
+
+ +
+
+ +
+
+
" +`; + +exports[`test uploader validation 2`] = ` +"
+
+
+
+ cat.png +
+
+ +
+
+
+
+ + + + +
+
+ +
+
+
" +`; diff --git a/packages/varlet-vue2-ui/src/uploader/__tests__/index.spec.js b/packages/varlet-vue2-ui/src/uploader/__tests__/index.spec.js new file mode 100644 index 0000000..a1bf942 --- /dev/null +++ b/packages/varlet-vue2-ui/src/uploader/__tests__/index.spec.js @@ -0,0 +1,258 @@ +import example from '../example' +import Uploader from '..' +import VarUploader from '../Uploader' +import ImagePreview from '../../image-preview' +import { mount } from '@vue/test-utils' +import Vue from 'vue' +import { delay, mockFileReader, mockStubs } from '../../utils/jest' + +const createEvent = (filename) => { + return { + target: { + files: [new File([], filename)], + }, + } +} + +test('test uploader example', () => { + const wrapper = mount(example) + expect(wrapper.html()).toMatchSnapshot() + wrapper.destroy() +}) + +test('test uploader plugin', () => { + Vue.use(Uploader) + expect(Vue.component(Uploader.name)).toBeTruthy() +}) + +test('test uploader onAfterRead', async () => { + const onAfterRead = jest.fn() + + const wrapper = mount(VarUploader, { + listeners: { + afterRead: onAfterRead, + }, + }) + + await wrapper.vm.handleChange(createEvent('cat.png')) + expect(onAfterRead).toHaveBeenCalledTimes(1) + + wrapper.destroy() +}) + +test('test uploader onBeforeRead', async () => { + const onAfterRead = jest.fn() + + const wrapper = mount(VarUploader, { + listeners: { + afterRead: onAfterRead, + beforeRead: (file) => file.name.endsWith('jpg'), + }, + }) + + await wrapper.vm.handleChange(createEvent('cat.png')) + expect(onAfterRead).toHaveBeenCalledTimes(0) + + await wrapper.vm.handleChange(createEvent('cat.jpg')) + expect(onAfterRead).toHaveBeenCalledTimes(1) + + wrapper.destroy() +}) + +test('test uploader preview', async () => { + const { mockRestore } = mockFileReader('data:image/png;base64,') + const { mockRestore: mockRestoreStubs } = mockStubs() + const onInput = jest.fn((value) => wrapper.setProps({ value })) + + const wrapper = mount(VarUploader, { + propsData: { + value: [], + }, + listeners: { + input: onInput, + }, + }) + + await wrapper.vm.handleChange(createEvent('cat.jpg')) + await delay(16) + await wrapper.find('.var-uploader__file').trigger('click') + await delay(100) + + expect(document.querySelector('.var-popup').style.display).toBe('') + ImagePreview.close() + await delay(300) + + mockRestoreStubs() + wrapper.destroy() + mockRestore() +}) + +test('test uploader onOversize', async () => { + const onOversize = jest.fn() + + const wrapper = mount(VarUploader, { + propsData: { + maxsize: -1, + }, + listeners: { + oversize: onOversize, + }, + }) + + await wrapper.vm.handleChange(createEvent('cat.png')) + expect(onOversize).toHaveBeenCalledTimes(1) + + wrapper.destroy() +}) + +test('test uploader onRemove', async () => { + const { mockRestore } = mockFileReader('data:image/png;base64,') + const onInput = jest.fn((value) => wrapper.setProps({ value })) + const onRemove = jest.fn() + + const wrapper = mount(VarUploader, { + propsData: { + value: [], + }, + listeners: { + remove: onRemove, + input: onInput, + }, + }) + + await wrapper.vm.handleChange(createEvent('cat.png')) + expect(onInput).toHaveBeenCalledTimes(1) + + await wrapper.find('.var-uploader__file-close').trigger('click') + expect(onInput).toHaveBeenCalledTimes(2) + expect(onRemove).toHaveBeenCalledTimes(1) + expect(wrapper.vm.value).toStrictEqual([]) + + wrapper.destroy() + mockRestore() +}) + +test('test uploader onBeforeRemove', async () => { + const { mockRestore } = mockFileReader('data:image/png;base64,') + const onInput = jest.fn((value) => wrapper.setProps({ value })) + const onBeforeRemove = jest.fn(() => false) + const onRemove = jest.fn() + + const wrapper = mount(VarUploader, { + propsData: { + value: [], + }, + listeners: { + beforeRemove: onBeforeRemove, + remove: onRemove, + input: onInput, + }, + }) + + await wrapper.vm.handleChange(createEvent('cat.png')) + expect(onInput).toHaveBeenCalledTimes(1) + + await wrapper.find('.var-uploader__file-close').trigger('click') + expect(onBeforeRemove).toHaveBeenCalledTimes(1) + expect(onInput).toHaveBeenCalledTimes(1) + expect(onRemove).toHaveBeenCalledTimes(0) + expect(wrapper.vm.value).toHaveLength(1) + + wrapper.destroy() + mockRestore() +}) + +test('test uploader validation', async () => { + const { mockRestore } = mockFileReader('data:image/png;base64,') + const onInput = jest.fn((value) => wrapper.setProps({ value })) + + const wrapper = mount(VarUploader, { + propsData: { + value: [], + rules: [(v) => v.length >= 1 || '您至少上传一个'], + }, + listeners: { + input: onInput, + }, + }) + + wrapper.vm.validate() + await delay(16) + expect(wrapper.html()).toMatchSnapshot() + expect(wrapper.find('.var-form-details__message').text()).toBe('您至少上传一个') + + await wrapper.vm.handleChange(createEvent('cat.png')) + await delay(16) + expect(onInput).toHaveBeenCalledTimes(1) + expect(wrapper.html()).toMatchSnapshot() + expect(wrapper.find('.var-form-details__message').exists()).toBeFalsy() + + wrapper.vm.reset() + await delay(16) + expect(wrapper.vm.value).toStrictEqual([]) + + wrapper.destroy() + mockRestore() +}) + +test('test uploader disabled', async () => { + const { mockRestore } = mockFileReader('data:image/png;base64,') + const onInput = jest.fn((value) => wrapper.setProps({ value })) + const onRemove = jest.fn() + const onAfterRead = jest.fn() + + const wrapper = mount(VarUploader, { + propsData: { + disabled: false, + value: [], + }, + listeners: { + afterRead: onAfterRead, + remove: onRemove, + input: onInput, + }, + }) + + expect(wrapper.html()).toMatchSnapshot() + await wrapper.vm.handleChange(createEvent('cat.png')) + expect(onAfterRead).toHaveBeenCalledTimes(1) + + await wrapper.setProps({ disabled: true }) + expect(wrapper.html()).toMatchSnapshot() + expect(wrapper.find('.var-uploader__action-input[disabled]').exists()).toBeTruthy() + + await wrapper.find('.var-uploader__file-close').trigger('click') + expect(onRemove).toHaveBeenCalledTimes(0) + + wrapper.destroy() + mockRestore() +}) + +test('test uploader length over maxlength in multiple mode', async () => { + const { mockRestore } = mockFileReader('data:image/png;base64,') + const onInput = jest.fn((value) => wrapper.setProps({ value })) + + const wrapper = mount(VarUploader, { + propsData: { + maxlength: 1, + value: [], + }, + listeners: { + input: onInput, + }, + }) + + expect(wrapper.html()).toMatchSnapshot() + + const event = { + target: { + files: [new File([], 'cat.png'), new File([], 'dog.png')], + }, + } + + await wrapper.vm.handleChange(event) + expect(wrapper.vm.value.length).toBe(1) + + wrapper.destroy() + mockRestore() +}) diff --git a/packages/varlet-vue2-ui/src/uploader/docs/en-US.md b/packages/varlet-vue2-ui/src/uploader/docs/en-US.md new file mode 100644 index 0000000..9b89d62 --- /dev/null +++ b/packages/varlet-vue2-ui/src/uploader/docs/en-US.md @@ -0,0 +1,325 @@ +# Uploader + +### Intro +It provides the ability to read files and preview pictures and videos. +Get the file upload server by listening for `after-read` events. + +### Install + +```js +import Vue from 'vue' +import { Uploader } from '@varlet-vue2/ui' + +Vue.use(Uploader) +``` + +### Basic Usage + +```html + +``` + +```js +export default { + data: () => ({ + files: [] + }), + methods: { + handleAfterRead(file) { + console.log(file) + } + } +} +``` + +### File Preview + +By analyzing the file URL suffix name to determine the file type, support image and video preview. + +```html + +``` + +```js +export default { + data: () => ({ + files: [ + { + url: 'https://varlet.gitee.io/varlet-ui/cat.jpg', + cover: 'https://varlet.gitee.io/varlet-ui/cat.jpg' + }, + { + url: 'https://www.runoob.com/try/demo_source/mov_bbb.mp4', + cover: 'https://varlet.gitee.io/varlet-ui/cover.jpg' + } + ] + }) +} +``` + +### Upload State + +Three uploading states, `loading`, `success` and `error`, are provided, and tool functions are provided to quickly obtain files with corresponding states. + +```html + +``` + +```js +export default { + data: () => ({ + files: [ + { + url: 'https://varlet.gitee.io/varlet-ui/cat.jpg', + cover: 'https://varlet.gitee.io/varlet-ui/cat.jpg', + state: 'loading' + }, + { + url: 'https://varlet.gitee.io/varlet-ui/cat.jpg', + cover: 'https://varlet.gitee.io/varlet-ui/cat.jpg', + state: 'success' + }, + { + url: 'https://varlet.gitee.io/varlet-ui/cat.jpg', + cover: 'https://varlet.gitee.io/varlet-ui/cat.jpg', + state: 'error' + } + ] + }), + methods: { + handleAfterRead(file) { + file.state = 'loading' + + setTimeout(() => { + file.state = 'success' + }, 1000) + } + } +} +``` + +### File Maxlength + +```html + +``` + +### File Size Limit + +If the limit is exceeded, the file will be blocked. You can get the file by listening for the `oversize` event. + +```html + +``` + +```js +import { Snackbar } from '@varlet-vue2/ui' + +export default { + data: () =>({ + files: [] + }), + methods: { + handleOversize() { + Snackbar.warning('file size exceeds limit') + } + } +} +``` + +### Upload Preprocessing + +Operate on a file by registering a `before-read` event that returns a false value to prevent the file from being read. + +```html + +``` + +```js +import { Snackbar } from '@varlet-vue2/ui' + +export default { + data: () => ({ + files: [] + }), + methods: { + handleBeforeRead() { + if (file.file.size <= 1 * 1024 * 1024) { + Snackbar.success('the file is less than 1M and can be uploaded') + return true + } else { + Snackbar.warning('the file is larger than 1M and cannot be uploaded') + return false + } + } + } +} +``` + +### Disabled + +```html + +``` + +### Readonly + +```html + +``` + +### Remove Preprocessing + +Before deleting the file, the `before-remove` event is triggered, and a falsy value is returned to prevent the delete operation. + +```html + +``` + +```js +import { Dialog } from '@varlet-vue2/ui' + +export default { + data: () => ({ + files: [ + { + url: 'https://varlet.gitee.io/varlet-ui/cat.jpg', + cover: 'https://varlet.gitee.io/varlet-ui/cat.jpg', + } + ] + }), + methods: { + async handleBeforeRemove() { + const action = await Dialog({ + title: 'Delete or not?', + message: 'Cannot be withdrawn after deletion' + }) + + return action === 'confirm' + } + } +} +``` + + +### Customize upload styles + +```html + + Upload + +``` + +### Validate + +The values are validated by passing in an array of validator, If the validator returns `true`, the validation passes. +Other values are converted to text as a user prompt. +The second argument is a collection of utility functions that can quickly get a collection of files that match the state. + +```html + +``` + +## API + +### Props + +| Prop | Description | Type | Default | +| --- | --- | --- | --- | +| `v-model` | File list | _VarFile[]_ | `[]` | +| `accept` | Accepted file type, consistent with the native attribute | _string_ | `image/*` | +| `capture` | Get the file, the same as the native property | _string_ | `-` | +| `multiple` | Whether to select multiple files | _boolean_ | `false` | +| `readonly` | Whether the readonly | _boolean_ | `false` | +| `disabled` | Whether the disabled | _boolean_ | `false` | +| `removable` | Whether the removable | _boolean_ | `true` | +| `maxlength` | Maximum number of files | _string \| number_ | `-` | +| `maxsize` | Maximum file size | _string \| number_ | `-` | +| `previewed` | Whether to allow preview | _boolean_ | `true` | +| `ripple` | Whether to open ripple | _boolean_ | `true` | +| `validate-trigger` | Timing to trigger validation, The optional value is `onChange` `onRemove` | _ValidateTriggers[]_ | `['onChange', 'onRemove']` | +| `rules` | The validation rules,Returns `true` to indicate that the validation passed,The remaining values are converted to text as user prompts | _Array<(v: VarFile, u: VarFileUtils) => any>_ | `-` | + +### VarFile + +| Prop | Description | Type | Default | +| --- | --- | --- | --- | +| `file` | Native file | _File_ | `-` | +| `name` | File name | _string_ | `-` | +| `url` | File url | _string_ | `-` | +| `cover` | File cover image | _string_ | `-` | +| `fit` | Cover image fill mode, Optional value is `fill` `contain` `cover` `none` `scale-down` | _string_ | `-` | +| `state` | File upload state, Optional value is `loading` `success` `error` | _string_ | `-` | + +### VarFileUtils + +| Method | Description | Arguments | Return | +| --- | --- | --- | --- | +| `getLoading` | Gets a collection of files for `state` is `loading` | `VarFile[]` | `VarFile[]` | +| `getSuccess` | Gets a collection of files for `state` is `success` | `VarFile[]` | `VarFile[]` | +| `getError` | Gets a collection of files for `state` is `error` | `VarFile[]` | `VarFile[]` | + +### Methods + +| Method | Description | Arguments | Return | +| --- | --- | --- | --- | +| `getLoading` | Gets a collection of files for `state` is `loading` | `VarFile[]` | `VarFile[]` | +| `getSuccess` | Gets a collection of files for `state` is `success` | `VarFile[]` | `VarFile[]` | +| `getError` | Gets a collection of files for `state` is `error` | `VarFile[]` | `VarFile[]` | +| `validate` | Trigger validate | `-` | `valid: Promise` | +| `resetValidation` | Clearing validate messages | `-` | `-` | +| `reset` | Clear the value of the binding(set to `[]`)and validate messages | `-` | `-` | + +### Events + +| Event | Description | Arguments | +| --- | --- | --- | +| `before-read` | Trigger returns a false value before a file is read to prevent the file from being read(support promise) | `file: VarFile` | +| `after-read` | Triggered after the file is read | `file: VarFile` | +| `oversize` | Triggered when the file size limit is exceeded | `file: VarFile` | +| `before-remove` | Triggered before file deletion, return false value to prevent file deletion (support promise) | `file: VarFile` | +| `remove` | Triggered when deleting a file. There is a true value to prevent deleting a file (support promise) | `file: VarFile` | + +### Slots + +| Slot | Description | Arguments | +| --- | --- | --- | +| `default` | Upload action content | `-` | + +### Style Variables + +Here are the CSS variables used by the component, Styles can be customized using [StyleProvider](#/en-US/style-provider) + +| Variable | Default | +| --- | --- | +| `--uploader-action-background` | `#f7f8fa` | +| `--uploader-action-icon-color` | `#888` | +| `--uploader-action-icon-size` | `24px` | +| `--uploader-action-margin` | `0 10px 10px 0` | +| `--uploader-file-size` | `80px` | +| `--uploader-file-margin` | `0 10px 10px 0` | +| `--uploader-file-name-background` | `#f7f8fa` | +| `--uploader-file-name-color` | `#888` | +| `--uploader-file-name-font-size` | `12px` | +| `--uploader-file-name-padding` | `10px` | +| `--uploader-file-text-align` | `center` | +| `--uploader-file-close-background` | `rgba(0, 0, 0, 0.3)` | +| `--uploader-file-close-size` | `24px` | +| `--uploader-file-close-icon-font-size` | `14px` | +| `--uploader-file-close-icon-color` | `#fff` | +| `--uploader-file-cover-fit` | `cover` | +| `--uploader-file-cover-background` | `#f7f8fa` | +| `--uploader-preview-video-width` | `100vw` | +| `--uploader-preview-video-height` | `100vw` | +| `--uploader-file-indicator-height` | `4px` | +| `--uploader-file-indicator-normal-color` | `var(--color-disabled)` | +| `--uploader-file-indicator-success-color` | `var(--color-success)` | +| `--uploader-file-indicator-error-color` | `var(--color-danger)` | +| `--uploader-disabled-color` | `#ddd` +| `--uploader-disabled-text-color` | `var(--color-text-disabled)` | +| `--uploader-loading-background` | `linear-gradient(90deg, hsla(0, 0%, 100%, 0), hsla(0, 0%, 100%, 0.3), hsla(0, 0%, 100%, 0))` | \ No newline at end of file diff --git a/packages/varlet-vue2-ui/src/uploader/docs/zh-CN.md b/packages/varlet-vue2-ui/src/uploader/docs/zh-CN.md new file mode 100644 index 0000000..0f64f6e --- /dev/null +++ b/packages/varlet-vue2-ui/src/uploader/docs/zh-CN.md @@ -0,0 +1,322 @@ +# 文件上传 + +### 介绍 +提供了文件读取、图片/视频预览能力。 +通过监听 `after-read` 事件获取文件上传服务器。 + +### 引入 + +```js +import Vue from 'vue' +import { Uploader } from '@varlet-vue2/ui' + +Vue.use(Uploader) +``` + +### 基本使用 + +```html + +``` + +```js +export default { + data: () => ({ + files: [] + }), + methods: { + handleAfterRead(file) { + console.log(file) + } + } +} +``` + +### 文件预览 + +通过分析文件的 url 后缀名判断文件类型,支持图片和视频预览。 + +```html + +``` + +```js +export default { + data: () => ({ + files: [ + { + url: 'https://varlet.gitee.io/varlet-ui/cat.jpg', + cover: 'https://varlet.gitee.io/varlet-ui/cat.jpg' + }, + { + url: 'https://www.runoob.com/try/demo_source/mov_bbb.mp4', + cover: 'https://varlet.gitee.io/varlet-ui/cover.jpg' + } + ] + }) +} +``` + +### 上传状态 + +提供了 `loading`、 `success`、 `error` 三种上传状态,并提供了工具函数快速获取对应状态的文件。 + +```html + +``` + +```js +export default { + data: () => ({ + files: [ + { + url: 'https://varlet.gitee.io/varlet-ui/cat.jpg', + cover: 'https://varlet.gitee.io/varlet-ui/cat.jpg', + state: 'loading' + }, + { + url: 'https://varlet.gitee.io/varlet-ui/cat.jpg', + cover: 'https://varlet.gitee.io/varlet-ui/cat.jpg', + state: 'success' + }, + { + url: 'https://varlet.gitee.io/varlet-ui/cat.jpg', + cover: 'https://varlet.gitee.io/varlet-ui/cat.jpg', + state: 'error' + } + ] + }), + methods: { + handleAfterRead(file) { + file.state = 'loading' + + setTimeout(() => { + file.state = 'success' + }, 1000) + } + } +} +``` + +### 文件数量限制 + +```html + +``` + +### 文件大小限制 + +超过限制会被阻止读取,可以通过监听 `oversize` 事件获取文件。 + +```html + +``` + +```js +import { Snackbar } from '@varlet-vue2/ui' + +export default { + data: () =>({ + files: [] + }), + methods: { + handleOversize() { + Snackbar.warning('文件大小超出限制') + } + } +} +``` + +### 上传预处理 + +通过注册 `before-read` 事件对文件进行操作,返回假值阻止文件读取。 + +```html + +``` + +```js +import { Snackbar } from '@varlet-vue2/ui' + +export default { + data: () => ({ + files: [] + }), + methods: { + handleBeforeRead() { + if (file.file.size <= 1 * 1024 * 1024) { + Snackbar.success('文件小于1M,可以上传') + return true + } else { + Snackbar.warning('文件大于1M,不能上传') + return false + } + } + } +} +``` + +### 禁用 + +```html + +``` + +### 只读 + +```html + +``` + +### 删除前处理 + +删除文件之前会触发 `before-remove` 事件,返回假值阻止删除操作。 + +```html + +``` + +```js +import { Dialog } from '@varlet-vue2/ui' + +export default { + data: () => ({ + files: [ + { + url: 'https://varlet.gitee.io/varlet-ui/cat.jpg', + cover: 'https://varlet.gitee.io/varlet-ui/cat.jpg', + } + ] + }), + methods: { + async handleBeforeRemove() { + const action = await Dialog({ + title: '是否删除?', + message: '删除后无法撤回' + }) + + return action === 'confirm' + } + } +} +``` + +### 自定义上传样式 + +```html + + 上传 + +``` + +### 字段校验 + +通过传入一个校验器数组可以对值进行校验,校验器返回 `true` 则为校验通过。 +以外的值将转换为文本作为用户提示。 +第二个参数是一个工具函数集合,可以快速获取符合状态的文件集合。 + +```html + +``` + +## API + +### 属性 + +| 参数 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| `v-model` | 文件列表 | _VarFile[]_ | `[]` | +| `accept` | 接受的文件类型,与原生属性一致 | _string_ | `image/*` | +| `capture` | 获取文件方式,与原生属性一致 | _string_ | `-` | +| `multiple` | 是否多选文件 | _boolean_ | `false` | +| `readonly` | 是否只读 | _boolean_ | `false` | +| `disabled` | 是否禁用 | _boolean_ | `false` | +| `removable` | 是否可以删除 | _boolean_ | `true` | +| `maxlength` | 最大文件个数 | _string \| number_ | `-` | +| `maxsize` | 最大文件大小 | _string \| number_ | `-` | +| `previewed` | 是否允许预览 | _boolean_ | `true` | +| `ripple` | 是否开启水波纹 | _boolean_ | `true` | +| `validate-trigger` | 触发验证的时机, 可选值为 `onChange` `onRemove` | _ValidateTriggers[]_ | `['onChange', 'onRemove']` | +| `rules` | 验证规则,返回 `true` 表示验证通过,其余的值则转换为文本作为用户提示 | _Array<(v: VarFile, u: VarFileUtils) => any>_ | `-` | + +### VarFile + +| 参数 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| `file` | 原生文件 | _File_ | `-` | +| `name` | 文件名 | _string_ | `-` | +| `url` | 文件地址 | _string_ | `-` | +| `cover` | 封面图 | _string_ | `-` | +| `fit` | 封面图填充模式,可选值为 `fill` `contain` `cover` `none` `scale-down` | _string_ | `-` | +| `state` | 文件上传状态,可选值为 `loading` `success` `error` | _string_ | `-` | + +### VarFileUtils + +| 方法名 | 说明 | 参数 | 返回值 | +| --- | --- | --- | --- | +| `getLoading` | 获取 `state` 等于 `loading` 的文件集合| `VarFile[]` | `VarFile[]` | +| `getSuccess` | 获取 `state` 等于 `success` 的文件集合| `VarFile[]` | `VarFile[]` | +| `getError` | 获取 `state` 等于 `error` 的文件集合 | `VarFile[]` | `VarFile[]` | + +### 方法 + +| 方法名 | 说明 | 参数 | 返回值 | +| --- | --- | --- | --- | +| `getLoading` | 获取 `state` 等于 `loading` 的文件集合| `VarFile[]` | `VarFile[]` | +| `getSuccess` | 获取 `state` 等于 `success` 的文件集合| `VarFile[]` | `VarFile[]` | +| `getError` | 获取 `state` 等于 `error` 的文件集合 | `VarFile[]` | `VarFile[]` | +| `validate` | 触发校验 | `-` | `valid: Promise` | +| `resetValidation` | 清空校验信息 | `-` | `-` | +| `reset` | 清空绑定的值(设置为 `[]`)和校验信息 | `-` | `-` | + +### 事件 + +| 事件名 | 说明 | 参数 | +| --- | --- | --- | +| `before-read` | 文件读取前触发,返回假值阻止文件读取(支持 promise) | `file: VarFile` | +| `after-read` | 文件读取后触发 | `file: VarFile` | +| `oversize` | 文件超过限制大小时触发 | `file: VarFile` | +| `before-remove` | 文件删除前触发,返回假值阻止文件删除(支持 promise) | `file: VarFile` | +| `remove` | 文件删除时触发 | `file: VarFile` | + +### 插槽 + +| 插槽名 | 说明 | 参数 | +| --- | --- | --- | +| `default` | 上传按钮内容 | `-` | + +### 样式变量 + +以下为组件使用的 css 变量,可以使用 [StyleProvider 组件](#/zh-CN/style-provider)进行样式定制 + +| 变量名 | 默认值 | +| --- | --- | +| `--uploader-action-background` | `#f7f8fa` | +| `--uploader-action-icon-color` | `#888` | +| `--uploader-action-icon-size` | `24px` | +| `--uploader-action-margin` | `0 10px 10px 0` | +| `--uploader-file-size` | `80px` | +| `--uploader-file-margin` | `0 10px 10px 0` | +| `--uploader-file-name-background` | `#f7f8fa` | +| `--uploader-file-name-color` | `#888` | +| `--uploader-file-name-font-size` | `12px` | +| `--uploader-file-name-padding` | `10px` | +| `--uploader-file-text-align` | `center` | +| `--uploader-file-close-background` | `rgba(0, 0, 0, 0.3)` | +| `--uploader-file-close-size` | `24px` | +| `--uploader-file-close-icon-font-size` | `14px` | +| `--uploader-file-close-icon-color` | `#fff` | +| `--uploader-file-cover-fit` | `cover` | +| `--uploader-file-cover-background` | `#f7f8fa` | +| `--uploader-preview-video-width` | `100vw` | +| `--uploader-preview-video-height` | `100vw` | +| `--uploader-file-indicator-height` | `4px` | +| `--uploader-file-indicator-normal-color` | `var(--color-disabled)` | +| `--uploader-file-indicator-success-color` | `var(--color-success)` | +| `--uploader-file-indicator-error-color` | `var(--color-danger)` | +| `--uploader-disabled-color` | `#ddd` | +| `--uploader-disabled-text-color` | `var(--color-text-disabled)` | +| `--uploader-loading-background` | `linear-gradient(90deg, hsla(0, 0%, 100%, 0), hsla(0, 0%, 100%, 0.3), hsla(0, 0%, 100%, 0))` | diff --git a/packages/varlet-vue2-ui/src/uploader/example/index.vue b/packages/varlet-vue2-ui/src/uploader/example/index.vue new file mode 100644 index 0000000..622d0e6 --- /dev/null +++ b/packages/varlet-vue2-ui/src/uploader/example/index.vue @@ -0,0 +1,165 @@ + + + + + diff --git a/packages/varlet-vue2-ui/src/uploader/example/locale/en-US.ts b/packages/varlet-vue2-ui/src/uploader/example/locale/en-US.ts new file mode 100644 index 0000000..73582fd --- /dev/null +++ b/packages/varlet-vue2-ui/src/uploader/example/locale/en-US.ts @@ -0,0 +1,17 @@ +export default { + basicUsage: 'Basic Usage', + preview: 'File Preview', + state: 'Upload State', + maxlength: 'File Maxlength', + maxsize: 'File Size Limit', + beforeRead: 'Upload Preprocessing', + disabled: 'Disabled', + readonly: 'Readonly', + validate: 'Validate', + validateMessage: 'There is a file that failed to upload', + style: 'Customize upload styles', + upload: 'Upload', + beforeRemove: 'Remove Preprocessing', + removeTitle: 'Delete or not?', + removeMessage: 'Cannot be withdrawn after deletion', +} diff --git a/packages/varlet-vue2-ui/src/uploader/example/locale/index.ts b/packages/varlet-vue2-ui/src/uploader/example/locale/index.ts new file mode 100644 index 0000000..d2e375e --- /dev/null +++ b/packages/varlet-vue2-ui/src/uploader/example/locale/index.ts @@ -0,0 +1,23 @@ +// lib +import _zhCN from '../../../locale/zh-CN' +import _enCN from '../../../locale/en-US' +// mobile example doc +import zhCN from './zh-CN' +import enUS from './en-US' +import { useLocale, add as _add, use as _use } from '../../../locale' + +const { add, use: exampleUse, pack, packs, merge } = useLocale() + +const use = (lang: string) => { + _use(lang) + exampleUse(lang) +} + +export { add, pack, packs, merge, use } + +// lib +_add('zh-CN', _zhCN) +_add('en-US', _enCN) +// mobile example doc +add('zh-CN', zhCN as any) +add('en-US', enUS as any) diff --git a/packages/varlet-vue2-ui/src/uploader/example/locale/zh-CN.ts b/packages/varlet-vue2-ui/src/uploader/example/locale/zh-CN.ts new file mode 100644 index 0000000..d396820 --- /dev/null +++ b/packages/varlet-vue2-ui/src/uploader/example/locale/zh-CN.ts @@ -0,0 +1,17 @@ +export default { + basicUsage: '基本使用', + preview: '文件预览', + state: '上传状态', + maxlength: '文件数量限制', + maxsize: '文件大小限制', + beforeRead: '上传预处理', + disabled: '禁用', + readonly: '只读', + validate: '字段校验', + validateMessage: '存在上传失败的文件', + style: '自定义上传样式', + upload: '上传', + beforeRemove: '删除前处理', + removeTitle: '是否删除?', + removeMessage: '删除后无法撤回', +} diff --git a/packages/varlet-vue2-ui/src/uploader/index.ts b/packages/varlet-vue2-ui/src/uploader/index.ts new file mode 100644 index 0000000..d0488c0 --- /dev/null +++ b/packages/varlet-vue2-ui/src/uploader/index.ts @@ -0,0 +1,10 @@ +import type { VueConstructor } from 'vue' +import Uploader from './Uploader.vue' + +Uploader.install = function (app: VueConstructor) { + app.component(Uploader.name, Uploader) +} + +export const _UploaderComponent = Uploader + +export default Uploader diff --git a/packages/varlet-vue2-ui/src/uploader/props.ts b/packages/varlet-vue2-ui/src/uploader/props.ts new file mode 100644 index 0000000..134b726 --- /dev/null +++ b/packages/varlet-vue2-ui/src/uploader/props.ts @@ -0,0 +1,65 @@ +import type { PropType } from 'vue' + +export interface VarFile { + id: number + file?: File + name?: string + url?: string + cover?: string + fit?: 'fill' | 'contain' | 'cover' | 'none' | 'scale-down' + state?: 'loading' | 'success' | 'error' +} + +export type ValidateTriggers = 'onChange' | 'onRemove' + +export const props = { + value: { + type: Array as PropType, + default: () => [], + }, + accept: { + type: String, + default: 'image/*', + }, + capture: { + type: [String, Boolean] as PropType, + default: undefined, + }, + multiple: { + type: Boolean, + default: false, + }, + readonly: { + type: Boolean, + default: false, + }, + disabled: { + type: Boolean, + default: false, + }, + removable: { + type: Boolean, + default: true, + }, + maxlength: { + type: [Number, String], + }, + maxsize: { + type: [Number, String], + }, + previewed: { + type: Boolean, + default: true, + }, + ripple: { + type: Boolean, + default: true, + }, + validateTrigger: { + type: Array as PropType>, + default: () => ['onChange', 'onRemove'], + }, + rules: { + type: Array as PropType any>>, + }, +} diff --git a/packages/varlet-vue2-ui/src/uploader/uploader.less b/packages/varlet-vue2-ui/src/uploader/uploader.less new file mode 100644 index 0000000..a533f6b --- /dev/null +++ b/packages/varlet-vue2-ui/src/uploader/uploader.less @@ -0,0 +1,219 @@ +@uploader-action-background: #f7f8fa; +@uploader-action-icon-color: #888; +@uploader-action-icon-size: 24px; +@uploader-action-margin: 0 10px 10px 0; +@uploader-file-size: 80px; +@uploader-file-margin: 0 10px 10px 0; +@uploader-file-name-background: #f7f8fa; +@uploader-file-name-color: #888; +@uploader-file-name-font-size: 12px; +@uploader-file-name-padding: 10px; +@uploader-file-text-align: center; +@uploader-file-close-background: rgba(0, 0, 0, 0.3); +@uploader-file-close-size: 24px; +@uploader-file-close-icon-font-size: 14px; +@uploader-file-close-icon-color: #fff; +@uploader-file-cover-fit: cover; +@uploader-file-cover-background: #f7f8fa; +@uploader-preview-video-width: 100vw; +@uploader-preview-video-height: 100vw; +@uploader-file-indicator-height: 4px; +@uploader-file-indicator-normal-color: var(--color-disabled); +@uploader-file-indicator-success-color: var(--color-success); +@uploader-file-indicator-error-color: var(--color-danger); +@uploader-disabled-color: var(--color-disabled); +@uploader-disabled-text-color: var(--color-text-disabled); +@uploader-loading-background: linear-gradient( + 90deg, + hsla(0, 0%, 100%, 0), + hsla(0, 0%, 100%, 0.3), + hsla(0, 0%, 100%, 0) +); + +:root { + --uploader-action-background: @uploader-action-background; + --uploader-action-icon-color: @uploader-action-icon-color; + --uploader-action-icon-size: @uploader-action-icon-size; + --uploader-action-margin: @uploader-action-margin; + --uploader-file-size: @uploader-file-size; + --uploader-file-margin: @uploader-file-margin; + --uploader-file-name-background: @uploader-file-name-background; + --uploader-file-name-color: @uploader-file-name-color; + --uploader-file-name-font-size: @uploader-file-name-font-size; + --uploader-file-name-padding: @uploader-file-name-padding; + --uploader-file-text-align: @uploader-file-text-align; + --uploader-file-close-background: @uploader-file-close-background; + --uploader-file-close-size: @uploader-file-close-size; + --uploader-file-close-icon-font-size: @uploader-file-close-icon-font-size; + --uploader-file-close-icon-color: @uploader-file-close-icon-color; + --uploader-file-cover-fit: @uploader-file-cover-fit; + --uploader-file-cover-background: @uploader-file-cover-background; + --uploader-preview-video-width: @uploader-preview-video-width; + --uploader-preview-video-height: @uploader-preview-video-height; + --uploader-file-indicator-height: @uploader-file-indicator-height; + --uploader-file-indicator-normal-color: @uploader-file-indicator-normal-color; + --uploader-file-indicator-success-color: @uploader-file-indicator-success-color; + --uploader-file-indicator-error-color: @uploader-file-indicator-error-color; + --uploader-disabled-color: @uploader-disabled-color; + --uploader-disabled-text-color: @uploader-disabled-text-color; + --uploader-loading-background: @uploader-loading-background; +} + +@keyframes var-uploader-loading-animation { + from { + transform: translateX(-100%); + } + + to { + transform: translateX(100%); + } +} + +.var-uploader { + width: 100%; + + &__file-list { + width: 100%; + display: flex; + flex-wrap: wrap; + } + + &__file { + position: relative; + width: var(--uploader-file-size); + height: var(--uploader-file-size); + margin: var(--uploader-file-margin); + overflow: hidden; + cursor: pointer; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); + } + + &__file-name { + position: absolute; + width: 100%; + height: 100%; + line-height: calc(var(--uploader-file-size) - var(--uploader-file-name-padding) * 2); + font-size: var(--uploader-file-name-font-size); + z-index: 0; + background: var(--uploader-file-name-background); + padding: var(--uploader-file-name-padding); + color: var(--uploader-file-name-color); + text-align: var(--uploader-file-text-align); + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + user-select: none; + transition: background-color 0.25s; + } + + &__file-cover { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 1; + background: var(--uploader-file-cover-background); + object-fit: var(--uploader-file-cover-fit); + pointer-events: none; + transition: background-color 0.25s; + } + + &__file-close { + position: absolute; + top: 0; + right: 0; + z-index: 2; + border-top: var(--uploader-file-close-size) solid var(--uploader-file-close-background); + border-left: var(--uploader-file-close-size) solid transparent; + } + + &__file-close-icon[var-uploader-cover] { + position: absolute; + top: calc(-1 * var(--uploader-file-close-size)); + right: 0; + font-size: var(--uploader-file-close-icon-font-size); + color: var(--uploader-file-close-icon-color); + } + + &__file-indicator { + position: absolute; + left: 0; + bottom: 0; + z-index: 2; + width: 100%; + height: var(--uploader-file-indicator-height); + transition: all 0.25s; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + background: var(--uploader-file-indicator-normal-color); + } + + &__action { + position: relative; + display: flex; + justify-content: center; + align-items: center; + color: var(--uploader-action-icon-color); + width: var(--uploader-file-size); + height: var(--uploader-file-size); + background: var(--uploader-action-background); + margin: var(--uploader-action-margin); + transition: background-color 0.25s; + } + + &__action-icon[var-uploader-cover] { + font-size: var(--uploader-action-icon-size); + } + + &__action-input { + position: absolute; + z-index: 1; + width: 100%; + height: 100%; + top: 0; + left: 0; + opacity: 0; + cursor: pointer; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); + } + + &__preview[var-uploader-cover] { + background: #000; + box-shadow: none; + } + + &__preview-video { + width: var(--uploader-preview-video-width); + height: var(--uploader-preview-video-height); + } + + &--loading { + &::after { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 1; + animation: var-uploader-loading-animation 0.6s infinite; + background: var(--uploader-loading-background); + transition: background-color 0.25s; + } + } + + &--success { + background: var(--uploader-file-indicator-success-color); + } + + &--error { + background: var(--uploader-file-indicator-error-color); + } + + &--disabled { + background: var(--uploader-disabled-color); + color: var(--uploader-disabled-text-color); + } +} diff --git a/packages/varlet-vue2-ui/types/index.d.ts b/packages/varlet-vue2-ui/types/index.d.ts index e69e73e..78ed214 100644 --- a/packages/varlet-vue2-ui/types/index.d.ts +++ b/packages/varlet-vue2-ui/types/index.d.ts @@ -57,5 +57,6 @@ export * from './list' export * from './imagePreview' export * from './collapse' export * from './collapseItem' +export * from './uploader' export * from './varComponent' export * from './varDirective' diff --git a/packages/varlet-vue2-ui/types/uploader.d.ts b/packages/varlet-vue2-ui/types/uploader.d.ts new file mode 100644 index 0000000..259659b --- /dev/null +++ b/packages/varlet-vue2-ui/types/uploader.d.ts @@ -0,0 +1,51 @@ +import { VarComponent } from './varComponent' + +export interface VarFile { + file?: File + name?: string + url?: string + cover?: string + fit?: 'fill' | 'contain' | 'cover' | 'none' | 'scale-down' + state?: 'loading' | 'success' | 'error' +} + +export type UploaderValidateTriggers = 'onChange' | 'onRemove' + +interface UploaderProps { + value?: VarFile[] + accept?: string + capture?: boolean | 'user' | 'environment' + multiple?: boolean + readonly?: boolean + disabled?: boolean + removable?: boolean + maxlength?: string | number + maxsize?: string | number + previewed?: boolean + ripple?: boolean + validateTrigger?: Array + rules?: Array<(v: VarFile) => any> + onBeforeRead?: (file: VarFile) => Promise | boolean + onAfterRead?: (file: VarFile) => any + onOversize?: (file: VarFile) => any + onRemove?: (file: VarFile) => any + onInput?: (files: VarFile[]) => any +} + +export class Uploader extends VarComponent { + $props: UploaderProps + + getLoading(varFiles: VarFile[]): VarFile[] + + getSuccess(varFiles: VarFile[]): VarFile[] + + getError(varFiles: VarFile[]): VarFile[] + + validate(): Promise + + resetValidation(): void + + reset(): void +} + +export class _UploaderComponent extends Uploader {}