提交 0d1ddbbf 编写于 作者: D doly mood 提交者: GitHub

Support Upload & Radio (#74)

* support radio component

* support upload component

* upgrade checkbox-group component
上级 e90253fc
build/*.js
config/*.js
example/modules/*.js
module.exports = {
root: true,
parser: 'babel-eslint',
parserOptions: {
sourceType: 'module'
},
// https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style
extends: 'standard',
// required to lint *.vue files
plugins: [
'html'
],
// add your custom rules here
'rules': {
// allow paren-less arrow functions
'arrow-parens': 0,
// allow async-await
'generator-star-spacing': 0,
// allow debugger during development
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
'no-tabs': 0,
'space-before-function-paren': 0
}
root: true,
parser: 'babel-eslint',
parserOptions: {
sourceType: 'module'
},
// https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style
extends: 'standard',
// required to lint *.vue files
plugins: [
'html'
],
// add your custom rules here
'rules': {
// allow paren-less arrow functions
'arrow-parens': 0,
// allow async-await
'generator-star-spacing': 0,
// allow debugger during development
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
'no-tabs': 0,
'space-before-function-paren': 0
}
}
......@@ -19,6 +19,7 @@
"button": "Button",
"checkbox": "Checkbox",
"checkbox-group": "CheckboxGroup",
"radio": "Radio",
"loading": "Loading",
"tip": "Tip"
}
......@@ -42,6 +43,12 @@
"slide": "Slide",
"index-list": "IndexList"
}
},
"advanced": {
"name": "Advanced",
"subList": {
"upload": "Upload"
}
}
}
},
......@@ -74,6 +81,7 @@
"button": "Button",
"checkbox": "Checkbox",
"checkbox-group": "CheckboxGroup",
"radio": "Radio",
"loading": "Loading",
"tip": "Tip"
}
......@@ -97,6 +105,12 @@
"slide": "Slide",
"index-list": "IndexList"
}
},
"advanced": {
"name": "高级",
"subList": {
"upload": "Upload"
}
}
}
},
......
......@@ -27,6 +27,43 @@
The value of `checkList` is an array, which represents the set of the values of `label` in selected checkboxs.
- Set options
Set options to generate checkboxes.
```html
<cube-checkbox-group v-model="checkList" :options="options" />
```
```js
export default {
data() {
return {
checkList: ['1', '4'],
options: [
{
label: 'Option1',
value: '1'
},
{
label: 'Option2',
value: '2'
},
{
label: 'Option3',
value: '3',
disabled: true
},
{
label: 'Option4',
value: '4',
disabled: true
}
]
}
}
}
```
- Horizontal order
You can set `horizontal` to change the style to horizontal order.
......@@ -45,9 +82,14 @@
| Attribute | Description | Type | Accepted Values | Default |
| - | - | - | - | - |
| horizontal | whether in horizontal order | Boolean | true/false | false |
| options | array of checkbox options | Array | - | - |
### Events
* `options` sub configuration
| Event Name | Description | Parameters |
| Attribute | Description | Type |
| - | - | - |
| input | triggers when the selecting state in the group changes | the set of values of selected checkboxs, which type is Array |
| label | label content | String |
| value | checkbox item value | String/Number |
| disabled | whether disabled | Boolean |
Note: each `options` item can be an string value, now both the`label` and `value` values are the string value.
......@@ -49,12 +49,5 @@
| Attribute | Description | Type | Accepted Values | Default |
| - | - | - | - | - |
| disabled | whether disabled | Boolean | true/false | false |
| position | position | String | left/right | left |
| position | icon position | String | left/right | left |
| label | if selected, then map the value to v-model | Boolean/String | - | '' |
### Events
| Event Name | Description | Parameters |
| - | - | - |
| input | triggers when the selecting state changes | the value of label if setted or boolean value which represents whether selected |
......@@ -27,6 +27,7 @@ cube-ui is an elegant mobile component library based on Vue.js.
- [Button](#/en-US/docs/button)
- [Checkbox](#/en-US/docs/checkbox)
- [CheckboxGroup](#/en-US/docs/checkbox-group)
- [Radio](#/zh-CN/docs/radio)
- [Loading](#/en-US/docs/loading)
- [Tip](#/en-US/docs/tip)
......@@ -99,6 +100,10 @@ Pay attention that the name of the API is `$create` + `${component name}`. For e
Scroll Components are all implemented based on [better-scroll](https://github.com/ustbhuangyi/better-scroll) and `Scroll` Component is the encapsulation of better-scroll.
#### Advanced
- [Upload](#/en-US/docs/upload)
### Modules
cube-ui has some special modules besides components.
......
## Radio
Radio component. You could set the options and the position of the radio's icon.
### Example
- Basic usage
```html
<cube-radio v-model="selected" :options="options" />
```
```js
export default {
data() {
return {
selected: '',
options: ['Option1', 'Option2']
}
}
}
```
The value of `options` is an array. The default `selected` value is `''`, which means no option will be selected by defaut. If you clicked one radio option, the `selected` will be set as the value of this option.
- Configure the label, value, disabled state of options and the position of icon.
```html
<cube-radio v-model="selected2" :options="options2" position="right" />
```
```js
export default {
data() {
return {
selected2: '3',
options2: [
{
label: 'Option1',
value: '1'
},
{
label: 'Option2',
value: '2'
},
{
label: 'Option3',
value: '3',
disabled: true
}
]
}
}
}
```
The `options` value can be an array which has some object items. You can set `label` and `value` in each item, and use `disabled` to configure whether the radio item's state is disabled.
If the `position` is set as `'right'`, the radio's icon will be posited at the right of the label.
- Horizontal order
```html
<cube-radio v-model="selected3" :options="options3" :horizontal="true" />
```
```js
export default {
data() {
return {
selected3: '3',
options3: [
{
label: '1',
value: '1'
},
{
label: '2',
value: '2'
},
{
label: '3',
value: '3',
disabled: true
}
]
}
}
}
```
You can use `horizontal` to configure the style to horizontal layout.
### Props configuration
| Attribute | Description | Type | Accepted Values | Default |
| - | - | - | - | - |
| options | the array of radio options | Array | - | - |
| position | icon position | String | left/right | left |
| horizontal | whether use horizontal layout | Boolean | true/false | false |
* `options` sub configuration
| Attribute | Description | Type |
| - | - | - |
| label | the text of label | String |
| value | the value of radio item | String/Number |
| disabled | whether the item is disabled | Boolean |
Note: Each item of `options` can be an string, Which means both the `label` and `value` will be set as this string.
## Upload
`Upload` component.
**Notice:** In this document, all the original File will be called **original file**, since the wrapped file object will be called **file object**. The structure of **file object** show as following:
| Attribute | Description | Type |
| - | - | - |
| name | file name | String |
| size | file size | Number |
| url | file url, created by URL.createObjectURL, for preview | String |
| base64 | file base64 value, the value is equaled to the original file's base64 value. It is `''` by default, but you can have some plugins to added this `base64` value, like the compress plugin below | String |
| status | file status, one of: ready, uploading, success, error | String |
| progress | file progress, number 0~1 | Number |
| file | the original file | File |
| response | response data(try to parse to JSON)| Object/Array/String |
| responseHeaders | all response headers | String |
### Example
- Basic usage
```html
<cube-upload
action="//jsonplaceholder.typicode.com/photos/"
:simultaneous-uploads="1"
@files-added="filesAdded" />
```
```js
export default {
methods: {
filesAdded(files) {
const maxSize = 1 * 1024 * 1024 // 1M
for (let k in files) {
const file = files[k]
if (file.size > maxSize) {
file.ignore = true
}
}
}
}
}
```
Set `action` to configure the upload target URL for the multipart POST request.
Set `simultaneous-uploads` to configure the max number of files uploading simultaneously .
The `files-added` event is used for file validation, and you can filter file by setting `file.ignore = true`.
- Compress and uploaded through Base64
```html
<cube-upload
ref="upload2"
:action="action2"
:simultaneous-uploads="1"
:process-file="processFile"
@file-submitted="fileSubmitted"></cube-upload>
```
```js
import compress from '../modules/image'
export default {
data() {
return {
action2: {
target: '//jsonplaceholder.typicode.com/photos/',
prop: 'base64Value'
}
}
},
methods: {
processFile(file, next) {
compress(file, {
compress: {
width: 1600,
height: 1600,
quality: 0.5
}
}, next)
},
fileSubmitted(file) {
file.base64Value = file.file.base64
}
}
}
```
The `action` is an object which contains `target` and `prop`. the `prop` could configure which property in file object will be uploaded).
The `process-file` is a function which is used to process the original file, like compress, `next` must be called with the processed file.
The `file-submitted` event will be trigged after the file is processed and added to the `upload.files` with a parameter -- the file object.
### Props configuration
| Attribute | Description | Type | Accepted Values | Demo |
| - | - | - | - | - |
| action | upload action config | String/Object | '' | { target: '/upload' } |
| max | max upload files number | Number | 10 | - |
| auto | whether auto start upload | Boolean | true | - |
| simultaneousUploads | the number of simultaneous uploads | Number | 1 | - |
| processFile | process the original file | Function | function (file, next) { next(file) } | - |
* `action` sub configuration
If `action` is a string, it will be transformed into `{ target: action }`.
| Attribute | Description | Type | Default |
| - | - | - | - |
| target | the upload target URL for the multipart POST request | String | - |
| fileName | the name of the multipart POST parameter | String | 'file' |
| prop | which property in file object will be uploaded | String | 'file' |
| headers | extra headers to include in the multipart POST | Object | {} |
| data | extra data to include in the multipart POST | Object | {} |
| withCredentials | Standard CORS requests would not send or set any cookies by default. In order to include cookies as part of the request, you need to set the withCredentials property to true | Boolean | false |
| timeout | upload request timeout value | Number | 0 |
| progressInterval | The time interval between progress reports (Unit: ms) | Number | 100 |
* `processFile` sub configuration
A function with two parameters: `(file, next)`, the `file` is the original file and the `next` callback must be called with the processed file.
### Events
| Event Name | Description | Parameters |
| - | - | - |
| files-added | triggers when files are added, usually used for file validation | original files |
| file-submitted | triggers when a file is added to the `upload.files` | the file object |
| file-removed | triggers when a file is removed | the file object |
| file-success | triggers when a file is uploaded successfully | the file object |
| file-error | triggers when a file is failed to upload | the file object |
| file-click | triggers when a file is clicked | the file object |
### Instance methods
| Method name | Description | Parameter |
| - | - | - |
| start | start uploading | - |
| pause | pause uploading | - |
| retry | retry uploading | - |
| removeFile | remove file | the file object |
......@@ -25,6 +25,43 @@
```
`checkList` 的值是一个数组,代表的是选中的复选框 `label` 的值的集合。
- 设置 options
还可以通过 options 生成各个复选框
```html
<cube-checkbox-group v-model="checkList" :options="options" />
```
```js
export default {
data() {
return {
checkList: ['1', '4'],
options: [
{
label: 'Option1',
value: '1'
},
{
label: 'Option2',
value: '2'
},
{
label: 'Option3',
value: '3',
disabled: true
},
{
label: 'Option4',
value: '4',
disabled: true
}
]
}
}
}
```
- 水平排列
可通过设置 `horizontal` 改变样式为水平排列
......@@ -42,9 +79,14 @@
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
| - | - | - | - | - |
| horizontal | 是否水平排列 | Boolean | true/false | false |
| options | 选项数组 | Array | - | - |
### 事件
* `options` 子配置项
| 事件名 | 说明 | 参数 |
| 参数 | 说明 | 类型 |
| - | - | - |
| input | 组内可选项选中状态发生改变时触发 | 选中的复选框的值的集合,类型数组 |
| label | 复选框显示文字 | String |
| value | 复选框的值 | String/Number |
| disabled | 复选框是否被禁用 | Boolean |
注:如果 `options` 中的项为字符串也是可以的,此时默认 `label``value` 的值都为该字符串的值。
......@@ -5,32 +5,40 @@
### 示例
- 基本用法
```html
<cube-checkbox v-model="checked">
Checkbox
</cube-checkbox>
```
如果选中了,则 `checked` 的值就为 `true`
- 禁用状态
```html
<cube-checkbox v-model="checked" :disabled="true">
Disabled Checkbox
</cube-checkbox>
```
设置 `disabled``true` 即为禁用状态
设置 `disabled``true` 即为禁用状态。
- 复选框图标位置
```html
<cube-checkbox v-model="checked" position="right">
Position Checkbox
</cube-checkbox>
```
设置 `position``'right'` 则复选框图标位置在右边
设置 `position``'right'` 则复选框图标位置在右边。
- 改变 model 的值
```html
<cube-checkbox v-model="checked" label="labelValue">
Set label Checkbox
</cube-checkbox>
```
设置 `label`,当复选框选中的时候,`checked` 的值就是 `'labelValue'`,当未选中的时候,`checked` 的值就是 `false`;所以其实在单个复选框的场景下,最好不要设置 `label`
### Props 配置
......@@ -40,9 +48,3 @@
| disabled | 是否被禁用 | Boolean | true/false | false |
| position | 位置 | String | left/right | left |
| label | 如果选中的话,则是把该值映射到 v-model 上 | Boolean/String | - | '' |
### 事件
| 事件名 | 说明 | 参数 |
| - | - | - |
| input | 选中状态发生改变时触发 | 设置的 label 的值或者是否选中的布尔值 |
......@@ -27,6 +27,7 @@ cube-ui 是基于 Vue.js 实现的精致移动端组件库。
- [Button 按钮](#/zh-CN/docs/button)
- [Checkbox 复选框](#/zh-CN/docs/checkbox)
- [CheckboxGroup 复选框组](#/zh-CN/docs/checkbox-group)
- [Radio 单选框](#/zh-CN/docs/radio)
- [Loading 加载中](#/zh-CN/docs/loading)
- [Tip 提示](#/zh-CN/docs/tip)
......@@ -100,6 +101,10 @@ API 调用:
滚动类组件都是基于 [better-scroll](https://github.com/ustbhuangyi/better-scroll) 实现,而 `Scroll` 组件就是对 better-scroll 的封装。
#### 高级
- [Upload 上传](#/zh-CN/docs/upload)
### 模块
除了组件之外,cube-ui 还有一些特殊的模块。
......
## Radio 单选框组
复选框组,可设置复选框组内容,样式等。
### 示例
- 基本用法
```html
<cube-radio v-model="selected" :options="options" />
```
```js
export default {
data() {
return {
selected: '',
options: ['Option1', 'Option2']
}
}
}
```
`options` 为选项数组,默认不选中任何的选项,点击其中一个,则对应的 `selected` 的值就为选中项的值。
- 设置 value,禁用状态,图标位置
```html
<cube-radio v-model="selected2" :options="options2" position="right" />
```
```js
export default {
data() {
return {
selected2: '3',
options2: [
{
label: 'Option1',
value: '1'
},
{
label: 'Option2',
value: '2'
},
{
label: 'Option3',
value: '3',
disabled: true
}
]
}
}
}
```
`options` 的值可以是对象组成的数组,默认可以设置 `label``value` 分别代表的是显示文案和单选框的值,如果对象中包含了 `disabled``true` 的值,那么此单选框就处于禁用状态。
设置 `position``'right'`,则单选框图标位置在右边。
- 水平排列
```html
<cube-radio v-model="selected3" :options="options3" :horizontal="true" />
```
```js
export default {
data() {
return {
selected3: '3',
options3: [
{
label: '1',
value: '1'
},
{
label: '2',
value: '2'
},
{
label: '3',
value: '3',
disabled: true
}
]
}
}
}
```
可通过设置 `horizontal``true` 改变样式为水平排列。
### Props 配置
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
| - | - | - | - | - |
| options | 选项数组 | Array | - | - |
| position | 图标位置 | String | left/right | left |
| horizontal | 是否水平排列 | Boolean | true/false | false |
* `options` 子配置项
| 参数 | 说明 | 类型 |
| - | - | - |
| label | 单选框显示文字 | String |
| value | 单选框的值 | String/Number |
| disabled | 单选框是否被禁用 | Boolean |
注:如果 `options` 中的项为字符串也是可以的,此时默认 `label``value` 的值都为该字符串的值。
## Upload 组件
`Upload` 上传组件。
**注:** 本文中所有的原始文件对象统称为**原始文件**,而经过包装后的文件对象称为**文件对象**,这个文件对象的结构如下:
| 属性 | 说明 | 类型 |
| - | - | - |
| name | 文件名 | String |
| size | 文件大小 | Number |
| url | 文件 url,通过 URL.createObjectURL 获得 | String |
| base64 | 文件 base64 的值,这个会从原始文件的 base64 属性获得(默认是没有的,但是插件可以添加,例如下边演示的压缩 compress 插件就会添加 base64 值)| String |
| status | 文件状态,包含四个: ready, uploading, success, error | String |
| progress | 文件上传进度,小数 0~1 | Number |
| file | 原始文件 | File |
| response | 响应内容(自动转 JSON) | Object/Array/String |
| responseHeaders | 响应头 | String |
### 示例
- 基本用法
```html
<cube-upload
action="//jsonplaceholder.typicode.com/photos/"
:simultaneous-uploads="1"
@files-added="filesAdded" />
```
```js
export default {
methods: {
filesAdded(files) {
const maxSize = 1 * 1024 * 1024 // 1M
for (let k in files) {
const file = files[k]
if (file.size > maxSize) {
file.ignore = true
}
}
}
}
}
```
配置 `action` 表示上传的 URL 地址,而 `simultaneous-uploads` 则表示支持的并发上传个数。
通过 `files-added` 事件可以实现文件过滤,设置 `file.ignore = true` 即可。
- 压缩图片且通过 Base64 上传
```html
<cube-upload
ref="upload2"
:action="action2"
:simultaneous-uploads="1"
:process-file="processFile"
@file-submitted="fileSubmitted" />
```
```js
import compress from '../modules/image'
export default {
data() {
return {
action2: {
target: '//jsonplaceholder.typicode.com/photos/',
prop: 'base64Value'
}
}
},
methods: {
processFile(file, next) {
compress(file, {
compress: {
width: 1600,
height: 1600,
quality: 0.5
}
}, next)
},
fileSubmitted(file) {
file.base64Value = file.file.base64
}
}
}
```
`action` 中除了有 `target` 目标上传地址外;还有 `prop` 配置,表示上传的时候采用处理后的 `file` 普通对象的哪个属性所对应的值上传,这里设置的就是 `base64Value` 的值。
`process-file` 则是一个函数,主要用于处理原生文件的,调用 `next` 回调的话,参数是处理完的文件对象,这里示例的就是调用 `compress` 做压缩,处理完后会回调 `next`
`file-submitted` 事件则是每个文件处理完后添加到 `upload` 实例的 `files` 数组中后触发,参数就是一个处理后的文件对象。
### Props 配置
| 参数 | 说明 | 类型 | 默认值 | 示例 |
| - | - | - | - | - |
| action | 上传行为配置项,最少包含上传目标的 URL 地址 | String/Object | '' | { target: '/upload' } |
| max | 最大上传文件个数 | Number | 10 | - |
| auto | 是否自动上传,即选择完文件后自动开始上传 | Boolean | true | - |
| simultaneousUploads | 并发上传数 | Number | 1 | - |
| processFile | 处理原始文件函数 | Function | function (file, next) { next(file) } | - |
* `action` 子配置项
如果 `action` 是字符串,则会被处理成 `{ target: action }` 这样结构。
| 参数 | 说明 | 类型 | 默认值 |
| - | - | - | - |
| target | 上传目标 URL | String | - |
| fileName | 上传文件时文件的参数名 | String | 'file' |
| prop | 上传的时候使用文件对象的 prop 属性所对应的值 | String | 'file' |
| headers | 自定义请求头 | Object | {} |
| data | 上传需要附加数据 | Object | {} |
| withCredentials | 标准的 CORS 请求是不会带上 cookie 的,如果想要带的话需要设置 withCredentials 为 true | Boolean | false |
| timeout | 请求超时时间 | Number | 0 | |
| progressInterval | 进度回调间隔(单位:ms) | Number | 100 |
* `processFile` 子配置项
一个函数,这个函数有两个参数:`(file, next)``file` 就是原始文件,`next` 为处理完毕后的回调函数,调用的时候需要传入处理后的文件。
### 事件
| 事件名 | 说明 | 参数 |
| - | - | - |
| files-added | 选择完文件后触发,一般可用作文件过滤 | 原始文件列表 |
| file-submitted | 每个文件处理完后添加到 `upload` 实例的 `files` 数组中后触发 | 文件对象 |
| file-removed | 文件被删除后触发 | 文件对象 |
| file-success | 文件上传成功后触发 | 文件对象 |
| file-error | 文件上传失败后触发 | 文件对象 |
| file-click | 文件点击后触发 | 文件对象 |
### 实例方法
| 方法名 | 说明 | 参数 |
| - | - | - |
| start | 开始上传 | - |
| pause | 暂停上传 | - |
| retry | 重试上传 | - |
| removeFile | 删除文件 | 文件对象 |
......@@ -35,6 +35,10 @@
path: '/checkbox-group',
text: 'CheckboxGroup'
},
{
path: '/radio',
text: 'Radio'
},
{
path: '/loading',
text: 'Loading'
......@@ -82,6 +86,10 @@
{
path: '/index-list',
text: 'IndexList'
},
{
path: '/upload',
text: 'Upload'
}
]
}
......
// clone https://github.com/Tencent/weui.js/blob/master/src/uploader/image.js
// updated by cube-ui
/*
* Tencent is pleased to support the open source community by making WeUI.js available.
*
* Copyright (C) 2017 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the MIT License (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://opensource.org/licenses/MIT
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* 检查图片是否有被压扁,如果有,返回比率
* ref to http://stackoverflow.com/questions/11929099/html5-canvas-drawimage-ratio-bug-ios
*/
function detectVerticalSquash(img) {
// 拍照在IOS7或以下的机型会出现照片被压扁的bug
var data;
var ih = img.naturalHeight;
var canvas = document.createElement('canvas');
canvas.width = 1;
canvas.height = ih;
var ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
try {
data = ctx.getImageData(0, 0, 1, ih).data;
} catch (err) {
console.log('Cannot check verticalSquash: CORS?');
return 1;
}
var sy = 0;
var ey = ih;
var py = ih;
while (py > sy) {
var alpha = data[(py - 1) * 4 + 3];
if (alpha === 0) {
ey = py;
} else {
sy = py;
}
py = (ey + sy) >> 1; // py = parseInt((ey + sy) / 2)
}
var ratio = (py / ih);
return (ratio === 0) ? 1 : ratio;
}
/**
* dataURI to blob, ref to https://gist.github.com/fupslot/5015897
* @param dataURI
*/
function dataURItoBuffer(dataURI){
var byteString = atob(dataURI.split(',')[1]);
var buffer = new ArrayBuffer(byteString.length);
var view = new Uint8Array(buffer);
for (var i = 0; i < byteString.length; i++) {
view[i] = byteString.charCodeAt(i);
}
return buffer;
}
function dataURItoBlob(dataURI) {
var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];
var buffer = dataURItoBuffer(dataURI);
return new Blob([buffer], {type: mimeString});
}
/**
* 获取图片的orientation
* ref to http://stackoverflow.com/questions/7584794/accessing-jpeg-exif-rotation-data-in-javascript-on-the-client-side
*/
function getOrientation(buffer){
var view = new DataView(buffer);
if (view.getUint16(0, false) != 0xFFD8) return -2;
var length = view.byteLength, offset = 2;
while (offset < length) {
var marker = view.getUint16(offset, false);
offset += 2;
if (marker == 0xFFE1) {
if (view.getUint32(offset += 2, false) != 0x45786966) return -1;
var little = view.getUint16(offset += 6, false) == 0x4949;
offset += view.getUint32(offset + 4, little);
var tags = view.getUint16(offset, little);
offset += 2;
for (var i = 0; i < tags; i++)
if (view.getUint16(offset + (i * 12), little) == 0x0112)
return view.getUint16(offset + (i * 12) + 8, little);
}
else if ((marker & 0xFF00) != 0xFF00) break;
else offset += view.getUint16(offset, false);
}
return -1;
}
/**
* 修正拍照时图片的方向
* ref to http://stackoverflow.com/questions/19463126/how-to-draw-photo-with-correct-orientation-in-canvas-after-capture-photo-by-usin
*/
function orientationHelper(canvas, ctx, orientation) {
const w = canvas.width, h = canvas.height;
if(orientation > 4){
canvas.width = h;
canvas.height = w;
}
switch (orientation) {
case 2:
ctx.translate(w, 0);
ctx.scale(-1, 1);
break;
case 3:
ctx.translate(w, h);
ctx.rotate(Math.PI);
break;
case 4:
ctx.translate(0, h);
ctx.scale(1, -1);
break;
case 5:
ctx.rotate(0.5 * Math.PI);
ctx.scale(1, -1);
break;
case 6:
ctx.rotate(0.5 * Math.PI);
ctx.translate(0, -h);
break;
case 7:
ctx.rotate(0.5 * Math.PI);
ctx.translate(w, -h);
ctx.scale(-1, 1);
break;
case 8:
ctx.rotate(-0.5 * Math.PI);
ctx.translate(-w, 0);
break;
}
}
/**
* 压缩图片
*/
function compress(file, options, callback) {
const reader = new FileReader();
reader.onload = function (evt) {
if(options.compress === false){
// 不启用压缩 & base64上传 的分支,不做任何处理,直接返回文件的base64编码
file.base64 = evt.target.result;
callback(file);
return;
}
// 启用压缩的分支
const img = new Image();
img.onload = function () {
const ratio = detectVerticalSquash(img);
const orientation = getOrientation(dataURItoBuffer(img.src));
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const maxW = options.compress.width;
const maxH = options.compress.height;
let w = img.width;
let h = img.height;
let dataURL;
if(w < h && h > maxH){
w = parseInt(maxH * img.width / img.height);
h = maxH;
}else if(w >= h && w > maxW){
h = parseInt(maxW * img.height / img.width);
w = maxW;
}
canvas.width = w;
canvas.height = h;
if(orientation > 0){
orientationHelper(canvas, ctx, orientation);
}
ctx.drawImage(img, 0, 0, w, h / ratio);
if(/image\/jpeg/.test(file.type) || /image\/jpg/.test(file.type)){
dataURL = canvas.toDataURL('image/jpeg', options.compress.quality);
}else{
dataURL = canvas.toDataURL(file.type);
}
if(options.type == 'file'){
if(/;base64,null/.test(dataURL) || /;base64,$/.test(dataURL)){
// 压缩出错,以文件方式上传的,采用原文件上传
console.warn('Compress fail, dataURL is ' + dataURL + '. Next will use origin file to upload.');
callback(file);
}else{
let blob = dataURItoBlob(dataURL);
blob.id = file.id;
blob.name = file.name;
blob.lastModified = file.lastModified;
blob.lastModifiedDate = file.lastModifiedDate;
callback(blob);
}
}else{
if(/;base64,null/.test(dataURL) || /;base64,$/.test(dataURL)){
// 压缩失败,以base64上传的,直接报错不上传
options.onError(file, new Error('Compress fail, dataURL is ' + dataURL + '.'));
callback();
}else{
file.base64 = dataURL;
callback(file);
}
}
};
img.src = evt.target.result;
};
reader.readAsDataURL(file);
}
export default compress;
......@@ -8,6 +8,10 @@
<cube-checkbox label="4" :disabled="true">Disabled & Checked Checkbox</cube-checkbox>
</cube-checkbox-group>
<p>checkList value : {{checkList}}</p>
<br>
<cube-checkbox-group v-model="checkList" :options="options">
</cube-checkbox-group>
<br>
<cube-checkbox-group v-model="checkList" :horizontal="true">
<cube-checkbox label="1">1</cube-checkbox>
<cube-checkbox label="2">2</cube-checkbox>
......@@ -24,7 +28,27 @@
export default {
data() {
return {
checkList: ['1', '4']
checkList: ['1', '4'],
options: [
{
label: 'Option1',
value: '1'
},
{
label: 'Option2',
value: '2'
},
{
label: 'Option3',
value: '3',
disabled: true
},
{
label: 'Option4',
value: '4',
disabled: true
}
]
}
},
components: {
......
<template>
<cube-page type="radio-view" title="Radio">
<template slot="content">
<cube-radio v-model="selected" :options="options" />
<p>selected value: {{selected}}</p>
<cube-radio v-model="selected2" :options="options2" position="right" />
<p>selected value: {{selected2}}</p>
<cube-radio v-model="selected3" :options="options3" :horizontal="true" />
<p>selected value: {{selected3}}</p>
</template>
</cube-page>
</template>
<script type="text/ecmascript-6">
import CubePage from '../components/cube-page.vue'
export default {
data() {
return {
selected: '',
options: ['Option1', 'Option2'],
selected2: '3',
options2: [
{
label: 'Option1',
value: '1'
},
{
label: 'Option2',
value: '2'
},
{
label: 'Option3',
value: '3',
disabled: true
}
],
selected3: '3',
options3: [
{
label: '1',
value: '1'
},
{
label: '2',
value: '2'
},
{
label: '3',
value: '3',
disabled: true
}
]
}
},
components: {
CubePage
}
}
</script>
<style lang="stylus" rel="stylesheet/stylus">
.radio-view
.content
>
*
margin: 10px 0
</style>
<template>
<cube-page type="upload-view" title="Upload">
<template slot="content">
<p>Normal upload(File max size 1MB): </p>
<div>
<cube-upload ref="upload" :action="action" :simultaneous-uploads="1" @files-added="filesAdded" />
<cube-button @click="upload" v-if="!isUploading">Upload</cube-button>
<cube-button @click="pause" v-else>Pause</cube-button>
<cube-button @click="retry">Retry</cube-button>
</div>
<p>Compress&Base64 upload: </p>
<div>
<cube-upload
ref="upload2"
:action="action2"
:simultaneous-uploads="1"
:process-file="processFile"
@file-submitted="fileSubmitted" />
</div>
</template>
</cube-page>
</template>
<script type="text/ecmascript-6">
import CubePage from '../components/cube-page.vue'
import compress from '../modules/image'
export default {
data() {
return {
action: '//jsonplaceholder.typicode.com/photos/',
action2: {
target: '//jsonplaceholder.typicode.com/photos/',
prop: 'base64Value'
},
isUploading: true
}
},
methods: {
upload() {
this.isUploading = true
this.$refs.upload.start()
},
pause() {
this.isUploading = false
this.$refs.upload.pause()
},
retry() {
this.$refs.upload.retry()
},
filesAdded(files) {
const maxSize = 1 * 1024 * 1024 // 1M
for (let k in files) {
const file = files[k]
if (file.size > maxSize) {
file.ignore = true
}
}
},
processFile(file, next) {
compress(file, {
compress: {
width: 1600,
height: 1600,
quality: 0.5
}
}, next)
},
fileSubmitted(file) {
file.base64Value = file.file.base64
}
},
components: {
CubePage
}
}
</script>
<style lang="stylus" rel="stylesheet/stylus">
.upload-view
.content
>
p, div
margin: 20px 0
div
.cube-btn, .cube-upload
margin: 10px 0
</style>
import Button from '../pages/button.vue'
import Checkbox from '../pages/checkbox.vue'
import CheckboxGroup from '../pages/checkbox-group.vue'
import Radio from '../pages/radio.vue'
import Loading from '../pages/loading.vue'
import Tip from '../pages/tip.vue'
import Popup from '../pages/popup.vue'
......@@ -15,6 +16,7 @@ import Slide from '../pages/slide.vue'
import IndexList from '../pages/index-list/index-list.vue'
import IndexListDefault from '../pages/index-list/default.vue'
import IndexListCustom from '../pages/index-list/custom.vue'
import Upload from '../pages/upload.vue'
const routes = [
{
......@@ -29,6 +31,10 @@ const routes = [
path: '/checkbox-group',
component: CheckboxGroup
},
{
path: '/radio',
component: Radio
},
{
path: '/loading',
component: Loading
......@@ -86,6 +92,10 @@ const routes = [
component: IndexListCustom
}
]
},
{
path: '/upload',
component: Upload
}
]
......
......@@ -62,6 +62,19 @@ $checkbox-disabled-icon-bgc := $color-light-grey-ss
// checkbox-group
$checkbox-group-horizontal-bdc := $color-light-grey-s
// radio
$radio-group-horizontal-bdc := $color-light-grey-s
$radio-color := $color-grey
$radio-bgc := $color-white
$radio-icon-color := $color-light-grey-s
$radio-icon-bgc := $color-white
/// selected
$radio-selected-icon-color := $color-orange
$radio-selected-icon-bgc := $color-white
/// disabled
$radio-disabled-icon-color := $color-light-grey-ss
$radio-disabled-icon-bgc := $color-light-grey-ss
// dialog
$dialog-color := $color-grey
$dialog-bgc := $color-white
......@@ -118,3 +131,18 @@ $tip-bgc := $color-dark-grey-opacity
// toast
$toast-color := $color-light-grey-s
$toast-bgc := rgba(37, 38, 45, 0.9)
// upload
$upload-btn-color := $color-grey
$upload-btn-bgc := $color-white
$upload-btn-active-bgc := $color-light-grey-opacity
$upload-btn-box-shadow := 0 0 6px 2px $color-grey-opacity
$upload-btn-border-color := #e5e5e5
$upload-file-bgc := $color-white
$upload-file-remove-color := rgba(0, 0, 0, .8)
$upload-file-remove-bgc := $color-white
$upload-file-state-bgc := $color-mask-bg
$upload-file-success-color := $color-orange
$upload-file-error-color := #f43530
$upload-file-status-bgc := $color-white
$upload-file-progress-color := $color-white
<template>
<div class="cube-checkbox-group" ref="group" :class="groupClass" :data-horz="horizontal">
<slot></slot>
<slot>
<cube-checkbox
v-for="(option, index) in options"
:key="index"
:label="option.value || option"
:disabled="option.disabled">{{option.label || option}}</cube-checkbox>
</slot>
</div>
</template>
<script type="text/ecmascript-6">
import CubeCheckbox from '../checkbox/checkbox.vue'
const COMPONENT_NAME = 'cube-checkbox-group'
const EVENT_INPUT = 'input'
......@@ -19,6 +26,12 @@
horizontal: {
type: Boolean,
default: false
},
options: {
type: Array,
default() {
return []
}
}
},
data () {
......@@ -52,6 +65,9 @@
this._value.splice(index, 1)
this.$emit(EVENT_INPUT, this._value)
})
},
components: {
CubeCheckbox
}
}
</script>
......
<template>
<div class="cube-radio-group" :class="_groupClass" :data-horz="horizontal">
<div class="cube-radio" v-for="option in options" :class="_containerClass" :data-pos="position">
<label class="cube-radio-wrap" :class="_wrapClass(option)">
<input class="cube-radio-input" type="radio" :disabled="option.disabled" v-model="radioValue" :value="option.value || option">
<span class="cube-radio-ui cubeic-round-border">
<i></i>
</span>
<span class="cube-radio-label">{{option.label || option}}</span>
</label>
</div>
</div>
</template>
<script type="text/ecmascript-6">
const COMPONENT_NAME = 'cube-radio'
const EVENT_INPUT = 'input'
export default {
name: COMPONENT_NAME,
props: {
value: String,
options: {
type: Array,
required: true
},
position: {
type: String,
default: 'left'
},
horizontal: {
type: Boolean,
default: false
}
},
data() {
return {
radioValue: this.value
}
},
computed: {
_containerClass() {
if (this.horizontal) {
return 'border-right-1px'
}
},
_groupClass() {
if (!this.horizontal) {
return 'border-top-1px border-bottom-1px'
}
}
},
watch: {
value(newV) {
this.radioValue = newV
},
radioValue(newV) {
this.$emit(EVENT_INPUT, newV)
}
},
methods: {
_wrapClass(option) {
return {
'cube-radio_selected': this.radioValue === (option.value || option),
'cube-radio_disabled': option.disabled,
'border-bottom-1px': !this.horizontal
}
}
}
}
</script>
<style lang="stylus" rel="stylesheet/stylus">
@require "../../common/stylus/variable.styl"
@require "../../common/stylus/mixin.styl"
$ui-width = 1.42em
.cube-radio-group[data-horz="true"]
display: flex
padding-left: 0
border-1px($checkbox-group-horizontal-bdc, 2px)
border-radius: 2px
.cube-radio
flex-fix()
text-align: center
padding-left: 10px
padding-right: 10px
&:after
border-color: $checkbox-group-horizontal-bdc
&:last-child
border-none()
&:last-child
border-none()
&[data-pos="right"]
.cube-radio-ui
position: relative
margin-left: .42em
order: 1
.cube-radio-label
margin-right: 0
.cube-radio-wrap
justify-content: center
.cube-radio
position: relative
padding: 0 16px
text-align: left
font-size: 100%
color: $radio-color
background-color: $radio-bgc
&[data-pos="right"]
.cube-radio-ui
margin-right: 0
position: absolute
right: 0
.cube-radio-label
margin-right: $ui-width
&:last-child
.cube-radio-wrap
border-none()
.cube-radio-wrap
position: relative
display: flex
align-items: center
box-sizing: border-box
width: 100%
height: 100%
padding: 11px 0
line-height: 1.5
word-break: break-word
word-wrap: break-word
.cube-radio-input
z-index: 1
position: absolute
top: 0
left: 0
width: 100%
height: 100%
opacity: 0
.cube-radio-ui
position: relative
width: 1em
height: 1em
margin-right: $ui-width - 1em
line-height: 1
color: transparent
border-radius: 50%
&::before, i
transition: all .2s
&::before
color: $radio-icon-color
display: inline-block
transform: scale(1.2)
i
position: absolute
top: 0
left: 0
overflow: hidden
width: 100%
height: 100%
border-radius: 50%
background-color: currentColor
transform: scale(.4)
&::before
content: ""
position: absolute
top: 50%
left: 50%
width: 50%
height: 50%
transform: translate(-50%, -50%) scale(.8)
background-color: currentColor
border-radius: 50%
.cube-radio_selected
.cube-radio-ui
color: $radio-selected-icon-color
&::before
color: transparent
i
transform: scale(1)
&::before
background-color: $radio-selected-icon-bgc
.cube-radio_disabled
.cube-radio-ui
color: $radio-disabled-icon-color
background-color: $radio-disabled-icon-bgc
&::before, i
transition: none
&::before
color: transparent
</style>
import {
STATUS_SUCCESS,
STATUS_UPLOADING,
STATUS_ERROR
} from './util'
export default function ajaxUpload(file, options, changeHandler) {
const {
target,
headers = {},
data = {},
fileName = 'file',
withCredentials,
timeout,
prop = 'file',
progressInterval = 100
} = options
file.progress = 0
file.status = STATUS_UPLOADING
const xhr = new window.XMLHttpRequest()
file._xhr = xhr
let progressTid = 0
if (xhr.upload) {
let lastProgressTime = Date.now()
xhr.upload.onprogress = function (e) {
if (e.total > 0) {
if (progressTid) {
clearTimeout(progressTid)
const now = Date.now()
const diff = now - lastProgressTime
if (diff >= progressInterval) {
computed()
} else {
progressTid = setTimeout(computed, diff)
}
} else {
// first time
computed()
progressTid = 1
}
}
function computed() {
file.progress = e.loaded / e.total
lastProgressTime = Date.now()
}
}
}
const formData = new window.FormData()
formData.append(fileName, file[prop])
Object.keys(data).forEach((key) => {
formData.append(key, data[key])
})
xhr.onload = function () {
if (xhr.status < 200 || xhr.status >= 300) {
setStatus(STATUS_ERROR)
return
}
let response = xhr.responseText || xhr.response
try {
response = JSON.parse(response)
} catch (e) {}
file.response = response
file.responseHeaders = xhr.getAllResponseHeaders()
setStatus(STATUS_SUCCESS)
}
xhr.onerror = function () {
setStatus(STATUS_ERROR)
}
xhr.ontimeout = function () {
setStatus(STATUS_ERROR)
}
xhr.open('POST', target, true)
if (withCredentials) {
xhr.withCredentials = true
}
Object.keys(headers).forEach((key) => {
xhr.setRequestHeader(key, headers[key])
})
if (timeout > 0) {
xhr.timeout = timeout
}
xhr.send(formData)
function setStatus(status) {
clearTimeout(progressTid)
progressTid = 0
file.progress = 1
file.status = status
changeHandler && changeHandler(file)
}
}
<template>
<div class="cube-upload-btn">
<slot>
<div class="cube-upload-btn-def"><i></i></div>
</slot>
<input class="cube-upload-input" type="file" @change="changeHandler" :multiple="multiple" :accept="accept">
</div>
</template>
<script type="text/ecmascript-6">
const COMPONENT_NAME = 'cube-upload-btn'
export default {
name: COMPONENT_NAME,
props: {
multiple: {
type: Boolean,
default: true
},
accept: {
type: String,
default: 'image/*'
}
},
methods: {
changeHandler(e) {
const fileEle = e.currentTarget
const files = fileEle.files
if (files) {
this.$parent.addFiles(files)
fileEle.value = null
}
}
}
}
</script>
<style lang="stylus" rel="stylesheet/stylus">
@require "../../common/stylus/variable.styl"
@require "../../common/stylus/mixin.styl"
.cube-upload-btn
position: relative
overflow: hidden
&:active
.cube-upload-btn-def
background-color: $upload-btn-active-bgc
.cube-upload-input
position: absolute
top: 0
left: 0
right: 0
bottom: 0
width: 100%
font-size: 0
opacity: 0
.cube-upload-btn-def
position: relative
width: 80px
height: 80px
box-sizing: border-box
background-color: $upload-btn-bgc
box-shadow: $upload-btn-box-shadow
border-radius: 2px
border-1px($upload-btn-border-color, 2px)
> i
&::before, &::after
content: ""
position: absolute
top: 50%
left: 50%
width: 20px
height: 2px
transform: translate(-50%, -50%)
background-color: $upload-btn-color
&::after
transform: translate(-50%, -50%) rotate(90deg)
</style>
<template>
<div class="cube-upload-file" @click="clickHandler">
<slot>
<div class="cube-upload-file-def" :style="fileStyle">
<i class="cubeic-wrong" @click.stop="removeFile"></i>
<div class="cube-upload-file-state" :class="fileStatusCls">
<i class="cube-upload-file-status" :class="statusCls"></i>
<span class="cube-upload-file-progress">{{fileProgress}}</span>
</div>
</div>
</slot>
</div>
</template>
<script type="text/ecmascript-6">
import { STATUS_SUCCESS, STATUS_ERROR } from './util'
const COMPONENT_NAME = 'cube-upload-file'
const STATUS_CLASS_MAP = {
success: 'cubeic-right',
error: 'cubeic-warn'
}
const EVENT_CLICK = 'click'
export default {
name: COMPONENT_NAME,
props: {
file: {
type: Object,
required: true
}
},
computed: {
fileStatusCls() {
const file = this.file
const status = file.status
if (file.progress >= 0.01 || status === STATUS_SUCCESS || status === STATUS_ERROR) {
return 'cube-upload-file_stat'
}
},
fileStyle() {
const url = this.file.url || this.file.base64
if (!url) {
return
}
return {
'background-image': `url("${url}")`
}
},
statusCls() {
const status = this.file.status
return STATUS_CLASS_MAP[status]
},
fileProgress() {
if (this.statusCls) {
return '100%'
}
const p = Math.min(Math.floor(this.file.progress * 100), 99)
return `${p}%`
}
},
methods: {
clickHandler() {
this.$emit(EVENT_CLICK, this.file)
},
removeFile() {
this.$parent.removeFile(this.file)
}
}
}
</script>
<style lang="stylus" rel="stylesheet/stylus">
@require "../../common/stylus/variable.styl"
@require "../../common/stylus/mixin.styl"
.cube-upload-file
position: relative
.cube-upload-file-def
position: relative
width: 80px
height: 80px
box-sizing: border-box
background: $upload-file-bgc no-repeat center
background-size: cover
border-radius: 2px
> .cubeic-wrong
position: absolute
z-index: 2
top: -2px
right: -2px
color: $upload-file-remove-color
font-size: $fontsize-large
background-color: $upload-file-remove-bgc
border-radius: 50%
&::before
display: inline-block
transform: scale(1.625)
transform-origin: center
.cube-upload-file-state
position: relative
width: 100%
height: 100%
display: flex
align-items: center
justify-content: center
overflow: hidden
opacity: 0
background-color: $upload-file-state-bgc
border-radius: 2px
transition: opacity .1s
&::before
content: "."
position: relative
left: -50%
display: block
width: 1px
height: 1px
margin-left: -1px
background-color: rgba(0, 0, 0, .1)
.cube-upload-file_stat
opacity: 1
.cube-upload-file-status
position: relative
z-index: 1
font-size: $fontsize-large-xxxx
display: none
.cube-upload-file-status.cubeic-right
display: block
color: $upload-file-success-color
.cube-upload-file-status.cubeic-warn
display: block
color: $upload-file-error-color
.cube-upload-file-status.cubeic-right, .cube-upload-file-status.cubeic-warn
&::after
content: ""
z-index: -1
position: absolute
left: 50%
top: 50%
transform: translate(-50%, -50%)
width: .56em
height: .56em
border-radius: 50%
background-color: $upload-file-status-bgc
+ .cube-upload-file-progress
display: none
.cube-upload-file-progress
color: $upload-file-progress-color
font-size: $fontsize-large-xx
</style>
<template>
<div class="cube-upload">
<slot>
<div class="cube-upload-def clear-fix">
<upload-file v-for="(file, i) in files" :file="file" :key="i" @click="fileClick"></upload-file>
<upload-btn></upload-btn>
</div>
</slot>
</div>
</template>
<script type="text/ecmascript-6">
import UploadBtn from './btn.vue'
import UploadFile from './file.vue'
import ajaxUpload from './ajax'
import {
processFiles,
newFile,
URL,
STATUS_READY,
STATUS_UPLOADING,
STATUS_ERROR,
STATUS_SUCCESS
} from './util'
const COMPONENT_NAME = 'cube-upload'
const EVENT_ADDED = 'files-added'
const EVENT_SUBMITTED = 'file-submitted'
const EVENT_REMOVED = 'file-removed'
const EVENT_SUCCESS = 'file-success'
const EVENT_ERROR = 'file-error'
const EVENT_CLICK = 'file-click'
export default {
name: COMPONENT_NAME,
props: {
action: {
type: [String, Object],
default: ''
},
max: {
type: Number,
default: 10
},
auto: {
type: Boolean,
default: true
},
simultaneousUploads: {
type: Number,
default: 1
},
processFile: {
type: Function,
default: function (file, cb) {
cb(file)
}
}
},
data() {
return {
files: [],
paused: !this.auto
}
},
computed: {
actionOptions() {
const action = this.action
if (typeof action === 'string') {
return action ? {
target: action
} : null
} else {
return action
}
}
},
methods: {
addFiles(files) {
this.$emit(EVENT_ADDED, files)
const filesLen = this.files.length
const newFiles = []
const maxLen = this.max - filesLen
let i = 0
let file = files[i]
while (newFiles.length < maxLen && file) {
if (!file.ignore) {
newFiles.push(file)
this.files.push(newFile())
}
file = files[++i]
}
processFiles(newFiles, this.processFile, (file, index) => {
this.$set(this.files, filesLen + index, file)
this.$emit(EVENT_SUBMITTED, file)
}, () => {
// waiting ui
this.$nextTick(() => {
this.upload()
})
})
},
removeFile(file) {
this.$emit(EVENT_REMOVED, file)
file._xhr && file._xhr.abort()
if (file.url) {
URL.revokeObjectURL(file.url)
}
const index = this.files.indexOf(file)
this.files.splice(index, 1)
this.upload()
},
fileClick(file) {
this.$emit(EVENT_CLICK, file)
},
upload(retry) {
const options = this.actionOptions
if (this.paused || !options) {
return
}
const len = this.files.length
let uploadingCount = 0
let i = 0
while (i < len && uploadingCount < this.simultaneousUploads) {
const file = this.files[i]
const status = file.status
if (status === STATUS_READY || (retry && status === STATUS_ERROR && file._retryId !== this.retryId)) {
ajaxUpload(file, options, (file) => {
if (status === STATUS_ERROR) {
file._retryId = this.retryId
}
this.$emit(file.status === STATUS_SUCCESS ? EVENT_SUCCESS : EVENT_ERROR, file)
this.upload(retry)
})
uploadingCount++
} else if (status === STATUS_UPLOADING) {
uploadingCount++
}
i++
}
},
start() {
this.paused = false
this.upload()
},
pause() {
this.paused = true
this.files.forEach((file) => {
if (file.status === STATUS_UPLOADING) {
file._xhr.abort()
file.status = STATUS_READY
}
})
},
retry() {
this.retryId = Date.now()
this.paused = false
this.upload(true)
}
},
components: {
UploadBtn,
UploadFile
}
}
</script>
<style lang="stylus" rel="stylesheet/stylus">
@require "../../common/stylus/variable.styl"
.cube-upload
position: relative
.cube-upload-def
margin-right: -10px
.cube-upload-btn, .cube-upload-file
float: left
margin: 0 10px 10px 0
</style>
export const URL = window.URL || window.webkitURL || window.mozURL
export const STATUS_READY = 'ready'
export const STATUS_UPLOADING = 'uploading'
export const STATUS_ERROR = 'error'
export const STATUS_SUCCESS = 'success'
export function processFiles(files, eachProcessFile, eachCb, cb) {
const fileItems = []
const len = files.length
let processedLen = 0
for (let i = 0; i < len; i++) {
processFile(files[i], i, eachProcessFile, function (item, index) {
processedLen++
fileItems[index] = item
eachCb(item, index)
if (processedLen === len) {
return cb(fileItems)
}
})
}
}
export function processFile(file, i, eachProcessFile, cb) {
eachProcessFile(file, function (file) {
const item = newFile(file.name, file.size, STATUS_READY, 0, file)
cb(item, i)
})
}
export function newFile(name = '', size = 0, status = '', progress = 0, file = null) {
const base64 = file && file.base64 || ''
const url = base64 ? '' : createURL(file)
return {
name,
size,
url,
base64,
status,
progress,
file
}
}
function createURL(file) {
if (file && URL) {
return URL.createObjectURL(file)
}
return ''
}
......@@ -10,8 +10,10 @@ import {
Toast,
ActionSheet,
CheckboxGroup,
Radio,
Slide,
IndexList,
Upload,
BScroll,
createAPI
} from './module'
......@@ -30,11 +32,13 @@ function install(Vue) {
Tip,
Toast,
CheckboxGroup,
Radio,
Slide,
IndexList,
ActionSheet,
Scroll,
Popup
Popup,
Upload
]
components.forEach((Component) => {
Component.install(Vue)
......
......@@ -2,6 +2,7 @@ import Style from './modules/style'
import Button from './modules/button'
import CheckboxGroup from './modules/checkbox-group'
import Radio from './modules/radio'
import Popup from './modules/popup'
import Dialog from './modules/dialog'
import Toast from './modules/toast'
......@@ -14,6 +15,8 @@ import TimePicker from './modules/time-picker'
import CascadePicker from './modules/cascade-picker'
import Scroll from './modules/scroll'
import Upload from './modules/upload'
import BScroll from './modules/better-scroll'
import createAPI from './modules/create-api'
......@@ -35,11 +38,13 @@ export {
Toast,
ActionSheet,
Checkbox,
Radio,
CheckboxGroup,
Slide,
SlideItem,
Loading,
IndexList,
Upload,
BScroll,
createAPI
}
import Radio from '../../components/radio/radio.vue'
Radio.install = function (Vue) {
Vue.component(Radio.name, Radio)
}
export default Radio
import Upload from '../../components/upload/upload.vue'
import UploadBtn from '../../components/upload/btn.vue'
import UploadFile from '../../components/upload/file.vue'
Upload.install = function (Vue) {
Vue.component(Upload.name, Upload)
Vue.component(UploadBtn.name, UploadBtn)
Vue.component(UploadFile.name, UploadFile)
}
Upload.Btn = UploadBtn
Upload.File = UploadFile
export default Upload
import Vue from 'vue2'
import Radio from '@/modules/radio'
import createVue from '../utils/create-vue'
describe('Radio.vue', () => {
let vm
afterEach(() => {
if (vm) {
vm.$parent.destroy()
vm = null
}
})
it('use', () => {
Vue.use(Radio)
expect(Vue.component(Radio.name))
.to.be.a('function')
})
it('should render correct contents', () => {
vm = createRadio()
const el = vm.$el
expect(el.className)
.to.equal('cube-radio-group my-radio border-top-1px border-bottom-1px')
const options = el.querySelectorAll('.cube-radio')
expect(options.length)
.to.equal(3)
expect(options[0].getAttribute('data-pos'))
.to.equal('right')
expect(options[0].querySelector('.cube-radio-wrap').className)
.to.equal('cube-radio-wrap border-bottom-1px')
expect(options[0].querySelector('.cube-radio-input').value)
.to.equal('1')
expect(options[0].querySelector('.cube-radio-label').textContent.trim())
.to.equal('Option1')
expect(options[1].querySelector('.cube-radio-wrap').className)
.to.equal('cube-radio-wrap border-bottom-1px')
expect(options[1].querySelector('.cube-radio-input').value)
.to.equal('Option2')
expect(options[1].querySelector('.cube-radio-label').textContent.trim())
.to.equal('Option2')
expect(options[2].querySelector('.cube-radio-wrap').className)
.to.equal('cube-radio-wrap cube-radio_selected cube-radio_disabled border-bottom-1px')
expect(options[2].querySelector('.cube-radio-input').value)
.to.equal('3')
expect(options[2].querySelector('.cube-radio-label').textContent.trim())
.to.equal('Option3')
})
it('should render correct contents - horizontal', () => {
vm = createRadio(true)
const el = vm.$el
expect(el.className)
.to.equal('cube-radio-group my-radio')
expect(el.getAttribute('data-horz'))
.to.equal('true')
const options = el.querySelectorAll('.cube-radio')
expect(options.length)
.to.equal(3)
expect(options[0].className)
.to.equal('cube-radio border-right-1px')
expect(options[0].querySelector('.cube-radio-wrap').className)
.to.equal('cube-radio-wrap')
expect(options[2].querySelector('.cube-radio-wrap').className)
.to.equal('cube-radio-wrap cube-radio_selected cube-radio_disabled')
})
it('should toggle v-model value', (done) => {
vm = createRadio()
expect(vm.$parent.selected)
.to.equal('3')
vm.$el.querySelector('.cube-radio-input').click()
setTimeout(() => {
expect(vm.$parent.selected)
.to.equal('1')
done()
})
})
})
function createRadio (horizontal = false) {
const vm = createVue({
template: `
<cube-radio v-model="selected" :options="options" class="my-radio" position="right" :horizontal="${horizontal}"></cube-radio>
`,
data: {
selected: '3',
options: [
{
label: 'Option1',
value: '1'
},
'Option2',
{
label: 'Option3',
value: '3',
disabled: true
}
]
}
})
return vm
}
import Vue from 'vue2'
import Upload from '@/modules/upload'
import instantiateComponent from '@/common/helpers/instantiate-component'
import '../utils/file'
import '../utils/xhr'
const UploadBtn = Upload.Btn
const UploadFile = Upload.File
describe('Upload.vue', () => {
let vm
afterEach(() => {
if (vm) {
vm.$parent.destroy()
vm = null
}
})
it('use', () => {
Vue.use(Upload)
expect(Vue.component(Upload.name))
.to.be.a('function')
expect(Vue.component(UploadBtn.name))
.to.be.a('function')
expect(Vue.component(UploadFile.name))
.to.be.a('function')
})
it('should render correct contents', () => {
vm = createUpload()
expect(vm.$el.className)
.to.equal('cube-upload')
expect(vm.$el.children[0].className)
.to.equal('cube-upload-def clear-fix')
expect(vm.$el.querySelectorAll('.cube-upload-btn-def').length)
.to.equal(1)
const input = vm.$el.querySelector('input')
expect(input.className)
.to.equal('cube-upload-input')
expect(input.type)
.to.equal('file')
expect(input.multiple)
.to.be.true
expect(input.accept)
.to.equal('image/*')
})
it('should add files & upload', function (done) {
this.timeout(1000)
vm = createFilesUpload()
// check data
expect(vm.files.length)
.to.equal(2)
expect(vm.files[0].name)
.to.equal('xx.x')
expect(vm.files[0].size)
.to.equal(3)
// check init content
setTimeout(() => {
const allFiles = vm.$el.querySelectorAll('.cube-upload-file')
expect(allFiles.length)
.to.equal(2)
expect(allFiles[0].querySelector('.cube-upload-file-def').style.backgroundImage)
.not.to.be.null
expect(allFiles[0].querySelector('.cube-upload-file-state').className)
.to.equal('cube-upload-file-state')
expect(allFiles[0].querySelector('.cube-upload-file-status').className)
.to.equal('cube-upload-file-status')
expect(allFiles[0].querySelector('.cube-upload-file-progress').textContent.trim())
.to.equal('0%')
}, 50)
// start uploading
setTimeout(() => {
// check data state
expect(vm.files[0]._xhr)
.not.to.be.null
expect(vm.files[0].status)
.to.equal('uploading')
expect(vm.files[0].progress)
.to.equal(0)
expect(vm.files[1].status)
.to.equal('ready')
// progress
vm.files[0]._xhr.triggerProgress(1, vm.files[0].size)
expect(vm.files[0].progress.toFixed(2))
.to.equal('0.33')
vm.$nextTick(() => {
const allFiles = vm.$el.getElementsByClassName('cube-upload-file')
expect(allFiles[0].querySelector('.cube-upload-file-state').className)
.to.equal('cube-upload-file-state cube-upload-file_stat')
expect(allFiles[0].querySelector('.cube-upload-file-progress').textContent.trim())
.to.equal('33%')
// progress agagin
vm.files[0]._xhr.triggerProgress(2, vm.files[0].size)
setTimeout(() => {
expect(vm.files[0].progress.toFixed(2))
.to.equal('0.67')
// success
vm.files[0]._xhr.triggerSuccess()
expect(vm.files[0].progress)
.to.equal(1)
expect(vm.files[0].status)
.to.equal('success')
setTimeout(() => {
expect(allFiles[0].querySelector('.cube-upload-file-status').className)
.to.equal('cube-upload-file-status cubeic-right')
// next file
expect(vm.files[1]._xhr)
.not.to.be.null
expect(vm.files[1].status)
.to.equal('uploading')
expect(vm.files[1].progress)
.to.equal(0)
// error
vm.files[1]._xhr.triggerError()
expect(vm.files[1].progress)
.to.equal(1)
expect(vm.files[1].status)
.to.equal('error')
setTimeout(() => {
expect(allFiles[1].querySelector('.cube-upload-file-status').className)
.to.equal('cube-upload-file-status cubeic-warn')
done()
})
})
}, 100)
})
}, 200)
})
it('should remove file', function (done) {
this.timeout(1000)
vm = createFilesUpload(3)
expect(vm.files.length)
.to.equal(3)
setTimeout(() => {
expect(vm.files[0].status)
.to.equal('uploading')
const allFiles = vm.$el.getElementsByClassName('cube-upload-file')
// click remove ele
allFiles[0].querySelector('.cubeic-wrong').click()
expect(vm.files.length)
.to.equal(2)
expect(vm.files[0].name)
.to.equal('zz.z')
done()
}, 200)
})
it('should start & pause upload', function (done) {
this.timeout(1000)
vm = createFilesUpload()
setTimeout(() => {
expect(vm.files[0].status)
.to.equal('uploading')
expect(vm.paused)
.to.be.false
vm.start()
expect(vm.paused)
.to.be.false
vm.pause()
expect(vm.paused)
.to.be.true
expect(vm.files[0].status)
.to.equal('ready')
vm.start()
expect(vm.paused)
.to.be.false
expect(vm.files[0].status)
.to.equal('uploading')
done()
}, 200)
})
it('should retry upload', function (done) {
this.timeout(1000)
vm = createFilesUpload()
setTimeout(() => {
vm.files[0]._xhr.triggerError()
vm.files[1]._xhr.triggerError()
vm.retry()
expect(vm.files[0].retryId)
.not.to.be.null
expect(vm.files[0].status)
.to.equal('uploading')
done()
}, 200)
})
it('should trigger events', function (done) {
this.timeout(1000)
const filesAddedHandler = sinon.spy()
const fileSubmittedHandler = sinon.spy()
const fileRemovedHandler = sinon.spy()
const fileSuccessHandler = sinon.spy()
const fileErrorHandler = sinon.spy()
const fileClickHandler = sinon.spy()
vm = createFilesUpload(3, {
'files-added': filesAddedHandler,
'file-submitted': fileSubmittedHandler,
'file-removed': fileRemovedHandler,
'file-success': fileSuccessHandler,
'file-error': fileErrorHandler,
'file-click': fileClickHandler
})
expect(filesAddedHandler)
.to.be.calledOnce
expect(filesAddedHandler.getCall(0).args[0].length)
.to.equal(4)
expect(fileSubmittedHandler)
.to.have.callCount(3)
setTimeout(() => {
// remove
const firstFile = vm.files[0]
const allFiles = vm.$el.getElementsByClassName('cube-upload-file')
// click remove ele
allFiles[0].querySelector('.cubeic-wrong').click()
expect(fileRemovedHandler)
.to.be.calledWith(firstFile)
// success
vm.files[0]._xhr.triggerSuccess()
expect(fileSuccessHandler)
.to.be.calledWith(vm.files[0])
// error
vm.files[1]._xhr.triggerError()
expect(fileErrorHandler)
.to.be.calledWith(vm.files[1])
// click
allFiles[1].click()
expect(fileClickHandler)
.to.be.calledWith(vm.files[0])
done()
}, 200)
})
function createUpload(props = { action: '/upload' }, events = {}) {
const vm = instantiateComponent(Vue, Upload, {
props,
on: events
})
return vm
}
function createFilesUpload(max = 2, events = {}) {
const vm = createUpload({
action: {
target: '/upload',
progressInterval: 30,
data: {
param: 'param'
},
headers: {
'my-header': 'my-header'
}
},
max
}, events)
const uploadBtn = vm.$children[0]
uploadBtn.changeHandler({
currentTarget: {
files: [
new window.File(['111'], 'xx.x'),
new window.File(['222'], 'yy.y', {
ignore: true
}),
new window.File(['333'], 'zz.z'),
new window.File(['444'], '44.44')
]
}
})
return vm
}
})
// fake File
function File(content, name, options) {
var blob = new window.Blob(content)
blob.name = name
if (options) {
for (let k in options) {
blob[k] = options[k]
}
}
return blob
}
window.File = File
// fake XMLHttpRequest
function XMLHttpRequest() {
this.method = ''
this.url = ''
this.async = false
this.upload = {}
this.data = null
}
XMLHttpRequest.prototype = {
open(method, url, async) {
this.method = method
this.url = url
this.async = async
this.headers = {}
},
send(data) {
this.data = data
if (this.timeout) {
this.timeoutId = setTimeout(() => {
this.triggerTimeout()
}, this.timeout)
}
},
abort() {
this.triggerError()
},
setRequestHeader(key, value) {
this.headers[key] = value
},
getAllResponseHeaders() {
return ''
},
triggerProgress(loaded = 0, total) {
this.upload.onprogress && this.upload.onprogress.call(this, {
loaded,
total
})
},
triggerSuccess(status = 200, msg = '{"state": "ok"}') {
this.status = status
this.responseText = msg
this.onload && this.onload()
},
triggerTimeout() {
this.ontimeout && this.ontimeout()
},
triggerError(status = 500) {
this.onerror && this.onerror(status)
}
}
window.XMLHttpRequest = XMLHttpRequest
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册