diff --git a/example/app.json b/example/app.json
index d2af4ef2..7e6a7040 100644
--- a/example/app.json
+++ b/example/app.json
@@ -37,6 +37,7 @@
"pages/checkbox/index",
"pages/goods-action/index",
"pages/swipe-cell/index",
+ "pages/uploader/index",
"pages/datetime-picker/index",
"pages/rate/index",
"pages/collapse/index",
@@ -92,6 +93,7 @@
"van-sticky": "./dist/sticky/index",
"van-submit-bar": "./dist/submit-bar/index",
"van-swipe-cell": "./dist/swipe-cell/index",
+ "van-uploader": "./dist/uploader/index",
"van-switch": "./dist/switch/index",
"van-tab": "./dist/tab/index",
"van-tabs": "./dist/tabs/index",
diff --git a/example/config.js b/example/config.js
index e6efe99d..68487ff9 100644
--- a/example/config.js
+++ b/example/config.js
@@ -76,6 +76,10 @@ export default [
{
path: '/switch',
title: 'Switch 开关'
+ },
+ {
+ path: '/uploader',
+ title: 'Uploader 文件上传'
}
]
},
diff --git a/example/pages/uploader/index.js b/example/pages/uploader/index.js
new file mode 100644
index 00000000..10c015d0
--- /dev/null
+++ b/example/pages/uploader/index.js
@@ -0,0 +1,44 @@
+import Page from '../../common/page';
+
+Page({
+ data: {
+ fileList1: [],
+ fileList2: [
+ { url: 'https://img.yzcdn.cn/vant/cat.jpeg', name: '图片1' },
+ { url: 'http://iph.href.lu/60x60?text=default', name: '图片2', isImage: true }
+ ],
+ fileList3: [],
+ fileList4: [],
+ fileList5: []
+ },
+
+ beforeRead(event) {
+ const { file, callback = () => {} } = event.detail;
+ if (file.path.indexOf('jpeg') < 0) {
+ wx.showToast({ title: '请选择jpg图片上传', icon: 'none' });
+ callback(false);
+ return;
+ }
+ callback(true);
+ },
+
+ afterRead(event) {
+ const { file, name } = event.detail;
+ const fileList = this.data[`fileList${name}`];
+
+ this.setData({ [`fileList${name}`]: fileList.concat(file) });
+ },
+
+ oversize() {
+ wx.showToast({ title: '文件超出大小限制', icon: 'none' });
+ },
+
+ delete(event) {
+ const { index, name } = event.detail;
+ const fileList = this.data[`fileList${name}`];
+ fileList.splice(index, 1);
+ this.setData({ [`fileList${name}`]: fileList });
+ },
+
+ clickPreview() {}
+});
diff --git a/example/pages/uploader/index.json b/example/pages/uploader/index.json
new file mode 100644
index 00000000..c142147a
--- /dev/null
+++ b/example/pages/uploader/index.json
@@ -0,0 +1,3 @@
+{
+ "navigationBarTitleText": "Uploader 文件上传"
+}
diff --git a/example/pages/uploader/index.wxml b/example/pages/uploader/index.wxml
new file mode 100644
index 00000000..81651c3c
--- /dev/null
+++ b/example/pages/uploader/index.wxml
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 上传图片
+
+
+
+
+
+
diff --git a/example/pages/uploader/index.wxss b/example/pages/uploader/index.wxss
new file mode 100644
index 00000000..10416bfb
--- /dev/null
+++ b/example/pages/uploader/index.wxss
@@ -0,0 +1 @@
+/* pages/tag/index.wxss */
\ No newline at end of file
diff --git a/packages/common/style/var.less b/packages/common/style/var.less
index edaf39f1..c3f7e8e4 100644
--- a/packages/common/style/var.less
+++ b/packages/common/style/var.less
@@ -279,6 +279,22 @@
@divider-content-left-width: 10%;
@divider-content-right-width: 10%;
+// Uploader
+@uploader-size: 80px;
+@uploader-icon-size: 24px;
+@uploader-icon-color: @gray-dark;
+@uploader-text-color: @gray-dark;
+@uploader-text-font-size: @font-size-sm;
+@uploader-upload-border-color: @gray-light;
+@uploader-upload-background-color: @white;
+@uploader-delete-color: @white;
+@uploader-delete-background-color: rgba(0, 0, 0, .45);
+@uploader-file-background-color: @background-color;
+@uploader-file-icon-size: 20px;
+@uploader-file-icon-color: @gray-darker;
+@uploader-file-name-font-size: @font-size-sm;
+@uploader-file-name-text-color: @gray-darker;
+
// DropdownMenu
@dropdown-menu-height: 50px;
@dropdown-menu-background-color: @white;
diff --git a/packages/uploader/README.md b/packages/uploader/README.md
new file mode 100644
index 00000000..56c5983e
--- /dev/null
+++ b/packages/uploader/README.md
@@ -0,0 +1,152 @@
+# Upload 上传文件
+
+### 引入
+
+在`app.json`或`index.json`中引入组件,默认为`ES6`版本,`ES5`引入方式参见[快速上手](#/quickstart)
+
+```json
+"usingComponents": {
+ "van-uploader": "path/to/vant-weapp/dist/uploader/index"
+}
+```
+
+## 代码演示
+
+### 基础用法
+
+文件上传完毕后会触发`after-read`回调函数,获取到对应的文件的临时地址,然后再使用`wx.uploadFile`将图片上传到远程服务器上
+
+#### wxml 示例
+
+```html
+
+```
+
+#### js 示例
+
+```js
+{
+ data: {
+ fileList: []
+ },
+ methods: {
+ afterRead(event) {
+ const { file } = event.detail;
+ // 当设置 mutiple 为 true 是 file 是一个数组,mutiple 默认为 false,file 是一个对象
+ wx.uploadFile({
+ url: 'https://example.weixin.qq.com/upload', //仅为示例,非真实的接口地址
+ filePath: file.path,
+ name: 'file',
+ formData: { 'user': 'test' },
+ success (res){
+ // 上传完成需要更新fileList
+ const { fileList = [] } = this.data;
+ fileList.push({ ...file, url: res.data });
+ this.setData({ fileList });
+ }
+ });
+ }
+ }
+}
+```
+
+### 图片预览
+
+通过向组件传入`file-list`属性,可以绑定已经上传的图片列表,并展示图片列表的预览图
+
+```html
+
+```
+
+```js
+{
+ data: {
+ fileList: [
+ { url: 'https://img.yzcdn.cn/vant/cat.jpeg', name: '图片1' },
+ // Uploader 根据文件后缀来判断是否为图片文件
+ // 如果图片 URL 中不包含类型信息,可以添加 isImage 标记来声明
+ { url: 'http://iph.href.lu/60x60?text=default', name: '图片2', isImage: true }
+ ]
+ }
+}
+```
+
+### 限制上传数量
+
+通过`max-count`属性可以限制上传文件的数量,上传数量达到限制后,会自动隐藏上传区域
+
+```html
+
+```
+
+### 自定义上传样式
+
+将`use-slot`属性设置为`true`,通过插槽可以自定义上传区域的样式
+
+```html
+
+ 上传图片
+
+```
+
+### 上传前校验
+
+将`use-before-read`属性设置为`true`,然后绑定 `before-read` 事件可以在上传前进行校验,调用 `callback` 方法传入 `true` 表示校验通过,传入 `false` 表示校验失败。
+
+```html
+
+```
+
+```js
+{
+ data: {
+ fileList: []
+ },
+
+ methods: {
+ beforeRead(event) {
+ const { file, callback = () => {} } = event.detail;
+ console.log('before上传', file);
+ if (file.type !== 'image') {
+ callback(false);
+ return;
+ }
+ callback(true);
+ }
+ }
+}
+```
+
+### Props
+
+| 参数 | 说明 | 类型 | 默认值 | 版本 |
+|-----------|-----------|-----------|-----------|-----------|
+| name | 标识符,可以在回调函数的第二项参数中获取 | _string \| number_ | - |
+| accept | 接受的文件类型, 可选值为`all` `image` `file` | _string_ | `image` |
+| preview-size | 预览图和上传区域的尺寸,默认单位为`px` | _string \| number_ | `80px` |
+| preview-image | 是否在上传完成后展示预览图 | _boolean_ | `true` |
+| preview-full-image | 是否在点击预览图后展示全屏图片预览 | _boolean_ | `true` |
+| multiple | 是否开启图片多选,部分安卓机型不支持 | _boolean_ | `false` |
+| disabled | 是否禁用文件上传 | _boolean_ | `false` |
+| capture | 图片选取模式,当`accept`为`image`类型时设置`capture`可选值为`camera`可以直接调起摄像头 | _string \| Array_ | `['album', 'camera']` |
+| disabled | 是否禁用文件上传 | _boolean_ | `false` |
+| max-size | 文件大小限制,单位为`byte` | _number_ | - |
+| max-count | 文件上传数量限制 | _number_ | - |
+| upload-text | 上传区域文字提示 | _string_ | - |
+| image-fit | 预览图裁剪模式,可选值参考小程序`image`组件的`mode`属性 | _string_ | `scaleToFill` |
+
+### Slot
+
+| 名称 | 说明 |
+| ---- | -------------------------------- |
+| - | 自定义上传样式,用法见上面的例子 |
+
+### Event
+
+| 事件名 | 说明 | 参数 |
+| ------------------ | -------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
+| bind:before-read | 文件读取前的回调函数,返回 `false` 可终止文件读取,绑定事件的同时需要将`use-before-read`属性设置为`true` | `event.detail.file`: 当前读取的文件,`event.detail.callback`: 回调函数,调用`callback(false)`终止文件读取 |
+| bind:after-read | 文件读取完成后的回调函数 | `event.detail.file`: 当前读取的文件 |
+| bind:oversize | 文件超出大小限制的回调函数 | - |
+| bind:click-preview | 点击预览图片的回调函数 | `event.detail.index`: 点击图片的序号值 |
+| bind:delete | 删除图片的回调函数 | `event.detail.index`: 删除图片的序号值 |
diff --git a/packages/uploader/index.json b/packages/uploader/index.json
new file mode 100644
index 00000000..0a336c08
--- /dev/null
+++ b/packages/uploader/index.json
@@ -0,0 +1,6 @@
+{
+ "component": true,
+ "usingComponents": {
+ "van-icon": "../icon/index"
+ }
+}
diff --git a/packages/uploader/index.less b/packages/uploader/index.less
new file mode 100644
index 00000000..159ebbcf
--- /dev/null
+++ b/packages/uploader/index.less
@@ -0,0 +1,91 @@
+@import '../common/style/var.less';
+
+.van-uploader {
+ position: relative;
+ display: inline-block;
+
+ &__wrapper {
+ display: flex;
+ flex-wrap: wrap;
+ }
+
+ &__upload {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ box-sizing: border-box;
+ width: @uploader-size;
+ height: @uploader-size;
+ margin: 0 @padding-xs @padding-xs 0;
+ background-color: @uploader-upload-background-color;
+ border: 1px dashed @uploader-upload-border-color;
+
+ &-icon {
+ display: inline-block;
+ width: 24px;
+ height: 24px;
+ color: @uploader-icon-color;
+ font-size: @uploader-icon-size;
+ }
+
+ &-text {
+ margin-top: @padding-xs;
+ color: @uploader-text-color;
+ font-size: @uploader-text-font-size;
+ }
+ }
+
+ &__preview {
+ position: relative;
+ margin: 0 @padding-xs @padding-xs 0;
+
+ &-image {
+ display: block;
+ width: @uploader-size;
+ height: @uploader-size;
+ }
+
+ &-delete {
+ position: absolute;
+ right: 0;
+ bottom: 0;
+ display: inline-block;
+ width: 18px;
+ height: 18px;
+ padding: 1px;
+ color: @uploader-delete-color;
+ background-color: @uploader-delete-background-color;
+ text-align: center;
+ }
+ }
+
+ &__file {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ width: @uploader-size;
+ height: @uploader-size;
+ background-color: @uploader-file-background-color;
+
+ &-icon {
+ display: inline-block;
+ width: 20px;
+ height: 20px;
+ color: @uploader-file-icon-color;
+ font-size: @uploader-file-icon-size;
+ }
+
+ &-name {
+ box-sizing: border-box;
+ width: 100%;
+ margin-top: @padding-xs;
+ padding: 0 5px;
+ color: @uploader-file-name-text-color;
+ font-size: @uploader-file-name-font-size;
+ text-align: center;
+ }
+ }
+}
diff --git a/packages/uploader/index.ts b/packages/uploader/index.ts
new file mode 100644
index 00000000..e8018507
--- /dev/null
+++ b/packages/uploader/index.ts
@@ -0,0 +1,186 @@
+import { VantComponent } from '../common/component';
+import { isImageFile } from './utils';
+import { addUnit } from '../common/utils';
+
+interface File {
+ path: string; // 上传临时地址
+ url: string; // 上传临时地址
+ size: number; // 上传大小
+ name: string; // 上传文件名称,accept="image" 不存在
+ type: string; // 上传类型,accept="image" 不存在
+ time: number; // 上传时间,accept="image" 不存在
+ image: boolean; // 是否为图片
+}
+
+VantComponent({
+ props: {
+ disabled: Boolean,
+ uploadText: String,
+ previewSize: {
+ type: null,
+ value: 90,
+ observer: 'setComputedPreviewSize'
+ },
+ name: {
+ type: [Number, String],
+ value: ''
+ },
+ accept: {
+ type: String,
+ value: 'image'
+ },
+ fileList: {
+ type: Array,
+ value: [],
+ observer: 'formatFileList'
+ },
+ maxSize: {
+ type: Number,
+ value: Number.MAX_VALUE
+ },
+ maxCount: {
+ type: Number,
+ value: 100
+ },
+ previewImage: {
+ type: Boolean,
+ value: true
+ },
+ previewFullImage: {
+ type: Boolean,
+ value: true
+ },
+ imageFit: {
+ type: String,
+ value: 'scaleToFill'
+ },
+ useSlot: Boolean,
+ useBeforeRead: Boolean
+ },
+
+ data: {
+ lists: [],
+ computedPreviewSize: '',
+ isInCount: true
+ },
+
+ methods: {
+ formatFileList() {
+ const { fileList = [], maxCount } = this.data;
+ const lists = fileList.map(item => ({
+ ...item,
+ isImage:
+ typeof item.isImage === 'undefined'
+ ? isImageFile(item)
+ : item.isImage
+ }));
+ this.setData({ lists, isInCount: lists.length < maxCount });
+ },
+
+ setComputedPreviewSize(val) {
+ this.setData({
+ computedPreviewSize: addUnit(val)
+ });
+ },
+
+ startUpload() {
+ if (this.data.disabled) return;
+ const {
+ name = '',
+ capture = ['album', 'camera'],
+ maxCount = 100,
+ multiple = false,
+ maxSize,
+ accept,
+ lists,
+ useBeforeRead = false // 是否定义了 beforeRead
+ } = this.data;
+
+ let chooseFile = null;
+ const newMaxCount = maxCount - lists.length;
+ // 设置为只选择图片的时候使用 chooseImage 来实现
+ if (accept === 'image') {
+ chooseFile = new Promise((resolve, reject) => {
+ wx.chooseImage({
+ count: multiple ? (newMaxCount > 9 ? 9 : newMaxCount) : 1, // 最多可以选择的数量,如果不支持多选则数量为1
+ sourceType: capture, // 选择图片的来源,相册还是相机
+ success: res => {
+ resolve(res);
+ },
+ fail: err => {
+ reject(err);
+ }
+ });
+ });
+ } else {
+ chooseFile = new Promise((resolve, reject) => {
+ wx.chooseMessageFile({
+ count: multiple ? newMaxCount : 1, // 最多可以选择的数量,如果不支持多选则数量为1
+ type: 'file',
+ success(res) {
+ resolve(res);
+ },
+ fail: err => {
+ reject(err);
+ }
+ });
+ });
+ }
+
+ chooseFile.then(res => {
+ const file: File | File[] = multiple ? res.tempFiles : res.tempFiles[0];
+
+ // 检查文件大小
+ if (file instanceof Array) {
+ const sizeEnable = file.every(item => item.size <= maxSize);
+ if (!sizeEnable) {
+ this.$emit('oversize', { name });
+ return;
+ }
+ } else if (file.size > maxSize) {
+ this.$emit('oversize', { name });
+ return;
+ }
+
+ // 触发上传之前的钩子函数
+ if (useBeforeRead) {
+ this.$emit('before-read', {
+ file,
+ name,
+ callback: result => {
+ if (result) {
+ // 开始上传
+ this.$emit('after-read', { file, name });
+ }
+ }
+ });
+ } else {
+ this.$emit('after-read', { file, name });
+ }
+ });
+ },
+
+ deleteItem(event) {
+ const { index } = event.currentTarget.dataset;
+ this.$emit('delete', { index, name: this.data.name });
+ },
+
+ doPreviewImage(event) {
+ if (!this.data.previewFullImage) return;
+ const curUrl = event.currentTarget.dataset.url;
+ const images = this.data.lists
+ .filter(item => item.isImage)
+ .map(item => item.url || item.path);
+
+ this.$emit('click-preview', { url: curUrl, name: this.data.name });
+
+ wx.previewImage({
+ urls: images,
+ current: curUrl,
+ fail() {
+ wx.showToast({ title: '预览图片失败', icon: 'none' });
+ }
+ });
+ }
+ }
+});
diff --git a/packages/uploader/index.wxml b/packages/uploader/index.wxml
new file mode 100644
index 00000000..a57cfda2
--- /dev/null
+++ b/packages/uploader/index.wxml
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
+
+
+ {{ item.name || item.url || item.path }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ uploadText }}
+
+
+
+
+
diff --git a/packages/uploader/utils.ts b/packages/uploader/utils.ts
new file mode 100644
index 00000000..52776021
--- /dev/null
+++ b/packages/uploader/utils.ts
@@ -0,0 +1,31 @@
+interface File {
+ path: string; // 上传临时地址
+ url: string; // 上传临时地址
+ size: number; // 上传大小
+ name: string; // 上传文件名称,accept="image" 不存在
+ type: string; // 上传类型,accept="image" 不存在
+ time: number; // 上传时间,accept="image" 不存在
+ image: boolean; // 是否为图片
+}
+
+const IMAGE_EXT = ['jpeg', 'jpg', 'gif', 'png', 'svg'];
+
+export function isImageUrl(url: string): boolean {
+ return IMAGE_EXT.some(ext => url.indexOf(`.${ext}`) !== -1);
+}
+
+export function isImageFile(item: File): boolean {
+ if (item.type) {
+ return item.type.indexOf('image') === 0;
+ }
+
+ if (item.path) {
+ return isImageUrl(item.path);
+ }
+
+ if (item.url) {
+ return isImageUrl(item.url);
+ }
+
+ return false;
+}