Initial commit
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"presets": [
|
||||
["env", {
|
||||
"modules": false,
|
||||
"targets": {
|
||||
"browsers": ["iOS >= 8", "Android >= 4"]
|
||||
}
|
||||
}]
|
||||
],
|
||||
"plugins": ["transform-object-rest-spread"],
|
||||
"env": {
|
||||
"test": {
|
||||
"presets": [["env", {
|
||||
"modules": false
|
||||
}], "stage-2"],
|
||||
"plugins": ["istanbul"]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
|
@ -0,0 +1,11 @@
|
|||
build/*.js
|
||||
config/*.js
|
||||
lib/*
|
||||
output/*
|
||||
examples/*
|
||||
site/*
|
||||
**/*.spec.*
|
||||
**/demo/data/**
|
||||
scroller.js
|
||||
animate.js
|
||||
gulpfile.js
|
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
parserOptions: {ecmaVersion: 8, sourceType: 'module', ecmaFeatures: {jsx: true, experimentalObjectRestSpread: true}},
|
||||
env: {es6: true, node: true, browser: true},
|
||||
plugins: ['html', 'json'],
|
||||
extends: ['eslint-config-aesir-mandatory'],
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
.DS_Store
|
||||
node_modules/
|
||||
dist/
|
||||
lib/
|
||||
output/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
test/unit/coverage
|
||||
test/e2e/reports
|
||||
selenium-debug.log
|
||||
site/public
|
||||
site/dist
|
||||
site/build/bin/algolia-key.js
|
||||
|
||||
docs/
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
.vscode
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
|
@ -0,0 +1,80 @@
|
|||
---
|
||||
title: 更新日志
|
||||
---
|
||||
|
||||
<!-- CUTOFF -->
|
||||
### 0.4.13
|
||||
|
||||
`2018-01-19`
|
||||
|
||||
- **Feature**
|
||||
- `ActionBar`属性`has-text`默认值为是否存在`slot`,即如果使用插槽可忽略`has-text`
|
||||
|
||||
- **Fix**
|
||||
- 修复`DatePicker`月份`undefined` #26
|
||||
- 修复`Icon`部分安卓机无法展示内置SVG问题 #27
|
||||
- 修复`ActionBar`的`Props`默认值设置无效 #28
|
||||
- 修复`TabPicker`在安卓`6.1`异步级联滑动问题
|
||||
|
||||
|
||||
### 0.4.8
|
||||
|
||||
`2018-01-16`
|
||||
|
||||
- **Feature**
|
||||
|
||||
- 新增组件`Codebox`, `Cashier`, `Chart`
|
||||
- `FieldItem`的属性`customized`默认值为是否存在`slot`,即如果使用插槽可忽略`customized` #23
|
||||
- `InputItem`新增属性`is-title-latent`用于支持表单标题延迟显示
|
||||
- `Radio`的`v-model`绑定由`options: Array<{text, value}>`中的`text`修改为`value`
|
||||
- `NumberKeyboard`新增属性`type`和`is-view`用于支持不同主题和键盘页面内嵌展示
|
||||
|
||||
- **Fix**
|
||||
- 修复`PopupTitleBar`无法引入
|
||||
- 修复`ImageViewer`部分安卓机无法关闭问题 #20
|
||||
- 修复`Picker`的`refresh`方法导致列表滚动异常 #24
|
||||
- 修复`InputItem`输入汉字异常 #25
|
||||
|
||||
|
||||
<!-- CUTOFF -->
|
||||
### 0.3.0
|
||||
|
||||
`2017-12-18`
|
||||
|
||||
- **Feature**
|
||||
|
||||
- `Radio`, `Selector`, `DropMenu`, `Tabs`, `TabPicker`支持`slot-scope`
|
||||
- `TabPicker`新增`data-struct`,`asyncFunc`支持普通,级联和异步三种数据结构
|
||||
- `Tip`新增`show/hide`事件
|
||||
- `Picker`新增`initial`事件
|
||||
- `DatePicker`新增`text-render`钩子方法满足列项内容自定义
|
||||
|
||||
- **Fix**
|
||||
- 修复`SwiperItem`无法引入错误
|
||||
- 修复`InputItem`, `NumberKeyboard`双向绑定异常
|
||||
- 修复`Popup`动画监听异常导致`hide`事件可能不会触发
|
||||
- 修复`Dialog`和`Toast`被遮盖问题
|
||||
- 修复`FieldItem`, `Tag`样式问题
|
||||
|
||||
### 0.2.0
|
||||
|
||||
`2017-11-28`
|
||||
|
||||
- **Feature**
|
||||
|
||||
- 新增组件`Radio`, `DatePicker`, `Captcha`
|
||||
- `Field`新增`solid`属性用来固定布局
|
||||
- `Steps`新增配置`icon`属性
|
||||
- `Agree`新增`slot`用来展示文案
|
||||
|
||||
- **Fix**
|
||||
- 修复部分文档,样式和错误
|
||||
|
||||
<!-- CUTOFF -->
|
||||
### 0.1.0
|
||||
|
||||
`2017-11-21`
|
||||
|
||||
- **Feature**
|
||||
|
||||
- 完成开发版开发,用于内部体验和测试
|
|
@ -0,0 +1,28 @@
|
|||
# Contribution Guideline
|
||||
|
||||
Thanks for considering to contribute this project. All issues and pull requests are highly appreciated.
|
||||
|
||||
## Pull Requests
|
||||
|
||||
Before sending pull request to this project, please read and follow guidelines below.
|
||||
|
||||
1. Branch: We only accept pull request on `dev` branch.
|
||||
2. Coding style: Follow the coding style used in mand-mobile.
|
||||
3. Commit message: Use English and be aware of your spelling.
|
||||
4. Test: Make sure to test your code.
|
||||
|
||||
Add device mode, API version, related log, screenshots and other related information in your pull request if possible.
|
||||
|
||||
NOTE: We assume all your contribution can be licensed under the [Apache License 2.0](https://github.com/didi/mand-mobile/blob/master/LICENSE).
|
||||
|
||||
## Issues
|
||||
|
||||
We love clearly described issues. :)
|
||||
|
||||
Following information can help us to resolve issues faster.
|
||||
|
||||
* Device mode and hardware information.
|
||||
* API version.
|
||||
* Logs.
|
||||
* Screenshots.
|
||||
* Steps to reproduce the issue.
|
|
@ -0,0 +1,433 @@
|
|||
Apache License
|
||||
|
||||
Version 2.0, January 2004
|
||||
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
|
||||
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
|
||||
|
||||
|
||||
1. Definitions.
|
||||
|
||||
|
||||
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
|
||||
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
|
||||
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
|
||||
other entities that control, are controlled by, or are under common
|
||||
|
||||
control with that entity. For the purposes of this definition,
|
||||
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
|
||||
direction or management of such entity, whether by contract or
|
||||
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
|
||||
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
|
||||
exercising permissions granted by this License.
|
||||
|
||||
|
||||
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
|
||||
including but not limited to software source code, documentation
|
||||
|
||||
source, and configuration files.
|
||||
|
||||
|
||||
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
|
||||
transformation or translation of a Source form, including but
|
||||
|
||||
not limited to compiled object code, generated documentation,
|
||||
|
||||
and conversions to other media types.
|
||||
|
||||
|
||||
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
|
||||
Object form, made available under the License, as indicated by a
|
||||
|
||||
copyright notice that is included in or attached to the work
|
||||
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
|
||||
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
|
||||
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
|
||||
the original version of the Work and any modifications or additions
|
||||
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
|
||||
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
|
||||
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
|
||||
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
|
||||
where such license applies only to those patent claims licensable
|
||||
|
||||
by such Contributor that are necessarily infringed by their
|
||||
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
|
||||
institute patent litigation against any entity (including a
|
||||
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
|
||||
or contributory patent infringement, then any patent licenses
|
||||
|
||||
granted to You under this License for that Work shall terminate
|
||||
|
||||
as of the date such litigation is filed.
|
||||
|
||||
|
||||
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
|
||||
modifications, and in Source or Object form, provided that You
|
||||
|
||||
meet the following conditions:
|
||||
|
||||
|
||||
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
|
||||
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
|
||||
stating that You changed the files; and
|
||||
|
||||
|
||||
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
|
||||
attribution notices from the Source form of the Work,
|
||||
|
||||
excluding those notices that do not pertain to any part of
|
||||
|
||||
the Derivative Works; and
|
||||
|
||||
|
||||
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
|
||||
include a readable copy of the attribution notices contained
|
||||
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
|
||||
of the following places: within a NOTICE text file distributed
|
||||
|
||||
as part of the Derivative Works; within the Source form or
|
||||
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
|
||||
within a display generated by the Derivative Works, if and
|
||||
|
||||
wherever such third-party notices normally appear. The contents
|
||||
|
||||
of the NOTICE file are for informational purposes only and
|
||||
|
||||
do not modify the License. You may add Your own attribution
|
||||
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
|
||||
that such additional attribution notices cannot be construed
|
||||
|
||||
as modifying the License.
|
||||
|
||||
|
||||
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
|
||||
may provide additional or different license terms and conditions
|
||||
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
|
||||
the conditions stated in this License.
|
||||
|
||||
|
||||
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
|
||||
this License, without any additional terms or conditions.
|
||||
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
|
||||
the terms of any separate license agreement you may have executed
|
||||
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
|
||||
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
|
||||
except as required for reasonable and customary use in describing the
|
||||
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
|
||||
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
|
||||
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
|
||||
incidental, or consequential damages of any character arising as a
|
||||
|
||||
result of this License or out of the use or inability to use the
|
||||
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
|
||||
other commercial damages or losses), even if such Contributor
|
||||
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
|
||||
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
|
||||
or other liability obligations and/or rights consistent with this
|
||||
|
||||
License. However, in accepting such obligations, You may act only
|
||||
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
|
||||
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
|
||||
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
|
||||
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
|
||||
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||
|
||||
replaced with your own identifying information. (Don't include
|
||||
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
|
||||
comment syntax for the file format. We also recommend that a
|
||||
|
||||
file or class name and description of purpose be included on the
|
||||
|
||||
same "printed page" as the copyright notice for easier
|
||||
|
||||
identification within third-party archives.
|
||||
|
||||
|
||||
|
||||
|
||||
Copyright (C) 2017 Beijing Didi Infinity Technology and Development Co.,Ltd. All rights reserved.
|
||||
|
||||
|
||||
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
You may obtain a copy of the License at
|
||||
|
||||
|
||||
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
|
||||
|
||||
|
||||
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.
|
|
@ -0,0 +1,141 @@
|
|||
<div align="center">
|
||||
<a href="#">
|
||||
<img width="80" src="./assets/logo.png" alt="LOGO">
|
||||
</a>
|
||||
</div>
|
||||
<div align="center">
|
||||
<a href="http://forthebadge.com">
|
||||
<img src="http://forthebadge.com/images/badges/made-with-vue.svg">
|
||||
</a>
|
||||
<a href="http://forthebadge.com">
|
||||
<img src="http://forthebadge.com/images/badges/built-with-love.svg">
|
||||
</a>
|
||||
<a href="http://forthebadge.com">
|
||||
<img src="http://forthebadge.com/images/badges/makes-people-smile.svg">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
# mand-mobile
|
||||
|
||||
[](https://travis-ci.org/adidi/mand-mobile)
|
||||
[](https://codecov.io/gh/didi/mand-mobile/branch/master)
|
||||
[](https://www.npmjs.org/package/mand-mobile)
|
||||
[](http://npmtrends.com/mand-mobile)
|
||||
[](http://isitmaintained.com/project/didi/mand-mobile "Average time to resolve an issue")
|
||||
[](http://isitmaintained.com/project/didi/mand-mobile "Percentage of issues still open")
|
||||
|
||||
A mobile UI toolkit, based on `Vue.js 2`, designed for financial scenes.
|
||||
|
||||
## Links
|
||||
|
||||
* [Home](https://didi.github.io/mand-mobile/)
|
||||
* [Developer Instruction](site/docs/development.md)
|
||||
* [Theme Customization](site/docs/theme.md)
|
||||
* [Change Log](CHANGELOG.md)
|
||||
* [Examples](https://didi.github.io/mand-mobile/example/)
|
||||
|
||||

|
||||
|
||||
## Install & Usage
|
||||
|
||||
### Install
|
||||
|
||||
```shell
|
||||
npm install mand-mobile --save
|
||||
```
|
||||
|
||||
### Import
|
||||
|
||||
* Use <a href="https://github.com/ant-design/babel-plugin-import" target="_blank">babel-plugin-import</a>
|
||||
or
|
||||
<a href="https://github.com/Brooooooklyn/ts-import-plugin" target="_blank">ts-import-plugin</a> (Recommended)
|
||||
|
||||
```javascript
|
||||
import { Button } from 'mand-mobile'
|
||||
```
|
||||
|
||||
* Manually import
|
||||
|
||||
```javascript
|
||||
import Button from 'mand-mobile/lib/button'
|
||||
import 'mand-mobile/lib/button/style'
|
||||
```
|
||||
|
||||
* Totally import
|
||||
|
||||
```javascript
|
||||
import Vue from 'vue'
|
||||
import mandMobile from 'mand-mobile'
|
||||
import 'mand-mobile/lib/mand-mobile.css'
|
||||
|
||||
Vue.use(mandMobile)
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
Select the components that you need to build your webapp. Find more details in [component preview](/mfe/mand-mobile/docs/preview).
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div id="app">
|
||||
<md-field title="form" class="block">
|
||||
<md-input-item
|
||||
title="name"
|
||||
placeholder="Please input your name."
|
||||
></md-input-item>
|
||||
<md-field-item
|
||||
title="sex"
|
||||
arrow="arrow-right"
|
||||
:value="sex"
|
||||
@click="isPickerShow = true"
|
||||
solid
|
||||
></md-field-item>
|
||||
<md-picker
|
||||
v-model="isPickerShow"
|
||||
:data="pickerData"
|
||||
title="sex"
|
||||
></md-picker>
|
||||
</md-field>
|
||||
<md-agree class="agree">
|
||||
Agree to the privacy policy.
|
||||
</md-agree>
|
||||
<md-action-bar :actions="actionBarData">
|
||||
¥128.00
|
||||
</md-action-bar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
Agree,
|
||||
ActionBar,
|
||||
Field,
|
||||
FieldItem,
|
||||
InputItem,
|
||||
Picker
|
||||
} from 'mand-mobile'
|
||||
|
||||
export default {
|
||||
name: 'app',
|
||||
|
||||
components: {
|
||||
[Agree.name]: Agree,
|
||||
[ActionBar.name]: ActionBar,
|
||||
[Field.name]: Field,
|
||||
[FieldItem.name]: FieldItem,
|
||||
[InputItem.name]: InputItem,
|
||||
[Picker.name]: Picker
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
isPickerShow: false,
|
||||
actionBarData: [{
|
||||
text: 'confirm'
|
||||
}],
|
||||
pickerData: [[{text:'male'},{text:'female'}]]
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
After Width: | Height: | Size: 643 B |
After Width: | Height: | Size: 3.0 KiB |
|
@ -0,0 +1,49 @@
|
|||
'use strict'
|
||||
const chalk = require('chalk')
|
||||
const semver = require('semver')
|
||||
const packageConfig = require('../package.json')
|
||||
const shell = require('shelljs')
|
||||
function exec (cmd) {
|
||||
return require('child_process').execSync(cmd).toString().trim()
|
||||
}
|
||||
|
||||
const versionRequirements = [
|
||||
{
|
||||
name: 'node',
|
||||
currentVersion: semver.clean(process.version),
|
||||
versionRequirement: packageConfig.engines.node
|
||||
}
|
||||
]
|
||||
|
||||
if (shell.which('npm')) {
|
||||
versionRequirements.push({
|
||||
name: 'npm',
|
||||
currentVersion: exec('npm --version'),
|
||||
versionRequirement: packageConfig.engines.npm
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = function () {
|
||||
const warnings = []
|
||||
for (let i = 0; i < versionRequirements.length; i++) {
|
||||
const mod = versionRequirements[i]
|
||||
if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
|
||||
warnings.push(mod.name + ': ' +
|
||||
chalk.red(mod.currentVersion) + ' should be ' +
|
||||
chalk.green(mod.versionRequirement)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (warnings.length) {
|
||||
console.log('')
|
||||
console.log(chalk.yellow('To use this template, you must update following to modules:'))
|
||||
console.log()
|
||||
for (let i = 0; i < warnings.length; i++) {
|
||||
const warning = warnings[i]
|
||||
console.log(' ' + warning)
|
||||
}
|
||||
console.log()
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,196 @@
|
|||
const prompt = require('inquirer').createPromptModule()
|
||||
const shellJs = require('shelljs')
|
||||
const path = require('path')
|
||||
const bluebird = require('bluebird')
|
||||
const ora = require('ora')
|
||||
const moment = require('moment')
|
||||
const userName = require('git-user-name')
|
||||
const userEmail = require('git-user-email')
|
||||
const fs = bluebird.promisifyAll(require('fs'))
|
||||
|
||||
// path
|
||||
const CWD = process.cwd()
|
||||
const COMPONENTS_PATH = path.resolve(CWD, './components')
|
||||
const EXPECT_SHELL = path.resolve(CWD, './build/template.exp')
|
||||
const DEMO_INDEX_PATH = path.resolve(CWD, './examples/demo-index.js')
|
||||
const COMPONENT_INDEX = path.resolve(CWD, './components/index.js')
|
||||
const COMPONENT_JSON = path.resolve(CWD, './examples/components.json')
|
||||
|
||||
function init (answers) {
|
||||
return Promise.resolve(answers)
|
||||
.then(checkNoRepeat)
|
||||
.then(create)
|
||||
.then(sync)
|
||||
.catch(err => {
|
||||
console.warn(err)
|
||||
})
|
||||
}
|
||||
|
||||
function checkNoRepeat(answers) {
|
||||
return fs.readdirAsync(COMPONENTS_PATH)
|
||||
.then(files => {
|
||||
return Promise.all(files.map(file => checkFile(COMPONENTS_PATH, file, answers.componentName))).then(() => answers)
|
||||
})
|
||||
}
|
||||
|
||||
function upperFirst(str) {
|
||||
return String.prototype.replace.call(str, /^\w/, function (match) {
|
||||
return String.prototype.toUpperCase.call(match)
|
||||
})
|
||||
}
|
||||
|
||||
function changeKebabToCamel(str) {
|
||||
return String.prototype.replace.call(str, /\-(\w)/g, function(match, p1) {
|
||||
return String.prototype.toUpperCase.call(p1)
|
||||
})
|
||||
}
|
||||
|
||||
function sync(answers) {
|
||||
return Promise.all([syncToComponentJson(answers), syncToExample(answers), syncToIndex(answers)]).then(() =>answers)
|
||||
}
|
||||
|
||||
function syncToExample(answers) {
|
||||
fs.readFileAsync(DEMO_INDEX_PATH, 'utf8')
|
||||
.then(str => {
|
||||
return compile(answers, str)
|
||||
})
|
||||
.then(str => fs.writeFileAsync(DEMO_INDEX_PATH, str, 'utf8'))
|
||||
.then(() => answers)
|
||||
}
|
||||
|
||||
function syncToIndex(answers) {
|
||||
/* 同步components/index文件 */
|
||||
fs.readFileAsync(COMPONENT_INDEX, 'utf8')
|
||||
.then(str => {
|
||||
return compile(answers, str)
|
||||
})
|
||||
.then(str => fs.writeFileAsync(COMPONENT_INDEX, str, 'utf8'))
|
||||
.then(() => answers)
|
||||
}
|
||||
|
||||
function syncToComponentJson(answers) {
|
||||
const json = require(COMPONENT_JSON)
|
||||
let index = json.findIndex(item => item.category === answers.componentType)
|
||||
if (index === -1) {
|
||||
index = json.length
|
||||
json.push({
|
||||
category: answers.componentType,
|
||||
list: [],
|
||||
})
|
||||
}
|
||||
json[index].list.push({
|
||||
name: answers.componentNameUpper,
|
||||
path: `/${answers.componentName}`,
|
||||
icon: answers.componentName,
|
||||
text: answers.componentCnName,
|
||||
})
|
||||
Array.prototype.forEach.call(json, element => {
|
||||
Array.prototype.sort.call(element.list, function(a, b) {
|
||||
return a.name > b.name ? 1 : -1
|
||||
})
|
||||
})
|
||||
Array.prototype.sort.call(json, (a, b) => {
|
||||
return a.category > b.category ? 1: -1
|
||||
})
|
||||
return fs.writeFileAsync(COMPONENT_JSON, JSON.stringify(json), 'utf8')
|
||||
.then(answers => answers)
|
||||
}
|
||||
|
||||
function compile(metaData, fileStr) {
|
||||
return String.prototype.replace.call(fileStr, /\/\*.*@init<%(.*)%>.*\*\//g, function (match, p1) {
|
||||
return (String.prototype.replace.call(p1, /\${(\w*)}/g, function (innMatch, innP1) {
|
||||
return metaData[innP1]
|
||||
})) + '\r' +match
|
||||
})
|
||||
}
|
||||
|
||||
function checkFile(dir, file, name) {
|
||||
const filePath = path.resolve(dir, `./${file}`)
|
||||
return fs.statAsync(filePath)
|
||||
.then(stat => {
|
||||
if (stat.isDirectory()) {
|
||||
if (name === file) {
|
||||
return Promise.reject(`组件库中已经存在名为${name}的组件!请仔细核对后重新创建`)
|
||||
}
|
||||
}
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
function exec (command, argvs) {
|
||||
const spinner = ora('Loading...').start()
|
||||
const result = shellJs.exec(`${command} ${argvs.map(item => `\'${item}\'`).join(' ')} >> /dev/null`)
|
||||
spinner.succeed(['执行完毕'])
|
||||
return result
|
||||
}
|
||||
|
||||
function create(answers) {
|
||||
exec('expect', [EXPECT_SHELL, answers.componentCnName, answers.componentName, answers.componentType, answers.componentDesc, answers.author, answers.time, COMPONENTS_PATH])
|
||||
return answers
|
||||
}
|
||||
|
||||
function getUserInfo() {
|
||||
let user = userName()
|
||||
if (!user) {
|
||||
user = 'anonymous'
|
||||
}
|
||||
const email = userEmail()
|
||||
if (email) {
|
||||
user += ` <${email}>`
|
||||
}
|
||||
return user
|
||||
}
|
||||
|
||||
function launch() {
|
||||
return prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'componentName',
|
||||
message: '请输入要创建的组件名称(kebab-case):',
|
||||
validate: function (str) {
|
||||
return /^[a-z][a-z|-]*[a-z]$/.test(str)
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
name: 'componentCnName',
|
||||
message: '请输入要创建的组件中文名称(中文):',
|
||||
},
|
||||
{
|
||||
type: 'list',
|
||||
choices: [
|
||||
"basic",
|
||||
"feedback",
|
||||
"form",
|
||||
"business",
|
||||
],
|
||||
name: 'componentType',
|
||||
message: '组件类型',
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
name: 'componentDesc',
|
||||
message: '组件描述',
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
name: 'author',
|
||||
message: '作者',
|
||||
default: getUserInfo(),
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
name: 'time',
|
||||
message: 'time',
|
||||
default: moment().format('YYYY年MM月DD日')
|
||||
}
|
||||
])
|
||||
.then(answers => {
|
||||
answers = Object.assign(answers, {
|
||||
componentNameUpper: upperFirst(changeKebabToCamel(answers.componentName))
|
||||
})
|
||||
return init(answers)
|
||||
})
|
||||
}
|
||||
|
||||
launch()
|
|
@ -0,0 +1,86 @@
|
|||
const wrap = require('word-wrap')
|
||||
|
||||
module.exports = {
|
||||
prompter: function(cz, commit) {
|
||||
cz.prompt([
|
||||
{
|
||||
type: "list",
|
||||
name: "type",
|
||||
message: "select the type of change that you\'re committing",
|
||||
choices: [
|
||||
{
|
||||
name: "feat: a feature addition (required)",
|
||||
value: "feat",
|
||||
},
|
||||
{
|
||||
name: "fix: fix a bug",
|
||||
value: "fix",
|
||||
},
|
||||
{
|
||||
name: "doc: a document modify or addition",
|
||||
value: "doc",
|
||||
},
|
||||
{
|
||||
name: "build: The front-end engineering",
|
||||
value: "build",
|
||||
},
|
||||
{
|
||||
name: "example: Example for component",
|
||||
value: "example"
|
||||
},
|
||||
{
|
||||
name: "test: unit test for component",
|
||||
value: "test"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: "input",
|
||||
name: "scoped",
|
||||
message: "affected components for this commit, for example: button\n",
|
||||
},
|
||||
{
|
||||
type: "input",
|
||||
name: "description",
|
||||
message: "Abstract of this commit, perfer English\n",
|
||||
validate: function (str) {
|
||||
return !!str
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "input",
|
||||
name: "issue",
|
||||
message: "Related issue number for this commit, please split with \'\,\'\n",
|
||||
validate: function (str) {
|
||||
return /(\d*\,)*\d*/.test(str)
|
||||
}
|
||||
}
|
||||
])
|
||||
.then(answers => {
|
||||
const maxLineWidth = 80;
|
||||
|
||||
const wrapOptions = {
|
||||
trim: true,
|
||||
newline: '\n',
|
||||
indent:'',
|
||||
width: maxLineWidth
|
||||
};
|
||||
|
||||
|
||||
// Hard limit this line
|
||||
let body = answers.type
|
||||
if (answers.scoped) {
|
||||
body += `(${answers.scoped}): `
|
||||
} else {
|
||||
body += ': '
|
||||
}
|
||||
body += answers.description
|
||||
if (answers.issue) {
|
||||
const issue = answers.issue.split(',')
|
||||
body += `[${issue.map(item => '#'+item).join(',')}]`
|
||||
}
|
||||
wrap(body, wrapOptions)
|
||||
commit(body)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,146 @@
|
|||
const path = require('path')
|
||||
const glob = require('glob')
|
||||
const compiler = require('vueify').compiler
|
||||
const bluebird = require('bluebird')
|
||||
const fs = bluebird.promisifyAll(require('fs'))
|
||||
const copy = require('recursive-copy')
|
||||
const stylus = require('stylus')
|
||||
const babel = bluebird.promisifyAll(require('babel-core'))
|
||||
const TARGET_LIB_BASE = 'lib'
|
||||
const SRC_BASE = 'components'
|
||||
|
||||
|
||||
function babelPluginInsertCssImportForVue ({ types: t }) {
|
||||
function computedSameDirCssPosition(filePath) {
|
||||
const filePathParse = path.parse(filePath)
|
||||
return `./style/${filePathParse.name}.css`
|
||||
}
|
||||
return {
|
||||
visitor: {
|
||||
Program(path, state) {
|
||||
const importLiteral = computedSameDirCssPosition(state.opts.filePath)
|
||||
path.unshiftContainer('body', t.ImportDeclaration([],t.StringLiteral(importLiteral)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function compileVueStylus (content, cb) {
|
||||
stylus(content)
|
||||
// .include(path.join(__dirname, 'src/*'))
|
||||
.import(path.join(__dirname, '../../components/_style/mixin/*.styl'))
|
||||
.import(path.join(__dirname, '../../node_modules/nib/lib/nib/vendor'))
|
||||
.import(path.join(__dirname, '../../node_modules/nib/lib/nib/gradients'))
|
||||
.import(path.join(__dirname, '../../node_modules/nib/lib/nib/flex'))
|
||||
.render((err, css) => {
|
||||
if (err) {
|
||||
throw err
|
||||
}
|
||||
cb(null, css)
|
||||
})
|
||||
}
|
||||
|
||||
function computedCompilerConfig(filePath) {
|
||||
return {
|
||||
extractCSS: true,
|
||||
babel: {
|
||||
plugins: [
|
||||
[babelPluginInsertCssImportForVue, {
|
||||
filePath,
|
||||
}]
|
||||
]
|
||||
},
|
||||
customCompilers: {
|
||||
stylus: compileVueStylus
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function move(destDir) {
|
||||
return new Promise((resolve, reject) => {
|
||||
copy(SRC_BASE, destDir, {filter: function(item) {
|
||||
if (/demo|test/.test(item)) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}}, function (err, result) {
|
||||
if (err) {
|
||||
reject(err)
|
||||
}
|
||||
resolve(result)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function compileVueAndReplace(filePath) {
|
||||
const styleDir = path.join(path.dirname(filePath), 'style')
|
||||
if (!fs.existsSync(styleDir)) {
|
||||
fs.mkdirSync(styleDir)
|
||||
}
|
||||
const fileBaseName = path.basename(filePath, '.vue')
|
||||
const cssFilePath = path.join(styleDir, `${fileBaseName}.css`)
|
||||
const jsFilePath = filePath.replace(/\.vue$/, '.js')
|
||||
console.info(cssFilePath, jsFilePath)
|
||||
const fileContent = fs.readFileSync(filePath, {
|
||||
encoding: 'utf8',
|
||||
})
|
||||
const config = computedCompilerConfig(filePath)
|
||||
compiler.applyConfig(config)
|
||||
let styleContent = ''
|
||||
const styleCb = res => {
|
||||
if (res.style) {
|
||||
styleContent = res.style
|
||||
}
|
||||
}
|
||||
compiler.on('style', styleCb)
|
||||
return new Promise((resolve, reject) => {
|
||||
compiler.compile(fileContent, filePath, (err, result) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
}
|
||||
compiler.removeListener('style', styleCb)
|
||||
fs.writeFileAsync(jsFilePath, result)
|
||||
.then(() => fs.writeFileAsync(cssFilePath, styleContent))
|
||||
.then(() => {
|
||||
return fs.unlinkAsync(filePath)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function compileJsAndReplace(filePath){
|
||||
babel.transformFileAsync(filePath)
|
||||
.then(({code}) => {
|
||||
return fs.writeFileAsync(filePath, code)
|
||||
})
|
||||
.catch(error => {
|
||||
console.info(`${filePath} build error::error.stack=${error.stack}`)
|
||||
})
|
||||
}
|
||||
|
||||
function compileAndReplaceAllJsFile() {
|
||||
const fileGlob = `${TARGET_LIB_BASE}/**/*.js`
|
||||
const jsFiles = glob.sync(fileGlob)
|
||||
return Promise.all(jsFiles.map(compileJsAndReplace))
|
||||
.catch(e => {
|
||||
console.info(e)
|
||||
})
|
||||
}
|
||||
|
||||
function compileAndReplaceAllVueFile() {
|
||||
const fileGlob = `${TARGET_LIB_BASE}/**/*.vue`
|
||||
const jsFiles = glob.sync(fileGlob)
|
||||
return Promise.all(jsFiles.map(compileVueAndReplace))
|
||||
.catch(e => {
|
||||
console.info(e)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
function main() {
|
||||
return move('lib')
|
||||
.then(() => Promise.all([compileAndReplaceAllJsFile(), compileAndReplaceAllVueFile()]))
|
||||
.catch(e => console.info(e))
|
||||
}
|
||||
|
||||
main()
|
|
@ -0,0 +1,28 @@
|
|||
const { rollupPlugin, EXAMPLE_OUTPUT_DIR, PROJECT_DIR } = require('./rollup-plugin-config')
|
||||
const rollup = require('rollup')
|
||||
const path = require('path')
|
||||
|
||||
const inputOptions = {
|
||||
input: path.resolve(PROJECT_DIR, 'examples/main.indemand.js'),
|
||||
plugins: rollupPlugin,
|
||||
}
|
||||
|
||||
const outputCommonjsOptions = {
|
||||
file: path.resolve(EXAMPLE_OUTPUT_DIR, 'mand-mobile-example.js'),
|
||||
format: 'umd',
|
||||
}
|
||||
|
||||
function build() {
|
||||
return rollup.rollup(inputOptions)
|
||||
.then(bundle => {
|
||||
return bundle.write(outputCommonjsOptions).then(() => {
|
||||
console.info('build example succ')
|
||||
})
|
||||
})
|
||||
.catch(err => {
|
||||
console.info(err)
|
||||
console.info('build error')
|
||||
})
|
||||
}
|
||||
|
||||
build()
|
|
@ -0,0 +1,43 @@
|
|||
const rollup = require('rollup')
|
||||
const path = require('path')
|
||||
const {
|
||||
LIB_DIR,
|
||||
PROJECT_DIR,
|
||||
rollupPlugin
|
||||
} = require('./rollup-plugin-config')
|
||||
|
||||
const inputOptions = {
|
||||
input: path.resolve(PROJECT_DIR, 'components/index.js'),
|
||||
external: ['vue'],
|
||||
plugins: rollupPlugin
|
||||
}
|
||||
const outputUmdOptions = {
|
||||
file: path.resolve(LIB_DIR, 'mand-mobile.umd.js'),
|
||||
format: 'umd',
|
||||
name: 'mand-mobile'
|
||||
}
|
||||
|
||||
const outputEsOptions = {
|
||||
file: path.resolve(LIB_DIR, 'mand-mobile.esm.js'),
|
||||
format: 'es',
|
||||
}
|
||||
|
||||
function build() {
|
||||
return rollup.rollup(inputOptions)
|
||||
.then(bundle => {
|
||||
return Promise.all([
|
||||
bundle.write(outputUmdOptions).then(() => {
|
||||
console.info('build umd module succ')
|
||||
}),
|
||||
bundle.write(outputEsOptions).then(() => {
|
||||
console.info('build es module succ')
|
||||
})
|
||||
])
|
||||
})
|
||||
.catch(err => {
|
||||
console.info(err)
|
||||
console.info('build error')
|
||||
})
|
||||
}
|
||||
|
||||
build()
|
|
@ -0,0 +1,65 @@
|
|||
const rollup = require('rollup')
|
||||
const path = require('path')
|
||||
const { rollupPlugin, DEV_OUTPUT_DIR, PROJECT_DIR } = require('./rollup-plugin-config')
|
||||
|
||||
|
||||
// express
|
||||
const livereload = require('livereload')
|
||||
const express = require('express')
|
||||
const history = require('connect-history-api-fallback')
|
||||
// const opn = require('opn')
|
||||
const port = 4000
|
||||
|
||||
const inputOptions = {
|
||||
input: path.resolve(PROJECT_DIR, 'examples/main.indemand.js'),
|
||||
plugins: rollupPlugin,
|
||||
}
|
||||
|
||||
const outputCommonjsOptions = {
|
||||
file: path.resolve(DEV_OUTPUT_DIR, 'mand-mobile-dev.js'),
|
||||
format: 'umd',
|
||||
}
|
||||
|
||||
function watch() {
|
||||
const watchOptions = {
|
||||
...inputOptions,
|
||||
output: outputCommonjsOptions,
|
||||
}
|
||||
const watcher = rollup.watch(watchOptions)
|
||||
watcher.on('event', e => {
|
||||
console.info(e)
|
||||
if (e.code === 'END') {
|
||||
console.info('resource rebuild')
|
||||
}
|
||||
if (e.code === 'ERROR') {
|
||||
console.info(e)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
function serve(path) {
|
||||
return express.static(path, {})
|
||||
}
|
||||
|
||||
function runServer() {
|
||||
// rollup buildwatch
|
||||
watch()
|
||||
|
||||
// livereload
|
||||
const lrserver = livereload.createServer()
|
||||
lrserver.watch(path.join(process.cwd(), 'output'))
|
||||
|
||||
// static resource server
|
||||
const app = express()
|
||||
app.use(history({
|
||||
verbose: true
|
||||
}))
|
||||
app.use('/', serve(DEV_OUTPUT_DIR))
|
||||
app.listen(port, function() {
|
||||
console.log('> Starting dev server...')
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
runServer()
|
|
@ -0,0 +1,159 @@
|
|||
const path = require('path')
|
||||
const os = require('os')
|
||||
const fs = require('fs')
|
||||
const aliasPlugin = require('rollup-plugin-alias')
|
||||
const replacePlugin = require('rollup-plugin-replace')
|
||||
const jsonPlugin = require('rollup-plugin-json')
|
||||
const urlPlugin = require('rollup-plugin-url')
|
||||
const nodeResolvePlugin = require('rollup-plugin-node-resolve')
|
||||
const vuePlugin = require('rollup-plugin-vue')
|
||||
const babel = require('rollup-plugin-babel')
|
||||
const common = require('rollup-plugin-commonjs')
|
||||
const stylusMixin = require('../stylus-mixin')
|
||||
const builtins = require('rollup-plugin-node-builtins')
|
||||
const uglify = require('rollup-plugin-uglify')
|
||||
const nodeGlobals = require('rollup-plugin-node-globals')
|
||||
const glob = require('rollup-plugin-glob-import')
|
||||
const progress = require('rollup-plugin-progress')
|
||||
const fillHtmlPlugin = require('rollup-plugin-template-html')
|
||||
const filesize = require('rollup-plugin-filesize')
|
||||
const postcss = require('rollup-plugin-postcss')
|
||||
// const postcssUrl = require('postcss-url')
|
||||
|
||||
const babelrc = require('babelrc-rollup').default
|
||||
|
||||
const isProduction = process.env.NODE_ENV === 'production'
|
||||
const isTest = process.env.NODE_ENV === 'testing'
|
||||
const isDev = !(isProduction || isTest)
|
||||
const isExample = process.env.BUILD_TYPE === 'example'
|
||||
|
||||
function resolve(dir) {
|
||||
return path.resolve(__dirname, '../..', dir)
|
||||
}
|
||||
const LIB_DIR = resolve('lib')
|
||||
const PROJECT_DIR = resolve('.')
|
||||
|
||||
const tmpDir = os.tmpdir()
|
||||
const DEV_OUTPUT_DIR = fs.mkdtempSync(`${tmpDir}${path.sep}`)
|
||||
const EXAMPLE_OUTPUT_DIR = resolve('docs/example')
|
||||
|
||||
const tmpTestDir = os.tmpdir()
|
||||
const TEST_OUTPUT_DIR = fs.mkdtempSync(`${tmpTestDir}${path.sep}`)
|
||||
|
||||
function vueWarpper() {
|
||||
let distDir = '', fileName = ''
|
||||
if (isDev) {
|
||||
distDir = DEV_OUTPUT_DIR
|
||||
fileName = 'mand-mobile-dev.css'
|
||||
} else if (isExample) {
|
||||
distDir = EXAMPLE_OUTPUT_DIR
|
||||
fileName = 'mand-mobile-example.css'
|
||||
} else if (isProduction) {
|
||||
distDir = LIB_DIR
|
||||
fileName = 'mand-mobile.css'
|
||||
} else if (isTest) {
|
||||
distDir = TEST_OUTPUT_DIR
|
||||
fileName = 'mand-mobile-test.css'
|
||||
}
|
||||
return vuePlugin({
|
||||
css: path.resolve(distDir, fileName),
|
||||
stylus: {
|
||||
use: [stylusMixin],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const vue = vueWarpper()
|
||||
// const css = cssWarpper()
|
||||
|
||||
function conditionHelper(condition, plugins) {
|
||||
return condition ? plugins : []
|
||||
}
|
||||
|
||||
const rollupPlugin = [
|
||||
// resolve
|
||||
...(conditionHelper(!isDev, [
|
||||
aliasPlugin({
|
||||
resolve: ['.js', '/index.js', '.css', '.vue', '.svg'], // @TODO '/index.js' hack
|
||||
'mand-mobile/components': resolve('components'),
|
||||
'mand-mobile/lib': resolve('lib'),
|
||||
'mand-mobile': resolve('lib/mand-mobile.esm.js'),
|
||||
'@examples/assets/images/bank-zs.svg': resolve('examples/assets/images/bank-zs.svg')
|
||||
}),
|
||||
])),
|
||||
...(conditionHelper(isDev, [
|
||||
aliasPlugin({
|
||||
resolve: ['.js', '/index.js', '.css', '.vue', '.svg'], // @TODO '/index.js' hack
|
||||
'mand-mobile/components': resolve('components'),
|
||||
'mand-mobile/lib': resolve('lib'),
|
||||
'mand-mobile': resolve('components'),
|
||||
'@examples/assets/images/bank-zs.svg': resolve('examples/assets/images/bank-zs.svg')
|
||||
}),
|
||||
])),
|
||||
nodeResolvePlugin({
|
||||
extensions: [ '.js', '.json', '.vue' ],
|
||||
}),
|
||||
...(conditionHelper(isTest, [
|
||||
common({
|
||||
exclude: ['components/_util/*.js'],
|
||||
namedExports: { 'avoriaz': ['mount', 'shallow'] },
|
||||
}),
|
||||
glob(),
|
||||
])),
|
||||
|
||||
// inject
|
||||
replacePlugin({
|
||||
'process.env.NODE_ENV': `"${process.env.NODE_ENV}"`
|
||||
}),
|
||||
...(conditionHelper(isTest, [
|
||||
builtins(),
|
||||
nodeGlobals(),
|
||||
])),
|
||||
|
||||
// resource
|
||||
urlPlugin({
|
||||
limit: 10 * 1024,
|
||||
}),
|
||||
jsonPlugin(),
|
||||
vue,
|
||||
postcss(),
|
||||
babel(babelrc({
|
||||
addModuleOptions: false,
|
||||
findRollupPresets: true,
|
||||
addExternalHelpersPlugin: false,
|
||||
})),
|
||||
|
||||
// dest
|
||||
...(conditionHelper(isProduction, [
|
||||
uglify({
|
||||
compress: {},
|
||||
}),
|
||||
])),
|
||||
...(conditionHelper(isDev, [
|
||||
fillHtmlPlugin({
|
||||
template: resolve('examples/index.html'),
|
||||
publicPath: '/',
|
||||
destFile: path.resolve(DEV_OUTPUT_DIR, 'index.html')
|
||||
})
|
||||
])),
|
||||
...(conditionHelper(isExample, [
|
||||
fillHtmlPlugin({
|
||||
template: resolve('examples/index.html'),
|
||||
publicPath: '/',
|
||||
destFile: path.resolve(EXAMPLE_OUTPUT_DIR, 'index.html')
|
||||
})
|
||||
])),
|
||||
// cli
|
||||
progress(),
|
||||
...(conditionHelper(isProduction, [
|
||||
filesize(),
|
||||
])),
|
||||
]
|
||||
|
||||
module.exports = {
|
||||
LIB_DIR,
|
||||
PROJECT_DIR,
|
||||
EXAMPLE_OUTPUT_DIR,
|
||||
DEV_OUTPUT_DIR,
|
||||
rollupPlugin,
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
const path = require('path')
|
||||
module.exports = function useMixin(style) {
|
||||
return style
|
||||
.import(path.join(__dirname, '../components/_style/mixin/*.styl'))
|
||||
.import(path.join(__dirname, '../node_modules/nib/lib/nib/vendor'))
|
||||
.import(path.join(__dirname, '../node_modules/nib/lib/nib/gradients'))
|
||||
.import(path.join(__dirname, '../node_modules/nib/lib/nib/flex'))
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
#!/usr/bin/expect
|
||||
|
||||
set timeout 30
|
||||
set chinese_name [lindex $argv 0]
|
||||
set component_name [lindex $argv 1]
|
||||
set component_type [lindex $argv 2]
|
||||
set component_desc [lindex $argv 3]
|
||||
set author [lindex $argv 4]
|
||||
set create_time [lindex $argv 5]
|
||||
set component_path [lindex $argv 6]
|
||||
spawn mfe init mfe-template-mfd-mobile "$component_path/${component_name}"
|
||||
|
||||
expect "组件中文名称,如:轻提示"
|
||||
|
||||
send "${chinese_name}\r"
|
||||
|
||||
expect "组件类型, 选项: basic feedback form business"
|
||||
|
||||
send "${component_type}\r"
|
||||
|
||||
expect "组件描述"
|
||||
|
||||
send "${component_desc}\r"
|
||||
|
||||
expect "Author"
|
||||
|
||||
send "${author}\r"
|
||||
|
||||
expect "name"
|
||||
|
||||
send "${component_name}\r"
|
||||
|
||||
expect "time"
|
||||
|
||||
send "${create_time}\r"
|
||||
|
||||
expect eof
|
||||
|
||||
exit
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
'use strict'
|
||||
require('../check-versions')()
|
||||
|
||||
process.env.NODE_ENV = 'production'
|
||||
|
||||
// // const ora = require('ora')
|
||||
const rm = require('rimraf')
|
||||
const path = require('path')
|
||||
const chalk = require('chalk')
|
||||
const webpack = require('webpack')
|
||||
const config = require('../../config')
|
||||
const webpackConfig = require('./webpack.example.conf')
|
||||
|
||||
|
||||
rm(path.join(config.example.assetsRoot, config.example.assetsSubDirectory), err => {
|
||||
if (err) {
|
||||
throw err
|
||||
}
|
||||
webpack(webpackConfig, function (err, stats) {
|
||||
// spinner.stop()
|
||||
if (err) {
|
||||
throw err
|
||||
}
|
||||
process.stdout.write(stats.toString({
|
||||
colors: true,
|
||||
modules: false,
|
||||
children: false,
|
||||
chunks: false,
|
||||
chunkModules: false
|
||||
}) + '\n\n')
|
||||
|
||||
if (stats.hasErrors()) {
|
||||
console.log(chalk.red(' Build failed with errors.\n'))
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log(chalk.cyan(' Build complete.\n'))
|
||||
console.log(chalk.yellow(
|
||||
' Tip: built files are meant to be served over an HTTP server.\n' +
|
||||
' Opening index.html over file:// won\'t work.\n'
|
||||
))
|
||||
})
|
||||
})
|
|
@ -0,0 +1,28 @@
|
|||
'use strict'
|
||||
require('../check-versions')()
|
||||
|
||||
process.env.NODE_ENV = 'production'
|
||||
|
||||
const chalk = require('chalk')
|
||||
const webpack = require('webpack')
|
||||
const webpackConfig = require('./webpack.build.conf')
|
||||
|
||||
webpack(webpackConfig, function (err, stats) {
|
||||
if (err) {
|
||||
throw err
|
||||
}
|
||||
process.stdout.write(stats.toString({
|
||||
colors: true,
|
||||
modules: false,
|
||||
children: false,
|
||||
chunks: false,
|
||||
chunkModules: false
|
||||
}) + '\n\n')
|
||||
|
||||
if (stats.hasErrors()) {
|
||||
console.log(chalk.red(' Build failed with errors.\n'))
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log(chalk.cyan(' Build complete.\n'))
|
||||
})
|
|
@ -0,0 +1,67 @@
|
|||
|
||||
/**
|
||||
* Build style entry of all components
|
||||
*/
|
||||
|
||||
const fs = require('fs-extra')
|
||||
const path = require('path')
|
||||
const components = require('../../examples/components.json')
|
||||
const dependencyTree = require('dependency-tree')
|
||||
const libDir = path.resolve(__dirname, '../../lib')
|
||||
|
||||
const SEP = path.sep
|
||||
|
||||
function generateComponentsList (components) {
|
||||
const list = ['field-item', 'swiper-item', 'popup-title-bar']
|
||||
components.map(nav =>
|
||||
nav.list.map(item =>
|
||||
list.push(item.path.substr(1))
|
||||
)
|
||||
)
|
||||
return list
|
||||
}
|
||||
|
||||
const componentList = generateComponentsList(components)
|
||||
function checkComponentHasStyle(componentName) {
|
||||
if (~componentName.indexOf('.js')) {
|
||||
componentName = componentName.replace('.js', '.css')
|
||||
return fs.existsSync(path.join(__dirname, `../lib/style/${componentName}`))
|
||||
} else {
|
||||
return fs.existsSync(path.join(__dirname, `../lib/style/${componentName}/index.css`))
|
||||
}
|
||||
}
|
||||
|
||||
function search(tree, checkList) {
|
||||
tree && Object.keys(tree).forEach(key => {
|
||||
search(tree[key], checkList)
|
||||
const component = key.split(`${SEP}mand-mobile${SEP}lib${SEP}`)[1].replace(`${SEP}index.js`, '').replace(`mixins${SEP}`, '')
|
||||
if (checkList.indexOf(component) === -1) {
|
||||
checkList.push(component)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Analyze component dependencies
|
||||
function analyzeDependencies(componentName, libDir) {
|
||||
const checkList = []
|
||||
search(dependencyTree({
|
||||
directory: libDir,
|
||||
filename: path.resolve(libDir, componentName, 'index.js'),
|
||||
filter: path => path.indexOf(`mand-mobile${SEP}lib${SEP}`) !== -1
|
||||
}), checkList)
|
||||
return checkList.filter(component => checkComponentHasStyle(component))
|
||||
}
|
||||
|
||||
componentList.forEach(componentName => {
|
||||
const content = analyzeDependencies(componentName, libDir).map(component => {
|
||||
if (~component.indexOf('.js')) {
|
||||
component = component.replace('.js', '.css')
|
||||
return `require('../../style/${component}')`
|
||||
} else {
|
||||
return `require('../../style/${component}/index.css')`
|
||||
}
|
||||
})
|
||||
content.unshift('require(\'../../style/global.css\')')
|
||||
fs.outputFileSync(path.join(libDir, componentName, './style/index.js'), content.join('\n'))
|
||||
})
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
/* eslint-disable */
|
||||
'use strict'
|
||||
require('eventsource-polyfill')
|
||||
var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true')
|
||||
|
||||
hotClient.subscribe(function (event) {
|
||||
if (event.action === 'reload') {
|
||||
window.location.reload()
|
||||
}
|
||||
})
|
|
@ -0,0 +1,107 @@
|
|||
'use strict'
|
||||
require('../check-versions')()
|
||||
|
||||
const config = require('../../config')
|
||||
if (!process.env.NODE_ENV) {
|
||||
process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV)
|
||||
}
|
||||
|
||||
const opn = require('opn')
|
||||
const path = require('path')
|
||||
const express = require('express')
|
||||
const webpack = require('webpack')
|
||||
const proxyMiddleware = require('http-proxy-middleware')
|
||||
let webpackConfig = require('./webpack.dev.conf')
|
||||
// default port where dev server listens for incoming traffic
|
||||
const port = process.env.PORT || config.dev.port
|
||||
// automatically open browser, if not set will be false
|
||||
const autoOpenBrowser = !!config.dev.autoOpenBrowser
|
||||
// Define HTTP proxies to your custom API backend
|
||||
// https://github.com/chimurai/http-proxy-middleware
|
||||
const proxyTable = config.dev.proxyTable
|
||||
|
||||
const app = express()
|
||||
const compiler = webpack(webpackConfig)
|
||||
const resolve = file => path.resolve(__dirname, file)
|
||||
const devMiddleware = require('webpack-dev-middleware')(compiler, {
|
||||
publicPath: webpackConfig.output.publicPath,
|
||||
})
|
||||
|
||||
const hotMiddleware = require('webpack-hot-middleware')(compiler, {
|
||||
log: false,
|
||||
heartbeat: 2000
|
||||
})
|
||||
|
||||
const serve = function (path) {
|
||||
return express.static(resolve(path), {})
|
||||
}
|
||||
|
||||
|
||||
// force page reload when html-webpack-plugin template changes
|
||||
// currently disabled until this is resolved:
|
||||
// https://github.com/jantimon/html-webpack-plugin/issues/680
|
||||
// compiler.plugin('compilation', function (compilation) {
|
||||
// compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) {
|
||||
// hotMiddleware.publish({ action: 'reload' })
|
||||
// cb()
|
||||
// })
|
||||
// })
|
||||
|
||||
// enable hot-reload and state-preserving
|
||||
// compilation error display
|
||||
app.use(hotMiddleware)
|
||||
|
||||
// proxy api requests
|
||||
Object.keys(proxyTable).forEach(function (context) {
|
||||
let options = proxyTable[context]
|
||||
if (typeof options === 'string') {
|
||||
options = { target: options }
|
||||
}
|
||||
app.use(proxyMiddleware(options.filter || context, options))
|
||||
})
|
||||
|
||||
// handle fallback for HTML5 history API
|
||||
app.use(require('connect-history-api-fallback')())
|
||||
|
||||
// serve webpack bundle output
|
||||
app.use(devMiddleware)
|
||||
app.use('/static', serve(path.join(__dirname, '../../static')))
|
||||
// serve pure static assets
|
||||
// const staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory)
|
||||
// app.use(staticPath, express.static('./static'))
|
||||
|
||||
var _resolve
|
||||
var _reject
|
||||
var readyPromise = new Promise((resolve, reject) => {
|
||||
_resolve = resolve
|
||||
_reject = reject
|
||||
})
|
||||
|
||||
var server
|
||||
var portfinder = require('portfinder')
|
||||
portfinder.basePort = port
|
||||
|
||||
console.log('> Starting dev server...')
|
||||
devMiddleware.waitUntilValid(() => {
|
||||
portfinder.getPort((err, port) => {
|
||||
if (err) {
|
||||
_reject(err)
|
||||
}
|
||||
process.env.PORT = port
|
||||
var uri = 'http://localhost:' + port
|
||||
console.log('> Listening at ' + uri + '\n')
|
||||
// when env is testing, don't need open it
|
||||
if (autoOpenBrowser && process.env.NODE_ENV !== 'testing') {
|
||||
opn(uri)
|
||||
}
|
||||
server = app.listen(port)
|
||||
_resolve()
|
||||
})
|
||||
})
|
||||
|
||||
module.exports = {
|
||||
ready: readyPromise,
|
||||
close: () => {
|
||||
server.close()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
'use strict'
|
||||
const path = require('path')
|
||||
const config = require('../../config')
|
||||
const ExtractTextPlugin = require('extract-text-webpack-plugin')
|
||||
const resolve = file => path.resolve(__dirname, file)
|
||||
|
||||
exports.assetsPath = function (_path) {
|
||||
const assetsSubDirectory = process.env.NODE_ENV === 'production'
|
||||
? config.build.assetsSubDirectory
|
||||
: config.dev.assetsSubDirectory
|
||||
return path.posix.join(assetsSubDirectory, _path)
|
||||
}
|
||||
|
||||
exports.cssLoaders = function (options) {
|
||||
options = options || {}
|
||||
|
||||
const cssLoader = {
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
minimize: process.env.NODE_ENV === 'production',
|
||||
sourceMap: options.sourceMap
|
||||
}
|
||||
}
|
||||
|
||||
// generate loader string to be used with extract text plugin
|
||||
function generateLoaders (loader, loaderOptions) {
|
||||
const loaders = [cssLoader]
|
||||
if (loader) {
|
||||
loaders.push({
|
||||
loader: loader + '-loader',
|
||||
options: Object.assign({}, loaderOptions, {
|
||||
sourceMap: options.sourceMap
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Extract CSS when that option is specified
|
||||
// (which is the case during production build)
|
||||
if (options.extract) {
|
||||
return ExtractTextPlugin.extract({
|
||||
use: loaders,
|
||||
fallback: 'vue-style-loader'
|
||||
})
|
||||
} else {
|
||||
return ['vue-style-loader'].concat(loaders)
|
||||
}
|
||||
}
|
||||
|
||||
// https://vue-loader.vuejs.org/en/configurations/extract-css.html
|
||||
const stylusMixins = [
|
||||
'~nib/lib/nib/vendor',
|
||||
'~nib/lib/nib/gradients.styl',
|
||||
'~nib/lib/nib/flex',
|
||||
resolve('../../components/_style/mixin/util.styl'),
|
||||
resolve('../../components/_style/mixin/theme.styl'),
|
||||
resolve('../../examples/theme.custom.styl')
|
||||
]
|
||||
return {
|
||||
css: generateLoaders(),
|
||||
postcss: generateLoaders(),
|
||||
stylus: generateLoaders('stylus', {
|
||||
import: stylusMixins
|
||||
}),
|
||||
styl: generateLoaders('stylus', {
|
||||
import: stylusMixins
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Generate loaders for standalone style files (outside of .vue)
|
||||
exports.styleLoaders = function (options) {
|
||||
const output = []
|
||||
const loaders = exports.cssLoaders(options)
|
||||
for (const extension in loaders) {
|
||||
if (loaders.hasOwnProperty(extension)) {
|
||||
const loader = loaders[extension]
|
||||
output.push({
|
||||
test: new RegExp('\\.' + extension + '$'),
|
||||
use: loader
|
||||
})
|
||||
}
|
||||
}
|
||||
return output
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
'use strict'
|
||||
const utils = require('./utils')
|
||||
const config = require('../../config')
|
||||
const isProduction = process.env.NODE_ENV === 'production'
|
||||
|
||||
module.exports = {
|
||||
loaders: utils.cssLoaders({
|
||||
sourceMap: isProduction
|
||||
? config.build.productionSourceMap
|
||||
: config.dev.cssSourceMap,
|
||||
extract: isProduction
|
||||
}),
|
||||
transformToRequire: {
|
||||
video: 'src',
|
||||
source: 'src',
|
||||
img: 'src',
|
||||
image: 'xlink:href'
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
'use strict'
|
||||
const path = require('path')
|
||||
const utils = require('./utils')
|
||||
const vueLoaderConfig = require('./vue-loader.conf')
|
||||
|
||||
function resolve (dir) {
|
||||
return path.join(__dirname, '../..', dir)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
resolve: {
|
||||
extensions: ['.js', '.vue', '.json'],
|
||||
alias: {
|
||||
'vue$': 'vue/dist/vue.runtime.esm.js',
|
||||
'@examples': resolve('examples'),
|
||||
'mand-mobile/lib': resolve('components'),
|
||||
'mand-mobile/components': resolve('components'),
|
||||
'mand-mobile': resolve('components'),
|
||||
}
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.(js|vue)$/,
|
||||
loader: 'eslint-loader',
|
||||
enforce: 'pre',
|
||||
include: [resolve('components'), resolve('examples'), resolve('test')],
|
||||
options: {
|
||||
quiet: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.vue$/,
|
||||
loader: 'vue-loader',
|
||||
options: vueLoaderConfig
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
loader: 'babel-loader?cacheDirectory',
|
||||
include: [resolve('components'), resolve('examples'), resolve('test')]
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpe?g|gif)(\?.*)?$/,
|
||||
loader: 'url-loader',
|
||||
options: {
|
||||
limit: 10000,
|
||||
name: utils.assetsPath('img/[name].[hash:7].[ext]')
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.svg$/,
|
||||
loader: 'svg-sprite-loader',
|
||||
include: [resolve('components'), resolve('examples/assets/images')]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
'use strict'
|
||||
const utils = require('./utils')
|
||||
const config = require('../../config')
|
||||
const webpack = require('webpack')
|
||||
const merge = require('webpack-merge')
|
||||
const baseWebpackConfig = require('./webpack.base.conf')
|
||||
const ExtractTextPlugin = require('extract-text-webpack-plugin')
|
||||
const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
|
||||
const ProgressBarPlugin = require('progress-bar-webpack-plugin')
|
||||
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
|
||||
const pkg = require('../../package.json')
|
||||
|
||||
const env = config.build.env
|
||||
|
||||
const webpackConfig = merge(baseWebpackConfig, {
|
||||
entry: {
|
||||
'mand-mobile': './components/index.js',
|
||||
},
|
||||
devtool: config.build.productionSourceMap ? '#source-map' : false,
|
||||
output: {
|
||||
path: config.build.assetsRoot,
|
||||
filename: utils.assetsPath('[name].js'),
|
||||
library: 'mand-mobile',
|
||||
libraryTarget: 'umd',
|
||||
umdNamedDefine: true
|
||||
},
|
||||
module: {
|
||||
rules: utils.styleLoaders({ sourceMap: config.build.cssSourceMap })
|
||||
},
|
||||
externals: {
|
||||
vue: {
|
||||
root: 'Vue',
|
||||
commonjs: 'vue',
|
||||
commonjs2: 'vue',
|
||||
amd: 'vue'
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
new ProgressBarPlugin({
|
||||
format: ' BUILD MAND_MOBILE [:bar] :percent (:elapsed seconds)',
|
||||
clear: false
|
||||
}),
|
||||
// http://vuejs.github.io/vue-loader/en/workflow/production.html
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': env,
|
||||
'MAN_VERSION': `'${pkg.version}'`
|
||||
}),
|
||||
// UglifyJs do not support ES6+, you can also use babel-minify for better treeshaking: https://github.com/babel/minify
|
||||
new webpack.optimize.UglifyJsPlugin({
|
||||
compress: {
|
||||
warnings: false
|
||||
},
|
||||
output: {
|
||||
ascii_only: true
|
||||
}
|
||||
}),
|
||||
// extract css into its own file
|
||||
new ExtractTextPlugin({
|
||||
filename: utils.assetsPath('[name].css')
|
||||
}),
|
||||
// Compress extracted CSS. We are using this plugin so that possible
|
||||
// duplicated CSS from different components can be deduped.
|
||||
new OptimizeCSSPlugin({
|
||||
cssProcessorOptions: {
|
||||
safe: true
|
||||
}
|
||||
}),
|
||||
new webpack.optimize.ModuleConcatenationPlugin()
|
||||
]
|
||||
})
|
||||
|
||||
if (process.env.npm_package_config_analysis) {
|
||||
webpackConfig.plugins.push(new BundleAnalyzerPlugin({
|
||||
// Can be `server`, `static` or `disabled`.
|
||||
// In `server` mode analyzer will start HTTP server to show bundle report.
|
||||
// In `static` mode single HTML file with bundle report will be generated.
|
||||
// In `disabled` mode you can use this plugin to just generate Webpack Stats JSON file by setting `generateStatsFile` to `true`.
|
||||
analyzerMode: 'server',
|
||||
// Host that will be used in `server` mode to start HTTP server.
|
||||
analyzerHost: 'localhost',
|
||||
// Port that will be used in `server` mode to start HTTP server.
|
||||
analyzerPort: 8888,
|
||||
// Path to bundle report file that will be generated in `static` mode.
|
||||
// Relative to bundles output directory.
|
||||
reportFilename: 'report.html',
|
||||
// Module sizes to show in report by default.
|
||||
// Should be one of `stat`, `parsed` or `gzip`.
|
||||
// See "Definitions" section for more information.
|
||||
defaultSizes: 'parsed',
|
||||
// Automatically open report in default browser
|
||||
openAnalyzer: true,
|
||||
// If `true`, Webpack Stats JSON file will be generated in bundles output directory
|
||||
generateStatsFile: false,
|
||||
// Name of Webpack Stats JSON file that will be generated if `generateStatsFile` is `true`.
|
||||
// Relative to bundles output directory.
|
||||
statsFilename: 'stats.json',
|
||||
// Options for `stats.toJson()` method.
|
||||
// For example you can exclude sources of your modules from stats file with `source: false` option.
|
||||
// See more options here: https://github.com/webpack/webpack/blob/webpack-1/lib/Stats.js#L21
|
||||
statsOptions: null,
|
||||
// Log level. Can be 'info', 'warn', 'error' or 'silent'.
|
||||
logLevel: 'info'
|
||||
}))
|
||||
}
|
||||
|
||||
module.exports = webpackConfig
|
|
@ -0,0 +1,58 @@
|
|||
const utils = require('./utils')
|
||||
const webpack = require('webpack')
|
||||
const config = require('../../config')
|
||||
const merge = require('webpack-merge')
|
||||
const baseWebpackConfig = require('./webpack.base.conf')
|
||||
const poststylus = require('poststylus')
|
||||
const pxtorem = require('postcss-pxtorem')
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin')
|
||||
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
|
||||
const DashBoardPlugin = require('webpack-dashboard/plugin')
|
||||
const pkg = require('../../package.json')
|
||||
// add hot-reload related code to entry chunks
|
||||
// Object.keys(baseWebpackConfig.entry).forEach(function (name) {
|
||||
// baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name])
|
||||
// })
|
||||
const pxtoremConfig = pxtorem({ rootValue: 100, propWhiteList: [] })
|
||||
|
||||
module.exports = merge(baseWebpackConfig, {
|
||||
entry: {
|
||||
'index': ['./build/webpack/dev-client', './examples/main.js']
|
||||
},
|
||||
output: {
|
||||
path: config.dev.assetsRoot,
|
||||
filename: '[name].js',
|
||||
chunkFilename: '[name].js',
|
||||
publicPath: config.dev.assetsPublicPath
|
||||
},
|
||||
module: {
|
||||
rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap })
|
||||
},
|
||||
// cheap-module-eval-source-map is faster for development
|
||||
devtool: '#cheap-module-eval-source-map',
|
||||
plugins: [
|
||||
new DashBoardPlugin(),
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': config.dev.env,
|
||||
'MAN_VERSION': `'${pkg.version}'`
|
||||
}),
|
||||
// https://github.com/seaneking/poststylus#webpack
|
||||
new webpack.LoaderOptionsPlugin({
|
||||
options: {
|
||||
stylus: {
|
||||
use: [poststylus(pxtoremConfig)]
|
||||
}
|
||||
}
|
||||
}),
|
||||
// https://github.com/glenjamin/webpack-hot-middleware#installation--usage
|
||||
new webpack.HotModuleReplacementPlugin(),
|
||||
new webpack.NoEmitOnErrorsPlugin(),
|
||||
// https://github.com/ampedandwired/html-webpack-plugin
|
||||
new HtmlWebpackPlugin({
|
||||
filename: config.dev.index,
|
||||
template: './examples/index.html',
|
||||
inject: true
|
||||
}),
|
||||
new FriendlyErrorsPlugin()
|
||||
]
|
||||
})
|
|
@ -0,0 +1,142 @@
|
|||
'use strict'
|
||||
const path = require('path')
|
||||
const utils = require('./utils')
|
||||
const webpack = require('webpack')
|
||||
const config = require('../../config')
|
||||
const merge = require('webpack-merge')
|
||||
const poststylus = require('poststylus')
|
||||
const pxtorem = require('postcss-pxtorem')
|
||||
const baseWebpackConfig = require('./webpack.base.conf')
|
||||
const CopyWebpackPlugin = require('copy-webpack-plugin')
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin')
|
||||
const ProgressBarPlugin = require('progress-bar-webpack-plugin')
|
||||
const ExtractTextPlugin = require('extract-text-webpack-plugin')
|
||||
const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
|
||||
|
||||
const env = config.example.env
|
||||
|
||||
const webpackConfig = merge(baseWebpackConfig, {
|
||||
entry: {
|
||||
'index': './examples/main.js',
|
||||
},
|
||||
module: {
|
||||
rules: utils.styleLoaders({
|
||||
sourceMap: config.example.productionSourceMap,
|
||||
extract: true
|
||||
})
|
||||
},
|
||||
devtool: config.example.productionSourceMap ? '#source-map' : false,
|
||||
output: {
|
||||
path: config.example.assetsRoot,
|
||||
filename: utils.assetsPath('js/[name].[chunkhash:7].js'),
|
||||
chunkFilename: utils.assetsPath('js/[name].[chunkhash:7].js'),
|
||||
publicPath: config.example.assetsPublicPath
|
||||
},
|
||||
plugins: [
|
||||
new ProgressBarPlugin({
|
||||
format: ' BUILD EXAMPLES [:bar] :percent (:elapsed seconds)',
|
||||
clear: false
|
||||
}),
|
||||
// http://vuejs.github.io/vue-loader/en/workflow/production.html
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': env
|
||||
}),
|
||||
// UglifyJs do not support ES6+, you can also use babel-minify for better treeshaking: https://github.com/babel/minify
|
||||
new webpack.optimize.UglifyJsPlugin({
|
||||
compress: {
|
||||
warnings: false
|
||||
},
|
||||
output: {
|
||||
ascii_only: true
|
||||
}
|
||||
}),
|
||||
// extract css into its own file
|
||||
new ExtractTextPlugin({
|
||||
filename: utils.assetsPath('css/[name].[contenthash:7].css')
|
||||
}),
|
||||
// Compress extracted CSS. We are using this plugin so that possible
|
||||
// duplicated CSS from different components can be deduped.
|
||||
new OptimizeCSSPlugin({
|
||||
cssProcessorOptions: {
|
||||
safe: true
|
||||
}
|
||||
}),
|
||||
// generate dist index.html with correct asset hash for caching.
|
||||
// you can customize output by editing /index.html
|
||||
// see https://github.com/ampedandwired/html-webpack-plugin
|
||||
new HtmlWebpackPlugin({
|
||||
filename: config.example.index,
|
||||
template: './examples/index.html',
|
||||
inject: true,
|
||||
// necessary to consistently work with multiple chunks via CommonsChunkPlugin
|
||||
chunksSortMode: 'dependency'
|
||||
}),
|
||||
// keep module.id stable when vender modules does not change
|
||||
new webpack.HashedModuleIdsPlugin(),
|
||||
// split vendor js into its own file
|
||||
new webpack.optimize.CommonsChunkPlugin({
|
||||
name: 'vendor',
|
||||
minChunks: function (module) {
|
||||
// any required modules inside node_modules are extracted to vendor
|
||||
return (
|
||||
module.resource &&
|
||||
/\.js$/.test(module.resource) &&
|
||||
module.resource.indexOf(
|
||||
path.join(__dirname, '../../node_modules')
|
||||
) === 0
|
||||
)
|
||||
}
|
||||
}),
|
||||
// extract webpack runtime and module manifest to its own file in order to
|
||||
// prevent vendor hash from being updated whenever app bundle is updated
|
||||
new webpack.optimize.CommonsChunkPlugin({
|
||||
name: 'manifest',
|
||||
chunks: ['vendor']
|
||||
}),
|
||||
// copy custom static assets
|
||||
new CopyWebpackPlugin([
|
||||
{
|
||||
from: path.resolve(__dirname, '../static'),
|
||||
to: config.example.assetsSubDirectory,
|
||||
ignore: ['.*']
|
||||
}
|
||||
]),
|
||||
new webpack.LoaderOptionsPlugin({
|
||||
options: {
|
||||
stylus: {
|
||||
use: [poststylus([
|
||||
pxtorem({
|
||||
rootValue: 100,
|
||||
propWhiteList: [],
|
||||
})
|
||||
])]
|
||||
}
|
||||
}
|
||||
})
|
||||
]
|
||||
})
|
||||
|
||||
if (config.example.productionGzip) {
|
||||
const CompressionWebpackPlugin = require('compression-webpack-plugin')
|
||||
|
||||
webpackConfig.plugins.push(
|
||||
new CompressionWebpackPlugin({
|
||||
asset: '[path].gz[query]',
|
||||
algorithm: 'gzip',
|
||||
test: new RegExp(
|
||||
'\\.(' +
|
||||
config.example.productionGzipExtensions.join('|') +
|
||||
')$'
|
||||
),
|
||||
threshold: 10240,
|
||||
minRatio: 0.8
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
if (config.example.bundleAnalyzerReport) {
|
||||
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
|
||||
webpackConfig.plugins.push(new BundleAnalyzerPlugin())
|
||||
}
|
||||
|
||||
module.exports = webpackConfig
|
|
@ -0,0 +1,32 @@
|
|||
'use strict'
|
||||
// This is the webpack config used for unit tests.
|
||||
|
||||
const utils = require('./utils')
|
||||
const webpack = require('webpack')
|
||||
const merge = require('webpack-merge')
|
||||
const ProgressBarPlugin = require('progress-bar-webpack-plugin')
|
||||
const ExtractTextPlugin = require('extract-text-webpack-plugin')
|
||||
const baseWebpackConfig = require('./webpack.base.conf')
|
||||
|
||||
const webpackConfig = merge(baseWebpackConfig, {
|
||||
// use inline sourcemap for karma-sourcemap-loader
|
||||
module: {
|
||||
rules: utils.styleLoaders()
|
||||
},
|
||||
devtool: '#cheap-module-eval-source-map',
|
||||
plugins: [
|
||||
new ProgressBarPlugin(),
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': require('../../config/test.env')
|
||||
}),
|
||||
// extract css into its own file
|
||||
new ExtractTextPlugin({
|
||||
filename: utils.assetsPath('[name].css')
|
||||
})
|
||||
]
|
||||
})
|
||||
|
||||
// no need for app entry during tests
|
||||
delete webpackConfig.entry
|
||||
|
||||
module.exports = webpackConfig
|
|
@ -0,0 +1,7 @@
|
|||
body
|
||||
font-family font-family-normal
|
||||
-webkit-tap-highlight-color transparent
|
||||
-webkit-font-smoothing antialiased
|
||||
-moz-osx-font-smoothing grayscale
|
||||
ol, li
|
||||
list-style none
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512"><title/><path d="M366.08 196.096c5.632 5.632 5.632 14.336 0 19.968l-99.84 99.84c-5.632 5.632-14.848 5.632-19.968 0l-100.352-99.84c-5.632-5.632-5.632-14.336 0-19.968s14.336-5.632 19.968 0L256 286.208l90.112-90.112c5.632-5.632 14.336-5.632 19.968 0z"/></svg>
|
After Width: | Height: | Size: 343 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512"><title/><path d="M315.904 366.08c-5.632 5.632-14.336 5.632-19.968 0l-99.84-100.352c-5.632-5.12-5.632-14.336 0-19.968l99.84-99.84c5.632-5.632 14.336-5.632 19.968 0s5.632 14.336 0 19.968L225.792 256l90.112 90.112c5.632 5.632 5.632 14.336 0 19.968z"/></svg>
|
After Width: | Height: | Size: 341 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512"><title/><path d="M196.096 145.92c5.632-5.632 14.336-5.632 19.968 0l99.84 99.84c5.632 5.632 5.632 14.848 0 19.968l-99.84 100.352c-5.632 5.632-14.336 5.632-19.968 0s-5.632-14.336 0-19.968L286.208 256l-90.112-90.112c-5.632-5.632-5.632-14.336 0-19.968z"/></svg>
|
After Width: | Height: | Size: 344 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512"><title/><path d="M145.92 315.904c-5.632-5.632-5.632-14.336 0-19.968l100.352-99.84c5.12-5.632 14.336-5.632 19.968 0l99.84 99.84c5.632 5.632 5.632 14.336 0 19.968s-14.336 5.632-19.968 0L256 225.792l-90.112 90.112c-5.632 5.632-14.336 5.632-19.968 0z"/></svg>
|
After Width: | Height: | Size: 342 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512"><title/><path d="M256 496C123.449 496 16 388.551 16 256S123.449 16 256 16s240 107.449 240 240-107.449 240-240 240zm-23.441-375l7.031 165H271l8.441-165h-46.879zm44.692 218.76c-5.921-5.809-13.069-8.719-21.439-8.719-8.381 0-15.461 2.91-21.24 8.719-5.779 5.831-8.681 12.881-8.681 21.18 0 9.499 3.03 16.89 9.079 22.17 6.049 5.291 13.129 7.931 21.24 7.931 7.969 0 14.951-2.681 20.94-8.029 5.981-5.34 8.97-12.701 8.97-22.069 0-8.299-2.959-15.349-8.869-21.18z"/></svg>
|
After Width: | Height: | Size: 547 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512"><title/><path d="M256 29.696C131.072 29.696 29.696 131.072 29.696 256S131.072 482.304 256 482.304 482.304 380.928 482.304 256 380.928 29.696 256 29.696zm90.112 296.448l-19.968 19.968L256 275.968l-70.144 70.144-19.968-19.968L236.032 256l-70.144-70.144 19.968-19.968L256 236.032l70.144-70.144 19.968 19.968L275.968 256l70.144 70.144z"/></svg>
|
After Width: | Height: | Size: 427 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512"><title/><path d="M256 29.696C131.072 29.696 29.696 131.072 29.696 256S131.072 482.304 256 482.304 482.304 380.928 482.304 256 380.928 29.696 256 29.696zm-22.528 304.64l.512.512-19.968 19.968L128 268.8l19.968-19.968 65.536 65.536 145.92-145.92 19.968 19.968-145.92 145.92z"/></svg>
|
After Width: | Height: | Size: 367 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512"><title/><path fill="none" stroke="#ccc" stroke-width="24.381" d="M467.81 256c0 116.98-94.83 211.81-211.81 211.81S44.19 372.98 44.19 256 139.02 44.19 256 44.19 467.81 139.02 467.81 256z"/></svg>
|
After Width: | Height: | Size: 280 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512"><title/><path d="M111.104 91.136L256 236.032 400.896 91.136l19.968 19.968L275.968 256l144.896 144.896-19.968 19.968L256 275.968 111.104 420.864l-19.968-19.968L236.032 256 91.136 111.104l19.968-19.968z"/></svg>
|
After Width: | Height: | Size: 296 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512"><title/><path d="M241.778 270.222v128c0 7.854 6.368 14.222 14.222 14.222s14.222-6.368 14.222-14.222v-128h128c7.854 0 14.222-6.368 14.222-14.222s-6.368-14.222-14.222-14.222h-128v-128c0-7.855-6.368-14.222-14.222-14.222s-14.222 6.367-14.222 14.222v128h-128c-7.855 0-14.222 6.368-14.222 14.222s6.367 14.222 14.222 14.222h128z"/><path d="M0 256C0 114.615 114.615 0 256 0s256 114.615 256 256-114.615 256-256 256S0 397.385 0 256zm28.445 0c0 125.675 101.88 227.555 227.555 227.555S483.555 381.675 483.555 256c0-125.675-101.88-227.555-227.555-227.555S28.445 130.325 28.445 256z"/></svg>
|
After Width: | Height: | Size: 664 B |
After Width: | Height: | Size: 543 B |
After Width: | Height: | Size: 947 B |
After Width: | Height: | Size: 493 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="670" height="512" viewBox="0 0 670 512"><title/><path d="M222.793 371.595L55.698 204.5-.001 260.198l222.793 222.793L640.529 65.254 584.831 9.555 222.793 371.593z"/><path d="M55.699 232.35L27.85 260.199l194.944 194.944L612.682 65.255l-27.849-27.849-362.038 362.038L55.7 232.349z"/></svg>
|
After Width: | Height: | Size: 333 B |
|
@ -0,0 +1 @@
|
|||
<svg class="lds-spinner" width="200" height="200" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid" style="background:0 0"><rect x="46.5" y="15.5" rx="12.09" ry="4.03" width="7" height="17" fill="#eee"><animate attributeName="opacity" values="1;0" dur="1s" begin="-0.9166666666666666s" repeatCount="indefinite"/></rect><rect x="46.5" y="15.5" rx="12.09" ry="4.03" width="7" height="17" fill="#eee" transform="rotate(30 50 50)"><animate attributeName="opacity" values="1;0" dur="1s" begin="-0.8333333333333334s" repeatCount="indefinite"/></rect><rect x="46.5" y="15.5" rx="12.09" ry="4.03" width="7" height="17" fill="#eee" transform="rotate(60 50 50)"><animate attributeName="opacity" values="1;0" dur="1s" begin="-0.75s" repeatCount="indefinite"/></rect><rect x="46.5" y="15.5" rx="12.09" ry="4.03" width="7" height="17" fill="#eee" transform="rotate(90 50 50)"><animate attributeName="opacity" values="1;0" dur="1s" begin="-0.6666666666666666s" repeatCount="indefinite"/></rect><rect x="46.5" y="15.5" rx="12.09" ry="4.03" width="7" height="17" fill="#eee" transform="rotate(120 50 50)"><animate attributeName="opacity" values="1;0" dur="1s" begin="-0.5833333333333334s" repeatCount="indefinite"/></rect><rect x="46.5" y="15.5" rx="12.09" ry="4.03" width="7" height="17" fill="#eee" transform="rotate(150 50 50)"><animate attributeName="opacity" values="1;0" dur="1s" begin="-0.5s" repeatCount="indefinite"/></rect><rect x="46.5" y="15.5" rx="12.09" ry="4.03" width="7" height="17" fill="#eee" transform="rotate(180 50 50)"><animate attributeName="opacity" values="1;0" dur="1s" begin="-0.4166666666666667s" repeatCount="indefinite"/></rect><rect x="46.5" y="15.5" rx="12.09" ry="4.03" width="7" height="17" fill="#eee" transform="rotate(210 50 50)"><animate attributeName="opacity" values="1;0" dur="1s" begin="-0.3333333333333333s" repeatCount="indefinite"/></rect><rect x="46.5" y="15.5" rx="12.09" ry="4.03" width="7" height="17" fill="#eee" transform="rotate(240 50 50)"><animate attributeName="opacity" values="1;0" dur="1s" begin="-0.25s" repeatCount="indefinite"/></rect><rect x="46.5" y="15.5" rx="12.09" ry="4.03" width="7" height="17" fill="#eee" transform="rotate(270 50 50)"><animate attributeName="opacity" values="1;0" dur="1s" begin="-0.16666666666666666s" repeatCount="indefinite"/></rect><rect x="46.5" y="15.5" rx="12.09" ry="4.03" width="7" height="17" fill="#eee" transform="rotate(300 50 50)"><animate attributeName="opacity" values="1;0" dur="1s" begin="-0.08333333333333333s" repeatCount="indefinite"/></rect><rect x="46.5" y="15.5" rx="12.09" ry="4.03" width="7" height="17" fill="#eee" transform="rotate(330 50 50)"><animate attributeName="opacity" values="1;0" dur="1s" begin="0s" repeatCount="indefinite"/></rect></svg>
|
After Width: | Height: | Size: 2.7 KiB |
|
@ -0,0 +1,310 @@
|
|||
/*
|
||||
* components
|
||||
*/
|
||||
|
||||
// button
|
||||
button-primary-fill = color-primary
|
||||
button-primary-fill-disabled = color-bg-disabled
|
||||
button-primary-fill-tap = color-bg-tap
|
||||
button-primary-width = 100%
|
||||
button-primary-height = 100px
|
||||
button-primary-font-size = 32px
|
||||
|
||||
button-ghost-fill = color-bg-base
|
||||
button-ghost-fill-tap = color-bg-tap
|
||||
button-ghost-primary-fill-tap = color-bg-tap-hightlight
|
||||
button-ghost-width = 160px
|
||||
button-ghost-height = 60px
|
||||
button-ghost-width-sm = 130px
|
||||
button-ghost-height-sm = 50px
|
||||
button-ghost-font-size = 24px
|
||||
button-ghost-color = color-border-element
|
||||
button-ghost-primary-color = color-primary
|
||||
|
||||
button-link-fill = color-bg-base
|
||||
button-link-fill-tap = color-bg-tap
|
||||
button-link-width = 100%
|
||||
button-link-height = 100px
|
||||
button-link-color = color-primary-tap
|
||||
|
||||
// icon
|
||||
icon-size-xs = 20px
|
||||
icon-size-sm = 24px
|
||||
icon-size-md = 32px
|
||||
icon-size-lg = 42px
|
||||
|
||||
// action-bar
|
||||
action-bar-width = 100%
|
||||
action-bar-height = 100px
|
||||
action-bar-button-font-size = 32px
|
||||
action-bar-text-font-size = 36px
|
||||
action-bar-button-color = color-text-base
|
||||
action-bar-button-color-hightlight = color-text-base-inverse
|
||||
action-bar-text-color = color-primary
|
||||
action-bar-button-fill = color-bg-base
|
||||
action-bar-button-fill-hightlight = color-primary
|
||||
action-bar-shadow = shadow-top
|
||||
action-bar-zindex = 100
|
||||
|
||||
// notice-bar
|
||||
notice-bar-fill = #4A4C5B
|
||||
notice-bar-color = color-text-base-inverse
|
||||
notice-bar-zindex = 1300
|
||||
|
||||
// stepper
|
||||
stepper-fill = color-primary-background
|
||||
stepper-disabled-opacity = opacity-disabled
|
||||
stepper-color = color-text-base
|
||||
stepper-height = 50px
|
||||
stepper-width-button = 50px
|
||||
stepper-width-input = 60px
|
||||
stepper-radius-button = 0 0 0 0
|
||||
stepper-radius-input = 0 0 0 0
|
||||
|
||||
// steps
|
||||
steps-color = color-text-disabled
|
||||
steps-color-active = color-primary
|
||||
steps-border = dotted 2px steps-color
|
||||
steps-border-active = solid 2px steps-color
|
||||
steps-size = 12px
|
||||
steps-size-active = 32px
|
||||
|
||||
// tab
|
||||
tab-color = color-primary-tap
|
||||
tab-font-size = 28px
|
||||
tab-height = 80px
|
||||
tab-ink-bar-height = 3px
|
||||
tab-zindex = 101
|
||||
|
||||
// field
|
||||
field-padding = h-gap-lg
|
||||
field-padding-h = 32px
|
||||
field-padding-v = 29px
|
||||
field-title-font-size = 28px
|
||||
field-title-weight = font-weight-medium = 500
|
||||
field-title-color = #333
|
||||
field-title-font-weight = font-weight-medium
|
||||
field-title-margin = 26px
|
||||
field-item-height = 100px
|
||||
field-item-padding-v = 22px
|
||||
field-item-bg-color = color-bg-base
|
||||
field-item-color = color-text-base
|
||||
field-item-color-check = color-text-minor
|
||||
field-item-color-action = color-text-link
|
||||
field-item-font-size = 28px
|
||||
field-item-font-size-check = 24px
|
||||
field-item-icon-color = #CCC
|
||||
field-item-border-color = #E6E6E6
|
||||
field-item-color-disabled = opacity-disabled
|
||||
|
||||
// input-item
|
||||
input-item-height = 100px
|
||||
input-item-title-width = 170px
|
||||
input-item-title-gap = 22px
|
||||
input-item-font-size = 28px
|
||||
input-item-title-latent-font-size = 26px
|
||||
input-item-font-size-large = 42px
|
||||
input-item-font-size-error = 22px
|
||||
input-item-font-weight = font-weight-normal
|
||||
input-item-color = color-text-base
|
||||
input-item-title-latent-color = #666
|
||||
input-item-color-disabled = opacity-disabled
|
||||
input-item-color-error = #FF525D
|
||||
input-item-placeholder = color-text-placeholder
|
||||
input-item-placeholder-hightlight = color-primary
|
||||
input-item-icon = color-text-placeholder // delete icon
|
||||
|
||||
// radio
|
||||
radio-fill = color-primary-tap
|
||||
|
||||
// switch
|
||||
switch-fill = color-primary
|
||||
switch-fill-inverse = color-bg-disabled
|
||||
switch-handle-color = #FFF
|
||||
switch-item-color-disabled = opacity-disabled
|
||||
|
||||
// agree
|
||||
agree-fill = color-primary
|
||||
agree-fill-inverse = color-bg-disabled
|
||||
agree-size-sm = 32px
|
||||
agree-size-lg = 44px
|
||||
|
||||
// action-sheet
|
||||
action-sheet-height = 120px
|
||||
action-sheet-font-size = 30px
|
||||
action-sheet-zindex = 1101
|
||||
|
||||
// picker
|
||||
picker-font-size = 30px
|
||||
picker-disabled-opacity = .2
|
||||
picker-color = color-text-base
|
||||
picker-zindex = 1100
|
||||
|
||||
// selector
|
||||
selector-height= 100px
|
||||
selector-disabled-opacity = .2
|
||||
selector-font-size = 30px
|
||||
selector-color = color-text-base
|
||||
selector-zindex = 1102
|
||||
|
||||
// dialog
|
||||
dialog-width = 534px
|
||||
dialog-radius = 0 0 0 0
|
||||
dialog-title-font-size = 32px
|
||||
dialog-text-font-size = 28px
|
||||
dialog-action-height = 100px
|
||||
dialog-action-font-size = 32px
|
||||
dialog-icon-size = 100px
|
||||
dialog-icon-fill = color-text-caption
|
||||
dialog-zindex = 1402
|
||||
|
||||
// toast
|
||||
toast-fill = rgba(0, 0, 0, .8)
|
||||
toast-font-size = 28px
|
||||
toast-color = #ccc
|
||||
toast-zindex = 1401
|
||||
|
||||
// tip
|
||||
tip-fill = rgba(74, 76, 91, 0.8)
|
||||
tip-font-size = 24px
|
||||
tip-color = #fff
|
||||
tip-zindex = 1300
|
||||
|
||||
// captcha
|
||||
captcha-zindex = 1400
|
||||
captcha-keyboard-zindex = 1403
|
||||
captcha-content-offset-top = 60px
|
||||
|
||||
// codebox
|
||||
codebox-font-size = font-body-normal
|
||||
codebox-width = 60px
|
||||
codebox-gutter = 10px
|
||||
codebox-border-color = color-border-base
|
||||
codebox-border-active-color = color-primary
|
||||
codebox-blink-color = color-primary
|
||||
codebox-input-height = 68px
|
||||
codebox-input-padding = 16px 32px
|
||||
codebox-input-font-size = 28px
|
||||
codebox-input-border-color = color-border-base
|
||||
codebox-dot-color = #000
|
||||
|
||||
// chart
|
||||
chart-line-color = #ccc
|
||||
chart-path-color = #fa8919
|
||||
chart-text-color = #666
|
||||
chart-label-size = 22px
|
||||
chart-value-size = 20px
|
||||
|
||||
// popup
|
||||
popup-title-bar-height = 110px
|
||||
popup-title-bar-font-size-button = 28px
|
||||
popup-title-bar-font-size-title = 36px
|
||||
popup-zindex = 1000
|
||||
|
||||
// drop-menu
|
||||
drop-menu-height = 82px
|
||||
drop-menu-zindex = 1200
|
||||
drop-menu-color = color-text-link
|
||||
drop-menu-font-size = font-body-normal
|
||||
|
||||
// number-keyboard
|
||||
number-keyboard-width = 100%
|
||||
number-keyboard-height = 428px
|
||||
number-keyboard-key-height = 107px
|
||||
number-keyboard-key-bg = #ebebeb
|
||||
number-keyboard-key-bg-tap = #f0f0f0
|
||||
number-keyboard-key-confirm-bg = color-primary
|
||||
number-keyboard-key-confirm-bg-tap = #DD7F49
|
||||
number-keyboard-key-font-size = 48px
|
||||
number-keyboard-key-color = color-text-minor
|
||||
number-keyboard-key-color-simple = #000
|
||||
number-keyboard-zindex = 1403
|
||||
|
||||
//tab-picker
|
||||
tab-picker-font-size = 28px
|
||||
tab-picker-color = color-text-base
|
||||
tab-picker-hignlight-color = color-primary
|
||||
tab-picker-min-height = 100px
|
||||
|
||||
// date-picker
|
||||
date-picker-font-size = 28px
|
||||
date-time-picker-font-size = 24px
|
||||
|
||||
/*
|
||||
* global color
|
||||
*/
|
||||
|
||||
// brand color
|
||||
color-primary = #fc9153 // 1st main color for buttons & hightlight text which is not clickable
|
||||
color-primary-tap = #3ca0e6 // 2nd main color for links and selected element
|
||||
color-primary-background = #f3f4f5 // 3rd main color for background
|
||||
|
||||
// text color
|
||||
color-text-base = #333 // default text color
|
||||
color-text-base-inverse = #fff // default inverse text color
|
||||
color-text-minor = #666 // auxiliary text color
|
||||
color-text-caption = #999 // subtitle and describe text color
|
||||
color-text-disabled = #ccc // input placeholder
|
||||
color-text-placeholder = #ccc // input placeholder
|
||||
color-text-hightlight = color-primary // hight text color
|
||||
color-text-link = color-primary-tap // link text color
|
||||
|
||||
// border color
|
||||
color-border-base = #d9d9d9 // defalut gap color of items
|
||||
color-border-minor = #ebebeb // gap color of items
|
||||
color-border-element = #999 // border color of element such as button
|
||||
|
||||
// background color
|
||||
color-bg-base = #fff // default background color
|
||||
color-bg-disabled = #CCC // background color for disabeld element
|
||||
color-bg-mask = rgba(0, 0, 0, .4) // background color for mask layer
|
||||
color-bg-tap = rgba(0, 0, 0, .08) // background color for element click state
|
||||
color-bg-tap-hightlight = rgba(252, 145, 83, .08) // background color for hightlight element click state
|
||||
|
||||
// opacity
|
||||
opacity-disabled = .4 // opacity of disabled button, switch, agree
|
||||
|
||||
/*
|
||||
* global size
|
||||
*/
|
||||
|
||||
// text size
|
||||
font-heading-large = 42px
|
||||
font-heading-medium = 36px
|
||||
font-heading-normal = 32px
|
||||
font-body-large = 30px
|
||||
font-body-normal = 28px
|
||||
font-minor-large = 24px
|
||||
font-minor-normal = 20px
|
||||
|
||||
font-weight-normal = 400
|
||||
font-weight-medium = 500
|
||||
font-weight-bold = 600
|
||||
|
||||
// radius size
|
||||
radius-normal = 4px
|
||||
radius-circle = 50%
|
||||
|
||||
// border size
|
||||
border-width-base = 2px
|
||||
|
||||
// gap size
|
||||
h-gap-sm = 12px
|
||||
h-gap-md = 20px
|
||||
h-gap-lg = 32px
|
||||
v-gap-sm = 12px
|
||||
v-gap-md = 20px
|
||||
v-gap-lg = 32px
|
||||
|
||||
/*
|
||||
* global other
|
||||
*/
|
||||
|
||||
// box shadow
|
||||
shadow-bottom = 0 2px 4px rgba(0, 0, 0, .12)
|
||||
shadow-top = 0 -2px 4px rgba(0, 0, 0, .12)
|
||||
|
||||
// animate
|
||||
ease-in-out-quint = cubic-bezier(.86, 0, .07, 1)
|
||||
|
||||
font-family-normal = "Helvetica Neue",Helvetica,"PingFang SC","Hiragino Sans GB","Microsoft YaHei","微软雅黑",Arial,sans-serif
|
|
@ -0,0 +1,105 @@
|
|||
absolute-pos(t = 0, r = 0, b = 0, l = 0)
|
||||
top t
|
||||
right r
|
||||
bottom b
|
||||
left l
|
||||
fixed-pos(t = 0, r = 0, b = 0, l = 0)
|
||||
absolute-pos(t, r, b, l)
|
||||
|
||||
clearfix()
|
||||
&:after
|
||||
content ""
|
||||
display table
|
||||
clear both
|
||||
// normalize()
|
||||
// margin 0
|
||||
// padding 0
|
||||
// list-style none
|
||||
|
||||
word-break()
|
||||
word-break break-all
|
||||
word-wrap break-word
|
||||
word-ellipsis()
|
||||
white-space nowrap
|
||||
overflow hidden
|
||||
text-overflow ellipsis
|
||||
|
||||
hairline-common(direction, color)
|
||||
content ''
|
||||
position absolute
|
||||
z-index 2
|
||||
background-color color
|
||||
transform-origin 100% 50%
|
||||
if direction == top
|
||||
transform scaleY(0.5) translateY(-100%)
|
||||
@media (min-resolution: 3dppx)
|
||||
transform scaleY(0.33) translateY(-100%)
|
||||
else if direction == bottom
|
||||
transform scaleY(0.5) translateY(100%)
|
||||
@media (min-resolution: 3dppx)
|
||||
transform scaleY(0.33) translateY(100%)
|
||||
else if direction == left
|
||||
transform scaleX(0.5) translateX(-100%)
|
||||
@media (min-resolution: 3dppx)
|
||||
transform scaleX(0.33) translateX(-100%)
|
||||
else if direction == right
|
||||
transform scaleX(0.5) translateX(100%)
|
||||
@media (min-resolution: 3dppx)
|
||||
transform scaleX(0.33) translateX(100%)
|
||||
|
||||
hairline(direction = all, color = color-line-1, radius = false)
|
||||
position relative
|
||||
if direction == top
|
||||
&::after
|
||||
hairline-common(direction, color)
|
||||
top 0
|
||||
left 0
|
||||
width 100%
|
||||
height border-width-base
|
||||
|
||||
else if direction == bottom
|
||||
&::before
|
||||
hairline-common(direction, color)
|
||||
bottom 0
|
||||
left 0
|
||||
width 100%
|
||||
height border-width-base
|
||||
|
||||
else if direction == left
|
||||
&::after
|
||||
hairline-common(direction, color)
|
||||
top 0
|
||||
left 0
|
||||
width border-width-base
|
||||
height 100%
|
||||
|
||||
else if direction == right
|
||||
&::before
|
||||
hairline-common(direction, color)
|
||||
top 0
|
||||
right 0
|
||||
width border-width-base
|
||||
height 100%
|
||||
|
||||
else
|
||||
&::after
|
||||
content ''
|
||||
position absolute
|
||||
top 0
|
||||
left 0
|
||||
width 200%
|
||||
height 200%
|
||||
border solid border-width-base color
|
||||
box-sizing border-box
|
||||
transform-origin 0 0
|
||||
transform scale(0.5)
|
||||
z-index 2
|
||||
if radius
|
||||
border-radius radius-normal * 2
|
||||
|
||||
svg-background(svg)
|
||||
background-image url(svg)
|
||||
|
||||
vertical-height(height)
|
||||
height height
|
||||
line-height height
|
|
@ -0,0 +1,204 @@
|
|||
const Animate = (global => {
|
||||
const time =
|
||||
Date.now ||
|
||||
(() => {
|
||||
return +new Date()
|
||||
})
|
||||
const desiredFrames = 60
|
||||
const millisecondsPerSecond = 1000
|
||||
|
||||
let running = {}
|
||||
let counter = 1
|
||||
|
||||
return {
|
||||
/**
|
||||
* A requestAnimationFrame wrapper / polyfill.
|
||||
*
|
||||
* @param callback {Function} The callback to be invoked before the next repaint.
|
||||
* @param root {HTMLElement} The root element for the repaint
|
||||
*/
|
||||
requestAnimationFrame: (() => {
|
||||
// Check for request animation Frame support
|
||||
const requestFrame =
|
||||
global.requestAnimationFrame ||
|
||||
global.webkitRequestAnimationFrame ||
|
||||
global.mozRequestAnimationFrame ||
|
||||
global.oRequestAnimationFrame
|
||||
let isNative = !!requestFrame
|
||||
|
||||
if (requestFrame && !/requestAnimationFrame\(\)\s*\{\s*\[native code\]\s*\}/i.test(requestFrame.toString())) {
|
||||
isNative = false
|
||||
}
|
||||
|
||||
if (isNative) {
|
||||
return (callback, root) => {
|
||||
requestFrame(callback, root)
|
||||
}
|
||||
}
|
||||
|
||||
const TARGET_FPS = 60
|
||||
let requests = {}
|
||||
let requestCount = 0
|
||||
let rafHandle = 1
|
||||
let intervalHandle = null
|
||||
let lastActive = +new Date()
|
||||
|
||||
return callback => {
|
||||
const callbackHandle = rafHandle++
|
||||
|
||||
// Store callback
|
||||
requests[callbackHandle] = callback
|
||||
requestCount++
|
||||
|
||||
// Create timeout at first request
|
||||
if (intervalHandle === null) {
|
||||
intervalHandle = setInterval(() => {
|
||||
const time = +new Date()
|
||||
const currentRequests = requests
|
||||
|
||||
// Reset data structure before executing callbacks
|
||||
requests = {}
|
||||
requestCount = 0
|
||||
|
||||
for (const key in currentRequests) {
|
||||
if (currentRequests.hasOwnProperty(key)) {
|
||||
currentRequests[key](time)
|
||||
lastActive = time
|
||||
}
|
||||
}
|
||||
|
||||
// Disable the timeout when nothing happens for a certain
|
||||
// period of time
|
||||
if (time - lastActive > 2500) {
|
||||
clearInterval(intervalHandle)
|
||||
intervalHandle = null
|
||||
}
|
||||
}, 1000 / TARGET_FPS)
|
||||
}
|
||||
|
||||
return callbackHandle
|
||||
}
|
||||
})(),
|
||||
|
||||
/**
|
||||
* Stops the given animation.
|
||||
*
|
||||
* @param id {Integer} Unique animation ID
|
||||
* @return {Boolean} Whether the animation was stopped (aka, was running before)
|
||||
*/
|
||||
stop(id) {
|
||||
const cleared = running[id] != null
|
||||
cleared && (running[id] = null)
|
||||
return cleared
|
||||
},
|
||||
|
||||
/**
|
||||
* Whether the given animation is still running.
|
||||
*
|
||||
* @param id {Integer} Unique animation ID
|
||||
* @return {Boolean} Whether the animation is still running
|
||||
*/
|
||||
isRunning(id) {
|
||||
return running[id] != null
|
||||
},
|
||||
|
||||
/**
|
||||
* Start the animation.
|
||||
*
|
||||
* @param stepCallback {Function} Pointer to function which is executed on every step.
|
||||
* Signature of the method should be `function(percent, now, virtual) { return continueWithAnimation; }`
|
||||
* @param verifyCallback {Function} Executed before every animation step.
|
||||
* Signature of the method should be `function() { return continueWithAnimation; }`
|
||||
* @param completedCallback {Function}
|
||||
* Signature of the method should be `function(droppedFrames, finishedAnimation) {}`
|
||||
* @param duration {Integer} Milliseconds to run the animation
|
||||
* @param easingMethod {Function} Pointer to easing function
|
||||
* Signature of the method should be `function(percent) { return modifiedValue; }`
|
||||
* @param root {Element ? document.body} Render root, when available. Used for internal
|
||||
* usage of requestAnimationFrame.
|
||||
* @return {Integer} Identifier of animation. Can be used to stop it any time.
|
||||
*/
|
||||
start(stepCallback, verifyCallback, completedCallback, duration, easingMethod, root) {
|
||||
const start = time()
|
||||
let lastFrame = start
|
||||
let percent = 0
|
||||
let dropCounter = 0
|
||||
const id = counter++
|
||||
|
||||
if (!root) {
|
||||
root = document.body
|
||||
}
|
||||
|
||||
// Compacting running db automatically every few new animations
|
||||
if (id % 20 === 0) {
|
||||
const newRunning = {}
|
||||
for (const usedId in running) {
|
||||
newRunning[usedId] = true
|
||||
}
|
||||
running = newRunning
|
||||
}
|
||||
|
||||
// This is the internal step method which is called every few milliseconds
|
||||
const step = virtual => {
|
||||
// Normalize virtual value
|
||||
const render = virtual !== true
|
||||
|
||||
// Get current time
|
||||
const now = time()
|
||||
|
||||
// Verification is executed before next animation step
|
||||
if (!running[id] || (verifyCallback && !verifyCallback(id))) {
|
||||
running[id] = null
|
||||
completedCallback &&
|
||||
completedCallback(desiredFrames - dropCounter / ((now - start) / millisecondsPerSecond), id, false)
|
||||
return
|
||||
}
|
||||
|
||||
// For the current rendering to apply let's update omitted steps in memory.
|
||||
// This is important to bring internal state variables up-to-date with progress in time.
|
||||
if (render) {
|
||||
const droppedFrames = Math.round((now - lastFrame) / (millisecondsPerSecond / desiredFrames)) - 1
|
||||
for (let j = 0; j < Math.min(droppedFrames, 4); j++) {
|
||||
step(true)
|
||||
dropCounter++
|
||||
}
|
||||
}
|
||||
|
||||
// Compute percent value
|
||||
if (duration) {
|
||||
percent = (now - start) / duration
|
||||
if (percent > 1) {
|
||||
percent = 1
|
||||
}
|
||||
}
|
||||
|
||||
// Execute step callback, then...
|
||||
let value = easingMethod ? easingMethod(percent) : percent
|
||||
value = isNaN(value) ? 0 : value
|
||||
if ((stepCallback(value, now, render) === false || percent === 1) && render) {
|
||||
running[id] = null
|
||||
completedCallback &&
|
||||
completedCallback(
|
||||
desiredFrames - dropCounter / ((now - start) / millisecondsPerSecond),
|
||||
id,
|
||||
percent === 1 || duration == null,
|
||||
)
|
||||
} else if (render) {
|
||||
lastFrame = now
|
||||
this.requestAnimationFrame(step, root)
|
||||
}
|
||||
}
|
||||
|
||||
// Mark as running
|
||||
running[id] = true
|
||||
|
||||
// Init first step
|
||||
this.requestAnimationFrame(step, root)
|
||||
|
||||
// Return unique animation ID
|
||||
return id
|
||||
},
|
||||
}
|
||||
})(window)
|
||||
|
||||
export default Animate
|
|
@ -0,0 +1,5 @@
|
|||
import {isProd} from './env'
|
||||
|
||||
export const warn = (msg, fn = 'error') => {
|
||||
!isProd && console[fn](`[Mand-Mobile]: ${msg}`)
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
// Development environment
|
||||
export const isProd = process.env.NODE_ENV === 'production'
|
||||
|
||||
// Browser environment sniffing
|
||||
export const inBrowser = typeof window !== 'undefined'
|
||||
export const UA = inBrowser && window.navigator.userAgent.toLowerCase()
|
||||
export const isAndroid = UA && UA.indexOf('android') > 0
|
||||
export const isIOS = UA && /iphone|ipad|ipod|ios/.test(UA)
|
|
@ -0,0 +1,76 @@
|
|||
export function formatValueByGapRule(gapRule, value, gap = ' ', range, isAdd = 1) {
|
||||
const arr = value ? value.split('') : []
|
||||
let showValue = ''
|
||||
const rule = []
|
||||
gapRule.split('|').some((n, j) => {
|
||||
rule[j] = +n + (rule[j - 1] ? +rule[j - 1] : 0)
|
||||
})
|
||||
let j = 0
|
||||
arr.some((n, i) => {
|
||||
// Remove the excess part
|
||||
if (i > rule[rule.length - 1] - 1) {
|
||||
return
|
||||
}
|
||||
if (i > 0 && i === rule[j]) {
|
||||
showValue = showValue + gap + n
|
||||
j++
|
||||
} else {
|
||||
showValue = showValue + '' + n
|
||||
}
|
||||
})
|
||||
let adapt = 0
|
||||
rule.some((n, j) => {
|
||||
if (range === +n + 1 + j) {
|
||||
adapt = 1 * isAdd
|
||||
}
|
||||
})
|
||||
range = typeof range !== 'undefined' ? (range === 0 ? 0 : range + adapt) : showValue.length
|
||||
return {value: showValue, range: range}
|
||||
}
|
||||
|
||||
export function formatValueByGapStep(step, value, gap = ' ', direction = 'right', range, isAdd = 1, oldValue = '') {
|
||||
if (value.length === 0) {
|
||||
return {value, range}
|
||||
}
|
||||
|
||||
const arr = value && value.split('')
|
||||
let _range = range
|
||||
let showValue = ''
|
||||
|
||||
if (direction === 'right') {
|
||||
for (let j = arr.length - 1, k = 0; j >= 0; j--, k++) {
|
||||
const m = arr[j]
|
||||
showValue = k > 0 && k % step === 0 ? m + gap + showValue : m + '' + showValue
|
||||
}
|
||||
if (isAdd === 1) {
|
||||
// 在添加的情况下,如果添加前字符串的长度减去新的字符串的长度为2,说明多了一个间隔符,需要调整range
|
||||
if (oldValue.length - showValue.length === -2) {
|
||||
_range = range + 1
|
||||
}
|
||||
} else {
|
||||
// 在删除情况下,如果删除前字符串的长度减去新的字符串的长度为2,说明少了一个间隔符,需要调整range
|
||||
if (oldValue.length - showValue.length === 2) {
|
||||
_range = range - 1
|
||||
}
|
||||
// 删除到最开始,range 保持 0
|
||||
if (_range <= 0) {
|
||||
_range = 0
|
||||
}
|
||||
}
|
||||
} else {
|
||||
arr.some((n, i) => {
|
||||
showValue = i > 0 && i % step === 0 ? showValue + gap + n : showValue + '' + n
|
||||
})
|
||||
const adapt = range % (step + 1) === 0 ? 1 * isAdd : 0
|
||||
_range = typeof range !== 'undefined' ? (range === 0 ? 0 : range + adapt) : showValue.length
|
||||
}
|
||||
|
||||
return {value: showValue, range: _range}
|
||||
}
|
||||
|
||||
export function trimValue(value, gap = ' ') {
|
||||
value = typeof value === 'undefined' ? '' : value
|
||||
const reg = new RegExp(gap, 'g')
|
||||
value = value.toString().replace(reg, '')
|
||||
return value
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
export * from './debug'
|
||||
export * from './env'
|
||||
export * from './store'
|
||||
export * from './lang'
|
|
@ -0,0 +1,71 @@
|
|||
export function noop() {}
|
||||
|
||||
/**
|
||||
* Include external script dynamically
|
||||
*/
|
||||
export function requireRemoteScript(src, callback) {
|
||||
const doc = document
|
||||
const head = doc.head || doc.getElementsByTagName('head')[0]
|
||||
|
||||
let node = doc.createElement('script')
|
||||
const supportOnload = 'onload' in node
|
||||
const onload = function() {
|
||||
node = null
|
||||
typeof callback === 'function' && callback()
|
||||
}
|
||||
|
||||
if (supportOnload) {
|
||||
node.onload = onload
|
||||
} else {
|
||||
node.onreadystatechange = function() {
|
||||
if (/loaded|complete/.test(node.readyState)) {
|
||||
onload()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
node.async = true
|
||||
node.crossOrigin = true
|
||||
node.charset = 'utf-8'
|
||||
node.src = src
|
||||
head.appendChild(node)
|
||||
}
|
||||
|
||||
export function getDpr() {
|
||||
const getParam = (name, str) => {
|
||||
const reg = new RegExp(`(^|,)${name}=([^,]*)(,|$)`, 'i')
|
||||
const r = str.match(reg)
|
||||
if (r != null) {
|
||||
return r[2]
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const viewPort = document.querySelector('meta[name=viewport]')
|
||||
|
||||
if (!viewPort) {
|
||||
return 1
|
||||
}
|
||||
|
||||
const viewPortContent = viewPort.getAttribute('content')
|
||||
const initialScale = +(getParam('initial-scale', viewPortContent) || 1)
|
||||
const maximumScale = +(getParam('maximum-scale', viewPortContent) || 1)
|
||||
const minimumScale = +(getParam('minimum-scale', viewPortContent) || 1)
|
||||
|
||||
return 1 / Math.min(initialScale, maximumScale, minimumScale)
|
||||
}
|
||||
|
||||
/**
|
||||
* transform a Function to Blob Url
|
||||
*/
|
||||
export function functionToUrl(fn) {
|
||||
const blob = new Blob([`(${fn.toString()})(null)`], {type: 'application/javascript'})
|
||||
return URL.createObjectURL(blob)
|
||||
}
|
||||
|
||||
/**
|
||||
* generate random id
|
||||
*/
|
||||
export function randomId(prefix = '', length = 8) {
|
||||
return `${prefix}-${parseInt(Math.random() * 10 ** length)}`
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
export const render = (function(global) {
|
||||
const docStyle = document.documentElement.style
|
||||
|
||||
let engine
|
||||
|
||||
if (global.opera && Object.prototype.toString.call(opera) === '[object Opera]') {
|
||||
engine = 'presto'
|
||||
} else if ('MozAppearance' in docStyle) {
|
||||
engine = 'gecko'
|
||||
} else if ('WebkitAppearance' in docStyle) {
|
||||
engine = 'webkit'
|
||||
} else if (typeof navigator.cpuClass === 'string') {
|
||||
engine = 'trident'
|
||||
}
|
||||
|
||||
const vendorPrefix = {
|
||||
trident: 'ms',
|
||||
gecko: 'Moz',
|
||||
webkit: 'Webkit',
|
||||
presto: 'O',
|
||||
}[engine]
|
||||
|
||||
const helperElem = document.createElement('div')
|
||||
const perspectiveProperty = vendorPrefix + 'Perspective'
|
||||
const transformProperty = vendorPrefix + 'Transform'
|
||||
|
||||
if (helperElem.style[perspectiveProperty] !== undefined) {
|
||||
return function(content, left, top) {
|
||||
// console.log(top)
|
||||
content.style[transformProperty] = `translate3d(${-left}px,${-top}px,0)`
|
||||
}
|
||||
} else if (helperElem.style[transformProperty] !== undefined) {
|
||||
return function(content, left, top) {
|
||||
content.style[transformProperty] = `translate(${-left}px,${-top}px,0)`
|
||||
}
|
||||
} else {
|
||||
return function(content, left, top) {
|
||||
content.style.marginLeft = left ? `${-left}px` : ''
|
||||
content.style.marginTop = top ? `${-top}px` : ''
|
||||
}
|
||||
}
|
||||
})(window)
|
|
@ -0,0 +1,915 @@
|
|||
/*
|
||||
* Based on the work of: Scroller
|
||||
* http://github.com/zynga/scroller
|
||||
*
|
||||
* Copyright 2011, Zynga Inc.
|
||||
* Licensed under the MIT License.
|
||||
* https://raw.github.com/zynga/scroller/master/MIT-LICENSE.txt
|
||||
*
|
||||
*/
|
||||
import {noop, warn, extend} from './index'
|
||||
|
||||
import Animate from './animate'
|
||||
|
||||
const members = {
|
||||
_isSingleTouch: false,
|
||||
_isTracking: false,
|
||||
_didDecelerationComplete: false,
|
||||
_isGesturing: false,
|
||||
_isDragging: false,
|
||||
_isDecelerating: false,
|
||||
_isAnimating: false,
|
||||
_clientLeft: 0,
|
||||
_clientTop: 0,
|
||||
_clientWidth: 0,
|
||||
_clientHeight: 0,
|
||||
_contentWidth: 0,
|
||||
_contentHeight: 0,
|
||||
_snapWidth: 100,
|
||||
_snapHeight: 100,
|
||||
_refreshHeight: null,
|
||||
_refreshActive: false,
|
||||
_refreshActivate: null,
|
||||
_refreshDeactivate: null,
|
||||
_refreshStart: null,
|
||||
_zoomLevel: 1,
|
||||
_scrollLeft: 0,
|
||||
_scrollTop: 0,
|
||||
_maxScrollLeft: 0,
|
||||
_maxScrollTop: 0,
|
||||
_scheduledLeft: 0,
|
||||
_scheduledTop: 0,
|
||||
_lastTouchLeft: null,
|
||||
_lastTouchTop: null,
|
||||
_lastTouchMove: null,
|
||||
_positions: null,
|
||||
_minDecelerationScrollLeft: null,
|
||||
_minDecelerationScrollTop: null,
|
||||
_maxDecelerationScrollLeft: null,
|
||||
_maxDecelerationScrollTop: null,
|
||||
_decelerationVelocityX: null,
|
||||
_decelerationVelocityY: null,
|
||||
}
|
||||
|
||||
const easeOutCubic = pos => {
|
||||
return Math.pow(pos - 1, 3) + 1
|
||||
}
|
||||
|
||||
const easeInOutCubic = pos => {
|
||||
if ((pos /= 0.5) < 1) {
|
||||
return 0.5 * Math.pow(pos, 3)
|
||||
}
|
||||
|
||||
return 0.5 * (Math.pow(pos - 2, 3) + 2)
|
||||
}
|
||||
|
||||
export default class Scroller {
|
||||
constructor(callback = noop, options) {
|
||||
this.options = {
|
||||
scrollingX: true,
|
||||
scrollingY: true,
|
||||
animating: true,
|
||||
animationDuration: 250,
|
||||
bouncing: true,
|
||||
locking: true,
|
||||
paging: false,
|
||||
snapping: false,
|
||||
zooming: false,
|
||||
minZoom: 0.5,
|
||||
maxZoom: 3,
|
||||
speedMultiplier: 1,
|
||||
scrollingComplete: noop,
|
||||
penetrationDeceleration: 0.03,
|
||||
penetrationAcceleration: 0.08,
|
||||
}
|
||||
extend(this.options, options)
|
||||
this._callback = callback
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the dimensions of the client (outer) and content (inner) elements.
|
||||
* Requires the available space for the outer element and the outer size of the inner element.
|
||||
* All values which are falsy (null or zero etc.) are ignored and the old value is kept.
|
||||
*
|
||||
* @param clientWidth {Integer ? null} Inner width of outer element
|
||||
* @param clientHeight {Integer ? null} Inner height of outer element
|
||||
* @param contentWidth {Integer ? null} Outer width of inner element
|
||||
* @param contentHeight {Integer ? null} Outer height of inner element
|
||||
*/
|
||||
setDimensions(clientWidth, clientHeight, contentWidth, contentHeight) {
|
||||
// Only update values which are defined
|
||||
if (clientWidth === +clientWidth) {
|
||||
this._clientWidth = clientWidth
|
||||
}
|
||||
|
||||
if (clientHeight === +clientHeight) {
|
||||
this._clientHeight = clientHeight
|
||||
}
|
||||
|
||||
if (contentWidth === +contentWidth) {
|
||||
this._contentWidth = contentWidth
|
||||
}
|
||||
|
||||
if (contentHeight === +contentHeight) {
|
||||
this._contentHeight = contentHeight
|
||||
}
|
||||
|
||||
// Refresh maximums
|
||||
this._computeScrollMax()
|
||||
|
||||
// Refresh scroll position
|
||||
this.scrollTo(this._scrollLeft, this._scrollTop, true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the client coordinates in relation to the document.
|
||||
*
|
||||
* @param left {Integer ? 0} Left position of outer element
|
||||
* @param top {Integer ? 0} Top position of outer element
|
||||
*/
|
||||
setPosition(left, top) {
|
||||
this._clientLeft = left || 0
|
||||
this._clientTop = top || 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the snapping (when snapping is active)
|
||||
*
|
||||
* @param width {Integer} Snapping width
|
||||
* @param height {Integer} Snapping height
|
||||
*/
|
||||
setSnapSize(width, height) {
|
||||
this._snapWidth = width
|
||||
this._snapHeight = height
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the scroll position and zooming values
|
||||
*
|
||||
* @return {Map} `left` and `top` scroll position and `zoom` level
|
||||
*/
|
||||
getValues() {
|
||||
return {
|
||||
left: this._scrollLeft,
|
||||
top: this._scrollTop,
|
||||
zoom: this._zoomLevel,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the maximum scroll values
|
||||
*
|
||||
* @return {Map} `left` and `top` maximum scroll values
|
||||
*/
|
||||
getScrollMax() {
|
||||
return {
|
||||
left: this._maxScrollLeft,
|
||||
top: this._maxScrollTop,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Activates pull-to-refresh. A special zone on the top of the list to start a list refresh whenever
|
||||
* the user event is released during visibility of this zone. This was introduced by some apps on iOS like
|
||||
* the official Twitter client.
|
||||
*
|
||||
* @param height {Integer} Height of pull-to-refresh zone on top of rendered list
|
||||
* @param activateCallback {Function} Callback to execute on activation. This is for signalling the user about a refresh is about to happen when he release.
|
||||
* @param deactivateCallback {Function} Callback to execute on deactivation. This is for signalling the user about the refresh being cancelled.
|
||||
* @param startCallback {Function} Callback to execute to start the real async refresh action. Call {@link #finishPullToRefresh} after finish of refresh.
|
||||
*/
|
||||
activatePullToRefresh(height, activateCallback, deactivateCallback, startCallback) {
|
||||
this._refreshHeight = height
|
||||
this._refreshActivate = activateCallback
|
||||
this._refreshDeactivate = deactivateCallback
|
||||
this._refreshStart = startCallback
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts pull-to-refresh manually.
|
||||
*/
|
||||
triggerPullToRefresh() {
|
||||
// Use publish instead of scrollTo to allow scrolling to out of boundary position
|
||||
// We don't need to normalize scrollLeft, zoomLevel, etc. here because we only y-scrolling when pull-to-refresh is enabled
|
||||
this._publish(this._scrollLeft, -this._refreshHeight, this._zoomLevel, true)
|
||||
|
||||
if (this._refreshStart) {
|
||||
this._refreshStart()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Signalizes that pull-to-refresh is finished.
|
||||
*/
|
||||
finishPullToRefresh() {
|
||||
this._refreshActive = false
|
||||
|
||||
if (this._refreshDeactivate) {
|
||||
this._refreshDeactivate()
|
||||
}
|
||||
|
||||
this.scrollTo(this._scrollLeft, this._scrollTop, true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Scrolls to the given position. Respect limitations and snapping automatically.
|
||||
*
|
||||
* @param left {Number?null} Horizontal scroll position, keeps current if value is <code>null</code>
|
||||
* @param top {Number?null} Vertical scroll position, keeps current if value is <code>null</code>
|
||||
* @param animate {Boolean?false} Whether the scrolling should happen using an animation
|
||||
* @param zoom {Number?null} Zoom level to go to
|
||||
*/
|
||||
scrollTo(left, top, animate, zoom = 1) {
|
||||
// Stop deceleration
|
||||
if (this._isDecelerating) {
|
||||
Animate.stop(this._isDecelerating)
|
||||
this._isDecelerating = false
|
||||
}
|
||||
|
||||
// Correct coordinates based on new zoom level
|
||||
if (zoom != null && zoom !== this._zoomLevel) {
|
||||
if (!this.options.zooming) {
|
||||
warn('Zooming is not enabled!')
|
||||
}
|
||||
zoom = zoom ? zoom : 1
|
||||
left *= zoom
|
||||
top *= zoom
|
||||
|
||||
// // Recompute maximum values while temporary tweaking maximum scroll ranges
|
||||
this._computeScrollMax(zoom)
|
||||
} else {
|
||||
// Keep zoom when not defined
|
||||
zoom = this._zoomLevel
|
||||
}
|
||||
|
||||
if (!this.options.scrollingX) {
|
||||
left = this._scrollLeft
|
||||
} else {
|
||||
if (this.options.paging) {
|
||||
left = Math.round(left / this._clientWidth) * this._clientWidth
|
||||
} else if (this.options.snapping) {
|
||||
left = Math.round(left / this._snapWidth) * this._snapWidth
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.options.scrollingY) {
|
||||
top = this._scrollTop
|
||||
} else {
|
||||
if (this.options.paging) {
|
||||
top = Math.round(top / this._clientHeight) * this._clientHeight
|
||||
} else if (this.options.snapping) {
|
||||
top = Math.round(top / this._snapHeight) * this._snapHeight
|
||||
}
|
||||
}
|
||||
|
||||
// Limit for allowed ranges
|
||||
left = Math.max(Math.min(this._maxScrollLeft, left), 0)
|
||||
top = Math.max(Math.min(this._maxScrollTop, top), 0)
|
||||
|
||||
// Don't animate when no change detected, still call publish to make sure
|
||||
// that rendered position is really in-sync with internal data
|
||||
if (left === this._scrollLeft && top === this._scrollTop) {
|
||||
animate = false
|
||||
}
|
||||
|
||||
// Publish new values
|
||||
if (!this._isTracking) {
|
||||
this._publish(left, top, zoom, animate)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Zooms to the given level. Supports optional animation. Zooms
|
||||
* the center when no coordinates are given.
|
||||
*
|
||||
* @param level {Number} Level to zoom to
|
||||
* @param animate {Boolean ? false} Whether to use animation
|
||||
* @param originLeft {Number ? null} Zoom in at given left coordinate
|
||||
* @param originTop {Number ? null} Zoom in at given top coordinate
|
||||
* @param callback {Function ? null} A callback that gets fired when the zoom is complete.
|
||||
*/
|
||||
zoomTo(level, animate, originLeft, originTop, callback) {
|
||||
if (!this.options.zooming) {
|
||||
warn('Zooming is not enabled!')
|
||||
}
|
||||
|
||||
// Add callback if exists
|
||||
if (callback) {
|
||||
this._zoomComplete = callback
|
||||
}
|
||||
|
||||
// Stop deceleration
|
||||
if (this._isDecelerating) {
|
||||
Animate.stop(this._isDecelerating)
|
||||
this._isDecelerating = false
|
||||
}
|
||||
|
||||
const oldLevel = this._zoomLevel
|
||||
|
||||
// Normalize input origin to center of viewport if not defined
|
||||
if (originLeft == null) {
|
||||
originLeft = this._clientWidth / 2
|
||||
}
|
||||
|
||||
if (originTop == null) {
|
||||
originTop = this._clientHeight / 2
|
||||
}
|
||||
|
||||
// Limit level according to configuration
|
||||
level = Math.max(Math.min(level, this.options.maxZoom), this.options.minZoom)
|
||||
|
||||
// Recompute maximum values while temporary tweaking maximum scroll ranges
|
||||
this._computeScrollMax(level)
|
||||
|
||||
// Recompute left and top coordinates based on new zoom level
|
||||
let left = (originLeft + this._scrollLeft) * level / oldLevel - originLeft
|
||||
let top = (originTop + this._scrollTop) * level / oldLevel - originTop
|
||||
|
||||
// Limit x-axis
|
||||
if (left > this._maxScrollLeft) {
|
||||
left = this._maxScrollLeft
|
||||
} else if (left < 0) {
|
||||
left = 0
|
||||
}
|
||||
|
||||
// Limit y-axis
|
||||
if (top > this._maxScrollTop) {
|
||||
top = this._maxScrollTop
|
||||
} else if (top < 0) {
|
||||
top = 0
|
||||
}
|
||||
|
||||
// Push values out
|
||||
this._publish(left, top, level, animate)
|
||||
}
|
||||
|
||||
doTouchStart(touches, timeStamp) {
|
||||
// Array-like check is enough here
|
||||
if (touches.length == null) {
|
||||
warn(`Invalid touch list: ${touches}`)
|
||||
}
|
||||
if (timeStamp instanceof Date) {
|
||||
timeStamp = timeStamp.valueOf()
|
||||
}
|
||||
if (typeof timeStamp !== 'number') {
|
||||
warn(`Invalid timestamp value: ${timeStamp}`)
|
||||
}
|
||||
|
||||
// Reset interruptedAnimation flag
|
||||
this._interruptedAnimation = true
|
||||
|
||||
// Stop deceleration
|
||||
if (this._isDecelerating) {
|
||||
Animate.stop(this._isDecelerating)
|
||||
this._isDecelerating = false
|
||||
this._interruptedAnimation = true
|
||||
}
|
||||
|
||||
// Stop animation
|
||||
if (this._isAnimating) {
|
||||
Animate.stop(this._isAnimating)
|
||||
this._isAnimating = false
|
||||
this._interruptedAnimation = true
|
||||
}
|
||||
|
||||
// Use center point when dealing with two fingers
|
||||
const isSingleTouch = touches.length === 1
|
||||
let currentTouchLeft, currentTouchTop
|
||||
|
||||
if (isSingleTouch) {
|
||||
currentTouchLeft = touches[0].pageX
|
||||
currentTouchTop = touches[0].pageY
|
||||
} else {
|
||||
currentTouchLeft = Math.abs(touches[0].pageX + touches[1].pageX) / 2
|
||||
currentTouchTop = Math.abs(touches[0].pageY + touches[1].pageY) / 2
|
||||
}
|
||||
|
||||
// Store initial positions
|
||||
this._initialTouchLeft = currentTouchLeft
|
||||
this._initialTouchTop = currentTouchTop
|
||||
|
||||
// Store current zoom level
|
||||
this._zoomLevelStart = this._zoomLevel
|
||||
|
||||
// Store initial touch positions
|
||||
this._lastTouchLeft = currentTouchLeft
|
||||
this._lastTouchTop = currentTouchTop
|
||||
|
||||
// Store initial move time stamp
|
||||
this._lastTouchMove = timeStamp
|
||||
|
||||
// Reset initial scale
|
||||
this._lastScale = 1
|
||||
|
||||
// Reset locking flags
|
||||
this._enableScrollX = !isSingleTouch && this.options.scrollingX
|
||||
this._enableScrollY = !isSingleTouch && this.options.scrollingY
|
||||
|
||||
// Reset tracking flag
|
||||
this._isTracking = true
|
||||
|
||||
// Reset deceleration complete flag
|
||||
this._didDecelerationComplete = false
|
||||
|
||||
// Dragging starts directly with two fingers, otherwise lazy with an offset
|
||||
this._isDragging = !isSingleTouch
|
||||
|
||||
// Some features are disabled in multi touch scenarios
|
||||
this._isSingleTouch = isSingleTouch
|
||||
|
||||
// Clearing data structure
|
||||
this._positions = []
|
||||
}
|
||||
|
||||
doTouchMove(touches, timeStamp, scale) {
|
||||
// Array-like check is enough here
|
||||
if (touches.length == null) {
|
||||
warn(`Invalid touch list: ${touches}`)
|
||||
}
|
||||
|
||||
if (timeStamp instanceof Date) {
|
||||
timeStamp = timeStamp.valueOf()
|
||||
}
|
||||
|
||||
if (typeof timeStamp !== 'number') {
|
||||
warn(`Invalid timestamp value: ${timeStamp}`)
|
||||
}
|
||||
|
||||
// Ignore event when tracking is not enabled (event might be outside of element)
|
||||
if (!this._isTracking) {
|
||||
return
|
||||
}
|
||||
|
||||
let currentTouchLeft, currentTouchTop
|
||||
|
||||
// Compute move based around of center of fingers
|
||||
if (touches.length === 2) {
|
||||
currentTouchLeft = Math.abs(touches[0].pageX + touches[1].pageX) / 2
|
||||
currentTouchTop = Math.abs(touches[0].pageY + touches[1].pageY) / 2
|
||||
} else {
|
||||
currentTouchLeft = touches[0].pageX
|
||||
currentTouchTop = touches[0].pageY
|
||||
}
|
||||
|
||||
const positions = this._positions
|
||||
|
||||
// Are we already is dragging mode?
|
||||
if (this._isDragging) {
|
||||
// Compute move distance
|
||||
const moveX = currentTouchLeft - this._lastTouchLeft
|
||||
const moveY = currentTouchTop - this._lastTouchTop
|
||||
|
||||
// Read previous scroll position and zooming
|
||||
let scrollLeft = this._scrollLeft
|
||||
let scrollTop = this._scrollTop
|
||||
let level = this._zoomLevel
|
||||
|
||||
// Work with scaling
|
||||
if (scale != null && this.options.zooming) {
|
||||
const oldLevel = level
|
||||
|
||||
// Recompute level based on previous scale and new scale
|
||||
level = level / this.__lastScale * scale
|
||||
|
||||
// Limit level according to configuration
|
||||
level = Math.max(Math.min(level, this.options.maxZoom), this.options.minZoom)
|
||||
|
||||
// Only do further compution when change happened
|
||||
if (oldLevel !== level) {
|
||||
// Compute relative event position to container
|
||||
var currentTouchLeftRel = currentTouchLeft - this._clientLeft
|
||||
var currentTouchTopRel = currentTouchTop - this._clientTop
|
||||
|
||||
// Recompute left and top coordinates based on new zoom level
|
||||
scrollLeft = (currentTouchLeftRel + scrollLeft) * level / oldLevel - currentTouchLeftRel
|
||||
scrollTop = (currentTouchTopRel + scrollTop) * level / oldLevel - currentTouchTopRel
|
||||
|
||||
// Recompute max scroll values
|
||||
this.__computeScrollMax(level)
|
||||
}
|
||||
}
|
||||
|
||||
if (this._enableScrollX) {
|
||||
scrollLeft -= moveX * this.options.speedMultiplier
|
||||
const maxScrollLeft = this._maxScrollLeft
|
||||
|
||||
if (scrollLeft > maxScrollLeft || scrollLeft < 0) {
|
||||
// Slow down on the edges
|
||||
if (this.options.bouncing) {
|
||||
scrollLeft += moveX / 2 * this.options.speedMultiplier
|
||||
} else if (scrollLeft > maxScrollLeft) {
|
||||
scrollLeft = maxScrollLeft
|
||||
} else {
|
||||
scrollLeft = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compute new vertical scroll position
|
||||
if (this._enableScrollY) {
|
||||
scrollTop -= moveY * this.options.speedMultiplier
|
||||
const maxScrollTop = this._maxScrollTop
|
||||
if (scrollTop > maxScrollTop || scrollTop < 0) {
|
||||
// Slow down on the edges
|
||||
if (this.options.bouncing) {
|
||||
scrollTop += moveY / 2 * this.options.speedMultiplier
|
||||
} else if (scrollTop > maxScrollTop) {
|
||||
scrollTop = maxScrollTop
|
||||
} else {
|
||||
scrollTop = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Keep list from growing infinitely (holding min 10, max 20 measure points)
|
||||
if (positions.length > 60) {
|
||||
positions.splice(0, 30)
|
||||
}
|
||||
|
||||
// Track scroll movement for decleration
|
||||
positions.push(scrollLeft, scrollTop, timeStamp)
|
||||
|
||||
// Sync scroll position
|
||||
this._publish(scrollLeft, scrollTop)
|
||||
|
||||
// Otherwise figure out whether we are switching into dragging mode now.
|
||||
} else {
|
||||
const minimumTrackingForScroll = this.options.locking ? 3 : 0
|
||||
const minimumTrackingForDrag = 5
|
||||
|
||||
const distanceX = Math.abs(currentTouchLeft - this._initialTouchLeft)
|
||||
const distanceY = Math.abs(currentTouchTop - this._initialTouchTop)
|
||||
|
||||
this._enableScrollX = this.options.scrollingX && distanceX >= minimumTrackingForScroll
|
||||
this._enableScrollY = this.options.scrollingY && distanceY >= minimumTrackingForScroll
|
||||
|
||||
positions.push(this._scrollLeft, this._scrollTop, timeStamp)
|
||||
|
||||
this._isDragging =
|
||||
(this._enableScrollX || this._enableScrollY) &&
|
||||
(distanceX >= minimumTrackingForDrag || distanceY >= minimumTrackingForDrag)
|
||||
if (this._isDragging) {
|
||||
this._interruptedAnimation = false
|
||||
}
|
||||
}
|
||||
|
||||
// Update last touch positions and time stamp for next event
|
||||
this._lastTouchLeft = currentTouchLeft
|
||||
this._lastTouchTop = currentTouchTop
|
||||
this._lastTouchMove = timeStamp
|
||||
}
|
||||
|
||||
doTouchEnd(timeStamp) {
|
||||
if (timeStamp instanceof Date) {
|
||||
timeStamp = timeStamp.valueOf()
|
||||
}
|
||||
|
||||
if (typeof timeStamp !== 'number') {
|
||||
warn(`Invalid timestamp value: ${timeStamp}`)
|
||||
}
|
||||
// Ignore event when tracking is not enabled (no touchstart event on element)
|
||||
// This is required as this listener ('touchmove') sits on the document and not on the element itthis.
|
||||
if (!this._isTracking) {
|
||||
return
|
||||
}
|
||||
|
||||
// Not touching anymore (when two finger hit the screen there are two touch end events)
|
||||
this._isTracking = false
|
||||
|
||||
// Be sure to reset the dragging flag now. Here we also detect whether
|
||||
// the finger has moved fast enough to switch into a deceleration animation.
|
||||
if (this._isDragging) {
|
||||
// Reset dragging flag
|
||||
this._isDragging = false
|
||||
|
||||
// Start deceleration
|
||||
// Verify that the last move detected was in some relevant time frame
|
||||
if (this._isSingleTouch && this.options.animating && timeStamp - this._lastTouchMove <= 100) {
|
||||
// Then figure out what the scroll position was about 100ms ago
|
||||
const positions = this._positions
|
||||
const endPos = positions.length - 1
|
||||
let startPos = endPos
|
||||
|
||||
// Move pointer to position measured 100ms ago
|
||||
for (let i = endPos; i > 0 && positions[i] > this._lastTouchMove - 100; i -= 3) {
|
||||
startPos = i
|
||||
}
|
||||
|
||||
// If start and stop position is identical in a 100ms timeframe,
|
||||
// we cannot compute any useful deceleration.
|
||||
if (startPos !== endPos) {
|
||||
// Compute relative movement between these two points
|
||||
const timeOffset = positions[endPos] - positions[startPos]
|
||||
const movedLeft = this._scrollLeft - positions[startPos - 2]
|
||||
const movedTop = this._scrollTop - positions[startPos - 1]
|
||||
|
||||
// Based on 50ms compute the movement to apply for each render step
|
||||
this._decelerationVelocityX = movedLeft / timeOffset * (1000 / 60)
|
||||
this._decelerationVelocityY = movedTop / timeOffset * (1000 / 60)
|
||||
|
||||
// How much velocity is required to start the deceleration
|
||||
const minVelocityToStartDeceleration = this.options.paging || this.options.snapping ? 4 : 1
|
||||
|
||||
// Verify that we have enough velocity to start deceleration
|
||||
if (
|
||||
Math.abs(this._decelerationVelocityX) > minVelocityToStartDeceleration ||
|
||||
Math.abs(this._decelerationVelocityY) > minVelocityToStartDeceleration
|
||||
) {
|
||||
// Deactivate pull-to-refresh when decelerating
|
||||
if (!this._refreshActive) {
|
||||
this._startDeceleration(timeStamp)
|
||||
}
|
||||
} else {
|
||||
this.options.scrollingComplete()
|
||||
}
|
||||
} else {
|
||||
this.options.scrollingComplete()
|
||||
}
|
||||
} else if (timeStamp - this._lastTouchMove > 100) {
|
||||
!this.options.snapping && this.options.scrollingComplete()
|
||||
}
|
||||
}
|
||||
|
||||
// If this was a slower move it is per default non decelerated, but this
|
||||
// still means that we want snap back to the bounds which is done here.
|
||||
// This is placed outside the condition above to improve edge case stability
|
||||
// e.g. touchend fired without enabled dragging. This should normally do not
|
||||
// have modified the scroll positions or even showed the scrollbars though.
|
||||
if (!this._isDecelerating) {
|
||||
if (this._refreshActive && this._refreshStart) {
|
||||
// Use publish instead of scrollTo to allow scrolling to out of boundary position
|
||||
// We don't need to normalize scrollLeft, zoomLevel, etc. here because we only y-scrolling when pull-to-refresh is enabled
|
||||
this._publish(this._scrollLeft, -this._refreshHeight, this._zoomLevel, true)
|
||||
|
||||
if (this._refreshStart) {
|
||||
this._refreshStart()
|
||||
}
|
||||
} else {
|
||||
if (this._interruptedAnimation || this._isDragging) {
|
||||
this.options.scrollingComplete()
|
||||
}
|
||||
|
||||
this.scrollTo(this._scrollLeft, this._scrollTop, true, this._zoomLevel)
|
||||
// Directly signalize deactivation (nothing todo on refresh?)
|
||||
if (this._refreshActive) {
|
||||
this._refreshActive = false
|
||||
if (this._refreshDeactivate) {
|
||||
this._refreshDeactivate()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fully cleanup list
|
||||
this._positions.length = 0
|
||||
}
|
||||
|
||||
_publish(left, top, zoom = 1, animate = false) {
|
||||
// Remember whether we had an animation, then we try to continue based on the current "drive" of the animation
|
||||
const wasAnimating = this._isAnimating
|
||||
|
||||
if (wasAnimating) {
|
||||
Animate.stop(wasAnimating)
|
||||
this._isAnimating = false
|
||||
}
|
||||
|
||||
if (animate && this.options.animating) {
|
||||
// Keep scheduled positions for scrollBy/zoomBy functionality
|
||||
this._scheduledLeft = left
|
||||
this._scheduledTop = top
|
||||
this._scheduledZoom = zoom
|
||||
|
||||
const oldLeft = this._scrollLeft
|
||||
const oldTop = this._scrollTop
|
||||
const oldZoom = this._zoomLevel
|
||||
|
||||
const diffLeft = left - oldLeft
|
||||
const diffTop = top - oldTop
|
||||
const diffZoom = zoom - oldZoom
|
||||
|
||||
const step = (percent, now, render) => {
|
||||
if (render) {
|
||||
this._scrollLeft = oldLeft + diffLeft * percent
|
||||
this._scrollTop = oldTop + diffTop * percent
|
||||
this._zoomLevel = oldZoom + diffZoom * percent
|
||||
// Push values out
|
||||
if (this._callback) {
|
||||
this._callback(this._scrollLeft, this._scrollTop, this._zoomLevel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const verify = id => {
|
||||
return this._isAnimating === id
|
||||
}
|
||||
|
||||
const completed = (renderedFramesPerSecond, animationId, wasFinished) => {
|
||||
if (animationId === this._isAnimating) {
|
||||
this._isAnimating = false
|
||||
}
|
||||
|
||||
if (this._didDecelerationComplete || wasFinished) {
|
||||
this.options.scrollingComplete()
|
||||
}
|
||||
|
||||
if (this.options.zooming) {
|
||||
this._computeScrollMax()
|
||||
if (this._zoomComplete) {
|
||||
this._zoomComplete()
|
||||
this._zoomComplete = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// When continuing based on previous animation we choose an ease-out animation instead of ease-in-out
|
||||
this._isAnimating = Animate.start(
|
||||
step,
|
||||
verify,
|
||||
completed,
|
||||
this.options.animationDuration,
|
||||
wasAnimating ? easeOutCubic : easeInOutCubic,
|
||||
)
|
||||
} else {
|
||||
this._scheduledLeft = this._scrollLeft = left
|
||||
this._scheduledTop = this._scrollTop = top
|
||||
this._scheduledZoom = this._zoomLevel = zoom
|
||||
|
||||
// Push values out
|
||||
if (this._callback) {
|
||||
this._callback(left, top)
|
||||
}
|
||||
|
||||
// Fix max scroll ranges
|
||||
if (this.options.zooming) {
|
||||
this._computeScrollMax()
|
||||
if (this._zoomComplete) {
|
||||
this._zoomComplete()
|
||||
this._zoomComplete = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_computeScrollMax(zoomLevel) {
|
||||
if (zoomLevel == null) {
|
||||
zoomLevel = this._zoomLevel
|
||||
}
|
||||
|
||||
this._maxScrollLeft = Math.max(this._contentWidth * zoomLevel - this._clientWidth, 0)
|
||||
this._maxScrollTop = Math.max(this._contentHeight * zoomLevel - this._clientHeight, 0)
|
||||
}
|
||||
|
||||
_startDeceleration(timeStamp) {
|
||||
if (this.options.paging) {
|
||||
const scrollLeft = Math.max(Math.min(this._scrollLeft, this._maxScrollLeft), 0)
|
||||
const scrollTop = Math.max(Math.min(this._scrollTop, this._maxScrollTop), 0)
|
||||
const clientWidth = this._clientWidth
|
||||
const clientHeight = this._clientHeight
|
||||
|
||||
// We limit deceleration not to the min/max values of the allowed range, but to the size of the visible client area.
|
||||
// Each page should have exactly the size of the client area.
|
||||
this._minDecelerationScrollLeft = Math.floor(scrollLeft / clientWidth) * clientWidth
|
||||
this._minDecelerationScrollTop = Math.floor(scrollTop / clientHeight) * clientHeight
|
||||
this._maxDecelerationScrollLeft = Math.ceil(scrollLeft / clientWidth) * clientWidth
|
||||
this._maxDecelerationScrollTop = Math.ceil(scrollTop / clientHeight) * clientHeight
|
||||
} else {
|
||||
this._minDecelerationScrollLeft = 0
|
||||
this._minDecelerationScrollTop = 0
|
||||
this._maxDecelerationScrollLeft = this._maxScrollLeft
|
||||
this._maxDecelerationScrollTop = this._maxScrollTop
|
||||
}
|
||||
|
||||
// Wrap class method
|
||||
const step = (percent, now, render) => {
|
||||
this._stepThroughDeceleration(render)
|
||||
}
|
||||
|
||||
// How much velocity is required to keep the deceleration running
|
||||
const minVelocityToKeepDecelerating = this.options.snapping ? 4 : 0.001
|
||||
|
||||
// Detect whether it's still worth to continue animating steps
|
||||
// If we are already slow enough to not being user perceivable anymore, we stop the whole process here.
|
||||
const verify = () => {
|
||||
const shouldContinue =
|
||||
Math.abs(this._decelerationVelocityX) >= minVelocityToKeepDecelerating ||
|
||||
Math.abs(this._decelerationVelocityY) >= minVelocityToKeepDecelerating
|
||||
if (!shouldContinue) {
|
||||
this._didDecelerationComplete = true
|
||||
}
|
||||
return shouldContinue
|
||||
}
|
||||
|
||||
const completed = (renderedFramesPerSecond, animationId, wasFinished) => {
|
||||
this._isDecelerating = false
|
||||
// if (this._didDecelerationComplete) {
|
||||
// this.options.scrollingComplete()
|
||||
// }
|
||||
|
||||
// Animate to grid when snapping is active, otherwise just fix out-of-boundary positions
|
||||
this.scrollTo(this._scrollLeft, this._scrollTop, this.options.snapping)
|
||||
}
|
||||
|
||||
// Start animation and switch on flag
|
||||
this._isDecelerating = Animate.start(step, verify, completed)
|
||||
}
|
||||
|
||||
_stepThroughDeceleration(render) {
|
||||
//
|
||||
// COMPUTE NEXT SCROLL POSITION
|
||||
//
|
||||
|
||||
// Add deceleration to scroll position
|
||||
let scrollLeft = this._scrollLeft + this._decelerationVelocityX
|
||||
let scrollTop = this._scrollTop + this._decelerationVelocityY
|
||||
|
||||
//
|
||||
// HARD LIMIT SCROLL POSITION FOR NON BOUNCING MODE
|
||||
//
|
||||
|
||||
if (!this.options.bouncing) {
|
||||
var scrollLeftFixed = Math.max(
|
||||
Math.min(this._maxDecelerationScrollLeft, scrollLeft),
|
||||
this._minDecelerationScrollLeft,
|
||||
)
|
||||
if (scrollLeftFixed !== scrollLeft) {
|
||||
scrollLeft = scrollLeftFixed
|
||||
this._decelerationVelocityX = 0
|
||||
}
|
||||
var scrollTopFixed = Math.max(Math.min(this._maxDecelerationScrollTop, scrollTop), this._minDecelerationScrollTop)
|
||||
if (scrollTopFixed !== scrollTop) {
|
||||
scrollTop = scrollTopFixed
|
||||
this._decelerationVelocityY = 0
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// UPDATE SCROLL POSITION
|
||||
//
|
||||
|
||||
if (render) {
|
||||
this._publish(scrollLeft, scrollTop, this._zoomLevel)
|
||||
} else {
|
||||
this._scrollLeft = scrollLeft
|
||||
this._scrollTop = scrollTop
|
||||
}
|
||||
|
||||
//
|
||||
// SLOW DOWN
|
||||
//
|
||||
|
||||
// Slow down velocity on every iteration
|
||||
if (!this.options.paging) {
|
||||
// This is the factor applied to every iteration of the animation
|
||||
// to slow down the process. This should emulate natural behavior where
|
||||
// objects slow down when the initiator of the movement is removed
|
||||
var frictionFactor = 0.95
|
||||
this._decelerationVelocityX *= frictionFactor
|
||||
this._decelerationVelocityY *= frictionFactor
|
||||
}
|
||||
|
||||
//
|
||||
// BOUNCING SUPPORT
|
||||
//
|
||||
|
||||
if (this.options.bouncing) {
|
||||
var scrollOutsideX = 0
|
||||
var scrollOutsideY = 0
|
||||
|
||||
// This configures the amount of change applied to deceleration/acceleration when reaching boundaries
|
||||
var penetrationDeceleration = this.options.penetrationDeceleration
|
||||
var penetrationAcceleration = this.options.penetrationAcceleration
|
||||
|
||||
// Check limits
|
||||
if (scrollLeft < this._minDecelerationScrollLeft) {
|
||||
scrollOutsideX = this._minDecelerationScrollLeft - scrollLeft
|
||||
} else if (scrollLeft > this._maxDecelerationScrollLeft) {
|
||||
scrollOutsideX = this._maxDecelerationScrollLeft - scrollLeft
|
||||
}
|
||||
|
||||
if (scrollTop < this._minDecelerationScrollTop) {
|
||||
scrollOutsideY = this._minDecelerationScrollTop - scrollTop
|
||||
} else if (scrollTop > this._maxDecelerationScrollTop) {
|
||||
scrollOutsideY = this._maxDecelerationScrollTop - scrollTop
|
||||
}
|
||||
|
||||
// Slow down until slow enough, then flip back to snap position
|
||||
if (scrollOutsideX !== 0) {
|
||||
if (scrollOutsideX * this._decelerationVelocityX <= 0) {
|
||||
this._decelerationVelocityX += scrollOutsideX * penetrationDeceleration
|
||||
} else {
|
||||
this._decelerationVelocityX = scrollOutsideX * penetrationAcceleration
|
||||
}
|
||||
}
|
||||
|
||||
if (scrollOutsideY !== 0) {
|
||||
if (scrollOutsideY * this._decelerationVelocityY <= 0) {
|
||||
this._decelerationVelocityY += scrollOutsideY * penetrationDeceleration
|
||||
} else {
|
||||
this._decelerationVelocityY = scrollOutsideY * penetrationAcceleration
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extend(Scroller.prototype, members)
|
|
@ -0,0 +1,128 @@
|
|||
import {noop} from './lang'
|
||||
/**
|
||||
* Mix properties into target object.
|
||||
*/
|
||||
export function extend(to, _from) {
|
||||
for (const key in _from) {
|
||||
to[key] = _from[key]
|
||||
}
|
||||
return to
|
||||
}
|
||||
|
||||
/**
|
||||
* Multiple Array traversal
|
||||
* @return 1 continue
|
||||
* @return 2 break
|
||||
*/
|
||||
export function traverse(data, childrenKeys = [], fn = noop) {
|
||||
if (!data) {
|
||||
return
|
||||
}
|
||||
if (typeof childrenKeys === 'function') {
|
||||
fn = childrenKeys
|
||||
childrenKeys = []
|
||||
}
|
||||
let level = 0 // current level
|
||||
let indexs = [] // index set of all levels
|
||||
const walk = curData => {
|
||||
for (let i = 0, len = curData.length; i < len; i++) {
|
||||
const isArray = Array.isArray(curData[i])
|
||||
const key = Array.isArray(childrenKeys) ? childrenKeys[level] : childrenKeys
|
||||
if (isArray || (curData[i] && curData[i][key])) {
|
||||
level++
|
||||
indexs.push(i)
|
||||
walk(isArray ? curData[i] : curData[i][key])
|
||||
} else if (level >= childrenKeys.length) {
|
||||
const res = fn(curData[i], level, [...indexs, i])
|
||||
if (res === 1) {
|
||||
continue
|
||||
} else if (res === 2) {
|
||||
break
|
||||
}
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
level = 0
|
||||
indexs = []
|
||||
}
|
||||
walk(data)
|
||||
}
|
||||
/**
|
||||
* Merge an Array of Objects into a single Object.
|
||||
*/
|
||||
export function toObject(arr) {
|
||||
const res = {}
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
if (arr[i]) {
|
||||
extend(res, arr[i])
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an Array-like object to a real Array.
|
||||
*/
|
||||
export function toArray(list, start) {
|
||||
start = start || 0
|
||||
let i = list.length - start
|
||||
const ret = []
|
||||
while (i--) {
|
||||
ret.unshift(list[i + start])
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
/**
|
||||
* whether item is in list or list equal item
|
||||
*/
|
||||
export function inArray(list, item) {
|
||||
return Array.isArray(list) ? !!~list.indexOf(item) : item === list
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a input value to a number for persistence.
|
||||
* If the conversion fails, return original string.
|
||||
*/
|
||||
export function toNumber(val) {
|
||||
const n = parseFloat(val)
|
||||
return isNaN(n) ? val : n
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a value to a string
|
||||
*/
|
||||
export function toString(val) {
|
||||
return val == null ? '' : typeof val === 'object' ? JSON.stringify(val, null, 2) : String(val)
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the two objects are equal or not shallowly
|
||||
*/
|
||||
|
||||
export function compareObjects(object0, object1) {
|
||||
let ret = true
|
||||
|
||||
if (!object0 || !object1) {
|
||||
ret = false
|
||||
} else if (typeof object0 !== 'object' || typeof object1 !== 'object') {
|
||||
ret = false
|
||||
} else if (JSON.stringify(object0) !== JSON.stringify(object1)) {
|
||||
ret = false
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
/**
|
||||
* Check object is empty
|
||||
*/
|
||||
export function isEmptyObject(obj) {
|
||||
for (const key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
---
|
||||
title: ActionBar 操作栏
|
||||
preview: https://didi.github.io/mand-mobile/examples/action-bar
|
||||
---
|
||||
|
||||
汇集若干文案或操作按钮的吸底边栏,可用于展示表单信息与提交按钮
|
||||
|
||||
### 引入
|
||||
|
||||
```javascript
|
||||
import { ActionBar } from 'mand-mobile'
|
||||
|
||||
Vue.component(ActionBar.name, ActionBar)
|
||||
```
|
||||
|
||||
### 代码演示
|
||||
<!-- DEMO -->
|
||||
|
||||
|
||||
### API
|
||||
|
||||
#### ActionBar Props
|
||||
|属性 | 说明 | 类型 | 默认值 | 备注|
|
||||
|----|-----|------|------|------|
|
||||
|actions|按钮组|Array<{text, disabled, onClick}>|-|`text`为按钮文案,<br/>`disabled`为是否禁用改按钮,<br/>`onClick`为点击事件响应函数,传参数与`click`事件相同|
|
||||
|has-text|是否显示文案|Boolean|是否含有`slot`|文案可通过`slot`传入|
|
||||
|
||||
|
||||
#### ActionBar Events
|
||||
|
||||
##### @click(event, action)
|
||||
按钮点击事件
|
||||
|
||||
|属性 | 说明 | 类型 |
|
||||
|----|-----|------|
|
||||
|action|actions列表中与被点击按钮对应的对象|Object: {text, disabled, ...}|
|
|
@ -0,0 +1,7 @@
|
|||
export default {
|
||||
'name': 'action-bar',
|
||||
'text': '操作栏',
|
||||
'category': 'basic',
|
||||
'description': '',
|
||||
'author': 'xuxiaoyan'
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
<template>
|
||||
<div class="md-example-child md-example-child-action-bar md-example-child-0">
|
||||
<md-action-bar :actions="data"></md-action-bar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
import {ActionBar, Toast} from 'mand-mobile'
|
||||
|
||||
export default {
|
||||
title: '通栏多按钮',
|
||||
name: 'action-bar-demo',
|
||||
height: 150,
|
||||
components: {
|
||||
[ActionBar.name]: ActionBar,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
data: [
|
||||
{
|
||||
text: '操作一',
|
||||
onClick: this.handleClick,
|
||||
},
|
||||
{
|
||||
text: '操作二',
|
||||
onClick: this.handleClick,
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleClick() {
|
||||
Toast.succeed('Click')
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,40 @@
|
|||
<template>
|
||||
<div class="md-example-child md-example-child-action-bar md-example-child-1">
|
||||
<md-action-bar :actions="data"></md-action-bar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
import {ActionBar, Toast} from 'mand-mobile'
|
||||
|
||||
export default {
|
||||
title: '通栏多按钮禁用',
|
||||
name: 'action-bar-demo',
|
||||
height: 150,
|
||||
components: {
|
||||
[ActionBar.name]: ActionBar,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
data: [
|
||||
{
|
||||
text: '操作一',
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
text: '操作二',
|
||||
onClick: this.handleClick,
|
||||
},
|
||||
{
|
||||
text: '操作三',
|
||||
disabled: true,
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleClick() {
|
||||
Toast.succeed('Click')
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,35 @@
|
|||
<template>
|
||||
<div class="md-example-child md-example-child-action-bar md-example-child-2">
|
||||
<md-action-bar :actions="data" @click="onBtnClick">
|
||||
¥128.00
|
||||
</md-action-bar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
import {ActionBar, Dialog} from 'mand-mobile'
|
||||
|
||||
export default {
|
||||
title: '通栏带文案',
|
||||
name: 'action-bar-demo',
|
||||
height: 150,
|
||||
components: {
|
||||
[ActionBar.name]: ActionBar,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
data: [
|
||||
{
|
||||
text: '操作',
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onBtnClick(event, action) {
|
||||
Dialog.alert({
|
||||
content: `${action.text}完成`,
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,27 @@
|
|||
<template>
|
||||
<div class="md-example action-bar">
|
||||
<section class="md-example-section" v-for="(demo, index) in demos" :key="index">
|
||||
<div class="md-example-title" v-html="demo.title"></div>
|
||||
<div class="md-example-content">
|
||||
<component :is="demo"></component>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
import createDemoModule from '../../../examples/create-demo-module'
|
||||
import Demo0 from './cases/demo0'
|
||||
import Demo1 from './cases/demo1'
|
||||
import Demo2 from './cases/demo2'
|
||||
|
||||
export default {
|
||||
...createDemoModule('action-bar', [Demo0, Demo1, Demo2]),
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
.md-example.action-bar
|
||||
.md-example-section
|
||||
.md-action-bar
|
||||
position relative !important
|
||||
</style>
|
|
@ -0,0 +1,117 @@
|
|||
<template>
|
||||
<div
|
||||
class="md-action-bar"
|
||||
:class="{'with-text': hasText, 'multi-action': !!this.actions.length }">
|
||||
<div class="md-action-bar-text" v-if="hasText">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div class="md-action-bar-button">
|
||||
<template v-for="(item, index) in actions">
|
||||
<div
|
||||
class="button-item"
|
||||
:key="index"
|
||||
:class="{disabled: !!item.disabled}"
|
||||
@click="$_onBtnClick($event, item)"
|
||||
v-html="item.text"
|
||||
></div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
import {isEmptyObject} from '../_util'
|
||||
|
||||
export default {
|
||||
name: 'md-action-bar',
|
||||
|
||||
props: {
|
||||
actions: {
|
||||
type: Array,
|
||||
default: [],
|
||||
},
|
||||
hasText: {
|
||||
type: Boolean,
|
||||
default() {
|
||||
return !isEmptyObject(this.$slots)
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
// MARK: events handler
|
||||
$_onBtnClick(event, action) {
|
||||
if (action.disabled) {
|
||||
return
|
||||
}
|
||||
action.onClick && action.onClick(event, action)
|
||||
this.$emit('click', event, action)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
.md-action-bar
|
||||
position fixed
|
||||
z-index action-bar-zindex
|
||||
left 0
|
||||
bottom 0
|
||||
width action-bar-width
|
||||
height action-bar-height
|
||||
padding-bottom constant(safe-area-inset-bottom)
|
||||
background color-bg-base
|
||||
clearfix()
|
||||
.md-action-bar-text
|
||||
float left
|
||||
display flex
|
||||
height 100%
|
||||
width 65%
|
||||
padding-left h-gap-lg
|
||||
align-items center
|
||||
overflow hidden
|
||||
color action-bar-text-color
|
||||
font-size action-bar-text-font-size
|
||||
box-sizing border-box
|
||||
.md-action-bar-button
|
||||
display flex
|
||||
height 100%
|
||||
.button-item
|
||||
display flex
|
||||
float right
|
||||
align-items center
|
||||
justify-content center
|
||||
flex 1
|
||||
color action-bar-button-color
|
||||
font-size action-bar-button-font-size
|
||||
font-weight font-weight-medium
|
||||
background action-bar-button-fill
|
||||
hairline(right, color-border-base)
|
||||
-webkit-user-select none
|
||||
-webkit-tap-highlight-color transparent
|
||||
&.disabled
|
||||
opacity opacity-disabled
|
||||
&:nth-last-of-type(2)
|
||||
&::before
|
||||
display none
|
||||
&:last-of-type
|
||||
background action-bar-button-fill-hightlight
|
||||
color action-bar-button-color-hightlight
|
||||
&.disabled
|
||||
background color-bg-disabled
|
||||
color color-text-base-inverse
|
||||
&::before
|
||||
display none
|
||||
&::after
|
||||
absolute-pos()
|
||||
display none
|
||||
content ''
|
||||
position absolute
|
||||
box-sizing border-box
|
||||
background-color color-bg-tap
|
||||
&:active::after
|
||||
display block
|
||||
|
||||
&.with-text.multi-action
|
||||
.md-action-bar-text
|
||||
width 40%
|
||||
</style>
|
|
@ -0,0 +1,65 @@
|
|||
import ActionBar from '../index'
|
||||
import {mount} from 'avoriaz'
|
||||
|
||||
describe('ActionBar', () => {
|
||||
let wrapper
|
||||
|
||||
afterEach(() => {
|
||||
wrapper && wrapper.destroy()
|
||||
})
|
||||
|
||||
it('create a action-bar', () => {
|
||||
wrapper = mount(ActionBar, {
|
||||
propsData: {
|
||||
actions: [
|
||||
{
|
||||
text: '1',
|
||||
},
|
||||
{
|
||||
text: '2',
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
const buttons = wrapper.find('.button-item')
|
||||
const button = wrapper.find('.button-item')[0]
|
||||
const eventStub = sinon.stub(wrapper.vm, '$emit')
|
||||
button.trigger('click')
|
||||
expect(buttons.length).to.equal(2)
|
||||
expect(eventStub.calledOnce).to.be.true
|
||||
expect(eventStub.calledWith('click')).to.be.true
|
||||
})
|
||||
|
||||
it('create a action-bar with disabled button', () => {
|
||||
wrapper = mount(ActionBar, {
|
||||
propsData: {
|
||||
actions: [
|
||||
{
|
||||
text: '1',
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
text: '2',
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
const button0 = wrapper.find('.button-item')[0]
|
||||
expect(button0.hasClass('disabled')).to.equal(true)
|
||||
})
|
||||
|
||||
it('create a action-bar with text', () => {
|
||||
wrapper = mount(ActionBar, {
|
||||
propsData: {
|
||||
actions: [
|
||||
{
|
||||
text: '1',
|
||||
},
|
||||
],
|
||||
hasText: true,
|
||||
},
|
||||
})
|
||||
const text = wrapper.find('.md-action-bar-text')
|
||||
expect(text.length > 0).to.equal(true)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,47 @@
|
|||
---
|
||||
title: ActionSheet 动作面板
|
||||
preview: https://didi.github.io/mand-mobile/examples/action-sheet
|
||||
---
|
||||
|
||||
用于提供场景相关的多个操作动作
|
||||
|
||||
### 引入
|
||||
|
||||
```javascript
|
||||
import { ActionSheet } from 'mand-mobile'
|
||||
|
||||
Vue.component(ActionSheet.name, ActionSheet)
|
||||
```
|
||||
|
||||
### 代码演示
|
||||
<!-- DEMO -->
|
||||
|
||||
### API
|
||||
|
||||
#### ActionSheet Props
|
||||
|属性 | 说明 | 类型 | 默认值 |
|
||||
|----|-----|------|------|
|
||||
|v-model|面板是否可见|Boolean| `false`|
|
||||
|title|面板标题|String|- |
|
||||
|options|面板选项| Array<{text, value}>| `[]`|
|
||||
|default-index|默认选中项| Boolean| `0`|
|
||||
|invalid-index|禁用选择项索引 |Number|`-1`|
|
||||
|cancel-text|取消按钮文案 |String |-|
|
||||
|
||||
#### ActionSheet Events
|
||||
|
||||
##### @selected(item)
|
||||
选择事件
|
||||
|
||||
|属性 | 说明 | 类型 |
|
||||
|----|-----|------|
|
||||
|item| 选中项的值 | Object: {text, value} |
|
||||
|
||||
##### @cancel()
|
||||
取消选择事件
|
||||
|
||||
##### @show()
|
||||
面板展示事件
|
||||
|
||||
##### @hide()
|
||||
面板隐藏事件
|
|
@ -0,0 +1,7 @@
|
|||
export default {
|
||||
'name': 'action-sheet',
|
||||
'text': '底部弹窗',
|
||||
'category': 'feedback',
|
||||
'description': '',
|
||||
'author': 'qiman'
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
<template>
|
||||
<div class="md-example-child md-example-child-action-sheet">
|
||||
<md-button @click.native="$_showActionSheet">唤起动作面板</md-button>
|
||||
<md-action-sheet
|
||||
v-model="value"
|
||||
:title="title"
|
||||
:default-index="defaultIndex"
|
||||
:invalid-index="invalidIndex"
|
||||
:cancel-text="cancelText"
|
||||
:options="options"
|
||||
@selected="$_selected"
|
||||
@cancel="$_cancel"
|
||||
></md-action-sheet>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
import {ActionSheet, Button, Dialog} from 'mand-mobile'
|
||||
|
||||
export default {
|
||||
name: 'action-sheet-demo',
|
||||
height: 500,
|
||||
components: {
|
||||
[ActionSheet.name]: ActionSheet,
|
||||
[Button.name]: Button,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
value: false,
|
||||
title: '操作说明的title',
|
||||
options: [
|
||||
{
|
||||
label: '选项1',
|
||||
value: 0,
|
||||
},
|
||||
{
|
||||
label: '选项2',
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
label: '选项3',
|
||||
value: 2,
|
||||
},
|
||||
],
|
||||
defaultIndex: 1,
|
||||
invalidIndex: 2,
|
||||
cancelText: '取消',
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
$_showActionSheet() {
|
||||
this.value = true
|
||||
},
|
||||
$_selected(item) {
|
||||
Dialog.alert({
|
||||
content: `selected: ${JSON.stringify(item)}`,
|
||||
})
|
||||
console.log('action-sheet selected:', JSON.stringify(item))
|
||||
},
|
||||
$_cancel() {
|
||||
Dialog.alert({
|
||||
content: 'cancel',
|
||||
})
|
||||
console.log('action-sheet cancel')
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,18 @@
|
|||
<template>
|
||||
<div class="md-example action-sheet">
|
||||
<section class="md-example-section" v-for="(demo, index) in demos" :key="index">
|
||||
<div class="md-example-title" v-html="demo.title"></div>
|
||||
<div class="md-example-content">
|
||||
<component :is="demo"></component>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
import createDemoModule from '../../../examples/create-demo-module'
|
||||
import Demo0 from './cases/demo0'
|
||||
|
||||
export default {
|
||||
...createDemoModule('action-sheet', [Demo0]),
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,146 @@
|
|||
<template>
|
||||
<div class="md-action-sheet">
|
||||
<md-popup
|
||||
v-model="isActionSheetShow"
|
||||
position="bottom"
|
||||
prevent-scroll
|
||||
@show="$_onShow"
|
||||
@hide="$_onHide"
|
||||
>
|
||||
<div class="md-action-sheet-content">
|
||||
<header v-if="title">{{ title }}</header>
|
||||
<ul>
|
||||
<template v-for="(item, index) in options">
|
||||
<li
|
||||
:key="index"
|
||||
:class="{
|
||||
'active': index === clickIndex,
|
||||
'disabled': index=== invalidIndex,
|
||||
'md-action-sheet-item': true
|
||||
}"
|
||||
@click="$_onSelect(item, index)"
|
||||
v-html="item.text || item.label"
|
||||
></li>
|
||||
</template>
|
||||
<li class="cancel-btn" @click="$_onCancel">{{ cancelText }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</md-popup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
import Popup from '../popup'
|
||||
import {inArray} from '../_util'
|
||||
|
||||
export default {
|
||||
name: 'md-action-sheet',
|
||||
|
||||
components: {
|
||||
[Popup.name]: Popup,
|
||||
},
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
options: {
|
||||
type: Array,
|
||||
default() {
|
||||
return []
|
||||
},
|
||||
},
|
||||
defaultIndex: {
|
||||
type: Number,
|
||||
default: -1,
|
||||
},
|
||||
invalidIndex: {
|
||||
type: Number,
|
||||
default: -1,
|
||||
},
|
||||
cancelText: {
|
||||
type: String,
|
||||
default: '取消',
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
isActionSheetShow: this.value,
|
||||
clickIndex: -1,
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
value(newVal) {
|
||||
this.isActionSheetShow = newVal
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
this.clickIndex = this.defaultIndex
|
||||
},
|
||||
|
||||
methods: {
|
||||
// MARK: events handler, 如 $_onButtonClick
|
||||
$_onShow() {
|
||||
this.$emit('show')
|
||||
},
|
||||
$_onHide() {
|
||||
this.$emit('hide')
|
||||
this.$emit('input', false)
|
||||
},
|
||||
$_onSelect(item, index) {
|
||||
if (index === this.invalidIndex || inArray(this.invalidIndex, index)) {
|
||||
return
|
||||
}
|
||||
this.clickIndex = index
|
||||
this.$emit('selected', item)
|
||||
this.$emit('input', false)
|
||||
},
|
||||
$_onCancel() {
|
||||
this.$emit('cancel')
|
||||
this.$emit('input', false)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.md-action-sheet
|
||||
color color-text-base
|
||||
-webkit-font-smoothing antialiased
|
||||
.md-action-sheet-content
|
||||
overflow hidden
|
||||
position relative
|
||||
width 100%
|
||||
font-size action-sheet-font-size
|
||||
background color-bg-base
|
||||
text-align center
|
||||
header
|
||||
vertical-height(action-sheet-height)
|
||||
padding 0 30px
|
||||
word-ellipsis()
|
||||
>ul
|
||||
li
|
||||
vertical-height(action-sheet-height)
|
||||
hairline(top, color-border-base)
|
||||
box-sizing border-box
|
||||
font-size font-body-normal
|
||||
&.active
|
||||
color button-primary-fill
|
||||
&.disabled
|
||||
opacity field-item-color-disabled
|
||||
&.cancel-btn
|
||||
height 132px
|
||||
line-height 120px
|
||||
&::before
|
||||
display block
|
||||
content ''
|
||||
height 12px
|
||||
background color-primary-background
|
||||
</style>
|
|
@ -0,0 +1,128 @@
|
|||
import ActionSheet from '../index'
|
||||
import {mount} from 'avoriaz'
|
||||
|
||||
describe('ActionSheet', () => {
|
||||
let wrapper
|
||||
|
||||
afterEach(() => {
|
||||
wrapper && wrapper.destroy()
|
||||
})
|
||||
|
||||
it('create a action-sheet', done => {
|
||||
wrapper = mount(ActionSheet, {
|
||||
propsData: {
|
||||
options: [
|
||||
{
|
||||
label: '选项1',
|
||||
value: 0,
|
||||
},
|
||||
{
|
||||
label: '选项2',
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
label: '选项3',
|
||||
value: 2,
|
||||
},
|
||||
],
|
||||
value: true,
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.hasClass('md-action-sheet')).to.be.true
|
||||
wrapper.vm.$nextTick(() => {
|
||||
expect(wrapper.find('.md-action-sheet-item').length).to.equal(3)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('create a action-sheet with defaultIndex', done => {
|
||||
wrapper = mount(ActionSheet, {
|
||||
propsData: {
|
||||
options: [
|
||||
{
|
||||
label: '选项1',
|
||||
value: 0,
|
||||
},
|
||||
{
|
||||
label: '选项2',
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
label: '选项3',
|
||||
value: 2,
|
||||
},
|
||||
],
|
||||
value: true,
|
||||
defaultIndex: 1,
|
||||
},
|
||||
})
|
||||
|
||||
wrapper.vm.$nextTick(() => {
|
||||
expect(wrapper.find('.md-action-sheet-item')[1].hasClass('active')).to.equal(true)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('action-sheet events selected', done => {
|
||||
wrapper = mount(ActionSheet, {
|
||||
propsData: {
|
||||
options: [
|
||||
{
|
||||
label: '选项1',
|
||||
value: 0,
|
||||
},
|
||||
{
|
||||
label: '选项2',
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
label: '选项3',
|
||||
value: 2,
|
||||
},
|
||||
],
|
||||
value: true,
|
||||
activeIndex: 1,
|
||||
},
|
||||
})
|
||||
const eventStub = sinon.stub(wrapper.vm, '$emit')
|
||||
|
||||
wrapper.vm.$nextTick(() => {
|
||||
wrapper.find('.md-action-sheet-item')[0].trigger('click')
|
||||
expect(wrapper.vm.clickIndex).equal(0)
|
||||
expect(eventStub.calledWith('selected')).to.be.true
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('selector events cancel', done => {
|
||||
wrapper = mount(ActionSheet, {
|
||||
propsData: {
|
||||
options: [
|
||||
{
|
||||
label: '选项1',
|
||||
value: 0,
|
||||
},
|
||||
{
|
||||
label: '选项2',
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
label: '选项3',
|
||||
value: 2,
|
||||
},
|
||||
],
|
||||
value: true,
|
||||
activeIndex: 1,
|
||||
},
|
||||
})
|
||||
const eventStub = sinon.stub(wrapper.vm, '$emit')
|
||||
|
||||
wrapper.vm.$nextTick(() => {
|
||||
const cancelBtn = wrapper.find('.cancel-btn')[0]
|
||||
cancelBtn.trigger('click')
|
||||
expect(eventStub.calledWith('cancel')).to.be.true
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,36 @@
|
|||
---
|
||||
title: Agree 勾选按钮
|
||||
preview: https://didi.github.io/mand-mobile/examples/agree
|
||||
---
|
||||
|
||||
用于标记切换某种状态,如协议勾选
|
||||
|
||||
### 引入
|
||||
|
||||
```javascript
|
||||
import { Agree } from 'mand-mobile'
|
||||
|
||||
Vue.component(Agree.name, Agree)
|
||||
```
|
||||
|
||||
### 代码演示
|
||||
<!-- DEMO -->
|
||||
|
||||
### API
|
||||
|
||||
#### Agree Props
|
||||
|属性 | 说明 | 类型 | 默认值 |
|
||||
|----|-----|------|------|
|
||||
|v-model|是否选中|Boolean|`false`|
|
||||
|disabled|是否禁用|Boolean|`false`|
|
||||
|size|按钮大小,可选值同icon|String|`md`|
|
||||
|
||||
#### Agree Events
|
||||
|
||||
##### @change(name, checked)
|
||||
勾选状态发生变化事件
|
||||
|
||||
|属性 | 说明 | 类型 |
|
||||
|----|-----|------|
|
||||
|name|单选按钮名称,唯一标识|Number/String|
|
||||
|checked|是否选中|Boolean|
|
|
@ -0,0 +1,7 @@
|
|||
export default {
|
||||
'name': 'agree',
|
||||
'text': '单选框',
|
||||
'category': 'form',
|
||||
'description': '',
|
||||
'author': 'chengyanjing'
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
<template>
|
||||
<div class="md-example-child md-example-child-agree md-example-child-0">
|
||||
<md-agree
|
||||
v-model="agreeConf.checked"
|
||||
:disabled="agreeConf.disabled"
|
||||
:size="agreeConf.size"
|
||||
@change="onChange(agreeConf.name, agreeConf.checked, $event)"
|
||||
>
|
||||
本人承诺投保人已充分了解本保险产品,并保证投保信息的真实性,理解并同意
|
||||
</md-agree>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
import {Agree} from 'mand-mobile'
|
||||
|
||||
export default {
|
||||
name: 'agree-demo',
|
||||
title: '选中状态',
|
||||
height: 120,
|
||||
components: {
|
||||
[Agree.name]: Agree,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
agreeConf: {
|
||||
checked: true,
|
||||
name: 'agree0',
|
||||
size: 'lg',
|
||||
disabled: false,
|
||||
introduction: '选中状态',
|
||||
},
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onChange(name, checked) {
|
||||
console.log(`agree name = ${name} is ${checked ? 'checked' : 'unchecked'}`)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,40 @@
|
|||
<template>
|
||||
<div class="md-example-child md-example-child-agree md-example-child-1">
|
||||
<md-agree
|
||||
v-model="agreeConf.checked"
|
||||
:disabled="agreeConf.disabled"
|
||||
:size="agreeConf.size"
|
||||
@change="onChange(agreeConf.name, agreeConf.checked, $event)"
|
||||
>
|
||||
本人承诺投保人已充分了解本保险产品,并保证投保信息的真实性,理解并同意
|
||||
</md-agree>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
import {Agree} from 'mand-mobile'
|
||||
|
||||
export default {
|
||||
name: 'agree-demo',
|
||||
title: '未选中状态',
|
||||
height: 120,
|
||||
components: {
|
||||
[Agree.name]: Agree,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
agreeConf: {
|
||||
checked: false,
|
||||
name: 'agree1',
|
||||
size: 'lg',
|
||||
disabled: false,
|
||||
introduction: '未选中状态',
|
||||
},
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onChange(name, checked) {
|
||||
console.log(`agree name = ${name} is ${checked ? 'checked' : 'unchecked'}`)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,40 @@
|
|||
<template>
|
||||
<div class="md-example-child md-example-child-agree md-example-child-2">
|
||||
<md-agree
|
||||
v-model="agreeConf.checked"
|
||||
:disabled="agreeConf.disabled"
|
||||
:size="agreeConf.size"
|
||||
@change="onChange(agreeConf.name, agreeConf.checked, $event)"
|
||||
>
|
||||
本人承诺投保人已充分了解本保险产品,并保证投保信息的真实性,理解并同意
|
||||
</md-agree>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
import {Agree} from 'mand-mobile'
|
||||
|
||||
export default {
|
||||
name: 'agree-demo',
|
||||
title: '选中不可用状态',
|
||||
height: 120,
|
||||
components: {
|
||||
[Agree.name]: Agree,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
agreeConf: {
|
||||
checked: true,
|
||||
name: 'agree2',
|
||||
size: 'lg',
|
||||
disabled: true,
|
||||
introduction: '选中不可用状态',
|
||||
},
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onChange(name, checked) {
|
||||
console.log(`agree name = ${name} is ${checked ? 'checked' : 'unchecked'}`)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,40 @@
|
|||
<template>
|
||||
<div class="md-example-child md-example-child-agree md-example-child-3">
|
||||
<md-agree
|
||||
v-model="agreeConf.checked"
|
||||
:disabled="agreeConf.disabled"
|
||||
:size="agreeConf.size"
|
||||
@change="onChange(agreeConf.name, agreeConf.checked, $event)"
|
||||
>
|
||||
本人承诺投保人已充分了解本保险产品,并保证投保信息的真实性,理解并同意
|
||||
</md-agree>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
import {Agree} from 'mand-mobile'
|
||||
|
||||
export default {
|
||||
name: 'agree-demo',
|
||||
title: '未选中不可用状态',
|
||||
height: 120,
|
||||
components: {
|
||||
[Agree.name]: Agree,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
agreeConf: {
|
||||
checked: false,
|
||||
name: 'agree3',
|
||||
size: 'lg',
|
||||
disabled: true,
|
||||
introduction: '未选中不可用状态',
|
||||
},
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onChange(name, checked) {
|
||||
console.log(`agree name = ${name} is ${checked ? 'checked' : 'unchecked'}`)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,27 @@
|
|||
<template>
|
||||
<div class="md-example agree">
|
||||
<section class="md-example-section" v-for="(demo, index) in demos" :key="index">
|
||||
<div class="md-example-title" v-html="demo.title"></div>
|
||||
<div class="md-example-content">
|
||||
<component :is="demo"></component>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
import createDemoModule from '../../../examples/create-demo-module'
|
||||
import Demo0 from './cases/demo0'
|
||||
import Demo1 from './cases/demo1'
|
||||
import Demo2 from './cases/demo2'
|
||||
import Demo3 from './cases/demo3'
|
||||
|
||||
export default {...createDemoModule('action-sheet', [Demo0, Demo1, Demo2, Demo3])}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
.md-example.agree
|
||||
.md-example-child
|
||||
padding v-gap-md h-gap-lg
|
||||
font-size font-minor-large
|
||||
background color-bg-base
|
||||
</style>
|
|
@ -0,0 +1,77 @@
|
|||
<template>
|
||||
<div
|
||||
class="md-agree"
|
||||
:class="[
|
||||
disabled ? 'disabled' : ''
|
||||
]">
|
||||
<div
|
||||
class="agree-icon"
|
||||
:class="[
|
||||
value ? 'checked' : ''
|
||||
]"
|
||||
@click="$_onChange($event)">
|
||||
<md-icon
|
||||
:name="iconName"
|
||||
:size="size"></md-icon>
|
||||
</div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
import Icon from '../icon'
|
||||
export default {
|
||||
name: 'md-agree',
|
||||
|
||||
components: {
|
||||
[Icon.name]: Icon,
|
||||
},
|
||||
|
||||
props: {
|
||||
value: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'md',
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
|
||||
computed: {
|
||||
iconName() {
|
||||
return this.value ? 'circle-right' : 'circle'
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
// MARK: events handler, 如 $_onButtonClick
|
||||
$_onChange(event) {
|
||||
if (this.disabled) {
|
||||
return
|
||||
}
|
||||
this.$emit('input', !this.value)
|
||||
this.$emit('change', event)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
.md-agree
|
||||
display flex
|
||||
&.disabled
|
||||
opacity opacity-disabled
|
||||
.agree-icon
|
||||
margin-right 10px
|
||||
color agree-fill-inverse
|
||||
&.checked
|
||||
color agree-fill
|
||||
</style>
|
|
@ -0,0 +1,49 @@
|
|||
import Agree from '../index'
|
||||
import {mount} from 'avoriaz'
|
||||
|
||||
describe('Agree', () => {
|
||||
let wrapper
|
||||
|
||||
afterEach(() => {
|
||||
wrapper && wrapper.destroy()
|
||||
})
|
||||
|
||||
it('create a simple agree', () => {
|
||||
wrapper = mount(Agree)
|
||||
expect(wrapper.hasClass('md-agree')).to.be.true
|
||||
})
|
||||
|
||||
it('create a simple checked agree and then uncheck it', () => {
|
||||
wrapper = mount(Agree, {
|
||||
propsData: {
|
||||
value: true,
|
||||
},
|
||||
})
|
||||
expect(wrapper.vm.iconName).to.equal('circle-right')
|
||||
|
||||
const eventStub = sinon.stub(wrapper.vm, '$emit')
|
||||
wrapper.find('.agree-icon')[0].trigger('click')
|
||||
expect(eventStub.calledWith('change')).to.be.true
|
||||
})
|
||||
|
||||
it('create a simple unchecked agree and then check it', () => {
|
||||
wrapper = mount(Agree, {
|
||||
propsData: {
|
||||
value: false,
|
||||
},
|
||||
})
|
||||
expect(wrapper.vm.iconName).to.equal('circle')
|
||||
|
||||
wrapper.vm.value = true
|
||||
expect(wrapper.vm.iconName).to.equal('circle-right')
|
||||
})
|
||||
|
||||
it('create a disabled agree', () => {
|
||||
wrapper = mount(Agree, {
|
||||
propsData: {
|
||||
disabled: true,
|
||||
},
|
||||
})
|
||||
expect(wrapper.hasClass('disabled')).to.be.true
|
||||
})
|
||||
})
|
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
title: Button 按钮
|
||||
preview: https://didi.github.io/mand-mobile/examples/button
|
||||
---
|
||||
|
||||
按钮组件,可配置多种不同的按钮样式
|
||||
|
||||
### 引入
|
||||
|
||||
```javascript
|
||||
import { Button } from 'mand-mobile'
|
||||
|
||||
Vue.component(Button.name, Button)
|
||||
```
|
||||
|
||||
### 代码演示
|
||||
<!-- DEMO -->
|
||||
|
||||
### API
|
||||
|
||||
#### Button Props
|
||||
|属性 | 说明 | 类型 | 默认值 | 备注 |
|
||||
|----|-----|------|------ |------|
|
||||
|type|按钮类型|String|`primary`|`primary`, `ghost`, `ghost-primary`, `link`|
|
||||
|size|按钮大小|String|`large`|`large`, `small`。仅在`type`为`ghost/ghost-primary`时生效|
|
||||
|icon|按钮图标|String|-|可选值请参考组件`Icon`|
|
||||
|disabled|是否禁用|Boolean|`false`|-|
|
|
@ -0,0 +1,18 @@
|
|||
<template>
|
||||
<div class="md-example-child md-example-child-button md-example-child-button-0">
|
||||
<md-button>Primary</md-button>
|
||||
<md-button disabled>Primary Disabled</md-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
import {Button} from 'mand-mobile'
|
||||
|
||||
export default {
|
||||
name: 'button-demo',
|
||||
title: '主按钮',
|
||||
components: {
|
||||
[Button.name]: Button,
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<template>
|
||||
<div class="md-example-child md-example-child-button md-example-child-button-1">
|
||||
<md-button type="ghost">Ghost</md-button>
|
||||
<md-button type="ghost" disabled style="margin-left:5px">Ghost</md-button>
|
||||
<md-button type="ghost-primary" style="margin-left:5px">Ghost-P</md-button>
|
||||
<md-button type="ghost-primary" disabled style="margin-left:5px">Ghost-P</md-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
import {Button} from 'mand-mobile'
|
||||
|
||||
export default {
|
||||
name: 'button-demo',
|
||||
title: '线性按钮',
|
||||
components: {
|
||||
[Button.name]: Button,
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<template>
|
||||
<div class="md-example-child md-example-child-button md-example-child-button-2">
|
||||
<md-button type="ghost" size="small">Ghost-S</md-button>
|
||||
<md-button type="ghost" size="small" disabled style="margin-left:5px">Ghost-S</md-button>
|
||||
<md-button type="ghost-primary" size="small" style="margin-left:5px">Ghost-P-S</md-button>
|
||||
<md-button type="ghost-primary" size="small" disabled style="margin-left:5px">Ghost-P-S</md-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
import {Button} from 'mand-mobile'
|
||||
|
||||
export default {
|
||||
name: 'button-demo',
|
||||
title: '线性按钮小尺寸',
|
||||
components: {
|
||||
[Button.name]: Button,
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
<template>
|
||||
<div class="md-example-child md-example-child-button md-example-child-button-3">
|
||||
<div class="md-example-box-content">每个人都有属于自己的一片森林,也许我们从来不曾去过,但它一直在那里,总会在那里。迷失的人迷失了,相逢的人会再相逢。</div>
|
||||
<md-button type="link">阅读全文</md-button>
|
||||
<div class="md-example-box-content" style="margin-top:10px">希望你可以记住我,记住我这样活过,这样在你身边待过。</div>
|
||||
<md-button type="link" icon="hollow-plus">加入收藏</md-button>
|
||||
<div class="md-example-box-content" style="margin-top:10px">少年时我们追求激情,成熟后却迷恋平庸。在我们寻找、伤害、背离之后,还能一如既往地相信爱情,这是一种勇气。</div>
|
||||
<md-button type="link" disabled>评论</md-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
import {Button} from 'mand-mobile'
|
||||
|
||||
export default {
|
||||
name: 'button-demo',
|
||||
title: '文字链接按钮',
|
||||
components: {
|
||||
[Button.name]: Button,
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.md-example-child-button-3
|
||||
.md-example-box-content
|
||||
float left
|
||||
width 100%
|
||||
padding 60px h-gap-lg
|
||||
color color-text-base
|
||||
font-size font-minor-large
|
||||
text-align left
|
||||
background color-bg-base
|
||||
box-sizing border-box
|
||||
line-height 1.5
|
||||
text-indent 2em
|
||||
.md-button
|
||||
float left
|
||||
</style>
|
|
@ -0,0 +1,63 @@
|
|||
<template>
|
||||
<div class="md-example button">
|
||||
<section class="md-example-section" v-for="(demo, index) in demos" :key="index">
|
||||
<div class="md-example-title" v-html="demo.title"></div>
|
||||
<div class="md-example-content">
|
||||
<component :is="demo"></component>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
import createDemoModule from '../../../examples/create-demo-module'
|
||||
import Demo0 from './cases/demo0'
|
||||
import Demo1 from './cases/demo1'
|
||||
import Demo2 from './cases/demo2'
|
||||
import Demo3 from './cases/demo3'
|
||||
|
||||
export default {
|
||||
...createDemoModule('button', [Demo0, Demo1, Demo2, Demo3]),
|
||||
}
|
||||
|
||||
// export default {
|
||||
// name: `button-demo`,
|
||||
// data() {
|
||||
// return {
|
||||
// demos: [Demo0, Demo1, Demo2, Demo3],
|
||||
// }
|
||||
// },
|
||||
// }
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
.md-example.button
|
||||
.md-example-section
|
||||
float left
|
||||
width 100%
|
||||
margin-bottom 10px
|
||||
h1
|
||||
margin-bottom 10px
|
||||
color color-gray-1
|
||||
font-size font-heading-normal
|
||||
font-weight normal
|
||||
.md-button
|
||||
float left
|
||||
margin-bottom 10px
|
||||
.md-example-box
|
||||
float left
|
||||
width 100%
|
||||
hairline(top)
|
||||
.md-example-box-content
|
||||
float left
|
||||
width 100%
|
||||
padding 60px h-gap-lg
|
||||
color color-text-base
|
||||
font-size font-minor-large
|
||||
text-align left
|
||||
background color-bg-base
|
||||
box-sizing border-box
|
||||
line-height 1.5
|
||||
text-indent 2em
|
||||
.md-button
|
||||
margin 0
|
||||
</style>
|
|
@ -0,0 +1,143 @@
|
|||
<template>
|
||||
<div
|
||||
class="md-button"
|
||||
:class="[type, size, disabled ? 'disabled' : '', icon ? 'with-icon' : '']"
|
||||
@click="$_onBtnClick"
|
||||
>
|
||||
<div class="md-button-inner">
|
||||
<template v-if="icon">
|
||||
<md-icon :name="icon"></md-icon>
|
||||
</template>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
import Icon from '../icon'
|
||||
export default {
|
||||
name: 'md-button',
|
||||
|
||||
components: {
|
||||
[Icon.name]: Icon,
|
||||
},
|
||||
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
default: 'primary',
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'large',
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
$_onBtnClick(event) {
|
||||
if (this.disabled) {
|
||||
event.stopImmediatePropagation()
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
.md-button
|
||||
-webkit-user-select none
|
||||
-webkit-tap-highlight-color transparent
|
||||
position relative
|
||||
text-align center
|
||||
border-radius radius-normal
|
||||
box-sizing border-box
|
||||
overflow visible
|
||||
// &.disabled
|
||||
// pointer-events none
|
||||
&::before
|
||||
absolute-pos()
|
||||
display none
|
||||
content ''
|
||||
position absolute
|
||||
box-sizing border-box
|
||||
&:active::before
|
||||
display block
|
||||
.md-button-inner
|
||||
width 100%
|
||||
height 100%
|
||||
overflow hidden
|
||||
text-overflow ellipsis
|
||||
word-break break-word
|
||||
white-space nowrap
|
||||
|
||||
// type
|
||||
&.primary
|
||||
background-color button-primary-fill
|
||||
color color-text-base-inverse
|
||||
&:active::before
|
||||
background-color button-primary-fill-tap
|
||||
&.disabled
|
||||
background-color button-primary-fill-disabled
|
||||
&.large, &.small
|
||||
width button-primary-width
|
||||
height button-primary-height
|
||||
line-height button-primary-height
|
||||
font-size button-primary-font-size
|
||||
font-weight font-weight-medium
|
||||
|
||||
&.ghost
|
||||
color button-ghost-color
|
||||
hairline(all, button-ghost-color, true)
|
||||
&:active::before
|
||||
background-color button-ghost-fill-tap
|
||||
&.ghost-primary
|
||||
color button-ghost-primary-color
|
||||
hairline(all, button-ghost-primary-color, true)
|
||||
&:active::before
|
||||
background-color button-ghost-primary-fill-tap
|
||||
&.ghost, &.ghost-primary
|
||||
&.disabled
|
||||
opacity opacity-disabled
|
||||
&.large
|
||||
width button-ghost-width
|
||||
height button-ghost-height
|
||||
line-height button-ghost-height
|
||||
font-size button-ghost-font-size
|
||||
&.small
|
||||
width button-ghost-width-sm
|
||||
height button-ghost-height-sm
|
||||
line-height button-ghost-height-sm
|
||||
font-size button-ghost-font-size
|
||||
|
||||
&.link
|
||||
background-color button-link-fill
|
||||
color button-link-color
|
||||
.md-button-inner
|
||||
hairline(top, color-border-base)
|
||||
hairline(bottom, color-border-base)
|
||||
display flex
|
||||
align-items center
|
||||
justify-content center
|
||||
&:active::before
|
||||
background-color button-link-fill-tap
|
||||
&.disabled
|
||||
opacity opacity-disabled
|
||||
&.large, &.small
|
||||
width button-link-width
|
||||
height button-link-height
|
||||
font-size font-heading-normal
|
||||
&.with-icon
|
||||
.md-icon
|
||||
display flex
|
||||
align-items center
|
||||
justify-content center
|
||||
margin-right h-gap-sm
|
||||
|
||||
</style>
|
|
@ -0,0 +1,96 @@
|
|||
import Button from '../index'
|
||||
import {mount} from 'avoriaz'
|
||||
|
||||
describe('Button', () => {
|
||||
let wrapper
|
||||
|
||||
afterEach(() => {
|
||||
wrapper && wrapper.destroy()
|
||||
})
|
||||
|
||||
it('create default button', () => {
|
||||
wrapper = mount(Button)
|
||||
|
||||
expect(wrapper.hasClass('md-button') && wrapper.hasClass('primary') && wrapper.hasClass('large')).to.equal(true)
|
||||
})
|
||||
|
||||
it('create primary button', () => {
|
||||
wrapper = mount(Button, {
|
||||
propsData: {
|
||||
type: 'primary',
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.hasClass('primary')).to.equal(true)
|
||||
})
|
||||
|
||||
it('create ghost button', () => {
|
||||
wrapper = mount(Button, {
|
||||
propsData: {
|
||||
type: 'ghost',
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.hasClass('ghost')).to.equal(true)
|
||||
})
|
||||
|
||||
it('create ghost-primary button', () => {
|
||||
wrapper = mount(Button, {
|
||||
propsData: {
|
||||
type: 'ghost-primary',
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.hasClass('ghost-primary')).to.equal(true)
|
||||
})
|
||||
|
||||
it('create link button', () => {
|
||||
wrapper = mount(Button, {
|
||||
propsData: {
|
||||
type: 'link',
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.hasClass('link')).to.equal(true)
|
||||
})
|
||||
|
||||
it('create disabled button', () => {
|
||||
wrapper = mount(Button, {
|
||||
propsData: {
|
||||
disabled: true,
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.hasClass('disabled')).to.equal(true)
|
||||
})
|
||||
|
||||
it('create icon button', () => {
|
||||
wrapper = mount(Button, {
|
||||
propsData: {
|
||||
icon: 'hollow-plus',
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.hasClass('with-icon')).to.equal(true)
|
||||
})
|
||||
|
||||
it('create large button', () => {
|
||||
wrapper = mount(Button, {
|
||||
propsData: {
|
||||
size: 'large',
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.hasClass('large')).to.equal(true)
|
||||
})
|
||||
|
||||
it('create small button', () => {
|
||||
wrapper = mount(Button, {
|
||||
propsData: {
|
||||
size: 'small',
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.hasClass('small')).to.equal(true)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,51 @@
|
|||
---
|
||||
title: Captcha 验证码
|
||||
preview: https://didi.github.io/mand-mobile/examples/captcha
|
||||
---
|
||||
|
||||
验证码校验窗口
|
||||
|
||||
### 引入
|
||||
|
||||
```javascript
|
||||
import { Captcha } from 'mand-mobile'
|
||||
|
||||
Vue.component(Captcha.name, Captcha)
|
||||
```
|
||||
|
||||
### 代码演示
|
||||
<!-- DEMO -->
|
||||
|
||||
### API
|
||||
|
||||
#### Captcha Props
|
||||
|属性 | 说明 | 类型 | 默认值|
|
||||
|----|-----|------|------|
|
||||
|v-model|验证码窗口是否显示|Boolean|`false`|
|
||||
|is-view|是否内嵌在页面内展示,否则以弹层形式|Boolean|`false`|
|
||||
|maxlength|字符最大输入长度, 若为`-1`则不限制输入长度|Number|`4`|
|
||||
|mask|是否掩码|Boolean|`false`|
|
||||
|system|是否使用系统默认键盘|Boolean|`false`|
|
||||
|title|窗口标题|String|-|
|
||||
|appendTo|挂载节点|HTMLElement|`document.body`|
|
||||
|count|倒计时时长, 设置为0的时候不显示倒计时按钮|Number|`60`|
|
||||
|
||||
|
||||
#### Captcha Methods
|
||||
|
||||
#### countdown()
|
||||
开始倒计时
|
||||
|
||||
#### Captcha Events
|
||||
|
||||
##### @show()
|
||||
验证码组件显示事件
|
||||
|
||||
#### @hide()
|
||||
验证码组件隐藏事件
|
||||
|
||||
#### @send(countdown)
|
||||
触发发送验证码事件, 会在第一次显示的时候触发, 其余情况则会在点击重发按钮后出发
|
||||
|
||||
#### @submit(code)
|
||||
用户提交输入内容事件
|
|
@ -0,0 +1,7 @@
|
|||
export default {
|
||||
'name': 'captcha',
|
||||
'text': '验证码窗口',
|
||||
'category': 'business',
|
||||
'description': '交互式验证码校验弹窗',
|
||||
'author': 'liuxinyumichael'
|
||||
}
|
|
@ -0,0 +1,117 @@
|
|||
<template>
|
||||
<div class="md-example-child md-example-child-captcha">
|
||||
<md-field title="文案">
|
||||
<md-input-item
|
||||
title="标题"
|
||||
v-model="title"
|
||||
></md-input-item>
|
||||
<md-input-item
|
||||
title="插槽内容"
|
||||
v-model="content"
|
||||
></md-input-item>
|
||||
</md-field>
|
||||
|
||||
<md-field title="配置">
|
||||
|
||||
<md-field-item
|
||||
title="限制验证码长度"
|
||||
customized
|
||||
align="right">
|
||||
<md-switch v-model="limit"></md-switch>
|
||||
</md-field-item>
|
||||
|
||||
<md-input-item
|
||||
title="验证码长度"
|
||||
type="tel"
|
||||
v-model="maxlength"
|
||||
></md-input-item>
|
||||
|
||||
<md-field-item
|
||||
title="采用掩码"
|
||||
customized
|
||||
align="right">
|
||||
<md-switch v-model="mask"></md-switch>
|
||||
</md-field-item>
|
||||
|
||||
<md-field-item
|
||||
title="使用系统键盘"
|
||||
customized
|
||||
align="right">
|
||||
<md-switch v-model="system"></md-switch>
|
||||
</md-field-item>
|
||||
|
||||
</md-field>
|
||||
<md-button @click.native="next">确定</md-button>
|
||||
|
||||
<md-captcha
|
||||
ref="captcha"
|
||||
v-model="show"
|
||||
:title="title"
|
||||
:maxlength="limit ? parseFloat(maxlength) : -1"
|
||||
:system="system"
|
||||
:mask="mask"
|
||||
:appendTo="appendTo"
|
||||
@submit="submit"
|
||||
@show="onShow"
|
||||
@hide="onHide"
|
||||
@send="onSend"
|
||||
>
|
||||
{{content}}
|
||||
</md-captcha>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
import {Button, Toast, Captcha, InputItem, Field, FieldItem, Switch} from 'mand-mobile'
|
||||
|
||||
export default {
|
||||
name: 'captcha-demo',
|
||||
title: '自定义',
|
||||
height: 650,
|
||||
components: {
|
||||
[Button.name]: Button,
|
||||
[Captcha.name]: Captcha,
|
||||
[InputItem.name]: InputItem,
|
||||
[Field.name]: Field,
|
||||
[FieldItem.name]: FieldItem,
|
||||
[Switch.name]: Switch,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
show: false,
|
||||
appendTo: document.querySelector('.doc-demo-box-priview') || document.body,
|
||||
title: '手机验证码',
|
||||
content: '哎哟,不错哟~',
|
||||
limit: true,
|
||||
maxlength: '6',
|
||||
mask: false,
|
||||
system: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
next() {
|
||||
this.show = true
|
||||
},
|
||||
submit(val) {
|
||||
const max = parseFloat(this.maxlength)
|
||||
if (max < 0 || val.length === max) {
|
||||
this.show = false
|
||||
Toast({
|
||||
content: `你输入了${val}`,
|
||||
})
|
||||
}
|
||||
},
|
||||
onSend() {
|
||||
this.$refs.captcha.countdown()
|
||||
},
|
||||
onShow() {},
|
||||
onHide() {},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
.md-example-child-captcha
|
||||
padding 20px
|
||||
.md-field
|
||||
margin-bottom 40px
|
||||
</style>
|
|
@ -0,0 +1,30 @@
|
|||
<template>
|
||||
<div class="md-example-child md-example-child-captcha-1">
|
||||
<md-captcha
|
||||
:maxlength="4"
|
||||
:isView="true"
|
||||
>
|
||||
验证码已发送至186****5407
|
||||
</md-captcha>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
import {Captcha} from 'mand-mobile'
|
||||
|
||||
export default {
|
||||
name: 'captcha-demo',
|
||||
title: '内联',
|
||||
height: 650,
|
||||
components: {
|
||||
[Captcha.name]: Captcha,
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
.md-example-child-captcha-1
|
||||
height 650px
|
||||
padding 30px 0
|
||||
.md-number-keyboard
|
||||
margin-top 30px
|
||||
</style>
|
|
@ -0,0 +1,48 @@
|
|||
<<template>
|
||||
<div class="md-example captcha">
|
||||
<section class="md-example-section" v-for="(demo, index) in demos" :key="index">
|
||||
<div class="md-example-title" v-html="demo.title"></div>
|
||||
<div class="md-example-content">
|
||||
<component :is="demo"></component>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
import createDemoModule from '../../../examples/create-demo-module'
|
||||
import Demo0 from './cases/demo0'
|
||||
// import Demo1 from './cases/demo1'
|
||||
export default {...createDemoModule('captcha', [Demo0])}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
.md-example.captcha
|
||||
.md-example-child
|
||||
padding 0
|
||||
.md-example-section
|
||||
float left
|
||||
width 100%
|
||||
margin-bottom 10px
|
||||
h1
|
||||
margin-bottom 10px
|
||||
color color-gray-1
|
||||
font-size font-heading-normal
|
||||
font-weight normal
|
||||
.md-example-box
|
||||
float left
|
||||
width 100%
|
||||
hairline(top)
|
||||
.md-example-box-content
|
||||
float left
|
||||
width 100%
|
||||
padding 60px h-gap-lg
|
||||
color color-text-base
|
||||
font-size font-minor-large
|
||||
text-align left
|
||||
background color-bg-base
|
||||
box-sizing border-box
|
||||
line-height 1.5
|
||||
text-indent 2em
|
||||
.md-button
|
||||
margin 0
|
||||
</style>
|
|
@ -0,0 +1,207 @@
|
|||
<template>
|
||||
<div class="md-captcha" v-show="isView || value || visible">
|
||||
<template v-if="isView">
|
||||
<div class="md-captcha-content">
|
||||
<h2 class="md-captcha-title" v-if="title" v-text="title"></h2>
|
||||
<div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
<md-button
|
||||
v-if="count"
|
||||
type="ghost"
|
||||
size="small"
|
||||
v-text="counterText"
|
||||
:disabled="this.isCounting"
|
||||
@click.native="$_onClickResend"
|
||||
></md-button>
|
||||
</div>
|
||||
<md-codebox
|
||||
ref="codebox"
|
||||
v-model="code"
|
||||
:maxlength="maxlength"
|
||||
:system="system"
|
||||
:closable="false"
|
||||
:isView="isView"
|
||||
:mask="mask"
|
||||
:autofocus="false"
|
||||
@submit="$_onSubmit"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<md-dialog
|
||||
:value="value"
|
||||
:closable="true"
|
||||
:appendTo="false"
|
||||
position="center"
|
||||
@input="$_onInput"
|
||||
@show="$_onShow"
|
||||
@hide="$_onHide"
|
||||
>
|
||||
<div class="md-captcha-content">
|
||||
<h2 class="md-captcha-title" v-if="title" v-text="title"></h2>
|
||||
<div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
<md-button
|
||||
v-if="count"
|
||||
type="ghost"
|
||||
size="small"
|
||||
v-text="counterText"
|
||||
:disabled="this.isCounting"
|
||||
@click.native="$_onClickResend"
|
||||
></md-button>
|
||||
</div>
|
||||
<md-codebox
|
||||
ref="codebox"
|
||||
v-model="code"
|
||||
:maxlength="maxlength"
|
||||
:system="system"
|
||||
:closable="false"
|
||||
:mask="mask"
|
||||
:autofocus="false"
|
||||
@submit="$_onSubmit"
|
||||
/>
|
||||
</md-dialog>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
import Dialog from '../dialog'
|
||||
import Codebox from '../codebox'
|
||||
import Button from '../button'
|
||||
|
||||
export default {
|
||||
name: 'md-captcha',
|
||||
|
||||
components: {
|
||||
[Dialog.name]: Dialog,
|
||||
[Codebox.name]: Codebox,
|
||||
[Button.name]: Button,
|
||||
},
|
||||
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
},
|
||||
value: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
maxlength: {
|
||||
type: [Number, String],
|
||||
default: 4,
|
||||
},
|
||||
mask: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
system: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
appendTo: {
|
||||
default: () => document.body,
|
||||
},
|
||||
count: {
|
||||
type: Number,
|
||||
default: 60,
|
||||
},
|
||||
isView: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
code: '',
|
||||
visible: false,
|
||||
counterText: '发送验证码',
|
||||
isCounting: false,
|
||||
firstShown: false,
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
value(val) {
|
||||
if (val) {
|
||||
this.code = ''
|
||||
if (!this.firstShown) {
|
||||
this.firstShown = true
|
||||
this.$emit('send', this.countdown)
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
if (this.appendTo && !this.isView) {
|
||||
this.appendTo.appendChild(this.$el)
|
||||
}
|
||||
if (this.value) {
|
||||
this.firstShown = true
|
||||
this.$emit('send', this.countdown)
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
// MARK: events handler, 如 $_onButtonClick
|
||||
$_onInput(val) {
|
||||
this.$emit('input', val)
|
||||
},
|
||||
$_onShow() {
|
||||
this.visible = true
|
||||
this.$refs.codebox.focus()
|
||||
this.$emit('show')
|
||||
},
|
||||
$_onHide() {
|
||||
this.visible = false
|
||||
this.$refs.codebox.blur()
|
||||
this.$emit('hide')
|
||||
},
|
||||
$_onSubmit(code) {
|
||||
this.$emit('submit', code)
|
||||
this.resetcount()
|
||||
},
|
||||
$_onClickResend() {
|
||||
this.$emit('send', this.countdown)
|
||||
},
|
||||
// MARK: public methods
|
||||
countdown() {
|
||||
clearInterval(this.__counter__)
|
||||
let i = this.count - 1
|
||||
this.isCounting = true
|
||||
this.counterText = `${i}s后重发`
|
||||
this.__counter__ = setInterval(() => {
|
||||
if (i === 0) {
|
||||
this.resetcount()
|
||||
} else {
|
||||
i--
|
||||
this.counterText = `${i}s后重发`
|
||||
}
|
||||
}, 1000)
|
||||
},
|
||||
resetcount() {
|
||||
this.isCounting = false
|
||||
this.counterText = '发送验证码'
|
||||
clearInterval(this.__counter__)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
.md-captcha
|
||||
.md-dialog .md-dialog-content
|
||||
margin-bottom number-keyboard-height
|
||||
.md-captcha-content
|
||||
text-align center
|
||||
margin-bottom 20px
|
||||
font-size 24px
|
||||
.md-captcha-title
|
||||
color color-text-base
|
||||
font-size 32px
|
||||
margin-bottom 15px
|
||||
.md-button
|
||||
margin 0.34rem auto
|
||||
</style>
|
|
@ -0,0 +1,74 @@
|
|||
import Captcha from '../index'
|
||||
import sinon from 'sinon'
|
||||
import {mount} from 'avoriaz'
|
||||
|
||||
describe('Captcha', () => {
|
||||
let wrapper
|
||||
afterEach(() => {
|
||||
wrapper && wrapper.destroy()
|
||||
})
|
||||
|
||||
it('create a simple captcha', () => {
|
||||
wrapper = mount(Captcha, {
|
||||
propsData: {
|
||||
system: true,
|
||||
value: true,
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.hasClass('md-captcha')).to.be.true
|
||||
})
|
||||
|
||||
it('create a captcha and not append to body', () => {
|
||||
wrapper = mount(Captcha, {
|
||||
propsData: {
|
||||
appendTo: false,
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.vm.$el.parentNode).not.to.equal(document.body)
|
||||
})
|
||||
|
||||
it('should clean code after shown again', () => {
|
||||
wrapper = mount(Captcha, {
|
||||
propsData: {
|
||||
value: false,
|
||||
},
|
||||
data: {
|
||||
code: '123',
|
||||
},
|
||||
})
|
||||
|
||||
wrapper.setProps({
|
||||
value: true,
|
||||
})
|
||||
|
||||
expect(wrapper.vm.code).to.equal('')
|
||||
})
|
||||
|
||||
it('emit submit events', done => {
|
||||
wrapper = mount(Captcha, {
|
||||
propsData: {
|
||||
maxlength: 4,
|
||||
value: false,
|
||||
isView: true,
|
||||
appendTo: false,
|
||||
},
|
||||
})
|
||||
|
||||
const eventStub = sinon.stub(wrapper.vm, '$emit')
|
||||
wrapper.setProps({
|
||||
value: true,
|
||||
})
|
||||
wrapper.setData({
|
||||
code: '123',
|
||||
})
|
||||
setTimeout(() => {
|
||||
wrapper.first('.keyboard-number-item').trigger('click')
|
||||
setTimeout(() => {
|
||||
expect(eventStub.calledWith('submit', '1231')).to.be.true
|
||||
done()
|
||||
}, 0)
|
||||
}, 500)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,85 @@
|
|||
---
|
||||
title: Cashier 收银台
|
||||
preview: https://didi.github.io/mand-mobile/examples/cashier
|
||||
---
|
||||
|
||||
业务支付弹窗,支持支付渠道选择和支付验证码发送
|
||||
|
||||
### 引入
|
||||
|
||||
```javascript
|
||||
import { Cashier } from 'mand-mobile'
|
||||
|
||||
Vue.component(Cashier.name, Cashier)
|
||||
```
|
||||
|
||||
### 代码演示
|
||||
<!-- DEMO -->
|
||||
|
||||
### API
|
||||
|
||||
#### Cashier Props
|
||||
|属性 | 说明 | 类型 | 默认值 | 备注|
|
||||
|----|-----|------|------|------|
|
||||
|v-model|收银台是否显示|Boolean|`false`|-|
|
||||
|channels|支付渠道数据源|Array<{text, value, icon}>|`[]`|`icon`可作为`className`或组件`Icon`的`name`属性|
|
||||
|default-index|默认选中支付渠道索引|Number|`0`|-|
|
||||
|title|收银台弹窗标题|String|`支付`|-|
|
||||
|payment-title|支付金额标题|String|`支付金额`|支持`html fragment`|
|
||||
|payment-amount|支付金额|String|`0.00`|支持`html fragment`|
|
||||
|payment-describe|支付金额说明|String|-|支持`html fragment`|
|
||||
|
||||
#### Cashier Methods
|
||||
|
||||
##### next(scene, option)
|
||||
进入收银台下一步
|
||||
|
||||
|参数 | 说明 | 类型 | 默认值|
|
||||
|----|-----|------|------|
|
||||
| scene | 步骤标识, 'captcha(发送验证码)', 'loading(支付中)', 'success(支付成功)', 'fail(支付失败)' | String |-|
|
||||
| option | 当前步骤配置 | Object |属性如下所示|
|
||||
|
||||
* `captcha` option
|
||||
|
||||
|属性 | 说明 | 类型 | 默认值 | 备注|
|
||||
|----|-----|------|------|------|
|
||||
|text|发送验证码说明 | String |-|支持`html fragment`|
|
||||
|maxlength|验证码位数 | Number |`4`|若为`-1`则不限制输入长度|
|
||||
|count|验证码重新发送倒计时 | Number |`60`|若为`0`则不显示重新发送|
|
||||
|onSend|验证码发送回调 | Function(countdown: Function) |-|`countdown`为开始倒计时方法|
|
||||
|onSubmit|验证码提交回调 | Function(code: String) |-|`code`为输入的验证码|
|
||||
|
||||
* `loading` option
|
||||
|
||||
|属性 | 说明 | 类型 | 默认值 | 备注|
|
||||
|----|-----|------|------|------|
|
||||
|text|支付中说明 | String |`支付结果查询中...`|支持`html fragment`|
|
||||
|
||||
* `success` option
|
||||
|
||||
|属性 | 说明 | 类型 | 默认值 | 备注|
|
||||
|----|-----|------|------|------|
|
||||
|text|支付成功说明 | String |`支付成功`|支持`html fragment`|
|
||||
|
||||
* `fail` option
|
||||
|
||||
|属性 | 说明 | 类型 | 默认值 | 备注|
|
||||
|----|-----|------|------|------|
|
||||
|text|支付失败说明 | String |`支付失败,请稍后重试`|支持`html fragment`|
|
||||
|
||||
#### Cashier Events
|
||||
|
||||
##### @select(item: {text, value})
|
||||
支付渠道选中事件
|
||||
|
||||
##### @pay(item: {text, value})
|
||||
支付渠道确认并发起支付事件
|
||||
|
||||
##### @cancel()
|
||||
取消支付事件
|
||||
|
||||
##### @show()
|
||||
收银台弹窗展示事件
|
||||
|
||||
##### @hide()
|
||||
收银台弹窗隐藏事件
|
|
@ -0,0 +1,7 @@
|
|||
export default {
|
||||
"name": "cashier",
|
||||
"text": "收银台",
|
||||
"category": "business",
|
||||
"description": "",
|
||||
"author": "xuxiaoyan"
|
||||
}
|