Initial commit

This commit is contained in:
moyus 2018-03-26 16:04:04 +08:00
commit 36c6454b79
481 changed files with 34110 additions and 0 deletions

19
.babelrc Normal file
View File

@ -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"]
}
}
}

9
.editorconfig Normal file
View File

@ -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

11
.eslintignore Normal file
View File

@ -0,0 +1,11 @@
build/*.js
config/*.js
lib/*
output/*
examples/*
site/*
**/*.spec.*
**/demo/data/**
scroller.js
animate.js
gulpfile.js

6
.eslintrc.js Normal file
View File

@ -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'],
}

24
.gitignore vendored Normal file
View File

@ -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

80
CHANGELOG.md Normal file
View File

@ -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**
- 完成开发版开发,用于内部体验和测试

28
CONTRIBUTING.md Normal file
View File

@ -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.

433
LICENSE Normal file
View File

@ -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.

141
README.md Normal file
View File

@ -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://img.shields.io/travis/didi/mand-mobile.svg?style=flat-square)](https://travis-ci.org/adidi/mand-mobile)
[![Codecov](https://img.shields.io/codecov/c/github/didi/mand-mobile/master.svg?style=flat-square)](https://codecov.io/gh/didi/mand-mobile/branch/master)
[![npm package](https://img.shields.io/npm/v/mand-mobile.svg?style=flat-square)](https://www.npmjs.org/package/mand-mobile)
[![NPM downloads](http://img.shields.io/npm/dm/mand-mobile.svg?style=flat-square)](http://npmtrends.com/mand-mobile)
[![Average time to resolve an issue](http://isitmaintained.com/badge/resolution/didi/mand-mobile.svg)](http://isitmaintained.com/project/didi/mand-mobile "Average time to resolve an issue")
[![Percentage of issues still open](http://isitmaintained.com/badge/open/didi/mand-mobile.svg)](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/)
![Examples Link](./assets/examples-qrcode.png)
## 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">
&yen;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>
```

BIN
assets/examples-qrcode.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 643 B

BIN
assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

49
build/check-versions.js Normal file
View File

@ -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)
}
}

196
build/component-init.js Normal file
View File

@ -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()

86
build/mand-change-log.js Normal file
View File

@ -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)
})
}
}

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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,
}

8
build/stylus-mixin.js Normal file
View File

@ -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'))
}

40
build/template.exp Normal file
View File

@ -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

View File

@ -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'
))
})
})

View File

@ -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'))
})

View File

@ -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'))
})

View File

@ -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()
}
})

107
build/webpack/dev-server.js Normal file
View File

@ -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()
}
}

84
build/webpack/utils.js Normal file
View File

@ -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
}

View File

@ -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'
}
}

View File

@ -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')]
}
]
}
}

View File

@ -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

View File

@ -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()
]
})

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 543 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 947 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 493 B

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

204
components/_util/animate.js Normal file
View File

@ -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

View File

@ -0,0 +1,5 @@
import {isProd} from './env'
export const warn = (msg, fn = 'error') => {
!isProd && console[fn](`[Mand-Mobile]: ${msg}`)
}

8
components/_util/env.js Normal file
View File

@ -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)

View File

@ -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
}

View File

@ -0,0 +1,4 @@
export * from './debug'
export * from './env'
export * from './store'
export * from './lang'

71
components/_util/lang.js Normal file
View File

@ -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)}`
}

View File

@ -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)

View File

@ -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)

128
components/_util/store.js Normal file
View File

@ -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
}

View File

@ -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, ...}|

View File

@ -0,0 +1,7 @@
export default {
'name': 'action-bar',
'text': '操作栏',
'category': 'basic',
'description': '',
'author': 'xuxiaoyan'
}

View File

@ -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>

View File

@ -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>

View File

@ -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">
&yen;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>

View File

@ -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>

View File

@ -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>

View File

@ -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)
})
})

View File

@ -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()
面板隐藏事件

View File

@ -0,0 +1,7 @@
export default {
'name': 'action-sheet',
'text': '底部弹窗',
'category': 'feedback',
'description': '',
'author': 'qiman'
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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()
})
})
})

View File

@ -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|

View File

@ -0,0 +1,7 @@
export default {
'name': 'agree',
'text': '单选框',
'category': 'form',
'description': '',
'author': 'chengyanjing'
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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
})
})

View File

@ -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`|-|

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

143
components/button/index.vue Normal file
View File

@ -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>

View File

@ -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)
})
})

View File

@ -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)
用户提交输入内容事件

View File

@ -0,0 +1,7 @@
export default {
'name': 'captcha',
'text': '验证码窗口',
'category': 'business',
'description': '交互式验证码校验弹窗',
'author': 'liuxinyumichael'
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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)
})
})

View File

@ -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()
收银台弹窗隐藏事件

View File

@ -0,0 +1,7 @@
export default {
"name": "cashier",
"text": "收银台",
"category": "business",
"description": "",
"author": "xuxiaoyan"
}

Some files were not shown because too many files have changed in this diff Show More