commit 36c6454b797a68acd58ba7f4bf370d7332deaed5 Author: moyus Date: Mon Mar 26 16:04:04 2018 +0800 Initial commit diff --git a/.babelrc b/.babelrc new file mode 100644 index 00000000..b0cf70c5 --- /dev/null +++ b/.babelrc @@ -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"] + } + } +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..9d08a1a8 --- /dev/null +++ b/.editorconfig @@ -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 diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000..c7388938 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,11 @@ +build/*.js +config/*.js +lib/* +output/* +examples/* +site/* +**/*.spec.* +**/demo/data/** +scroller.js +animate.js +gulpfile.js diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 00000000..cdaea3fe --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,6 @@ +module.exports = { + parserOptions: {ecmaVersion: 8, sourceType: 'module', ecmaFeatures: {jsx: true, experimentalObjectRestSpread: true}}, + env: {es6: true, node: true, browser: true}, + plugins: ['html', 'json'], + extends: ['eslint-config-aesir-mandatory'], +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..dd22c00d --- /dev/null +++ b/.gitignore @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..ffaa5e1d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,80 @@ +--- +title: 更新日志 +--- + + +### 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 + + + +### 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** + - 修复部分文档,样式和错误 + + +### 0.1.0 + +`2017-11-21` + +- **Feature** + + - 完成开发版开发,用于内部体验和测试 \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..7d4c2cfc --- /dev/null +++ b/CONTRIBUTING.md @@ -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. \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..cee01710 --- /dev/null +++ b/LICENSE @@ -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. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 00000000..1c7c6e8a --- /dev/null +++ b/README.md @@ -0,0 +1,141 @@ +
+ + LOGO + +
+
+ + + + + + + + + +
+ +# 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 babel-plugin-import + or + ts-import-plugin (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 + + + +``` diff --git a/assets/examples-qrcode.png b/assets/examples-qrcode.png new file mode 100644 index 00000000..96d69f6e Binary files /dev/null and b/assets/examples-qrcode.png differ diff --git a/assets/logo.png b/assets/logo.png new file mode 100644 index 00000000..3f677295 Binary files /dev/null and b/assets/logo.png differ diff --git a/build/check-versions.js b/build/check-versions.js new file mode 100644 index 00000000..ca407bb1 --- /dev/null +++ b/build/check-versions.js @@ -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) + } +} diff --git a/build/component-init.js b/build/component-init.js new file mode 100644 index 00000000..06f352b6 --- /dev/null +++ b/build/component-init.js @@ -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() diff --git a/build/mand-change-log.js b/build/mand-change-log.js new file mode 100644 index 00000000..1d345289 --- /dev/null +++ b/build/mand-change-log.js @@ -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) + }) + } +} \ No newline at end of file diff --git a/build/rollup/build-component.rollup.js b/build/rollup/build-component.rollup.js new file mode 100644 index 00000000..fbffe394 --- /dev/null +++ b/build/rollup/build-component.rollup.js @@ -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() \ No newline at end of file diff --git a/build/rollup/build-example.rollup.js b/build/rollup/build-example.rollup.js new file mode 100644 index 00000000..9b0da805 --- /dev/null +++ b/build/rollup/build-example.rollup.js @@ -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() \ No newline at end of file diff --git a/build/rollup/build-mand-mobile.rollup.js b/build/rollup/build-mand-mobile.rollup.js new file mode 100644 index 00000000..7c6a91b8 --- /dev/null +++ b/build/rollup/build-mand-mobile.rollup.js @@ -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() \ No newline at end of file diff --git a/build/rollup/dev-server.rollup.js b/build/rollup/dev-server.rollup.js new file mode 100644 index 00000000..2e868194 --- /dev/null +++ b/build/rollup/dev-server.rollup.js @@ -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() \ No newline at end of file diff --git a/build/rollup/rollup-plugin-config.js b/build/rollup/rollup-plugin-config.js new file mode 100644 index 00000000..3c863c1f --- /dev/null +++ b/build/rollup/rollup-plugin-config.js @@ -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, +} \ No newline at end of file diff --git a/build/stylus-mixin.js b/build/stylus-mixin.js new file mode 100644 index 00000000..689392de --- /dev/null +++ b/build/stylus-mixin.js @@ -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')) +} \ No newline at end of file diff --git a/build/template.exp b/build/template.exp new file mode 100644 index 00000000..bc1583f2 --- /dev/null +++ b/build/template.exp @@ -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 + diff --git a/build/webpack/build-example.js b/build/webpack/build-example.js new file mode 100644 index 00000000..c417aa61 --- /dev/null +++ b/build/webpack/build-example.js @@ -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' + )) + }) +}) diff --git a/build/webpack/build-mand-mobile.js b/build/webpack/build-mand-mobile.js new file mode 100644 index 00000000..d07963ab --- /dev/null +++ b/build/webpack/build-mand-mobile.js @@ -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')) +}) diff --git a/build/webpack/build-style-entry.js b/build/webpack/build-style-entry.js new file mode 100644 index 00000000..4c980c15 --- /dev/null +++ b/build/webpack/build-style-entry.js @@ -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')) +}) + diff --git a/build/webpack/dev-client.js b/build/webpack/dev-client.js new file mode 100644 index 00000000..2f75dd53 --- /dev/null +++ b/build/webpack/dev-client.js @@ -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() + } +}) diff --git a/build/webpack/dev-server.js b/build/webpack/dev-server.js new file mode 100644 index 00000000..7cf2bc28 --- /dev/null +++ b/build/webpack/dev-server.js @@ -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() + } +} diff --git a/build/webpack/utils.js b/build/webpack/utils.js new file mode 100644 index 00000000..a0700644 --- /dev/null +++ b/build/webpack/utils.js @@ -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 +} diff --git a/build/webpack/vue-loader.conf.js b/build/webpack/vue-loader.conf.js new file mode 100644 index 00000000..3a69c882 --- /dev/null +++ b/build/webpack/vue-loader.conf.js @@ -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' + } +} diff --git a/build/webpack/webpack.base.conf.js b/build/webpack/webpack.base.conf.js new file mode 100644 index 00000000..b565b813 --- /dev/null +++ b/build/webpack/webpack.base.conf.js @@ -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')] + } + ] + } +} diff --git a/build/webpack/webpack.build.conf.js b/build/webpack/webpack.build.conf.js new file mode 100644 index 00000000..67367f3e --- /dev/null +++ b/build/webpack/webpack.build.conf.js @@ -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 \ No newline at end of file diff --git a/build/webpack/webpack.dev.conf.js b/build/webpack/webpack.dev.conf.js new file mode 100644 index 00000000..abf99805 --- /dev/null +++ b/build/webpack/webpack.dev.conf.js @@ -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() + ] +}) diff --git a/build/webpack/webpack.example.conf.js b/build/webpack/webpack.example.conf.js new file mode 100644 index 00000000..336a82a8 --- /dev/null +++ b/build/webpack/webpack.example.conf.js @@ -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 diff --git a/build/webpack/webpack.test.conf.js b/build/webpack/webpack.test.conf.js new file mode 100644 index 00000000..ace7d10e --- /dev/null +++ b/build/webpack/webpack.test.conf.js @@ -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 diff --git a/components/_style/global.styl b/components/_style/global.styl new file mode 100644 index 00000000..8139913a --- /dev/null +++ b/components/_style/global.styl @@ -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 diff --git a/components/_style/images/arrow-down.svg b/components/_style/images/arrow-down.svg new file mode 100644 index 00000000..36c5532c --- /dev/null +++ b/components/_style/images/arrow-down.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/components/_style/images/arrow-left.svg b/components/_style/images/arrow-left.svg new file mode 100644 index 00000000..68d344b8 --- /dev/null +++ b/components/_style/images/arrow-left.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/components/_style/images/arrow-right.svg b/components/_style/images/arrow-right.svg new file mode 100644 index 00000000..fc969c56 --- /dev/null +++ b/components/_style/images/arrow-right.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/components/_style/images/arrow-up.svg b/components/_style/images/arrow-up.svg new file mode 100644 index 00000000..66263a95 --- /dev/null +++ b/components/_style/images/arrow-up.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/components/_style/images/circle-alert.svg b/components/_style/images/circle-alert.svg new file mode 100644 index 00000000..259ffe0d --- /dev/null +++ b/components/_style/images/circle-alert.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/components/_style/images/circle-cross.svg b/components/_style/images/circle-cross.svg new file mode 100644 index 00000000..9ff6aef5 --- /dev/null +++ b/components/_style/images/circle-cross.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/components/_style/images/circle-right.svg b/components/_style/images/circle-right.svg new file mode 100644 index 00000000..b8408fc9 --- /dev/null +++ b/components/_style/images/circle-right.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/components/_style/images/circle.svg b/components/_style/images/circle.svg new file mode 100644 index 00000000..7e5632b1 --- /dev/null +++ b/components/_style/images/circle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/components/_style/images/cross.svg b/components/_style/images/cross.svg new file mode 100644 index 00000000..f6238c5e --- /dev/null +++ b/components/_style/images/cross.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/components/_style/images/hollow-plus.svg b/components/_style/images/hollow-plus.svg new file mode 100644 index 00000000..c85dde5b --- /dev/null +++ b/components/_style/images/hollow-plus.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/components/_style/images/keyboard-del-simple.png b/components/_style/images/keyboard-del-simple.png new file mode 100644 index 00000000..bb4c74fe Binary files /dev/null and b/components/_style/images/keyboard-del-simple.png differ diff --git a/components/_style/images/keyboard-del.png b/components/_style/images/keyboard-del.png new file mode 100644 index 00000000..a3e78ca1 Binary files /dev/null and b/components/_style/images/keyboard-del.png differ diff --git a/components/_style/images/keyboard-hide.png b/components/_style/images/keyboard-hide.png new file mode 100644 index 00000000..339ccd6e Binary files /dev/null and b/components/_style/images/keyboard-hide.png differ diff --git a/components/_style/images/right.svg b/components/_style/images/right.svg new file mode 100644 index 00000000..d9903b20 --- /dev/null +++ b/components/_style/images/right.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/components/_style/images/spinner.svg b/components/_style/images/spinner.svg new file mode 100644 index 00000000..1e8e05fc --- /dev/null +++ b/components/_style/images/spinner.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/components/_style/mixin/theme.styl b/components/_style/mixin/theme.styl new file mode 100644 index 00000000..03c19d98 --- /dev/null +++ b/components/_style/mixin/theme.styl @@ -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 diff --git a/components/_style/mixin/util.styl b/components/_style/mixin/util.styl new file mode 100644 index 00000000..67f38861 --- /dev/null +++ b/components/_style/mixin/util.styl @@ -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 \ No newline at end of file diff --git a/components/_util/animate.js b/components/_util/animate.js new file mode 100644 index 00000000..c226390b --- /dev/null +++ b/components/_util/animate.js @@ -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 diff --git a/components/_util/debug.js b/components/_util/debug.js new file mode 100644 index 00000000..1fcb64a5 --- /dev/null +++ b/components/_util/debug.js @@ -0,0 +1,5 @@ +import {isProd} from './env' + +export const warn = (msg, fn = 'error') => { + !isProd && console[fn](`[Mand-Mobile]: ${msg}`) +} diff --git a/components/_util/env.js b/components/_util/env.js new file mode 100644 index 00000000..d9b90f74 --- /dev/null +++ b/components/_util/env.js @@ -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) diff --git a/components/_util/formate-value.js b/components/_util/formate-value.js new file mode 100644 index 00000000..1491592c --- /dev/null +++ b/components/_util/formate-value.js @@ -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 +} diff --git a/components/_util/index.js b/components/_util/index.js new file mode 100644 index 00000000..b08a0d2c --- /dev/null +++ b/components/_util/index.js @@ -0,0 +1,4 @@ +export * from './debug' +export * from './env' +export * from './store' +export * from './lang' diff --git a/components/_util/lang.js b/components/_util/lang.js new file mode 100644 index 00000000..a1de47f7 --- /dev/null +++ b/components/_util/lang.js @@ -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)}` +} diff --git a/components/_util/render.js b/components/_util/render.js new file mode 100644 index 00000000..83353ae7 --- /dev/null +++ b/components/_util/render.js @@ -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) diff --git a/components/_util/scroller.js b/components/_util/scroller.js new file mode 100644 index 00000000..684a9b42 --- /dev/null +++ b/components/_util/scroller.js @@ -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 null + * @param top {Number?null} Vertical scroll position, keeps current if value is null + * @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) diff --git a/components/_util/store.js b/components/_util/store.js new file mode 100644 index 00000000..af28007d --- /dev/null +++ b/components/_util/store.js @@ -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 +} diff --git a/components/action-bar/README.md b/components/action-bar/README.md new file mode 100644 index 00000000..a4b1bc74 --- /dev/null +++ b/components/action-bar/README.md @@ -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) +``` + +### 代码演示 + + + +### API + +#### ActionBar Props +|属性 | 说明 | 类型 | 默认值 | 备注| +|----|-----|------|------|------| +|actions|按钮组|Array<{text, disabled, onClick}>|-|`text`为按钮文案,
`disabled`为是否禁用改按钮,
`onClick`为点击事件响应函数,传参数与`click`事件相同| +|has-text|是否显示文案|Boolean|是否含有`slot`|文案可通过`slot`传入| + + +#### ActionBar Events + +##### @click(event, action) +按钮点击事件 + +|属性 | 说明 | 类型 | +|----|-----|------| +|action|actions列表中与被点击按钮对应的对象|Object: {text, disabled, ...}| diff --git a/components/action-bar/component.js b/components/action-bar/component.js new file mode 100644 index 00000000..1031779e --- /dev/null +++ b/components/action-bar/component.js @@ -0,0 +1,7 @@ +export default { + 'name': 'action-bar', + 'text': '操作栏', + 'category': 'basic', + 'description': '', + 'author': 'xuxiaoyan' +} diff --git a/components/action-bar/demo/cases/demo0.vue b/components/action-bar/demo/cases/demo0.vue new file mode 100644 index 00000000..f41ed2d9 --- /dev/null +++ b/components/action-bar/demo/cases/demo0.vue @@ -0,0 +1,36 @@ + + + diff --git a/components/action-bar/demo/cases/demo1.vue b/components/action-bar/demo/cases/demo1.vue new file mode 100644 index 00000000..864406e6 --- /dev/null +++ b/components/action-bar/demo/cases/demo1.vue @@ -0,0 +1,40 @@ + + + diff --git a/components/action-bar/demo/cases/demo2.vue b/components/action-bar/demo/cases/demo2.vue new file mode 100644 index 00000000..38fb45ab --- /dev/null +++ b/components/action-bar/demo/cases/demo2.vue @@ -0,0 +1,35 @@ + + + diff --git a/components/action-bar/demo/index.vue b/components/action-bar/demo/index.vue new file mode 100644 index 00000000..7a9d382d --- /dev/null +++ b/components/action-bar/demo/index.vue @@ -0,0 +1,27 @@ + + + + + \ No newline at end of file diff --git a/components/action-bar/index.vue b/components/action-bar/index.vue new file mode 100644 index 00000000..00acb600 --- /dev/null +++ b/components/action-bar/index.vue @@ -0,0 +1,117 @@ + + + + + diff --git a/components/action-bar/test/index.spec.js b/components/action-bar/test/index.spec.js new file mode 100644 index 00000000..f4e9eb32 --- /dev/null +++ b/components/action-bar/test/index.spec.js @@ -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) + }) +}) diff --git a/components/action-sheet/README.md b/components/action-sheet/README.md new file mode 100644 index 00000000..002c97b2 --- /dev/null +++ b/components/action-sheet/README.md @@ -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) +``` + +### 代码演示 + + +### 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() +面板隐藏事件 diff --git a/components/action-sheet/component.js b/components/action-sheet/component.js new file mode 100644 index 00000000..216a7cb6 --- /dev/null +++ b/components/action-sheet/component.js @@ -0,0 +1,7 @@ +export default { + 'name': 'action-sheet', + 'text': '底部弹窗', + 'category': 'feedback', + 'description': '', + 'author': 'qiman' +} diff --git a/components/action-sheet/demo/cases/demo0.vue b/components/action-sheet/demo/cases/demo0.vue new file mode 100644 index 00000000..2b7e7f97 --- /dev/null +++ b/components/action-sheet/demo/cases/demo0.vue @@ -0,0 +1,67 @@ + + + diff --git a/components/action-sheet/demo/index.vue b/components/action-sheet/demo/index.vue new file mode 100644 index 00000000..682e177a --- /dev/null +++ b/components/action-sheet/demo/index.vue @@ -0,0 +1,18 @@ + + + diff --git a/components/action-sheet/index.vue b/components/action-sheet/index.vue new file mode 100644 index 00000000..504aacf7 --- /dev/null +++ b/components/action-sheet/index.vue @@ -0,0 +1,146 @@ + + + + + diff --git a/components/action-sheet/test/index.spec.js b/components/action-sheet/test/index.spec.js new file mode 100644 index 00000000..99e7e33b --- /dev/null +++ b/components/action-sheet/test/index.spec.js @@ -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() + }) + }) +}) diff --git a/components/agree/README.md b/components/agree/README.md new file mode 100644 index 00000000..f6bf2d2d --- /dev/null +++ b/components/agree/README.md @@ -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) +``` + +### 代码演示 + + +### API + +#### Agree Props +|属性 | 说明 | 类型 | 默认值 | +|----|-----|------|------| +|v-model|是否选中|Boolean|`false`| +|disabled|是否禁用|Boolean|`false`| +|size|按钮大小,可选值同icon|String|`md`| + +#### Agree Events + +##### @change(name, checked) +勾选状态发生变化事件 + +|属性 | 说明 | 类型 | +|----|-----|------| +|name|单选按钮名称,唯一标识|Number/String| +|checked|是否选中|Boolean| diff --git a/components/agree/component.js b/components/agree/component.js new file mode 100644 index 00000000..bf904191 --- /dev/null +++ b/components/agree/component.js @@ -0,0 +1,7 @@ +export default { + 'name': 'agree', + 'text': '单选框', + 'category': 'form', + 'description': '', + 'author': 'chengyanjing' +} diff --git a/components/agree/demo/cases/demo0.vue b/components/agree/demo/cases/demo0.vue new file mode 100644 index 00000000..3e75b9bd --- /dev/null +++ b/components/agree/demo/cases/demo0.vue @@ -0,0 +1,40 @@ + + + \ No newline at end of file diff --git a/components/agree/demo/cases/demo1.vue b/components/agree/demo/cases/demo1.vue new file mode 100644 index 00000000..f5db5198 --- /dev/null +++ b/components/agree/demo/cases/demo1.vue @@ -0,0 +1,40 @@ + + + \ No newline at end of file diff --git a/components/agree/demo/cases/demo2.vue b/components/agree/demo/cases/demo2.vue new file mode 100644 index 00000000..2fd675b0 --- /dev/null +++ b/components/agree/demo/cases/demo2.vue @@ -0,0 +1,40 @@ + + + diff --git a/components/agree/demo/cases/demo3.vue b/components/agree/demo/cases/demo3.vue new file mode 100644 index 00000000..3348f200 --- /dev/null +++ b/components/agree/demo/cases/demo3.vue @@ -0,0 +1,40 @@ + + + \ No newline at end of file diff --git a/components/agree/demo/index.vue b/components/agree/demo/index.vue new file mode 100644 index 00000000..61b17994 --- /dev/null +++ b/components/agree/demo/index.vue @@ -0,0 +1,27 @@ + + + + + diff --git a/components/agree/index.vue b/components/agree/index.vue new file mode 100644 index 00000000..921da787 --- /dev/null +++ b/components/agree/index.vue @@ -0,0 +1,77 @@ + + + + + diff --git a/components/agree/test/index.spec.js b/components/agree/test/index.spec.js new file mode 100644 index 00000000..89d5ca1a --- /dev/null +++ b/components/agree/test/index.spec.js @@ -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 + }) +}) diff --git a/components/button/README.md b/components/button/README.md new file mode 100644 index 00000000..2af5f92c --- /dev/null +++ b/components/button/README.md @@ -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) +``` + +### 代码演示 + + +### 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`|-| diff --git a/components/button/demo/cases/demo0.vue b/components/button/demo/cases/demo0.vue new file mode 100644 index 00000000..e458fb02 --- /dev/null +++ b/components/button/demo/cases/demo0.vue @@ -0,0 +1,18 @@ + + + + diff --git a/components/button/demo/cases/demo1.vue b/components/button/demo/cases/demo1.vue new file mode 100644 index 00000000..cff03aa2 --- /dev/null +++ b/components/button/demo/cases/demo1.vue @@ -0,0 +1,20 @@ + + + + diff --git a/components/button/demo/cases/demo2.vue b/components/button/demo/cases/demo2.vue new file mode 100644 index 00000000..90b5604c --- /dev/null +++ b/components/button/demo/cases/demo2.vue @@ -0,0 +1,20 @@ + + + + diff --git a/components/button/demo/cases/demo3.vue b/components/button/demo/cases/demo3.vue new file mode 100644 index 00000000..0e12c83d --- /dev/null +++ b/components/button/demo/cases/demo3.vue @@ -0,0 +1,38 @@ + + + + + diff --git a/components/button/demo/index.vue b/components/button/demo/index.vue new file mode 100644 index 00000000..65aa4a87 --- /dev/null +++ b/components/button/demo/index.vue @@ -0,0 +1,63 @@ + + + + + diff --git a/components/button/index.vue b/components/button/index.vue new file mode 100644 index 00000000..e43ca473 --- /dev/null +++ b/components/button/index.vue @@ -0,0 +1,143 @@ + + + + + diff --git a/components/button/test/index.spec.js b/components/button/test/index.spec.js new file mode 100644 index 00000000..9f470048 --- /dev/null +++ b/components/button/test/index.spec.js @@ -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) + }) +}) diff --git a/components/captcha/README.md b/components/captcha/README.md new file mode 100644 index 00000000..a07930b2 --- /dev/null +++ b/components/captcha/README.md @@ -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) +``` + +### 代码演示 + + +### 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) +用户提交输入内容事件 diff --git a/components/captcha/component.js b/components/captcha/component.js new file mode 100644 index 00000000..d7e4041b --- /dev/null +++ b/components/captcha/component.js @@ -0,0 +1,7 @@ +export default { + 'name': 'captcha', + 'text': '验证码窗口', + 'category': 'business', + 'description': '交互式验证码校验弹窗', + 'author': 'liuxinyumichael' +} diff --git a/components/captcha/demo/cases/demo0.vue b/components/captcha/demo/cases/demo0.vue new file mode 100644 index 00000000..f1ca1799 --- /dev/null +++ b/components/captcha/demo/cases/demo0.vue @@ -0,0 +1,117 @@ + + + + + diff --git a/components/captcha/demo/cases/demo1.vue b/components/captcha/demo/cases/demo1.vue new file mode 100644 index 00000000..384b8c86 --- /dev/null +++ b/components/captcha/demo/cases/demo1.vue @@ -0,0 +1,30 @@ + + + + + diff --git a/components/captcha/demo/index.vue b/components/captcha/demo/index.vue new file mode 100644 index 00000000..5d518d43 --- /dev/null +++ b/components/captcha/demo/index.vue @@ -0,0 +1,48 @@ +< + + + + diff --git a/components/captcha/index.vue b/components/captcha/index.vue new file mode 100644 index 00000000..3fb06c07 --- /dev/null +++ b/components/captcha/index.vue @@ -0,0 +1,207 @@ + + + + + diff --git a/components/captcha/test/index.spec.js b/components/captcha/test/index.spec.js new file mode 100644 index 00000000..68ad6772 --- /dev/null +++ b/components/captcha/test/index.spec.js @@ -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) + }) +}) diff --git a/components/cashier/README.md b/components/cashier/README.md new file mode 100644 index 00000000..b3aa2384 --- /dev/null +++ b/components/cashier/README.md @@ -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) +``` + +### 代码演示 + + +### 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() +收银台弹窗隐藏事件 diff --git a/components/cashier/component.js b/components/cashier/component.js new file mode 100644 index 00000000..1949acd2 --- /dev/null +++ b/components/cashier/component.js @@ -0,0 +1,7 @@ +export default { + "name": "cashier", + "text": "收银台", + "category": "business", + "description": "", + "author": "xuxiaoyan" +} diff --git a/components/cashier/demo/cases/demo0.vue b/components/cashier/demo/cases/demo0.vue new file mode 100644 index 00000000..a0ab2e8e --- /dev/null +++ b/components/cashier/demo/cases/demo0.vue @@ -0,0 +1,194 @@ + + + + + \ No newline at end of file diff --git a/components/cashier/demo/index.vue b/components/cashier/demo/index.vue new file mode 100644 index 00000000..c7e3d818 --- /dev/null +++ b/components/cashier/demo/index.vue @@ -0,0 +1,22 @@ +< + + + + diff --git a/components/cashier/index.vue b/components/cashier/index.vue new file mode 100644 index 00000000..21f299ac --- /dev/null +++ b/components/cashier/index.vue @@ -0,0 +1,421 @@ + + + + + diff --git a/components/cashier/rolling.vue b/components/cashier/rolling.vue new file mode 100644 index 00000000..a3c7f6fe --- /dev/null +++ b/components/cashier/rolling.vue @@ -0,0 +1,122 @@ + + + + + diff --git a/components/cashier/test/index.spec.js b/components/cashier/test/index.spec.js new file mode 100644 index 00000000..77a17d32 --- /dev/null +++ b/components/cashier/test/index.spec.js @@ -0,0 +1,156 @@ +import Cashier from '../index' +import {mount} from 'avoriaz' +import {setTimeout} from 'timers' + +describe('Cashier', () => { + let wrapper + + afterEach(() => { + wrapper && wrapper.destroy() + }) + + const channels = [ + { + icon: 'cashier-icon-1', + text: '招商银行储蓄卡(0056)支付', + value: '001', + }, + { + icon: 'cashier-icon-2', + text: '支付宝支付', + value: '002', + }, + { + icon: 'cashier-icon-3', + text: '微信支付', + value: '003', + }, + { + icon: 'cashier-icon-4', + text: 'QQ钱包支付', + value: '004', + }, + { + icon: 'cashier-icon-5', + text: '一网通支付', + value: '005', + }, + ] + + it('cashier default-index', done => { + wrapper = mount(Cashier) + + const eventStub = sinon.stub(wrapper.vm, '$emit') + + wrapper.vm.channels = channels + wrapper.vm.defaultIndex = 1 + wrapper.vm.value = true + wrapper.vm.$nextTick(() => { + expect( + wrapper + .find('.choose-channel-item')[0] + .text() + .trim(), + ).to.equal(channels[1].text) + expect(wrapper.vm.activeChannelIndex).to.equal(1) + + const moreBtn = wrapper.find('.choose-channel-more')[0] + moreBtn.trigger('click') + wrapper.vm.$nextTick(() => { + expect(wrapper.find('.choose-channel-item').length).to.equal(channels.length) + + const item = wrapper.find('.choose-channel-item')[2] + item.trigger('click') + expect(eventStub.calledWith('select')).to.be.true + expect(wrapper.vm.activeChannelIndex).to.equal(2) + + const confirm = wrapper.find('.md-cashier-pay-button')[0] + confirm.trigger('click') + expect(eventStub.calledWith('pay')).to.be.true + + wrapper.vm.value = false + done() + }) + }) + }) + + it('cashier captcha', done => { + wrapper = mount(Cashier, { + propsData: {channels}, + }) + + wrapper.vm.value = true + wrapper.vm.next('captcha', { + text: '123', + }) + wrapper.vm.$nextTick(() => { + expect(wrapper.find('.md-cashier-captcha').length > 0).to.be.true + const cancel = wrapper.find('.md-popup-cancel')[0] + cancel.trigger('click') + done() + }) + }) + + it('cashier loading', done => { + wrapper = mount(Cashier, { + propsData: {channels}, + }) + + wrapper.vm.value = true + wrapper.vm.next('loading', { + text: '123', + }) + wrapper.vm.$nextTick(() => { + expect(wrapper.find('.md-cashier-loading').length > 0).to.be.true + expect( + wrapper + .find('.md-cashier-block-text')[0] + .text() + .trim(), + ).to.equal('123') + done() + }) + }) + + it('cashier success', done => { + wrapper = mount(Cashier, { + propsData: {channels}, + }) + + wrapper.vm.value = true + wrapper.vm.next('success', { + text: '123', + }) + wrapper.vm.$nextTick(() => { + expect(wrapper.find('.md-cashier-success').length > 0).to.be.true + expect( + wrapper + .find('.md-cashier-block-text')[0] + .text() + .trim(), + ).to.equal('123') + done() + }) + }) + + it('cashier fail', done => { + wrapper = mount(Cashier, { + propsData: {channels}, + }) + + wrapper.vm.value = true + wrapper.vm.next('fail', { + text: '123', + }) + wrapper.vm.$nextTick(() => { + expect(wrapper.find('.md-cashier-fail').length > 0).to.be.true + expect( + wrapper + .find('.md-cashier-block-text')[0] + .text() + .trim(), + ).to.equal('123') + done() + }) + }) +}) diff --git a/components/chart/README.md b/components/chart/README.md new file mode 100644 index 00000000..6e6d8cea --- /dev/null +++ b/components/chart/README.md @@ -0,0 +1,69 @@ +--- +title: Chart 折线图表 +preview: https://didi.github.io/mand-mobile/examples/chart +--- + +SVG折线图表, 可绘制多条折线并配置不同的显示规则。 + +### 引入 + +```javascript +import { Chart } from 'mand-mobile' + +Vue.component(Chart.name, Chart) +``` + +### 代码演示 + + +### API + +#### Chart Props +| 属性 | 说明 |类型 | 默认值 | 必填 | +|----|-----|------|------|------| +| size | 图表绘制区域大小, 元素可为带单位字符串或者纯数字(默认为px) | Array | `[480, 320]` | 可选| +| max | 纵坐标最大值 | number | 若不填则会自动计算数据中最大值 | 可选| +| min | 纵坐标最表最小值, 建议设置为`0` | number | 若为空则会自动计算数据中最小值 | 可选| +| lines | 纵坐标最多画几条线 | number | `5` | 可选| +| step | 纵坐标递减的单位值 | number | 若为空则根据`lines`, `max`, `min`自动计算平均值 | 可选| +| format | 纵坐标标签格式化函数 | Function | `val => val` | 可选| +| labels | 横坐标的标签 | Array | - | 必填| +| datasets | 数据值, 格式参考下面的说明 | Array | - | 必填| +| shift | 纵坐标偏移量 | Number | 0.6 | 可选| + +#### `datasets` +其为对象数组,每个对象定义了一组折线相关属性, 如下说明 + +```javascript +{ + color: '#ff5858', // 颜色, 可选, 默认为橘色 + theme: 'heat', // 主题, 可选heat, region, 默认为空 + width: 1, // 宽度, 可选, 默认为1 + values: [15, 20] // 数据数组 +} +``` + +#### 覆盖样式 +默认图表样式如下 + +```stylus +.md-chart + line + stroke #ccc + stroke-width 0.5 + stroke-linecap square + path + stroke #fa8919 + stroke-width 1 + stroke-linecap butt + .md-chart-axis-y + text + fill #666 + font-size 0.2rem + text-anchor end + .md-chart-axis-x + text + fill #666 + font-size 0.22rem + text-anchor middle +``` diff --git a/components/chart/component.js b/components/chart/component.js new file mode 100644 index 00000000..3183c9af --- /dev/null +++ b/components/chart/component.js @@ -0,0 +1,7 @@ +export default { + 'name': 'chart', + 'text': '折线图表', + 'category': 'business', + 'description': '基于 SVG 的折线图表组件', + 'author': 'liuxinyumichael' +} diff --git a/components/chart/demo/cases/demo0.vue b/components/chart/demo/cases/demo0.vue new file mode 100644 index 00000000..7eb58c2f --- /dev/null +++ b/components/chart/demo/cases/demo0.vue @@ -0,0 +1,49 @@ + + + + + diff --git a/components/chart/demo/cases/demo1.vue b/components/chart/demo/cases/demo1.vue new file mode 100644 index 00000000..7312b4c7 --- /dev/null +++ b/components/chart/demo/cases/demo1.vue @@ -0,0 +1,46 @@ + + + + + diff --git a/components/chart/demo/cases/demo2.vue b/components/chart/demo/cases/demo2.vue new file mode 100644 index 00000000..17e3406c --- /dev/null +++ b/components/chart/demo/cases/demo2.vue @@ -0,0 +1,46 @@ + + + + + diff --git a/components/chart/demo/index.vue b/components/chart/demo/index.vue new file mode 100644 index 00000000..8d4cbda4 --- /dev/null +++ b/components/chart/demo/index.vue @@ -0,0 +1,24 @@ + + + + + diff --git a/components/chart/index.vue b/components/chart/index.vue new file mode 100644 index 00000000..ea364e74 --- /dev/null +++ b/components/chart/index.vue @@ -0,0 +1,297 @@ + + + + + diff --git a/components/chart/test/index.spec.js b/components/chart/test/index.spec.js new file mode 100644 index 00000000..aaac8cc1 --- /dev/null +++ b/components/chart/test/index.spec.js @@ -0,0 +1,92 @@ +import Chart from '../index' +import {mount} from 'avoriaz' + +describe('Chart', () => { + let wrapper + + afterEach(() => { + wrapper && wrapper.destroy() + }) + + it('create a simple chart', () => { + wrapper = mount(Chart, { + propsData: { + labels: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'], + datasets: [ + { + values: [12, 15, 20, 23, 20, 30, 32, 38, 36, 40, 50, 55, 52], + }, + ], + }, + }) + expect(wrapper.contains('.md-chart-graph')).to.be.true + }) + + it('create a chart with multiple datasets', () => { + wrapper = mount(Chart, { + propsData: { + size: ['7rem', '4rem'], + max: 60, + min: 0, + step: 10, + lines: 5, + labels: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'], + datasets: [ + { + color: '#5e64ff', + width: 1, + values: [8, 15, 20, 23, 20, 30, 32, 38, 36, 40, 50, 55, 52], + }, + { + width: 1, + values: [10, 20, 25, 30, 28, 35, 38, 42, 40, 40, 45, 42, 45], + }, + ], + }, + }) + expect(wrapper.vm.paths.length).to.equal(2) + }) + + it('create a heat chart', () => { + wrapper = mount(Chart, { + propsData: { + size: ['7rem', '4rem'], + max: 60, + min: 0, + step: 10, + lines: 5, + labels: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'], + datasets: [ + { + width: 1, + color: 'red', + theme: 'heat', + values: [8, 15, 20, 23, 20, 30, 32, 38, 36, 40, 50, 55, 52], + }, + ], + }, + }) + expect(wrapper.contains('#path-fill-gradient-red')).to.be.true + }) + + it('create a region chart', () => { + wrapper = mount(Chart, { + propsData: { + size: ['7rem', '4rem'], + max: 60, + min: 0, + step: 10, + lines: 5, + labels: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'], + datasets: [ + { + width: 1, + theme: 'region', + values: [8, 15, 20, 23, 20, 30, 32, 38, 36, 40, 50, 55, 52], + }, + ], + }, + }) + expect(wrapper.contains('.md-chart-path-area')).to.be.true + }) +}) diff --git a/components/codebox/README.md b/components/codebox/README.md new file mode 100644 index 00000000..f08d543d --- /dev/null +++ b/components/codebox/README.md @@ -0,0 +1,44 @@ +--- +title: CodeBox 验证码输入框 +preview: https://didi.github.io/mand-mobile/examples/codebox +--- + +验证码输入框 + +### 引入 + +```javascript +import { Codebox } from 'mand-mobile' + +Vue.component(Codebox.name, Codebox) +``` + +### 代码演示 + + +### API + +#### Codebox Props +|属性 | 说明 | 类型 | 默认值| +|----|-----|------|------| +|v-model|验证码字符串|String|-| +|maxlength|字符最大输入长度, 若为`-1`则不限制输入长度|Number|4| +|autofocus|是否直通聚焦拉起键盘, 对系统键盘不生效|Boolean|`false`| +|mask|是否掩码|Boolean|`false`| +|closable|点击输入框及键盘其他区域是否收起键盘|Boolean|`true`| +|ok-text|键盘确认键文案|String|`确认`| +|disorder|数字键盘是否乱序|Boolean|`false`| +|system|是否使用系统默认键盘|Boolean|`false`| + +#### Codebox Methods + +##### focus() +聚焦输入 + +##### blur() +失焦隐藏键盘 + +#### Codebox Events + +#### @submit(code) +用户提交输入内容事件 diff --git a/components/codebox/component.js b/components/codebox/component.js new file mode 100644 index 00000000..9b7f9b22 --- /dev/null +++ b/components/codebox/component.js @@ -0,0 +1,7 @@ +export default { + 'name': 'codebox', + 'text': '验证码输入框', + 'category': 'business', + 'description': '验证码/密码输入框组件', + 'author': 'liuxinyumichael' +} diff --git a/components/codebox/demo/cases/demo0.vue b/components/codebox/demo/cases/demo0.vue new file mode 100644 index 00000000..887bb8ad --- /dev/null +++ b/components/codebox/demo/cases/demo0.vue @@ -0,0 +1,25 @@ + + + diff --git a/components/codebox/demo/cases/demo1.vue b/components/codebox/demo/cases/demo1.vue new file mode 100644 index 00000000..eae437dc --- /dev/null +++ b/components/codebox/demo/cases/demo1.vue @@ -0,0 +1,25 @@ + + + diff --git a/components/codebox/demo/cases/demo2.vue b/components/codebox/demo/cases/demo2.vue new file mode 100644 index 00000000..8d00ca7d --- /dev/null +++ b/components/codebox/demo/cases/demo2.vue @@ -0,0 +1,24 @@ + + + diff --git a/components/codebox/demo/cases/demo3.vue b/components/codebox/demo/cases/demo3.vue new file mode 100644 index 00000000..b109e495 --- /dev/null +++ b/components/codebox/demo/cases/demo3.vue @@ -0,0 +1,25 @@ + + + diff --git a/components/codebox/demo/index.vue b/components/codebox/demo/index.vue new file mode 100644 index 00000000..7aa2aff9 --- /dev/null +++ b/components/codebox/demo/index.vue @@ -0,0 +1,23 @@ + + + + + diff --git a/components/codebox/index.vue b/components/codebox/index.vue new file mode 100644 index 00000000..bce3a751 --- /dev/null +++ b/components/codebox/index.vue @@ -0,0 +1,286 @@ + + + + + + diff --git a/components/codebox/test/index.spec.js b/components/codebox/test/index.spec.js new file mode 100644 index 00000000..65dd30bb --- /dev/null +++ b/components/codebox/test/index.spec.js @@ -0,0 +1,113 @@ +import Codebox from '../index' +import sinon from 'sinon' +import {mount} from 'avoriaz' + +describe('Codebox', () => { + let wrapper + afterEach(() => { + wrapper && wrapper.destroy() + }) + + it('create a simple codebox', () => { + wrapper = mount(Codebox) + + expect(wrapper.hasClass('md-codebox-wrapper')).to.be.true + }) + + it('create a codebox with input', () => { + wrapper = mount(Codebox, { + propsData: { + maxlength: -1, + }, + }) + expect(wrapper.contains('.md-codebox-holder')).to.be.true + }) + + it('create a codebox with custom keyboard', () => { + wrapper = mount(Codebox, { + propsData: { + system: false, + isView: true, + }, + }) + + expect(wrapper.contains('.md-number-keyboard')).to.be.true + }) + + it('emit input/submit events', done => { + wrapper = mount(Codebox, { + propsData: { + isView: true, + }, + }) + + const eventStub = sinon.stub(wrapper.vm, '$emit') + wrapper.setData({ + code: '123', + }) + wrapper.first('.keyboard-number-item').trigger('click') + setTimeout(() => { + expect(eventStub.calledWith('input')).to.be.true + expect(eventStub.calledWith('submit', '1231')).to.be.true + done() + }, 0) + }) + + it('click codebox to focus with custom keyboard', done => { + wrapper = mount(Codebox, { + propsData: { + maxlength: -1, + }, + }) + const eventStub = sinon.stub(wrapper.vm, '$emit') + wrapper.first('.md-codebox').trigger('click') + expect(wrapper.vm.focused).to.be.true + wrapper.first('.keyboard-number-item').trigger('click') + wrapper.first('.confirm').trigger('click') + setTimeout(() => { + expect(wrapper.vm.focused).to.be.false + expect(eventStub.calledWith('submit', '1')).to.be.true + done() + }, 0) + }) + + it('click codebox to focus with system keyboard', () => { + wrapper = mount(Codebox, { + propsData: { + system: true, + }, + }) + + wrapper.first('.md-codebox').trigger('click') + expect(wrapper.vm.focused).to.be.true + }) + + it('delete code char after clicking delete button', () => { + wrapper = mount(Codebox, { + data: { + code: '1234', + }, + }) + + wrapper.first('.delete').trigger('click') + expect(wrapper.vm.code).to.equal('123') + }) + + it('enter code char after clicking number button', done => { + wrapper = mount(Codebox, { + propsData: { + maxlength: 4, + isView: true, + }, + data: { + code: '12', + }, + }) + + wrapper.first('.keyboard-number-item').trigger('click') + setTimeout(() => { + expect(wrapper.vm.code).to.equal('121') + done() + }, 0) + }) +}) diff --git a/components/date-picker/README.md b/components/date-picker/README.md new file mode 100644 index 00000000..aed596a4 --- /dev/null +++ b/components/date-picker/README.md @@ -0,0 +1,173 @@ +--- +title: DatePicker 时间选择器 +preview: https://didi.github.io/mand-mobile/examples/date-picker +--- + +选择日期或者时间,支持年/月/日/时/分和按照范围选择 + +### 引入 + +```javascript +import { DatePicker } from 'mand-mobile' + +Vue.component(DatePicker.name, DatePicker) +``` + +### 代码演示 + + +### API + +#### DatePicker Props +|属性 | 说明 | 类型 | 默认值 | 备注 | +|----|-----|------|------|------| +|type|日期选择类型|String|`date`|`date`, `time`, `datetime`, `custom`| +|custom-types|自定义类型包含的`日期元素`, `[yyyy, MM, dd, hh, mm]`|Array|-|仅用于type为`custom`| +|minDate|最小可选日期|Date|-|-| +|maxDate|最大可选日期|Date|-|-| +|default-date|初始选中日期|Date|-|-| +|minute-step|分钟数递增步长|Number|`1`|-| +|unit-text|元素单位展示文案设置|Array|`['年', '月', '日', '时', '分']`|复杂逻辑使用`text-render`| +|text-render|自定义选项展示文案方法|Function(typeFormat, column0Value, column1Value, ...): String|-|如果使用`text-render`则`unit-text`无效, 示例见附录| +|today-text|今天展示文案设置|String|`今天`|使用`&`可占位日期数字,如`&(今天)`| +|half-day-text|上下午展示文案设置|Array|`['上午', '下午']`|-| +|is-twelve-hours|是否为12时制|Boolean|`false`|-| +|is-view|是否内嵌在页面内展示, 否则以弹层形式|Boolean|`false`|-| +|title|选择器标题|String|-|-| +|ok-text|选择器确认文案|String|`确认`|-| +|cancel-text|选择器取消文案|String|`取消`|-| + +#### DatePicker Methods + +##### getFormatDate(format): dateStr +获取特定格式的日期时间字符串(`format`中的`日期元素`需在列数据中存在),需在`initialed`事件触发之后或异步调用 + +|参数 | 说明 | 类型 | 默认 | +|----|-----|------|------| +|format|格式|String|`yyyy-MM-dd hh:mm`| + +返回 + +|属性 | 说明 | 类型 | +|----|-----|------| +|dateStr|日期时间字符串|String| + +> 列表项值属性介绍见附录 + +##### getColumnValue(index): activeItemValue +获取某列当前选中项的值,需在`initialed`事件触发之后或异步调用 + +|参数 | 说明 | 类型 | +|----|-----|------| +|index|列索引|Number| + +返回 + +|属性 | 说明 | 类型 | +|----|-----|------| +|activeItemValue|选中项的值|Object: {text, value, typeFormat}| + +##### getColumnValues(): columnsValue +获取所有列选中项的值,需在`initialed`事件触发之后或异步调用 + +返回 + +|属性 | 说明 | 类型 | +|----|-----|------| +|columnsValue|所有列选中项的值|Array<{text, value, typeFormat}>| + +##### getColumnIndex(index): activeItemIndex +获取某列当前选中项的索引值,需在`initialed`事件触发之后或异步调用 + +|参数 | 说明 | 类型 | +|----|-----|------| +|index|列索引|Number| + +返回 + +|属性 | 说明 | 类型 | +|----|-----|------| +|activeItemIndex|选中项的索引值|Number| + +##### getColumnIndexs(): columnsIndex +获取所有列选中项的索引值,需在`initialed`事件触发之后或异步调用 + +返回 + +|属性 | 说明 | 类型 | +|----|-----|------| +|columnsIndex|所有列选中项的索引值|Array| + +#### DatePicker Events + +##### @initialed() +选择器数据初始化完成事件 + +##### @change(columnIndex, itemIndex, value) +选择器选中项更改事件 + +|参数 | 说明 | 类型 | +|----|-----|------| +|columnIndex|更改列的索引值|Number| +|itemIndex|更改列选中项的索引值|Number| +|value|更改列选中项的的值|Object: {text, value, typeFormat}| + +##### @confirm(columnsValue) +选择器确认选择事件(仅`is-view`为`false`) + +|参数 | 说明 | 类型 | +|----|-----|------| +|columnsValue|所有列选中项的值|Array<{text, value, typeFormat}>| + +#### 附录 + +* columnData + +```javascript + +const columnData = [ + // 年 + [ + { + text: '2017年', // 日期元素展示文案 + value: 2017, // 日期元素数字 + typeFormat: 'yyyy' // 日期元素类型 yyyy, MM, dd, hh, mm, HalfDay + } + ], + // 月, 日,时, 分 + [ + //.., + ], + // 上午/下午 + [ + { + text: '上午', + value: 0, + typeFormat: 'HalfDay' + }, { + text: '下午', + value: 1, + typeFormat: 'HalfDay' + } + ] +] +``` + +* textRender + +```javascript + + export default { + // ... + methods: { + textRender() { + const args = Array.prototype.slice.call(arguments) + const typeFormat = args[0] // 类型 + const column0Value = args[1] // 第1列选中值 + const column1Value = args[2] // 第2列选中值 + const column2Value = args[3] // 第2列选中值 + }, + } + // ... + } +``` diff --git a/components/date-picker/component.js b/components/date-picker/component.js new file mode 100644 index 00000000..97b650cf --- /dev/null +++ b/components/date-picker/component.js @@ -0,0 +1,7 @@ +export default { + "name": "date-picker", + "text": "日期选择器", + "category": "feedback", + "description": "", + "author": "xuxiaoyan" +} diff --git a/components/date-picker/demo/cases/demo0.vue b/components/date-picker/demo/cases/demo0.vue new file mode 100644 index 00000000..59caeecd --- /dev/null +++ b/components/date-picker/demo/cases/demo0.vue @@ -0,0 +1,37 @@ + + + diff --git a/components/date-picker/demo/cases/demo1.vue b/components/date-picker/demo/cases/demo1.vue new file mode 100644 index 00000000..c960af10 --- /dev/null +++ b/components/date-picker/demo/cases/demo1.vue @@ -0,0 +1,23 @@ + + + \ No newline at end of file diff --git a/components/date-picker/demo/cases/demo2.vue b/components/date-picker/demo/cases/demo2.vue new file mode 100644 index 00000000..cd81e51e --- /dev/null +++ b/components/date-picker/demo/cases/demo2.vue @@ -0,0 +1,34 @@ + + + diff --git a/components/date-picker/demo/cases/demo3.vue b/components/date-picker/demo/cases/demo3.vue new file mode 100644 index 00000000..8c705fd4 --- /dev/null +++ b/components/date-picker/demo/cases/demo3.vue @@ -0,0 +1,76 @@ + + + \ No newline at end of file diff --git a/components/date-picker/demo/index.vue b/components/date-picker/demo/index.vue new file mode 100644 index 00000000..4f67141d --- /dev/null +++ b/components/date-picker/demo/index.vue @@ -0,0 +1,20 @@ + + + \ No newline at end of file diff --git a/components/date-picker/index.vue b/components/date-picker/index.vue new file mode 100644 index 00000000..ba40e2bb --- /dev/null +++ b/components/date-picker/index.vue @@ -0,0 +1,531 @@ + + + + + diff --git a/components/date-picker/test/index.spec.js b/components/date-picker/test/index.spec.js new file mode 100644 index 00000000..d8c7ff54 --- /dev/null +++ b/components/date-picker/test/index.spec.js @@ -0,0 +1,93 @@ +import DatePicker from '../index' +import {mount} from 'avoriaz' + +describe('DatePicker', () => { + let wrapper + + afterEach(() => { + wrapper && wrapper.destroy() + }) + + it('create a date picker', done => { + const date = new Date() + wrapper = mount(DatePicker, { + propsData: { + isView: true, + minDate: new Date('2013/9/9'), + maxDate: new Date('2020/9/9'), + defaultDate: date, + }, + }) + const eventStub = sinon.stub(wrapper.vm, '$emit') + + wrapper.vm.$nextTick(() => { + expect(wrapper.find('.md-picker-column-item').length).to.equal(3) + setTimeout(() => { + expect(eventStub.calledWith('initialed')).to.be.true + expect(wrapper.instance().getFormatDate('yyyy-MM-dd')).to.equal( + `${date.getFullYear()}-${date.getMonth() + 1 < 10 + ? '0' + (date.getMonth() + 1) + : date.getMonth() + 1}-${date.getDate() < 10 ? '0' + date.getDate() : date.getDate()}`, + ) + done() + }, 50) + }) + }) + + it('create a time picker', done => { + const date = new Date() + wrapper = mount(DatePicker, { + propsData: { + type: 'time', + unitText: ['', '', '', 'h', 'm'], + halfDayText: ['AM', 'PM'], + isView: true, + isTwelveHours: true, + defaultDate: date, + }, + }) + + wrapper.vm.$nextTick(() => { + expect(wrapper.find('.md-picker-column-item').length).to.equal(3) + // setTimeout(() => { + done() + // }, 0) + }) + }) + + it('create a datetime picker', done => { + const date = new Date() + wrapper = mount(DatePicker, { + propsData: { + type: 'datetime', + isView: true, + defaultDate: date, + }, + }) + + wrapper.vm.$nextTick(() => { + expect(wrapper.find('.md-picker-column-item').length).to.equal(5) + // setTimeout(() => { + done() + // }, 0) + }) + }) + + it('create a custom picker', done => { + const date = new Date() + wrapper = mount(DatePicker, { + propsData: { + type: 'custom', + value: true, + customTypes: ['dd', 'hh', 'mm'], + isTwelveHours: true, + }, + }) + + wrapper.vm.$nextTick(() => { + expect(wrapper.find('.md-picker-column-item').length).to.equal(3) + wrapper.vm.isPickerShow = false + done() + }) + }) +}) diff --git a/components/dialog/README.md b/components/dialog/README.md new file mode 100644 index 00000000..30d1132e --- /dev/null +++ b/components/dialog/README.md @@ -0,0 +1,93 @@ +--- +title: Dialog 模态窗 +preview: https://didi.github.io/mand-mobile/examples/dialog +--- + +交互式模态窗口 + +### 引入 + +```javascript +import { Dialog } from 'mand-mobile' + +Vue.component(Dialog.name, Dialog) +``` + +### 代码演示 + + +### API + +#### Dialog Props +|属性 | 说明 | 类型 | 默认值|备注| +|----|-----|------|------|------| +| v-model | 双向绑定是否显示窗口 | Boolean | `false`|-| +| title | 窗口标题 | String | -|-| +| icon | Icon组件图标名称 | String | -|如需自定义图标, 请查看`Icon`组件| +| closable | 是否显示关闭按钮 | Boolean | `true`|-| +| btns | 底部操作按钮组 | Array | `[]`|-| +| append-to | 组件的挂载节点 | HTMLElement | `document.body`|-| +| has-mask | 是否有蒙层 | Boolean | `true`|-| +| mask-closable | 点击蒙层是否可关闭弹出层 | Boolean | `false`|-| +| position | 弹出层位置, `center/top/bottom/left/right` | String | `center`|-| +| transition | 弹出层过度动画, `fade, slide-up/down/left/right` | String | `fade`|-| +| prevent-scroll | 是否禁止滚动穿透 | Boolean | false |-| +| prevent-scroll-exclude | 禁止滚动排除元素 | String | -|-| + +### Dialog Slots +组件子元素会被当做默认插槽内容使用,适合于不需要标题的自定义窗口内容的场景。 + +#### Dialog.close() +隐藏弹窗 + +#### Dialog.confirm(props) +静态方法创建确认模态窗口, 返回Dialog实例 + +|属性 | 说明 | 类型 | 默认值| +|----|-----|------|------| +| icon | 图标 | String | -| +| title | 窗口标题 | String | -| +| content | 正文内容 | String | -| +| cancelText | 底部取消按钮文字 | String | `取消`| +| confirmText | 底部确认按钮文字 | String | `确认`| +| onConfirm | 点击确认按钮回调函数 | Function | -| + +#### Dialog.alert(props) +静态方法创建警告模态窗口, 返回Dialog实例 + +|属性 | 说明 | 类型 | 默认值| +|----|-----|------|------| +| icon | 图标 | String | -| +| title | 窗口标题 | String | -| +| content | 正文内容 | String | -| +| confirmText | 底部确认按钮文字 | String | `确认`| +| onConfirm | 点击确认按钮回调函数 | Function | -| + +#### Dialog.succeed(props) +静态方法创建成功确认模态窗口, 返回Dialog实例 + +|属性 | 说明 | 类型 | 默认值| +|----|-----|------|------| +| title | 窗口标题 | String | -| +| content | 正文内容 | String | -| +| confirmText | 底部确认按钮文字 | String | `确认`| +| onConfirm | 点击确认按钮回调函数 | Function | -| + +#### Dialog.failed(props) +静态方法创建失败确认模态窗口, 返回Dialog实例 + +|属性 | 说明 | 类型 | 默认值| +|----|-----|------|------| +| title | 窗口标题 | String | -| +| content | 正文内容 | String | -| +| confirmText | 底部确认按钮文字 | String | `确认`| +| onConfirm | 点击确认按钮回调函数 | Function | -| + + +#### Dialog Events + +##### @show() +模态窗口显示后触发的事件 + +##### @hide() +模态窗口隐藏后触发的事件 diff --git a/components/dialog/component.js b/components/dialog/component.js new file mode 100644 index 00000000..1c1cfb3d --- /dev/null +++ b/components/dialog/component.js @@ -0,0 +1,7 @@ +export default { + 'name': 'dialog', + 'text': '模态窗口', + 'category': 'feedback', + 'description': '弹出式交互窗口', + 'author': 'liuxinyumichael' +} diff --git a/components/dialog/demo/cases/demo0.vue b/components/dialog/demo/cases/demo0.vue new file mode 100644 index 00000000..0d3fa33b --- /dev/null +++ b/components/dialog/demo/cases/demo0.vue @@ -0,0 +1,108 @@ + + + diff --git a/components/dialog/demo/cases/demo1.vue b/components/dialog/demo/cases/demo1.vue new file mode 100644 index 00000000..e71e2510 --- /dev/null +++ b/components/dialog/demo/cases/demo1.vue @@ -0,0 +1,55 @@ + + + diff --git a/components/dialog/demo/index.vue b/components/dialog/demo/index.vue new file mode 100644 index 00000000..81aecc38 --- /dev/null +++ b/components/dialog/demo/index.vue @@ -0,0 +1,23 @@ + + + + + diff --git a/components/dialog/dialog.vue b/components/dialog/dialog.vue new file mode 100644 index 00000000..adb8733c --- /dev/null +++ b/components/dialog/dialog.vue @@ -0,0 +1,189 @@ + + + + + diff --git a/components/dialog/index.js b/components/dialog/index.js new file mode 100644 index 00000000..affcaa39 --- /dev/null +++ b/components/dialog/index.js @@ -0,0 +1,86 @@ +import Vue from 'vue' +import Dialog from './dialog' +const DialogConstructor = Vue.extend(Dialog) + +const noop = function() {} + +const generate = function({title = '', icon = '', content = '', closable = false, btns = []}) { + const vm = new DialogConstructor({ + propsData: { + value: true, + title, + icon, + content, + closable, + btns, + }, + }).$mount() + + vm.$on('input', val => { + if (!val) { + vm.value = false + } + }) + vm.$on('hide', () => { + vm.$destroy() + document.body.removeChild(vm.$el) + }) + + return vm +} + +Dialog.confirm = ({title = '', icon = '', content = '', cancelText = '取消', confirmText = '确定', onConfirm = noop}) => { + const vm = generate({ + title, + icon, + content, + btns: [ + { + text: cancelText, + handler: () => vm.close(), + }, + { + text: confirmText, + handler: () => { + if (onConfirm() !== false) { + vm.close() + } + }, + }, + ], + }) + + return vm +} + +Dialog.alert = ({title = '', icon = '', content = '', confirmText = '确定', onConfirm = noop}) => { + const vm = generate({ + title, + icon, + content, + btns: [ + { + text: confirmText, + handler: () => { + if (onConfirm() !== false) { + vm.close() + } + }, + }, + ], + }) + + return vm +} + +Dialog.succeed = props => { + props.icon = 'circle-right' + return Dialog.confirm(props) +} + +Dialog.failed = props => { + props.icon = 'circle-cross' + return Dialog.confirm(props) +} + +export default Dialog diff --git a/components/dialog/test/index.spec.js b/components/dialog/test/index.spec.js new file mode 100644 index 00000000..d328f006 --- /dev/null +++ b/components/dialog/test/index.spec.js @@ -0,0 +1,103 @@ +import Dialog from '../dialog.vue' +import sinon from 'sinon' +import {mount} from 'avoriaz' +// import { setTimeout } from 'timers'; + +describe('Dialog', () => { + let wrapper + + afterEach(() => { + wrapper && wrapper.destroy() + }) + + it('create a simple dialog', () => { + wrapper = mount(Dialog, { + propsData: { + appendTo: false, + }, + }) + + expect(wrapper.hasClass('md-dialog')).to.equal(true) + }) + + it('create a simple dialog and append to body', function() { + wrapper = mount(Dialog, { + propsData: { + appendTo: document.body, + }, + }) + + expect(wrapper.vm.$el.parentNode).to.equal(document.body) + }) + + it('has custom title', () => { + wrapper = mount(Dialog, { + propsData: { + title: 'Dialog Title', + }, + }) + + expect(wrapper.first('.md-dialog-title').text()).to.equal('Dialog Title') + }) + + it('has custom content', () => { + wrapper = mount(Dialog, { + propsData: { + content: 'Lorem ipsum dolor sit amet.', + }, + }) + + expect(wrapper.first('.md-dialog-body div').text()).to.equal('Lorem ipsum dolor sit amet.') + }) + + it('has custom icon', () => { + wrapper = mount(Dialog, { + propsData: { + icon: 'circle-right', + }, + }) + + expect(wrapper.contains('.md-icon-circle-right')).to.equal(true) + }) + + it('handle button action click event', () => { + const clickHandler = sinon.stub() + wrapper = mount(Dialog, { + propsData: { + btns: [ + { + text: 'Cancel', + handler: null, + }, + { + text: 'Confirm', + handler: clickHandler, + }, + ], + }, + }) + + wrapper.find('.md-dialog-actions a')[0].trigger('click') + wrapper.find('.md-dialog-actions a')[1].trigger('click') + expect(clickHandler.called).to.equal(true) + }) + + // it('emit input/hide/show event', (done) => { + // wrapper = mount(Dialog, { + // propsData: { + // value: true + // } + // }) + // const eventStub = sinon.stub(wrapper.vm, '$emit') + // setTimeout(() => { + // expect(eventStub.calledWith('show')).to.be.true + // wrapper.first('.md-dialog-close').trigger('click') + // wrapper.setProps({ value: false }) + // setTimeout(() => { + // expect(eventStub.calledWith('input')).to.be.true + // expect(eventStub.calledWith('hide')).to.be.true + // done() + // }, 500) + // }, 500) + // }) +}) diff --git a/components/drop-menu/README.md b/components/drop-menu/README.md new file mode 100644 index 00000000..915ad486 --- /dev/null +++ b/components/drop-menu/README.md @@ -0,0 +1,66 @@ +--- +title: DropMenu 下拉菜单 +preview: https://didi.github.io/mand-mobile/examples/drop-menu +--- + +下拉菜单可用于列表筛选 + +### 引入 + +```javascript +import { DropMenu } from 'mand-mobile' + +Vue.component(DropMenu.name, DropMenu) +``` + +### 代码演示 + + +### API + +#### DropMenu Props +|属性 | 说明 | 类型 | 默认值 | 备注| +|----|-----|------|------|------| +|data|数据源|Array<{text, disabled, options, ...}>|`[]`|`disabled`为是否禁用,`options`类型为`Array<{text, disabled, ...}>`| +|defaultValue|初始值|Array|`[]`|-| +|option-render|返回各选项渲染内容|Function({text, disabled, ...}):String|-|`vue 2.1.0+`可使用`slot-scope`,参考`Radio`| + +#### DropMenu Methods + +##### getSelectedValue(index): listItem +获取某菜单项选中值 + +|参数 | 说明 | 类型 | 默认值| +|----|-----|------|------| +|index|菜单项索引值|Number|-| + +返回 + +|属性 | 说明 | 类型| +|----|-----|------| +|listItem|选项数据|Object: {text, disabled, options, ...}| + +##### getSelectedValues(): listItems +获取所有菜单项选中值 + +返回 + +|属性 | 说明 | 类型| +|----|-----|------| +|listItems|选项数据|Array<{text, disabled, options, ...}>| + +#### DropMenu Events + +##### @change(barItem, listItem) +选中某项事件 + +|属性 | 说明 | 类型| +|----|-----|------| +|barItem|菜单项数据|Object: {text, disabled, options, ...}| +|listItem|选项数据|Object: {text, disabled, ...}| + +##### @show() +下拉菜单展示事件 + +##### @hide() +下拉菜单隐藏事件 diff --git a/components/drop-menu/component.js b/components/drop-menu/component.js new file mode 100644 index 00000000..878511da --- /dev/null +++ b/components/drop-menu/component.js @@ -0,0 +1,7 @@ +export default { + 'name': 'drop-menu', + 'text': '下拉菜单', + 'category': 'basic', + 'description': '', + 'author': 'xuxiaoyan' +} diff --git a/components/drop-menu/demo/cases/demo0.vue b/components/drop-menu/demo/cases/demo0.vue new file mode 100644 index 00000000..a5811898 --- /dev/null +++ b/components/drop-menu/demo/cases/demo0.vue @@ -0,0 +1,52 @@ + + + \ No newline at end of file diff --git a/components/drop-menu/demo/cases/demo1.vue b/components/drop-menu/demo/cases/demo1.vue new file mode 100644 index 00000000..f4559e97 --- /dev/null +++ b/components/drop-menu/demo/cases/demo1.vue @@ -0,0 +1,66 @@ + + + \ No newline at end of file diff --git a/components/drop-menu/demo/cases/demo2.vue b/components/drop-menu/demo/cases/demo2.vue new file mode 100644 index 00000000..cdf4a263 --- /dev/null +++ b/components/drop-menu/demo/cases/demo2.vue @@ -0,0 +1,57 @@ + + + \ No newline at end of file diff --git a/components/drop-menu/demo/cases/demo3.vue b/components/drop-menu/demo/cases/demo3.vue new file mode 100644 index 00000000..47485528 --- /dev/null +++ b/components/drop-menu/demo/cases/demo3.vue @@ -0,0 +1,61 @@ + + + + + \ No newline at end of file diff --git a/components/drop-menu/demo/index.vue b/components/drop-menu/demo/index.vue new file mode 100644 index 00000000..a6ee35cf --- /dev/null +++ b/components/drop-menu/demo/index.vue @@ -0,0 +1,43 @@ + + + + + \ No newline at end of file diff --git a/components/drop-menu/index.vue b/components/drop-menu/index.vue new file mode 100644 index 00000000..db91744e --- /dev/null +++ b/components/drop-menu/index.vue @@ -0,0 +1,236 @@ + + + + + diff --git a/components/drop-menu/test/index.spec.js b/components/drop-menu/test/index.spec.js new file mode 100644 index 00000000..8709f141 --- /dev/null +++ b/components/drop-menu/test/index.spec.js @@ -0,0 +1,193 @@ +import DropMenu from '../index' +import {mount} from 'avoriaz' + +describe('DropMenu', () => { + let wrapper + + afterEach(() => { + wrapper && wrapper.destroy() + }) + + it('create a simple drop-menu', done => { + wrapper = mount(DropMenu) + expect(wrapper.hasClass('md-drop-menu')).to.be.true + expect(wrapper.vm.data.length).to.equal(0) + + wrapper.vm.data = [ + { + text: 'hello', + options: [ + { + text: 'world', + }, + ], + }, + ] + + wrapper.vm.$nextTick(() => { + const barItem = wrapper.find('.bar-item') + expect(barItem.length).to.equal(1) + expect(barItem[0].text()).to.equal('hello') + done() + }) + }) + + it('drop-menu bar item click', done => { + wrapper = mount(DropMenu, { + propsData: { + data: [ + { + text: 'hello', + options: [ + { + text: 'world', + }, + ], + }, + ], + }, + }) + const barItem = wrapper.find('.bar-item') + const mockData = [{text: 'world'}] + + barItem[0].trigger('click') + expect(barItem[0].hasClass('active')).to.true + expect(wrapper.instance().isPopupShow).to.true + expect(JSON.stringify(wrapper.instance().activeMenuListData)).to.equal(JSON.stringify(mockData)) + + wrapper.vm.$nextTick(() => { + const listItem = wrapper.find('.md-radio-item') + expect(listItem.length).to.equal(1) + barItem[0].trigger('click') + done() + }) + }) + + it('drop-menu list item click', done => { + wrapper = mount(DropMenu, { + propsData: { + data: [ + { + text: 'hello', + options: [ + { + text: 'world', + }, + ], + }, + ], + }, + }) + const barItem = wrapper.find('.bar-item') + const mockData = [{text: 'world'}] + + barItem[0].trigger('click') + wrapper.vm.$nextTick(() => { + const listItem = wrapper.find('.md-radio-item') + listItem[0].trigger('click') + + expect(JSON.stringify(wrapper.instance().selectedMenuListItem)).to.equal(JSON.stringify(mockData)) + expect(barItem[0].text()).to.equal('world') + expect(wrapper.instance().getSelectedValue(0).text).to.equal('world') + expect(wrapper.instance().getSelectedValues()[0].text).to.equal('world') + done() + }) + }) + + it('drop-menu events', done => { + wrapper = mount(DropMenu, { + propsData: { + data: [ + { + text: 'hello', + options: [ + { + text: 'world', + }, + ], + }, + ], + }, + }) + + const eventStub = sinon.stub(wrapper.vm, '$emit') + const barItem = wrapper.find('.bar-item')[0] + + barItem.trigger('click') + expect(wrapper.vm.isPopupShow).to.equal(true) + expect(wrapper.vm.activeMenuBarIndex).to.equal(0) + setTimeout(() => { + const listItem = wrapper.find('.md-radio-item')[0] + listItem.trigger('click') + expect(eventStub.calledWith('change')).to.be.true + setTimeout(() => { + done() + }, 500) + }, 500) + }) + + it('create a disabled drop-menu', done => { + wrapper = mount(DropMenu, { + propsData: { + data: [ + { + text: 'hello', + disabled: true, + }, + { + text: 'hello', + options: [ + { + text: 'hello', + disabled: true, + }, + ], + }, + ], + }, + }) + const barItem = wrapper.find('.bar-item') + expect(barItem[0].hasClass('disabled')).to.true + + barItem[1].trigger('click') + setTimeout(() => { + const listItem = wrapper.find('.md-radio-item') + expect(listItem[0].hasClass('disabled')).to.true + done() + }, 500) + }) + + it('create a drop-menu with defult value', done => { + wrapper = mount(DropMenu, { + propsData: { + data: [ + { + text: 'hello', + options: [ + { + text: 'world', + }, + { + text: 'space', + }, + ], + }, + ], + defaultValue: ['space'], + }, + }) + + wrapper.vm.$nextTick(() => { + const barItem = wrapper.find('.bar-item') + expect(barItem[0].hasClass('selected')).to.true + expect(barItem[0].text()).to.equal('space') + + barItem[0].trigger('click') + setTimeout(() => { + const listItem = wrapper.find('.md-radio-item') + expect(listItem[1].hasClass('selected')).to.true + done() + }, 300) + }) + }) +}) diff --git a/components/field-item/index.vue b/components/field-item/index.vue new file mode 100644 index 00000000..8d2e645c --- /dev/null +++ b/components/field-item/index.vue @@ -0,0 +1,170 @@ + + + + + diff --git a/components/field-item/test/index.spec.js b/components/field-item/test/index.spec.js new file mode 100644 index 00000000..5582e98c --- /dev/null +++ b/components/field-item/test/index.spec.js @@ -0,0 +1,87 @@ +import FieldItem from '../index' +import {mount} from 'avoriaz' + +describe('FieldItem', () => { + let wrapper + + afterEach(() => { + wrapper && wrapper.destroy() + }) + + it('create a simple field-item', () => { + wrapper = mount(FieldItem, { + propsData: { + title: 'field item title', + }, + }) + + expect(wrapper.hasClass('md-field-item')).to.be.true + expect(wrapper.vm.title).to.equal('field item title') + }) + + it('create a simple field-item with brief', () => { + wrapper = mount(FieldItem, { + propsData: { + title: 'field item title', + brief: 'field item brief', + }, + }) + + expect(wrapper.find('.md-field-item-brief').length).to.equal(1) + }) + + it('create a simple field-item with arrow', () => { + wrapper = mount(FieldItem, { + propsData: { + title: 'field item title', + brief: 'field item brief', + arrow: 'arrow-right', + }, + }) + + expect(wrapper.hasClass('has-arrow')).to.be.true + + const eventStub = sinon.stub(wrapper.vm, '$emit') + wrapper.trigger('click') + expect(eventStub.calledWith('click')).to.be.true + }) + + it('create a field-item with solid title', () => { + wrapper = mount(FieldItem, { + propsData: { + title: 'field item title', + brief: 'field item brief', + arrow: 'arrow-right', + solid: true, + }, + }) + + expect(wrapper.find('.solid').length).to.equal(1) + }) + + it('create a field-item with customized value align right', () => { + wrapper = mount(FieldItem, { + propsData: { + title: 'field item title', + brief: 'field item brief', + customized: true, + align: 'right', + }, + }) + + expect(wrapper.vm.customized).to.be.true + }) + + it('create a disabled field-item', () => { + wrapper = mount(FieldItem, { + propsData: { + title: 'field item title', + brief: 'field item brief', + arrow: 'arrow-right', + disabled: true, + }, + }) + + expect(wrapper.hasClass('disabled')).to.be.true + }) +}) diff --git a/components/field/README.md b/components/field/README.md new file mode 100644 index 00000000..dcae32d3 --- /dev/null +++ b/components/field/README.md @@ -0,0 +1,47 @@ +--- +title: Field 区域列表组合 +preview: https://didi.github.io/mand-mobile/examples/field +--- + +区域列表垂直排列,显示当前的内容、状态和可进行的操作 + +### 引入 + +```javascript +import { Field, FieldItem } from 'mand-mobile' + +Vue.component(Field.name, Field) +Vue.component(FieldItem.name, FieldItem) +``` + +### 代码演示 + + +### API + +#### Field Props +|属性 | 说明 | 类型 | 默认值|备注| +|----|-----|------|------|------| +|title|标题|String|-|-| + +#### FieldItem Props +|属性 | 说明 | 类型 | 默认值 |备注| +|----|-----|------|------|------| +|name|标识|Number/String| `-1`|-| +|title|标题|String|-|-| +|brief|子标题|String|-|-| +|disabled|是否禁用|Boolean|`true`|-| +|arrow|箭头名称|String|-|`arrow-up`, `arrow-right`, `arrow-down`, `arrow-left`| +|customized|内容是否自定义|Boolean|是否有`slot`|-| +|align|自定义内容时,内容位置|String|`left`|`left`, `right`, `center`| +|value|内容|String|-|-| +|solid|是否固定标题宽度,超出会自动换行|Boolean|`false`|-| + +#### FieldItem Events + +##### @click(name) +点击事件 + +|属性 | 说明 | 类型| +|----|-----|------| +|name|fieldItem标识|Number/String| diff --git a/components/field/component.js b/components/field/component.js new file mode 100644 index 00000000..f6f65d3a --- /dev/null +++ b/components/field/component.js @@ -0,0 +1,7 @@ +export default { + 'name': 'field', + 'text': '组合列表', + 'category': 'form', + 'description': '单个连续模块垂直排列,显示当前的内容、状态和可进行的操作', + 'author': 'chengyanjing' +} diff --git a/components/field/demo/cases/demo0.vue b/components/field/demo/cases/demo0.vue new file mode 100644 index 00000000..646595aa --- /dev/null +++ b/components/field/demo/cases/demo0.vue @@ -0,0 +1,62 @@ + + + + + \ No newline at end of file diff --git a/components/field/demo/cases/demo1.vue b/components/field/demo/cases/demo1.vue new file mode 100644 index 00000000..923ec975 --- /dev/null +++ b/components/field/demo/cases/demo1.vue @@ -0,0 +1,72 @@ + + + + + \ No newline at end of file diff --git a/components/field/demo/cases/demo2.vue b/components/field/demo/cases/demo2.vue new file mode 100644 index 00000000..dd0428ab --- /dev/null +++ b/components/field/demo/cases/demo2.vue @@ -0,0 +1,61 @@ + + + + + \ No newline at end of file diff --git a/components/field/demo/index.vue b/components/field/demo/index.vue new file mode 100644 index 00000000..e61028da --- /dev/null +++ b/components/field/demo/index.vue @@ -0,0 +1,18 @@ + + + \ No newline at end of file diff --git a/components/field/index.vue b/components/field/index.vue new file mode 100644 index 00000000..0ec86e91 --- /dev/null +++ b/components/field/index.vue @@ -0,0 +1,50 @@ + + + + + diff --git a/components/field/test/index.spec.js b/components/field/test/index.spec.js new file mode 100644 index 00000000..815099a1 --- /dev/null +++ b/components/field/test/index.spec.js @@ -0,0 +1,29 @@ +import Field from '../index' +import {mount} from 'avoriaz' + +describe('Field', () => { + let wrapper + + afterEach(() => { + wrapper && wrapper.destroy() + }) + + it('create a simple field with title', () => { + wrapper = mount(Field, { + propsData: { + title: 'field title', + }, + }) + expect(wrapper.hasClass('md-field')).to.be.true + expect(wrapper.vm.title).to.equal('field title') + }) + + it('create a simple field without title', () => { + wrapper = mount(Field, { + propsData: { + title: '', + }, + }) + expect(wrapper.find('.md-field-title').length).to.equal(0) + }) +}) diff --git a/components/icon/README.md b/components/icon/README.md new file mode 100644 index 00000000..8538d1ce --- /dev/null +++ b/components/icon/README.md @@ -0,0 +1,82 @@ +--- +title: Icon 图标 +preview: https://didi.github.io/mand-mobile/examples/icon +--- + +SVG 图标 + +### 引入 + +```javascript +import { Icon } from 'mand-mobile' + +Vue.component(Icon.name, Icon) +``` + +### 使用指南 + +组件库内置图标可直接使用,`arrow-up/down/left/right`, `circle-alert/cross/right`,`hollo-plus`,`cross`,`spinner` + +其他自定义图标需使用svg-sprite-loader,svg文件名即图标名称 + +1. 安装依赖 + +```shell +npm install svg-sprite-loader --save-dev +``` + +2. webpack配置 + +```javascript +const path = require('path') + +module.exports = { + module: { + loaders: [ + { + test: /\.svg$/i, + loader: 'svg-sprite-loader', + include: [ + // 将某个路径下所有svg交给 svg-sprite-loader 插件处理 + path.resovle(__dirname, 'src/my-project-svg-folder') + ], + } + ] + } +} +``` +3. 引入图标 + +```vue + + + +``` + +### 代码演示 + + +### API + +#### Icon Props +|属性 | 说明 | 类型 | 默认值| 备注| +|----|-----|------|------|------| +|name|图标名称|String|-|-| +|size|图标大小|String|`md`|`xs`, `sm`, `md`, `lg`| +|color|图标颜色|String|`currentColor`|该颜色值会作为`fill`的值被设置在`svg`图标上| diff --git a/components/icon/default-svg-list.js b/components/icon/default-svg-list.js new file mode 100644 index 00000000..70d3c466 --- /dev/null +++ b/components/icon/default-svg-list.js @@ -0,0 +1,26 @@ +// import '../_style/images/hollow-plus.svg' +// import '../_style/images/arrow-up.svg' +export default { + 'hollow-plus': '', + + 'arrow-up': '', + + 'arrow-down': '', + + 'arrow-left': '', + + 'arrow-right': '', + + 'cross': '', + 'circle-alert': '', + + 'circle-cross': '', + + 'circle-right': '', + + 'spinner': '', + + 'right': '', + + 'circle': '' +} diff --git a/components/icon/demo/cases/demo0.vue b/components/icon/demo/cases/demo0.vue new file mode 100644 index 00000000..ca1a8563 --- /dev/null +++ b/components/icon/demo/cases/demo0.vue @@ -0,0 +1,59 @@ + + + diff --git a/components/icon/demo/cases/demo1.vue b/components/icon/demo/cases/demo1.vue new file mode 100644 index 00000000..43ba53d8 --- /dev/null +++ b/components/icon/demo/cases/demo1.vue @@ -0,0 +1,31 @@ + + + diff --git a/components/icon/demo/cases/demo2.vue b/components/icon/demo/cases/demo2.vue new file mode 100644 index 00000000..4f4f3acf --- /dev/null +++ b/components/icon/demo/cases/demo2.vue @@ -0,0 +1,31 @@ + + + diff --git a/components/icon/demo/index.vue b/components/icon/demo/index.vue new file mode 100644 index 00000000..6ad31ba5 --- /dev/null +++ b/components/icon/demo/index.vue @@ -0,0 +1,35 @@ + + + + + diff --git a/components/icon/index.vue b/components/icon/index.vue new file mode 100644 index 00000000..67bccc9e --- /dev/null +++ b/components/icon/index.vue @@ -0,0 +1,57 @@ + + + + + \ No newline at end of file diff --git a/components/icon/load-spirte.js b/components/icon/load-spirte.js new file mode 100644 index 00000000..289b6c21 --- /dev/null +++ b/components/icon/load-spirte.js @@ -0,0 +1,40 @@ +// inspried by https://github.com/kisenka/svg-sprite-loader/blob/master/runtime/browser-sprite.js +// Much simplified, do make sure run this after document ready +import icons from './default-svg-list' + +const svgSprite = contents => ` + + + ${contents} + + +` + +const renderSvgSprite = () => { + const symbols = Object.keys(icons) + .map(iconName => { + const svgContent = icons[iconName].split('svg')[1] + return `` + }) + .join('') + return svgSprite(symbols) +} + +const loadSprite = () => { + if (!document) { + return + } + const existing = document.getElementById('__MAND_MOBILE_SVG_SPRITE_NODE__') + const mountNode = document.body + + if (!existing) { + mountNode.insertAdjacentHTML('afterbegin', renderSvgSprite()) + } +} + +export default loadSprite diff --git a/components/icon/test/index.spec.js b/components/icon/test/index.spec.js new file mode 100644 index 00000000..dca2f9f0 --- /dev/null +++ b/components/icon/test/index.spec.js @@ -0,0 +1,17 @@ +import Icon from '../index' +import {mount} from 'avoriaz' + +describe('Icon', () => { + let wrapper + + it('create a red icon', () => { + wrapper = mount(Icon, { + propsData: { + name: 'hollow-plus', + color: 'red', + }, + attachToDocument: true, + }) + expect(wrapper.hasStyle('fill', 'red')).to.be.true + }) +}) diff --git a/components/image-reader/README.md b/components/image-reader/README.md new file mode 100644 index 00000000..ef556a26 --- /dev/null +++ b/components/image-reader/README.md @@ -0,0 +1,71 @@ +--- +title: ImageReader 图片选择器 +preview: https://didi.github.io/mand-mobile/examples/image-reader +--- + +用于相册照片读取或拉起拍照 + +### 引入 + +```javascript +import { ImageReader } from 'mand-mobile' +import imageProcessor from 'mand-mobile/lib/image-reader/image-processor' // 图片处理插件,用法参考#imageProcessor + +Vue.component(ImageReader.name, ImageReader) +``` + +### 代码演示 + + +### API + +#### ImageReader Props +|属性 | 说明 | 类型 | 默认值 | 备注| +|----|-----|------|------|------| +|name|标识|String|-|可用于区分多个选择器| +|size|图片尺寸限制|String/Number|-|单位`kb`| +|mime|支持图片类型|Array|`*`|如`['jpeg','png']`| +|is-camera-only|是否只支持拍照|Boolean|`false`|-| +|is-multiple|是否支持选择多张|Boolean|`false`|-| +|amount|选择多张|Number|-|只在`is-multiple`为`true`时有效| + +#### ImageReader Events + +##### @select +图片选择完成事件,还未开始读取 + +##### @complete(name, { dataUrl, blob }) +图片选择读取完成事件 + +|属性 | 说明 | 类型| +|----|-----|------| +|name|选择器标识|String| +|dataUrl|图片Base64|String| +|blob|图片Blob对象,可用于`formData`|Blob| + +### imageProcessor + +用于图片轴向修正,图片质量压缩,宽高控制 + +#### 引入 + +```javascript +import imageProcessor from 'mand-mobile/lib/image-reader/image-processor' + +/** + * options 图片处理配置 + * fn(dataUrl, blob) 处理完成回调 + * @return Promise({dataUrl, blob}) + */ +imageProcessor(options[, fn]) + +``` + +#### options + +|属性 | 说明 | 类型| 备注| +|----|-----|------|------| +|dataUrl|图片Base64|String|-| +|width|图片宽度|Number|单位`px`, 宽度超出时等比缩放| +|height|图片高度|Number|单位`px`, 高度超出时等比缩放| +|quality|图片质量|Number|取值范围`0-1`| diff --git a/components/image-reader/component.js b/components/image-reader/component.js new file mode 100644 index 00000000..12fddeed --- /dev/null +++ b/components/image-reader/component.js @@ -0,0 +1,7 @@ +export default { + 'name': 'image-reader', + 'text': '图片选择器', + 'category': 'basic', + 'description': '', + 'author': 'xuxiaoyan' +} diff --git a/components/image-reader/demo/cases/demo0.vue b/components/image-reader/demo/cases/demo0.vue new file mode 100644 index 00000000..b9e45f14 --- /dev/null +++ b/components/image-reader/demo/cases/demo0.vue @@ -0,0 +1,123 @@ + + + + + \ No newline at end of file diff --git a/components/image-reader/demo/cases/demo1.vue b/components/image-reader/demo/cases/demo1.vue new file mode 100644 index 00000000..88806320 --- /dev/null +++ b/components/image-reader/demo/cases/demo1.vue @@ -0,0 +1,132 @@ + + + + + \ No newline at end of file diff --git a/components/image-reader/demo/index.vue b/components/image-reader/demo/index.vue new file mode 100644 index 00000000..d0e6530c --- /dev/null +++ b/components/image-reader/demo/index.vue @@ -0,0 +1,20 @@ + + + + + diff --git a/components/image-reader/image-dataurl.js b/components/image-reader/image-dataurl.js new file mode 100644 index 00000000..db64e24b --- /dev/null +++ b/components/image-reader/image-dataurl.js @@ -0,0 +1,50 @@ +/** + * DataURI to ArrayBuffer + * @param {*} dataURI + */ +export function dataURIToArrayBuffer(dataURI) { + // 'data:image/jpeg;dataURI,...' => 'image/jpeg' + // contentType = contentType || dataURI.match(/^data:([^;]+);base64,/mi)[1] || '' + dataURI = dataURI.replace(/^data:([^;]+);base64,/gim, '').replace(/\s/g, '') + + const binary = atob(dataURI) + const len = binary.length + const buffer = new ArrayBuffer(len) + const view = new Uint8Array(buffer) + + for (let i = 0; i < len; i++) { + view[i] = binary.charCodeAt(i) + } + + return buffer +} + +/** + * Base64 to Blob + * @param {String} dataURI + */ +export function dataURItoBlob(dataURI) { + // convert base64/URLEncoded data component to raw binary data held in a string + let byteString + + if (dataURI.split(',')[0].indexOf('base64') >= 0) { + byteString = atob(dataURI.split(',')[1]) + } else { + byteString = unescape(dataURI.split(',')[1]) + } + + // separate out the mime component + const mimeString = dataURI + .split(',')[0] + .split(':')[1] + .split(';')[0] + + // write the bytes of the string to a typed array + const ia = new Uint8Array(byteString.length) + + for (let i = 0; i < byteString.length; i++) { + ia[i] = byteString.charCodeAt(i) + } + + return new Blob([ia.buffer], {type: mimeString}) +} diff --git a/components/image-reader/image-processor.js b/components/image-reader/image-processor.js new file mode 100644 index 00000000..f0be61ae --- /dev/null +++ b/components/image-reader/image-processor.js @@ -0,0 +1,222 @@ +import {dataURIToArrayBuffer, dataURItoBlob} from './image-dataurl' +import {noop, requireRemoteScript} from '../_util' + +const UA = (userAgent => { + const isOldIos = /OS (\d)_.* like Mac OS X/g.exec(userAgent) + const isOldAndroid = /Android (\d.*?);/g.exec(userAgent) || /Android\/(\d.*?) /g.exec(userAgent) + + // IOS8.3- + // ndroid4.5- + // ios + // android + // QQ Browser + return { + oldIOS: isOldIos ? +isOldIos.pop() < 8.3 : false, + oldAndroid: isOldAndroid ? +isOldAndroid.pop().substr(0, 3) < 4.5 : false, + ios: /\(i[^;]+;( U;)? CPU.+Mac OS X/.test(userAgent), + android: /Android/g.test(userAgent), + mQQBrowser: /MQQBrowser/g.test(userAgent), + } +})(navigator.userAgent) + +/** +* Get Orientation of EXIF +* @param {Object} dataURL +* @souce http://stackoverflow.com/questions/7584794/accessing-jpeg-exif-rotation-data-in-javascript-on-the-client-side +*/ +/* istanbul ignore next */ +function getOrientation(dataURL) { + const buffer = dataURIToArrayBuffer(dataURL) + const view = new DataView(buffer) + + if (view.getUint16(0, false) !== 0xffd8) { + return -2 + } + + const length = view.byteLength + let offset = 2 + + while (offset < length) { + const marker = view.getUint16(offset, false) + offset += 2 + + if (marker === 0xffe1) { + const tmp = view.getUint32((offset += 2), false) + + if (tmp !== 0x45786966) { + return -1 + } + + const little = view.getUint16((offset += 6), false) === 0x4949 + offset += view.getUint32(offset + 4, little) + + const tags = view.getUint16(offset, little) + offset += 2 + + for (let i = 0; i < tags; i++) { + if (view.getUint16(offset + i * 12, little) === 0x0112) { + return view.getUint16(offset + i * 12 + 8, little) + } + } + } else if ((marker & 0xff00) !== 0xff00) { + break + } else { + offset += view.getUint16(offset, false) + } + } + + return -1 +} +/* istanbul ignore next */ +function getImageSize(img, orientation, maxWidth, maxHeight) { + const ret = { + width: img.width, + height: img.height, + } + + if ('5678'.indexOf(orientation) > -1) { + ret.width = img.height + ret.height = img.width + } + + // 如果原图小于设定,采用原图 + if (ret.width < maxWidth || ret.height < maxHeight) { + return ret + } + + const scale = ret.width / ret.height + + if (maxWidth && maxHeight) { + if (scale >= maxWidth / maxHeight) { + if (ret.width > maxWidth) { + ret.width = maxWidth + ret.height = Math.ceil(maxWidth / scale) + } + } else { + if (ret.height > maxHeight) { + ret.height = maxHeight + ret.width = Math.ceil(maxHeight * scale) + } + } + } else if (maxWidth) { + if (maxWidth < ret.width) { + ret.width = maxWidth + ret.height = Math.ceil(maxWidth / scale) + } + } else if (maxHeight < ret.height) { + ret.width = Math.ceil(maxHeight * scale) + ret.height = maxHeight + } + + // 超过这个值base64无法生成,在IOS上 + if (ret.width >= 3264 || ret.height >= 2448) { + ret.width *= 0.8 + ret.height *= 0.8 + } + + return ret +} +/* istanbul ignore next */ +function makeCanvas(img, orientation, maxWidth, maxHeight, quality) { + const {width, height} = getImageSize(img, orientation, maxWidth, maxHeight) + const canvas = document.createElement('canvas') + const ctx = canvas.getContext('2d') + + canvas.width = width + canvas.height = height + ctx.drawImage(img, 0, 0, width, height) + + let base64 = null + switch (orientation) { + case 3: + ctx.rotate(180 * Math.PI / 180) + ctx.drawImage(img, -width, -height, width, height) + break + case 6: + ctx.rotate(90 * Math.PI / 180) + ctx.drawImage(img, 0, -width, height, width) + break + case 8: + ctx.rotate(270 * Math.PI / 180) + ctx.drawImage(img, -height, 0, height, width) + break + case 2: + ctx.translate(width, 0) + ctx.scale(-1, 1) + ctx.drawImage(img, 0, 0, width, height) + break + case 4: + ctx.translate(width, 0) + ctx.scale(-1, 1) + ctx.rotate(180 * Math.PI / 180) + ctx.drawImage(img, -width, -height, width, height) + break + case 5: + ctx.translate(width, 0) + ctx.scale(-1, 1) + ctx.rotate(90 * Math.PI / 180) + ctx.drawImage(img, 0, -width, height, width) + break + case 7: + ctx.translate(width, 0) + ctx.scale(-1, 1) + ctx.rotate(270 * Math.PI / 180) + ctx.drawImage(img, -height, 0, height, width) + break + default: + ctx.drawImage(img, 0, 0, width, height) + } + + if (UA.oldIOS || UA.oldAndroid || UA.mQQBrowser || !navigator.userAgent) { + /* global JPEGEncoder */ + const encoder = new JPEGEncoder() + const newImg = ctx.getImageData(0, 0, canvas.width, canvas.height) + base64 = encoder.encode(newImg, quality * 100) + } else { + base64 = canvas.toDataURL('image/jpeg', quality) + } + + return base64 +} +/** + * Image Process + * @param options Object: { dataUrl, width, height, quality} + * @param fn dataUrl => void + */ +export default function(options, fn = noop) { + return new Promise((resolve, reject) => { + const {dataUrl, width, height, quality} = options + const orientation = getOrientation(dataUrl) + const blob = dataURItoBlob(dataUrl) + /* istanbul ignore next */ + if (orientation > 1 || quality < 1 || width || height) { + const img = new Image() + img.src = dataUrl + img.onload = () => { + const newDataUrl = makeCanvas(img, orientation, width, height, quality) + const newBlob = dataURItoBlob(newDataUrl) + fn(newDataUrl, newBlob) + resolve({ + dataUrl: newDataUrl, + blob: newBlob, + }) + } + img.onerror = () => { + fn(null) + reject(new Error('image load error')) + } + } else { + fn(dataUrl, blob) + resolve({ + dataUrl, + blob, + }) + } + }) +} + +// Import jpeg_encoder_basic for compatibility if necessary +if (UA.oldIOS || UA.oldAndroid || UA.mQQBrowser || !navigator.userAgent) { + /* istanbul ignore next */ + requireRemoteScript('//manhattan.didistatic.com/static/manhattan/mfd/image-reader/jpeg_encoder_basic.js') +} diff --git a/components/image-reader/image-reader.js b/components/image-reader/image-reader.js new file mode 100644 index 00000000..b91cb1ce --- /dev/null +++ b/components/image-reader/image-reader.js @@ -0,0 +1,91 @@ +/** + * Read Image In Web Worker or main thread + * + * STATUS + * 0: success + * 100: 'browser does not support', + * 101: 'picture size is beyond the preset', + * 102: 'picture read failure', + * 103: 'the number of pictures exceeds the limit' + */ + +export default function(global) { + /** + * Constructor + * @param{*} [Array]files 图片文件 + * @param{*} [Boolean]isWebWorker 是否为webwork模式调用 + * @param{*} [Number]size 单张图片大小限制 + * @param{*} [Function]complete 非webwork模式时 回调 res { errorCode: '0', file, dataUrl } + */ + function ImageReader(options) { + /* istanbul ignore if */ + if (!options.files) { + return + } + + this.files = options.files + this.index = 0 + this.size = options.size || 0 + + if (!options.isWebWorker && options.complete) { + this.callback = options.complete + } + + this.readImage(options.files[this.index]) + } + + ImageReader.prototype.readImage = function(file) { + // iterator + const next = + this.files && this.index < this.files.length - 1 + ? () => { + this.index += 1 + this.readImage(this.files[this.index]) + } + : null + + const onReadImageComplete = msg => { + /* istanbul ignore else */ + if (this.callback) { + this.callback(msg) + } else { + postMessage(msg) + } + next && next() + } + + if (!this.size || file.size <= this.size) { + const reader = new FileReader() + + reader.onload = readerEvt => { + const dataUrl = readerEvt.target.result + onReadImageComplete({errorCode: 0, file, dataUrl}) + } + reader.onerror = () => { + /* istanbul ignore next */ + onReadImageComplete({errorCode: 102}) + } + + reader.readAsDataURL(file) + } else { + onReadImageComplete({errorCode: 101}) + } + } + + const onmessageCallback = function(workerEvt) { + const imageReader = new ImageReader(workerEvt.data) + return imageReader + } + + if (global) { + return function(data) { + return onmessageCallback({data}) + } + } else { + /* global onmessage */ + /* eslint no-unused-vars: 0 */ + /* eslint no-global-assign: 0 */ + /* istanbul ignore next */ + onmessage = onmessageCallback + } +} diff --git a/components/image-reader/index.vue b/components/image-reader/index.vue new file mode 100644 index 00000000..b8e54d75 --- /dev/null +++ b/components/image-reader/index.vue @@ -0,0 +1,185 @@ + + + + + diff --git a/components/image-reader/test/file.mock.js b/components/image-reader/test/file.mock.js new file mode 100644 index 00000000..4024f319 --- /dev/null +++ b/components/image-reader/test/file.mock.js @@ -0,0 +1,18 @@ +const _dataUrl = + 'data:image/jpeg;base64,/9j/4QAYRXhpZgAASUkqAAgAAAAAAAAAAAAAAP/sABFEdWNreQABAAQAAAAeAAD/4QMpaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLwA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA1LjAtYzA2MCA2MS4xMzQ3NzcsIDIwMTAvMDIvMTItMTc6MzI6MDAgICAgICAgICI+IDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+IDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDUzUgV2luZG93cyIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpFN0FDRjU4MzJDOTUxMUU2ODU3NUFENDA4MDE5NDRGRSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpFN0FDRjU4NDJDOTUxMUU2ODU3NUFENDA4MDE5NDRGRSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkU3QUNGNTgxMkM5NTExRTY4NTc1QUQ0MDgwMTk0NEZFIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkU3QUNGNTgyMkM5NTExRTY4NTc1QUQ0MDgwMTk0NEZFIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+/+4AIUFkb2JlAGTAAAAAAQMAEAMCAwYAAAj7AAAOogAAH1b/2wCEABALCwsMCxAMDBAXDw0PFxsUEBAUGx8XFxcXFx8eFxoaGhoXHh4jJSclIx4vLzMzLy9AQEBAQEBAQEBAQEBAQEABEQ8PERMRFRISFRQRFBEUGhQWFhQaJhoaHBoaJjAjHh4eHiMwKy4nJycuKzU1MDA1NUBAP0BAQEBAQEBAQEBAQP/CABEIASwBLAMBIgACEQEDEQH/xAC/AAEAAgMBAQAAAAAAAAAAAAAAAQIFBgcDBAEBAQEBAAAAAAAAAAAAAAAAAAECAxAAAQMDAwMEAgEFAQEAAAAAAQARAgMEBSESBhAgMTAiExZBFEBQYIAVBzIzEQABAgQDBQUCCQoFBQEAAAABEQIAITEDQVESYXGBIgSRobEyE8HREDBA8OFCsiMUIPFSYnKCktI0BYCiwjNTQ2NzgyREEgABBAICAwEAAAAAAAAAAAABACAwMWAREIFwIVFx/9oADAMBAAIRAxEAAADoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADB6RNdTcxudLczk6W5vY6Mra5EEtZxsu8Oe+S9Hc59DoTQvsNxVtcgAAAAAAAa3z7oWgZ6Vi8TULiMni9is6CxuR1ymJHIPP7Pkx2iLQVkEWg6fldT2zfIEAAAAAAFTTNU+r5s9q/X82Rzq77Yzr4fDIfOnwdP5r07fL6Bvnz3A7TquOs1mGotBCYXP9D5P1fXORcAAAAAAMDntFmtaGes/f8EGbYSM3M+OMg+rqPI+o755E+TXPQsPDHaJQCCZrKus8l6PcZwa5gAAAAAOZ9K5PN+U3Z35hUTBWZhI3XS/vs6lp238ouPlmYnRMTFZCJi5Tc9OzdnRhrkAAAAABjuZdXw83oMb/wCUuht7qaNO6+a6Y3GU02+1fMu7ck3rXbnDtm9JrVo2oapXbBqs7TJq3rscm7zS+uQAAAAAGMxOJ16dN5nRqy73OhSm7+Om0Oi+nJ/quehah4ek28aF2/c+NeDHbcfx/wBLOxW4/K9gnj8nXrchk7DOBz1yCAAAAa9qf369npbzXarWUVWgik0qlqXT0+n5fol8oRXjS9Ga+vn6F5iVTEraYGZ2XRITr3prmx65ggAA8zmXwIz2mQqIqvQr53pZWa2PT6vj3A1OOlYtNE8fp+Ym9PQusWCyr0sTMDZd95f1C8wuQAGOyOCXncwz1WrYgrF4gRS9KqtKfb1T5vs1zBOeap23l03h7XTSLytJBalibVmLdc5B0/WMmLgABqO3c+msBats9ETBaiCUCIVJTJ1n6fP03xAc06XzmawKLTpFoCUEWqLwiLbxo202byNcgAGrbSXlleqprlVerjlLqw5VTrA5O6wOTevVB5+hcAOe9C+NeTz0m81zO3TrnLY6vaOSuuWrkN+vSci2/b5QLkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/2gAIAQIAAQUA/rpKdOE46uE4ThOPSPnoO8eiUfHSHQ+eo9A9CHW1bVHuHoHtCPaPPeQmTFMUyZFMmKYpimPoOnKcpynRKdAp06dP/Ef1W7x2H0T3DsPj0D3Dz2MmTJkybsZMmTJk39n/AP/aAAgBAwABBQD+uiJK2FbCth67SthWwrafSh46Hx3Ea+hEMB56VOkfHWfnviNejp1U6Adk/HfAJ+whxAa9ktR3xkAN4W8LeFuC3BRcLcFuC3Bbgt0fQEQ2wLaFtCEUAAGUo6iK2LYtiIY9sQG6noPCPnsMQUQx9A9HAjvC/PbPz1Hnt/JPSOvdPsh59CHntl46gst5W8reVvK3lbz1BZbwt63ret6MnH9nf//aAAgBAQABBQD/AAS5dfXFjiByLOKjyfOwq3fJ8rUvI8q5DCP27kDQ5lyCI+38gnOG7b1u+c46jKpz68Iq83zcl91zoX3XOgfeM2BS57kwqX/QKYEJxqQ/g86i+F/IkQKf/wAkYo7Vxqy/bzXU+LgCNx4A1GgILnXd+GccYujdYP8Ag85m2H2hMyESIinUfbJm14jksXjKtLkWEqoEEdMrR+HJMHZEIBk2ngeDwG63Wv8AB55didaQKYAWMBUkaLI0tJUCpU2U6bDEBsV05nbxo5kPJNodR5DJijEyXCrk0c1/AnONOGSvZ318XJYLFuYmWvuJkKTylFq4Pw2L/pdOet+35RPtO0RBYuNwZvziq4tsl/A5hfm1xUggPczrGV6VKHzW5BnSkpbVOOtxpCzlutOnLLz9zNGLEBAe5pMPIdOvAx1x+1Yevzq5E8iSXcMAwchAudNwiiUDJsBX/Ywyyl7HH4+c5zn+W0JcDz7gHQ8hcOuPmwXr8lrC4zf5Oo1ifES5R3PuJWoUjrwXIxnQXO8g61T6op9PztILSKiAFwG6Bj61SYp061T5qsKUJicYANApniNxQEgGTFiCVgLqdpmJSEY5S8nf3w9q1CYAiOpAALFEziBTM1KM4nhtx8Gc9bP1/wBfDB1ECKjqTJ4vJidwkYuJSIJ93vAtpmFxeTMLKo4lEyJEmQJC0BOkt0inUZyAlVqSVjXla3oII9W7tLe9t5cMwxI4XhwZcHxZX0bGueC45jwO0KPAaZR4AGHAIAS/5/UevxvG4ypV5zhyJUeN152vDo3tL6Der6HfgHgV6R9Bvl9Bv1HgN8B9BvWlwK/VPgN2Z0ofHS9XM521w8fvlihzqwb73jdx53jAhznFlT57ZhR/6BAVKPOeN1hLmXHoxuucWey+qG4ritalV6gMuCX1tRiJRIRz+FjWhksdUH71iv3rJC7tSv3rJftWxPrc1uTWy/uAkCpak7jEORIEKUXhSpTpyiVaiPyXJc7tCzzc0RUq0xWv70Kg/wATgyEg8ZAAMTucblw69N1h/T5dmLnG20+S517irUrVtw3EMS7nQuIgkNIOJSaAJJsZz/2Fy25vcQ6u57LYSLGMXjJDySAtUdAPAJA49n54WrDneMJt69K5o+jzmv8ALlAQACXckl5ptQSUxip6EsTqI6bscQclVINWEQC8yL9v1g+1yBCLrRyEPAOhYHc6cEMW4RfC4xPo8huPmzEZbIbUAQZNE6OTtW9kdVI+0iQADLFgnIVWM47ZAnS9A/UgXgWaDiRYCMnluKGvSOh/9LcuCXMqeS9CtUFGjWqfJUkEZAqJi0nepI7o+0EAgFpS8SlpF3xc/jrYPiX+wtvp/HmyXAqEqd5Qq0KFN/iHmmCE4UQCgCY+U4RDCYIk4bjdwKGc9DP1xQwwgpAglgWCk5LPEEAnY8oiJaMjsQgXxnGc3cSo040aXTn9hcU72nCXwxgVGCiCQQ0fyGAiQCEDBOQKFQ0KtKpGrT7+ZVfjwMYkCOpDkMx03SBjEglS0Et5RGgEkIusOBHFdSARySlGnnIs4coSKAAW4kD3B2DstzICJRBXHbsXeG7+e3DUYhCMdsS8jAxjKMtxiU0UfLEEHWI0CxwbH9nLg2ckdD5OgY7wCCRIEykxACIIkSCSCVwKtGWN7+aVpV8yCBTG5O63iJjKAiNzQACMtCHW4CQaMQFbxEaHZzSAGbDGBDk+9ExAIJk+ugW/eBL5AQQPc3A6sxkO/lXG619KWIzEDHE5MR/199ERx2Qkhi8oYyxWWMRisoZyxGTgKuLypIxuSmjiMoFQw2XnKjGcaPZzqO3L+1/c8JSES4LFAEgRiDq8STLQRhR+Q8Ixt3Srfyb/ABOPyUfp/HXjxXj8VHjeCjGOAwkZDD4kE4zGlCwsQI2dpE/FTXx00IxH+G3/2gAIAQICBj8Ayk+LrxL/2gAIAQMCBj8Axowhg4EgaONSEu38aZjsMtXB7b6ZatWtSdwaj7PHUQh/A7cvUlQ0qVKlrD//2gAIAQEBBj8A/wACRf01w2rty4xge3zATcU/hhfxt7+I14wx7+re5jSC5pQqBUeWL3UdL1dxli4S+1bKANYfLylQJQE6sn9pjD3lsBOqX9y3/LABuscazttVOAhjRdaCSEDbbZ9xhuvzINSUXH8gs6ezc6gtKapMbwVT3QtrpLbAv1i53hpghvosnVrCSBs1OMf7jM/I2PPbOE7Y7khEsk56D/NA9SxZeMUDmn7RgfiOjIVFNt4dVcCB4w17SrXAOacwZ/Imn9G+wpwcIQlE4zWK4Ti3L6rRwSBVfmsBPdEkGRjp2Vbbf6ryMBb55rmQn5NxhkWvIQbCYzOOzCJ8uZwEbDXxiUeGETKEzJwhJ81FjpHkq5jfTctVtnTPgPkTGpN95s9zXGDI0rko+mGla1hrCpSQEHlK5kRpLTkSRGOKBKfTHUXeuuOt3ntDbZ0lw0eZ3lByEDT1lsL+kdH20gEFQaH4eqtogbdeEGQcUmY/VGGUSSXGOzxim8pkYEp5HHH2RsHGfzESwkd2FI6rpTW29txq5Pani35F0/RA/wC203X5K/lb3AxtJjSp4SwhxIo0S3mEPAAwASVzmghZ7/ojHcsOzCIs6mOjFEs2/sj4S9oT1mNuFM/J/pgJwEAqhWkBK+MESmCibfzwCZLUiFpODNTScoFoyb1Ftzf3hzj7PyFz3lGtBc4nACZi/wBY9R6zyWjJo5WjgEhMcUEBRsG1JisX3eUgtaoC55QgfhQNJnkZGcFrXh5aFIA9qJxgEPGpoJe5DXBC4QDqaSPMd+FILyQQSBLZujp1CH0mKP3R8PSjH0yTmmqCqIDIQQZAYCJ4ZCClE+mJ8wTZlCmQND898TCe+kdL1BKC3dY44BC4A/ITZYUf1TvS26EJf7oWjpLlOApTT24QFCTXYU9kXWPuBhLgVOQ1CRhBdYgrzRpF22cxrbhnOBzsTDmFYRQmSwWuoXNTcBhFh0hqtsMqTaPhvBpVnTgWWIcW+b/MTEp7vpg8IA3AxmahI0upitIGJOMAmeJ3JKJkjEmOm6g1u2mOO8ifyDp+m1ANs29Rw5rhSfARiD3ZwMNUwZUX2QdUziq02R78vmYdQVIwgjYEP54QlGjiZQVIE13z2QioqyPCsdHdx9INO9nL7Pgv9W7/AKTTpGbjytHaYL3uW49XOLsSZqYWeHbLCKIkKMMN0DBCk55iCMaccawSQMxsNYQJkUoolAcDgqItJxaaSpsufbPbqHc75B1TweUO0D/1gNlOEaiBZVmmEHeqEVnCORZgcKZwcZyJks/ZBkiYnaU8YKgoc9pzibZgoPdAJrUH80EAImM4vf255R9p3q2x+o5A4DcfH4LH9tY4TPq3gvBg8TAWuAzTZBCJkZp2xsCcvE++ADkcgBGSFB3ZxOaT4yzinKJZQoCKlKxSTaJHV9JRC26zjyO8B8e646jAXHcAsPvXEcbrjccTm4l0BbrQHFNJVVkuyC/WHO+qG0OBnCGe/fAQ1rlSFdU0xrQSjx9sCYCeBP0QVKEie6FqoEttJcY6W7bKBz223Aqha86XDsMFxo0EnhF/q31uuJDdgkBwaEiZSc8cxAIMsaSnluMSoVn3rABKALxTCNrsK0WJVqpoMYC0bntB90EhpdnIVM4GsIDRcF3Rba4yvsdalRfMO9vx/WXFT7pzRjN/IPGJDygBCpEtiwRgZgjCJ402z27oykqDLZBLaSQ5Cc+6CAUkTKSwcBVEkBhADVFCuRSJSEgMROXthUmZyFFnFu4iFrwQTMkqTF+4KttPcODSYdVVNIU7DlvlAwRCiZ/RCAzyRYRs1CAnHGABKsFMQUE03QjBNFHh4QXNJAUGsI55pTApHT9TT0bjLnYRq7oBEwZj453TdS3XafUUO8EQv3wFQBclPeIBBvBMNYT7O2Cl6+F2s/khfXv7lYn2I5OovA4roITdpEf1T5L9Vu/2RLrTXG0D/rgf/cpCBTa2J/yQ4DrjOn3VP88S61qf+Ig9z4Zc/uP9zYGMM7Vtn3rknp8ziN6Q62bN19twLSoaGkUIm6Bo6rqOjUkaL1r1QCQXALbcD2wb3Rf3GzftqmtrDWpBR0D/AOq0UMuV1BCDqrNAByuyMT6myqr5XR/VWkQjyuxhfxdo7CHRPqrVcnUSE/FWiNzs4VvU2i7HzCnAx951NoMVeUOJQ1yhltS7Q0N1GpQIp+Ot+s11x95dLWpRtSSY/pruElbUxPprq1QFpPjCHp76YEBpln5onYv9jU+1BSzfUVGltP4s45Okun9otbvoXRqu9E5vTtm9zX6ngfs6QO+FHUOYcnW3+xpEah1JdsDH7sWwW9BafcuUD7g0sG1Jk90Of1CuL3FzycdRU+2CRZKELMihMl3x5QwNQJmTzc3COq6a7dbbe97XWmOIbqk7VphQQQaJ8DrDuttNuMJa4FwABBQhTKFt9VZcMxcaa8Y/qLUq87ffH9Ra/jb74CXrZ1eVHtmmU4/qLX8bffCC8xVRNQrlX48WQ5W9NbaEyc7mPsiZSU9h/MY1EogKjL6IJnUAHZWAV8yg45pSCQUEwd2cToKIRIO2wbY3EZLjBa9p4UrHKDKgNYDcV+Y74VoA5irs5GWUJ5TQnKS+FIJoFM6r81goQoIJl3JALHuaZojjjuiXU3Vp53e+Gg1QVxMFwC+MHAD5+2BiuGzeN0IE0mQ4GERZElcTSCRVU2DKcMtvcXXOncbZJKnT5mz3FPjLTOkf6d68SrkBIaJSXMmAnWP1BZSpPBEg9Ref6tx557jiSSZVxhTiUXE59ixPyopAKKJQmDVnkAIUKD9Va1lGBcMZ1SM3KpOMu6KqvmwrjBeKIdxkgWKlO/5mHN1AtDQUwJgnDFcJLId8AmpcZEpIgKDsESVVcUOJQjhSAWGeqWEpxQGQQEbICgIKIBGriMVEpQDwG2cFJAKdtMeMEkYLljE5gpq7UlCAKQF4ie+NJGwfMxd1W/VsX0BYChDm0cJHOOexeaFQlGnFMHQzqLLtdq60OY7MH4plpv8A+e2B+85Xe0QD53maJRCkB8pzI4n6DDjMAyTCaNEEGZcUpIrJB2xI1ICURVWWMFDRVd3zhCEx3TTCFPznh2QA76vADd2wVw5ipT51gtUnAEbEh5XyjHYJwBJfrE0We6Gk0UlKKqe0wSuouBcpwIdXvSCcdYl2xWRAI7KQo2YpjCE6aCGmjqjEpElSoiYoi4CMsvGCAqCiQhCYjanvgGiSJKyw7pwgGFVVKe6Pw31+kdp3tfzDvUfFdZfw1kNxUMl4NgFDqSc6ISY0kIAi4ypSEQEgkbJJXiIXVyma40y9sEBBpRDOeyERSSibhOAApWZINRqjLEDfig3QAhmUIrNPbCynNaiW+CRIHDiIvUUAj5pEyDiCVyxjSoABVEnzBJ7oUijdElqTNYf+q5sNRVAE0iZwku8QirOkxj7oBEiZ+yCZAEaUmeaq90AAEjOgwMJWiLko98FShVAhM9sESNFFFovhAnXlnmd0AEKBJcRuwi907nct+2qfrMKjuLvibl00ttLiv6oWHOc5XPcXEmc4IJKFStJTCndB1I1FkN0xPfEhLigUYCDPmMjtQSwgtPlAIlVN8NJkSS0E75+EaQiY1SWMBlACCSkpGFFCE3KkEiRXwiZTPgkdVcPlAn3xb63+4vdas3RrtdPbk5zXTDrjpouQ7YT8KaIvqXFy/Th9z+2XnNuibbN46mEoia01DvjqLF1ht3bLtNxhCFrgZiG4oIAPlmpiQQkrup7oSrgFBoZJE5zkaqDs3RRB7KOSNKySZXMShyColmlV74ckg5UTImExCkHaPmIIAU0OajmoI6S4SjXP0n94en7fiesuKn3Tmg0m/l9sbQNQTEIkO1JQEgnElPZBBT6yz/cmYcteXlxOojLfCuGk6gQabD3QhOzbMJ2BY0uM0JJA4+yO1wCTEvoiZSo2gAiAVSQVopIxnQzmJGcuEHSQhlt8wHGHi10zhZ6o8t9yC3ocfPXLKcMtN8ttoa3c0J8NzrW2j+G6i0xrrrQrfUYSEelCiJDFxbPdKJVJQbZGNQoQK50hqoJeXEIBBQg81BTCEqitnvWNTcUKGnLArIEBd+ffGK07RxgawQtMalEgEUFN/CeMWr1tHOtua4Da10MuN8r2hw3EL8ReAIHqPtsQ4q4FO6FaVqlV5iRP90QCqlyONStECwAvNVxOSbIAcAGgKs/qgrMdvCFJ5moFGYDU8YMtS6tRWiCCQAWkIcEQETgAlCvMlQoUDsgKEUzAzFK7YKCgRqyUlWrwBgOTUChAMhzOPjCuoqHKZkg7o6MCno26fsj8hCFBqDHVsa0NZ6nK1ol5WmghqgooNPKMzACLIEb1AQw0kDUSUbjPfwhQNq0M8UgF0iS2WIUCvZCkzXlOQdKCaKgA2Jh2whFSUSUsfGJKVMgQhliOyJNlWZSaAjshQFcFJBkcHNTsjpbmrU5rBbeR+kzl+I6XpgfM51wrm0Bre3UYDSJT7PKnYYQmRUuGR/NAfMIF05FfdAJGxNhlxglVC4TUSy3QUcqFBlPlJnI0KQrEAQ6VOJK9pnExIFe9BDXNqiyNRMkgb4mFIRQamgQJvhpB1ajnIyke6KagQrF/SBl9lY6Uf9q3gn1Rh+T1I8rSWn/JP3wVnIK4SrMQrQQS1dRmGo4hJxpIllsWOUriFFEovbCgFUUlxWp+mEdzOkQUkCJj58ICKZESQmQEOopqZnu4QrTRE2IC2NQ+tIHcadso1IVCyxkdSS3xfsCtu7q4PaET+H4gWnA6LDGtC0/TLuOoDhCDlBCEGUyDMQiIwALva2f2oAmAgkJzE+2Gh0zqJImPq6t0pxpQgBoKBTpUDl7Ia0lQgFMWzgApIoMgUoNyiCQTmilFIX2xWuIxACA7ISSAAlZTBw7YCVaVaANmUaRTyosjzVMWmgqAxoB3D8l4mQ4MeUw5foWCFnmZoQQKQQpd5iSahQBPhDgJFxRqyKJUxOkzwIp2GJrqVEXEzickWQmmMtkaUCSDcpnhlAJULJxxrWGlygOQt3jmpwMB0q1Cic4rQgSEhUKsdRaUaLlhrjtdbLGf6viPxvRN13US7aCc0kDgtdogh/Q3ySSFFtxCVwGyE/BdQEArbuZAz5Zzjn6S+jiJOt3A6Qr5Y0/hLztQkDauE1xRsIej6hwUOU2ripIEJpgH8H1BqCBZuVQgFdGEEjo+owc5bTxMJjpyE4KdH1BAys3FphywXXOkvhNRAFp6TEvqiCG9HfUuKD0nkgaVly5wv4LqNQC6vSeQqSFIDGdH1BJKK+25qyUTcAEBNTFtr01hoDkooE/yWknzMY7dVso0lMGgCVFxrjBA+sOYezugn9UEuElUqR2JEzI+yUF5dJpV2NJS8IqEKJsIn9EErIhqcDXuhA4KSnMhmoJkN6QXErqCBSMFEo1qCSQZpnqpxgtaw3Ca6Zr2bYvdbftvtMNv0m+o0tc4ktcUBwGlPlTW9bYF3SukqWuC15mEGF/CGRX/AHb1VX/k2xLo25zc8z4ujSOhtESHM3VT9pY1DoLGrM22nxEah0PTg5+kz+WJ9JYKU+7Z7oAHTWgBT7tvugFti20ihDGj2R5G9gjyjsESAG7/AAbf/9k=' + +window.File = function() { + this.name = 'test' +} +window.FileReader = function() { + this.readAsDataURL = this.readAsText = function() { + this.onload && + this.onload({ + target: { + result: _dataUrl, + }, + }) + } +} + +export const dataUrl = _dataUrl diff --git a/components/image-reader/test/index.spec.js b/components/image-reader/test/index.spec.js new file mode 100644 index 00000000..abca1d0b --- /dev/null +++ b/components/image-reader/test/index.spec.js @@ -0,0 +1,54 @@ +import ImageReader from '../index' +import triggerEvent from '../../popup/test/touch-trigger' +import {mount} from 'avoriaz' +import imageProcessor from '../image-processor' +import {dataUrl} from './file.mock' +import Promise from 'es6-promise' + +Promise.polyfill() + +describe('ImageReader', () => { + let wrapper + + afterEach(() => { + wrapper && wrapper.destroy() + }) + + it('create a image-reader', () => { + wrapper = mount(ImageReader, { + propsData: { + size: 10, + }, + }) + + expect(wrapper.contains('input')).to.equal(true) + + window.Worker = null + wrapper.vm.$_readFile({ + files: [ + { + name: '123.jpg', + lastModified: 1501728385000, + lastModifiedDate: '', + webkitRelativePath: '', + size: 100, + }, + { + name: '123.jpg', + lastModified: 1501728385000, + lastModifiedDate: '', + webkitRelativePath: '', + size: 57070, + }, + ], + }) + }) + it('image-reader processor', () => { + imageProcessor({ + dataUrl, + width: 200, + height: 200, + quality: 0.1, + }) + }) +}) diff --git a/components/image-viewer/README.md b/components/image-viewer/README.md new file mode 100644 index 00000000..b043fd9a --- /dev/null +++ b/components/image-viewer/README.md @@ -0,0 +1,29 @@ +--- +title: ImageViewer 图片查看器 +preview: https://didi.github.io/mand-mobile/examples/image-viewer +--- + +用于浏览多张图片,并可对图片进行滑动切换 + +### 引入 + +```javascript +import { ImageViewer } from 'mand-mobile' + +Vue.component(ImageViewer.name, ImageViewer) +``` + + +### 代码演示 + + +### API + +#### ImageViewer Props +|属性 | 说明 | 类型 | 默认值 | 备注| +|----|-----|------|------|------| +| show | 是否显示查看器 | Boolean | `false`| - | +| list |展示图片列表 | Array | `[]` | -| +| initial-index | 初始索引值 | Number | `0` | - | +| has-dots | 是否展示图片索引值 | Boolean | `true` | - | + diff --git a/components/image-viewer/component.js b/components/image-viewer/component.js new file mode 100644 index 00000000..d277e7c0 --- /dev/null +++ b/components/image-viewer/component.js @@ -0,0 +1,7 @@ +export default { + 'name': 'image-viewer', + 'text': '图片查看器', + 'category': 'basic', + 'description': '图片查看器', + 'author': 'linyufei' +} diff --git a/components/image-viewer/demo/cases/demo0.vue b/components/image-viewer/demo/cases/demo0.vue new file mode 100644 index 00000000..cd241be0 --- /dev/null +++ b/components/image-viewer/demo/cases/demo0.vue @@ -0,0 +1,76 @@ + + + + + \ No newline at end of file diff --git a/components/image-viewer/demo/index.vue b/components/image-viewer/demo/index.vue new file mode 100644 index 00000000..9db4a342 --- /dev/null +++ b/components/image-viewer/demo/index.vue @@ -0,0 +1,19 @@ + + + + + diff --git a/components/image-viewer/index.vue b/components/image-viewer/index.vue new file mode 100644 index 00000000..70545506 --- /dev/null +++ b/components/image-viewer/index.vue @@ -0,0 +1,173 @@ + + + + + diff --git a/components/image-viewer/test/index.spec.js b/components/image-viewer/test/index.spec.js new file mode 100644 index 00000000..8a40314e --- /dev/null +++ b/components/image-viewer/test/index.spec.js @@ -0,0 +1,39 @@ +import ImageViewer from '../index' +import {mount} from 'avoriaz' + +describe('ImageViewer', () => { + let wrapper + + afterEach(() => { + wrapper && wrapper.destroy() + }) + + it('create a simple image-viewer', () => { + wrapper = mount(ImageViewer) + + expect(wrapper.hasClass('md-image-viewer')).to.be.true + expect(wrapper.vm.show).to.equal(false) + expect(wrapper.vm.initialIndex).to.equal(0) + expect(wrapper.vm.hasDots).to.equal(true) + }) + + it('imageViewer method afterChange', () => { + wrapper = mount(ImageViewer) + wrapper.vm.$_afterChange(1, 2) + expect(wrapper.vm.currentImgIndex).to.equal(2) + }) + + it('imageViewer method viewerClick', () => { + wrapper = mount(ImageViewer) + wrapper.find('.md-image-viewer')[0].trigger('click') + expect(wrapper.vm.isViewerShow).to.equal(false) + }) + + it('imageViewer method imgsInit', () => { + wrapper = mount(ImageViewer) + wrapper.vm.list = ['aaa.jpg', 'bbb.jpg'] + wrapper.vm.$_imgsInit() + expect(wrapper.vm.imgs[0].url).to.equal('aaa.jpg') + expect(wrapper.vm.imgs[1].url).to.equal('bbb.jpg') + }) +}) diff --git a/components/index.js b/components/index.js new file mode 100644 index 00000000..0c35efac --- /dev/null +++ b/components/index.js @@ -0,0 +1,154 @@ +/* eslint comma-dangle: ["error", "always-multiline"] */ + +// 组件引入 +import './_style/global.styl' +import {warn} from './_util' +import Button from './button' +import Icon from './icon' +import Popup from './popup' +import PopupTitleBar from './popup-title-bar' +import ActionBar from './action-bar' +import ActionSheet from './action-sheet' +import DropMenu from './drop-menu' +import TabBar from './tab-bar' +import Picker from './picker' +import Selector from './selector' +import Swiper from './swiper' +import SwiperItem from './swiper-item' +import Toast from './toast' +import Tip from './tip' +import Tabs from './tabs' +import Tag from './tag' +import InputItem from './input-item' +import Stepper from './stepper' +import Steps from './steps' +import NoticeBar from './notice-bar' +import ImageReader from './image-reader' +import ImageViewer from './image-viewer' +import NumberKeyboard from './number-keyboard' +import Landscape from './landscape' +import ResultPage from './result-page' +import TabPicker from './tab-picker' +import Dialog from './dialog' +import Field from './field' +import FieldItem from './field-item' +import Switch from './switch' +import Agree from './agree' +import Radio from './radio' +import DatePicker from './date-picker' +import Captcha from './captcha' +import Codebox from './codebox' +import Cashier from './cashier' +import Chart from './chart' +/* @init<%import ${componentNameUpper} from './${componentName}'%> */ + +// 全量引入提醒 +warn( + 'You are using a whole package of mand-mobile, ' + + 'please use https://www.npmjs.com/package/babel-plugin-import to reduce app bundle size.', + 'warn', +) + +/* global MAN_VERSION */ +const version = /* @echo MAN_VERSION */ MAN_VERSION + +// 单个组件暴露 +export const components = { + Button, + Icon, + Popup, + PopupTitleBar, + ActionBar, + ActionSheet, + DropMenu, + Picker, + Selector, + TabBar, + Swiper, + SwiperItem, + Tip, + Tabs, + Tag, + InputItem, + Stepper, + Steps, + NoticeBar, + ImageReader, + ImageViewer, + NumberKeyboard, + Landscape, + ResultPage, + TabPicker, + Dialog, + Field, + FieldItem, + Switch, + Agree, + Radio, + DatePicker, + Captcha, + Codebox, + Cashier, + Chart, + /* @init<%${componentNameUpper},%> */ +} + +// 定义插件安装方法 +const install = function(Vue) { + if (!Vue || install.installed) { + return + } + + components.forEach(component => { + component.name && Vue.component(component.name, component) + }) +} + +// 集合组件暴露 +export { + install, + version, + Button, + Icon, + Popup, + PopupTitleBar, + ActionBar, + ActionSheet, + DropMenu, + Picker, + Selector, + TabBar, + Swiper, + SwiperItem, + Toast, + Tip, + Tabs, + Tag, + InputItem, + Stepper, + Steps, + NoticeBar, + ImageReader, + ImageViewer, + NumberKeyboard, + Landscape, + ResultPage, + TabPicker, + Dialog, + Field, + FieldItem, + Switch, + Agree, + Radio, + DatePicker, + Captcha, + Codebox, + Cashier, + Chart, + /* @init<%${componentNameUpper},%> */ +} + +export default { + install, + version, +} diff --git a/components/input-item/README.md b/components/input-item/README.md new file mode 100644 index 00000000..29e4236c --- /dev/null +++ b/components/input-item/README.md @@ -0,0 +1,81 @@ +--- +title: InputItem 输入框 +preview: https://didi.github.io/mand-mobile/examples/input-item +--- + +单行文本输入框,支持特殊场景文本格式化 + +### 引入 + +```javascript +import { InputItem } from 'mand-mobile' + +Vue.component(InputItem.name, InputItem) +``` + +### 代码演示 + + +### API + +#### InputItem Props +|属性 | 说明 | 类型 | 默认值 | 备注| +|----|-----|------|------|------| +|type|表单类型,特殊类型自带文本格式化|String|`text`|`text`,`bankCard`,`phone`,
`money`,`password`| +|name|表单名称|String|-|事件入参之一,可用于区分表单组件| +|v-model|表单值|String|-|-| +|title|表单左侧标题|String|-|可以传入`HtmlFragment`,也可直接使用`slot left`代替| +|placeholder|表单占位符|String|-|-| +|maxlength|表单最大字符数|String/Number|-|`phone`类型固定为11| +|size|表单尺寸|String|`normal`|`large`,`normal`| +|align|表单文本对齐方式|String|`left`|`left`,`center`,`right`| +|error|表单错误提示信息|String|-|-| +|readonly|表单是否只读|Boolean|`false`|-| +|disabled|表单是否禁用|Boolean|`false`|-| +|is-title-latent|表单标题是否隐藏|Boolean|`false`|表单获得焦点或内容不为空时展示| +|is-highlight|表单是否高亮|Boolean|`false`|只影响`placeholder`字体颜色| +|is-formative|表单文本是否根据类型自动格式化|Boolean|`type`为`bankCard`,`phone`, `money`默认为`true`,否则为`false`|-| +|formation|表单文本格式化回调方法|Function(name, curValue, curPos): {value: curValue, range: curPos}|-|传入参数`name`为表单名称,`curValue`为表单值,`curPos`为表单光标当前所在位置
返回参数`value`格式化值, `range`表单光标格式化后所在位置| +|clearable|表单是否使用清除控件|Boolean|`false`|-| +|is-virtual-keyboard|表单是否使用金融数字键盘控件|Boolean|`false`|-| +|virtual-keyboard-disorder|金融数字键盘数字键乱序|Boolean|`false`|-| +|virtual-keyboard-ok-text|金融数字键盘确认键文案|String|`确定`|-| + +#### InputItem Slots + +#### left +左侧插槽,一般用于放置图标等 + +#### right +右侧插槽,一般用于放置图标等 + +#### InputItem Methods + +##### focus() +表单获得焦点 + +##### blur() +表单失去焦点 + +##### getValue() +获取表单值 + +#### InputItem Events + +##### @focus(name) +表单获得焦点事件 + +##### @blur(name) +表单失去焦点事件 + +##### @change(name, value) +表单值变化事件 + +##### @confirm(name, value) +表单值确认事件, 仅使用金融数字键盘或组件在`form`元素内时有效 + +##### @keyup(name, event) +表单按键按下事件,仅`is-virtual-keyboard`为`false`时触发 + +##### @keydown(name, event) +表单按键释放事件,仅`is-virtual-keyboard`为`false`时触发 diff --git a/components/input-item/component.js b/components/input-item/component.js new file mode 100644 index 00000000..27d62bf4 --- /dev/null +++ b/components/input-item/component.js @@ -0,0 +1,7 @@ +export default { + 'name': 'input-item', + 'text': '输入框', + 'category': 'form', + 'description': '', + 'author': 'xuxiaoyan' +} diff --git a/components/input-item/cursor.js b/components/input-item/cursor.js new file mode 100644 index 00000000..24ee3a2f --- /dev/null +++ b/components/input-item/cursor.js @@ -0,0 +1,44 @@ +/** + * get position of input cursor + */ +export function getCursorsPosition(ctrl) { + /* istanbul ignore if */ + if (!ctrl) { + return 0 + } + let CaretPos = 0 // IE Support + /* istanbul ignore next */ + if (document.selection) { + ctrl.focus() + const Sel = document.selection.createRange() + Sel.moveStart('character', -ctrl.value.length) + CaretPos = Sel.text.length + } else if (ctrl.selectionStart || ctrl.selectionStart === '0') { + // Firefox support + CaretPos = ctrl.selectionStart + } + return CaretPos +} + +/** + * set position of input cursor + */ +export function setCursorsPosition(ctrl, pos) { + /* istanbul ignore if */ + if (!ctrl) { + return + } + setTimeout(() => { + /* istanbul ignore next */ + if (ctrl.setSelectionRange) { + ctrl.focus() + ctrl.setSelectionRange(pos, pos) + } else if (ctrl.createTextRange) { + const range = ctrl.createTextRange() + range.collapse(true) + range.moveEnd('character', pos) + range.moveStart('character', pos) + range.select() + } + }, 0) +} diff --git a/components/input-item/demo/cases/demo0.vue b/components/input-item/demo/cases/demo0.vue new file mode 100644 index 00000000..ec41c24f --- /dev/null +++ b/components/input-item/demo/cases/demo0.vue @@ -0,0 +1,60 @@ + + + \ No newline at end of file diff --git a/components/input-item/demo/cases/demo1.vue b/components/input-item/demo/cases/demo1.vue new file mode 100644 index 00000000..ff05e728 --- /dev/null +++ b/components/input-item/demo/cases/demo1.vue @@ -0,0 +1,32 @@ + + + \ No newline at end of file diff --git a/components/input-item/demo/cases/demo2.vue b/components/input-item/demo/cases/demo2.vue new file mode 100644 index 00000000..8c2899fc --- /dev/null +++ b/components/input-item/demo/cases/demo2.vue @@ -0,0 +1,56 @@ + + + \ No newline at end of file diff --git a/components/input-item/demo/cases/demo3.vue b/components/input-item/demo/cases/demo3.vue new file mode 100644 index 00000000..ee51ca2c --- /dev/null +++ b/components/input-item/demo/cases/demo3.vue @@ -0,0 +1,102 @@ + + + + + diff --git a/components/input-item/demo/cases/demo4.vue b/components/input-item/demo/cases/demo4.vue new file mode 100644 index 00000000..c27ce068 --- /dev/null +++ b/components/input-item/demo/cases/demo4.vue @@ -0,0 +1,33 @@ + + + \ No newline at end of file diff --git a/components/input-item/demo/index.vue b/components/input-item/demo/index.vue new file mode 100644 index 00000000..02488a7a --- /dev/null +++ b/components/input-item/demo/index.vue @@ -0,0 +1,20 @@ + + + diff --git a/components/input-item/index.vue b/components/input-item/index.vue new file mode 100644 index 00000000..f356d6d0 --- /dev/null +++ b/components/input-item/index.vue @@ -0,0 +1,657 @@ + + + + + diff --git a/components/input-item/keycode.js b/components/input-item/keycode.js new file mode 100644 index 00000000..8a78ddf1 --- /dev/null +++ b/components/input-item/keycode.js @@ -0,0 +1,32 @@ +// http://www.t086.com/article/4315 +export const keyCodeList = { + bankCard: ['8', '13', '48-57', '96-105', '108', '229'], + phone: ['8', '13', '48-57', '96-105', '108', '229'], + money: ['8', '13', '48-57', '96-105', '108', '110', '190', '229'], +} + +export function isValidKey(type, code) { + const list = keyCodeList[type] || '' + + if (!list) { + return true + } + + let res = false + + for (let i = 0, len = list.length; i < len; i++) { + const itemParts = list[i].split('-') + const min = +itemParts[0] + const max = +itemParts[1] || null + + if (max === null && code === min) { + res = true + break + } else if (max !== null && code >= min && code <= max) { + res = true + break + } + } + + return res +} diff --git a/components/input-item/test/index.spec.js b/components/input-item/test/index.spec.js new file mode 100644 index 00000000..aea76152 --- /dev/null +++ b/components/input-item/test/index.spec.js @@ -0,0 +1,139 @@ +import InputItem from '../index' +import triggerEvent from '../../popup/test/touch-trigger' +import {mount} from 'avoriaz' + +describe('InputItem', () => { + let wrapper + + afterEach(() => { + wrapper && wrapper.destroy() + }) + + it('create a input-item', () => { + wrapper = mount(InputItem, { + propsData: { + value: 'test', + }, + }) + + const eventStub = sinon.stub(wrapper.vm, '$emit') + const input = wrapper.find('.md-input-item-input')[0] + wrapper.vm.inputValue = '123' + // expect(eventStub.calledWith('change')).to.be.true + + input.trigger('focus') + expect(eventStub.calledWith('focus')).to.be.true + input.trigger('blur') + expect(eventStub.calledWith('blur')).to.be.true + + wrapper.vm.focus() + triggerEvent(input.element, 'keydown', 0, 0, 49) + triggerEvent(input.element, 'keyup', 0, 0, 49) + + wrapper.vm.blur() + expect(wrapper.vm.getValue()).to.equal('123') + + wrapper.setProps({value: '456'}) + expect(wrapper.vm.getValue()).to.equal('456') + }) + + it('input-item keyup/down', () => { + wrapper = mount(InputItem, { + propsData: { + type: 'bankCard', + value: '123123', + }, + }) + + const eventStub = sinon.stub(wrapper.vm, '$emit') + const input = wrapper.find('.md-input-item-input')[0] + + wrapper.vm.focus() + triggerEvent(input.element, 'keydown', 0, 0, 49) + triggerEvent(input.element, 'keyup', 0, 0, 49) + + triggerEvent(input.element, 'keydown', 0, 0, 11) + triggerEvent(input.element, 'keyup', 0, 0, 11) + + triggerEvent(input.element, 'keydown', 0, 0, 13) + triggerEvent(input.element, 'keyup', 0, 0, 13) + expect(eventStub.calledWith('confirm')).to.be.true + // wrapper.vm.blur() + }) + + it('phone input-item', () => { + wrapper = mount(InputItem, { + propsData: { + type: 'phone', + value: '123123123123123123', + }, + }) + + expect(wrapper.vm.inputValue.length).to.equal(13) + }) + + it('input-item with clear btn', () => { + wrapper = mount(InputItem, { + propsData: { + value: 'test', + clearable: true, + }, + }) + expect(wrapper.vm.isInputEmpty).to.be.false + const clearBtn = wrapper.find('.md-input-item-clear')[0] + clearBtn.trigger('click') + expect(wrapper.vm.isInputEmpty).to.be.true + }) + + it('input-item with number-keyborad', done => { + wrapper = mount(InputItem, { + propsData: { + type: 'money', + isVirtualKeyboard: true, + }, + }) + + const eventStub = sinon.stub(wrapper.vm, '$emit') + const input = wrapper.find('.md-input-item-fake')[0] + + // input.trigger('click') + + wrapper.vm.focus() + setTimeout(() => { + const keyborad = wrapper.find('.md-number-keyboard')[0] + keyborad.find('.delete')[0].trigger('click') + keyborad.find('.keyboard-number-item')[1].trigger('click') + keyborad.find('.keyboard-number-item')[9].trigger('click') + keyborad.find('.keyboard-number-item')[9].trigger('click') + keyborad.find('.keyboard-number-item')[2].trigger('click') + keyborad.find('.keyboard-number-item')[2].trigger('click') + keyborad.find('.delete')[0].trigger('click') + expect(wrapper.vm.inputValue).to.equal('2.3') + keyborad.find('.confirm')[0].trigger('click') + expect(eventStub.calledWith('confirm')).to.be.true + wrapper.vm.blur() + done() + }, 500) + // wrapper.vm.blur() + }) + + it('input-item disabled', () => { + wrapper = mount(InputItem, { + propsData: { + disabled: true, + isVirtualKeyboard: true, + value: '123', + formation(name, curValue, curPos) { + return { + value: curValue, + range: curPos, + } + }, + }, + }) + + const input = wrapper.find('.md-input-item-fake')[0] + + input.trigger('click') + }) +}) diff --git a/components/landscape/README.md b/components/landscape/README.md new file mode 100644 index 00000000..60f4c4ba --- /dev/null +++ b/components/landscape/README.md @@ -0,0 +1,34 @@ +--- +title: Landscape 输入框 +preview: https://didi.github.io/mand-mobile/examples/landscape +--- + +用于在浮层中显示广告或说明 + +### 引入 + +```javascript +import { Landscape } from 'mand-landscape' + +Vue.component(Landscape.name, Landscape) +``` + +### 代码演示 + + +### API + +#### Landscape Props +|属性 | 说明 | 类型 | 默认值| +|----|-----|------|------| +|v-model|是否展示|Boolean|`false`| +|has-mask|是否有蒙层|Boolean|`true`| +|scroll|内容区域是否可以滚动|Boolean|`false`| + +#### Landscape Events + +##### @show() +弹出层展示事件 + +##### @hide() +弹出层隐藏事件 diff --git a/components/landscape/component.js b/components/landscape/component.js new file mode 100644 index 00000000..e542f64a --- /dev/null +++ b/components/landscape/component.js @@ -0,0 +1,7 @@ +export default { + 'name': 'landscape', + 'text': '压屏', + 'category': 'business', + 'description': '用于展示压屏广告或通知的组件。', + 'author': 'zhaozhe' +} diff --git a/components/landscape/demo/cases/demo0.vue b/components/landscape/demo/cases/demo0.vue new file mode 100644 index 00000000..92d4acb9 --- /dev/null +++ b/components/landscape/demo/cases/demo0.vue @@ -0,0 +1,67 @@ + + + + + \ No newline at end of file diff --git a/components/landscape/demo/index.vue b/components/landscape/demo/index.vue new file mode 100644 index 00000000..b038da96 --- /dev/null +++ b/components/landscape/demo/index.vue @@ -0,0 +1,17 @@ + + + diff --git a/components/landscape/index.vue b/components/landscape/index.vue new file mode 100644 index 00000000..943dbbbb --- /dev/null +++ b/components/landscape/index.vue @@ -0,0 +1,99 @@ + + + + + diff --git a/components/landscape/test/index.spec.js b/components/landscape/test/index.spec.js new file mode 100644 index 00000000..17db5b12 --- /dev/null +++ b/components/landscape/test/index.spec.js @@ -0,0 +1,49 @@ +import Landscape from '../index' +import {mount} from 'avoriaz' + +describe('Landscape', () => { + let wrapper + + afterEach(() => { + wrapper && wrapper.destroy() + }) + + it('create a simple landscape', () => { + wrapper = mount(Landscape) + + expect(wrapper.hasClass('md-landscape')).to.be.true + }) + + it('create a simple landscape and open it', done => { + wrapper = mount(Landscape, { + propsData: { + value: false, + }, + }) + expect(wrapper.find('.md-popup-mask')[0].element.style.display).to.equal('none') + + wrapper.vm.value = true + + setTimeout(() => { + expect(wrapper.find('.md-popup-mask')[0].element.style.display).to.equal('') + done() + }, 300) + }) + + it('create a simple landscape and close it', done => { + wrapper = mount(Landscape, { + propsData: { + value: true, + }, + }) + setTimeout(() => { + expect(wrapper.find('.md-popup-mask')[0].element.style.display).to.equal('') + wrapper.find('.close')[0].trigger('click') + }, 300) + + setTimeout(() => { + expect(wrapper.find('.md-popup-mask')[0].element.style.display).to.equal('none') + done() + }, 600) + }) +}) diff --git a/components/notice-bar/README.md b/components/notice-bar/README.md new file mode 100644 index 00000000..485312b8 --- /dev/null +++ b/components/notice-bar/README.md @@ -0,0 +1,28 @@ +--- +title: Notice 通告栏 +preview: https://didi.github.io/mand-mobile/examples/notice-bar +--- + +通常用于系统提醒、活动提醒等通知 + +### 引入 + +```javascript +import { NoticeBar } from 'mand-mobile' + +Vue.component(NoticeBar.name, NoticeBar) +``` + + +### 代码演示 + + +### API + +#### NoticeBar Props +|属性 | 说明 | 类型 | 默认值 | 备注| +|----|-----|------|------|------| +|closable|是否可关闭|Boolean|`true`|-| +|time|显示时长|Number|`0`|单位为`ms`,不需要自动消失可将其置为`0`| +|icon|在开始位置的图标样式|String|`circle-alert`|-| + diff --git a/components/notice-bar/component.js b/components/notice-bar/component.js new file mode 100644 index 00000000..51cdd480 --- /dev/null +++ b/components/notice-bar/component.js @@ -0,0 +1,7 @@ +export default { + 'name': 'notice-bar', + 'text': '通告栏', + 'category': 'basic', + 'description': '通告栏', + 'author': 'linyufei' +} diff --git a/components/notice-bar/demo/cases/demo0.vue b/components/notice-bar/demo/cases/demo0.vue new file mode 100644 index 00000000..55172e41 --- /dev/null +++ b/components/notice-bar/demo/cases/demo0.vue @@ -0,0 +1,16 @@ + + + \ No newline at end of file diff --git a/components/notice-bar/demo/cases/demo1.vue b/components/notice-bar/demo/cases/demo1.vue new file mode 100644 index 00000000..4e811580 --- /dev/null +++ b/components/notice-bar/demo/cases/demo1.vue @@ -0,0 +1,21 @@ + + + diff --git a/components/notice-bar/demo/cases/demo2.vue b/components/notice-bar/demo/cases/demo2.vue new file mode 100644 index 00000000..7ad1cdfe --- /dev/null +++ b/components/notice-bar/demo/cases/demo2.vue @@ -0,0 +1,17 @@ + + + \ No newline at end of file diff --git a/components/notice-bar/demo/index.vue b/components/notice-bar/demo/index.vue new file mode 100644 index 00000000..6ed5e056 --- /dev/null +++ b/components/notice-bar/demo/index.vue @@ -0,0 +1,19 @@ + + + diff --git a/components/notice-bar/index.vue b/components/notice-bar/index.vue new file mode 100644 index 00000000..8df72c3a --- /dev/null +++ b/components/notice-bar/index.vue @@ -0,0 +1,79 @@ + + + + + diff --git a/components/notice-bar/test/index.spec.js b/components/notice-bar/test/index.spec.js new file mode 100644 index 00000000..f379a56b --- /dev/null +++ b/components/notice-bar/test/index.spec.js @@ -0,0 +1,46 @@ +import NoticeBar from '../index' +import {mount} from 'avoriaz' + +describe('NoticeBar', () => { + let wrapper + + afterEach(() => { + wrapper && wrapper.destroy() + }) + + it('create a simple notice-bar', () => { + wrapper = mount(NoticeBar) + + expect(wrapper.hasClass('md-notice-bar')).to.be.true + expect(wrapper.vm.closable).to.equal(true) + expect(wrapper.vm.time).to.equal(0) + expect(wrapper.vm.icon).to.equal('circle-alert') + }) + + it('mount time is not null', done => { + wrapper = mount(NoticeBar, { + propsData: { + time: 500, + }, + }) + setTimeout(() => { + expect(wrapper.vm.isShow).to.equal(false) + done() + }, 1000) + }) + + it('notice-bar method hide', done => { + wrapper = mount(NoticeBar) + wrapper.vm.$_hide(500) + setTimeout(() => { + expect(wrapper.vm.isShow).to.equal(false) + done() + }, 1000) + }) + + it('notice-bar method close', () => { + wrapper = mount(NoticeBar) + wrapper.vm.$_close() + expect(wrapper.vm.isShow).to.equal(false) + }) +}) diff --git a/components/number-keyboard/README.md b/components/number-keyboard/README.md new file mode 100644 index 00000000..a3027014 --- /dev/null +++ b/components/number-keyboard/README.md @@ -0,0 +1,51 @@ +--- +title: NumberKeyboard 数字键盘 +preview: https://didi.github.io/mand-mobile/examples/number-keyboard +--- + +一般用于密码,验证码或支付金额输入等金融场景 + +### 引入 + +```javascript +import { NumberKeyboard } from 'mand-mobile' + +Vue.component(NumberKeyboard.name, NumberKeyboard) +``` + +### 代码演示 + + +### API + +#### NumberKeyboard Props +|属性 | 说明 | 类型 | 默认值| 备注| +|----|-----|------|------|------| +|v-model|键盘是否展示|Boolean|`false`|-| +|is-view|是否内嵌在页面内展示,否则以弹层形式|Boolean|`false`|-| +|type|键盘类型|String|`professional`|`professional`有确认键和小数点常用于价格或金额输入,`simple`一般用于密码或验证码输入| +|disorder|键盘数字键是否乱序|Boolean|`false`| -| +|ok-text|键盘确认键文案|String|`确认`|-| + +#### NumberKeyboard Methods + +##### show() +展示键盘 + +##### hide() +隐藏键盘 + +#### NumberKeyboard Events + +##### @enter(val) +数字键点击事件 + +属性 | 说明 | 类型 +----|-----|------ +val | 数字 | Number + +##### @delete() +回退键点击事件 + +##### @confirm() +确认键点击事件 diff --git a/components/number-keyboard/component.js b/components/number-keyboard/component.js new file mode 100644 index 00000000..3e53fcc3 --- /dev/null +++ b/components/number-keyboard/component.js @@ -0,0 +1,7 @@ +export default { + 'name': 'number-keyboard', + 'text': '数字键盘', + 'category': 'form', + 'description': '', + 'author': 'xuxiaoyan' +} diff --git a/components/number-keyboard/demo/cases/demo0.vue b/components/number-keyboard/demo/cases/demo0.vue new file mode 100644 index 00000000..4ef28e1d --- /dev/null +++ b/components/number-keyboard/demo/cases/demo0.vue @@ -0,0 +1,52 @@ + + + + + diff --git a/components/number-keyboard/demo/cases/demo1.vue b/components/number-keyboard/demo/cases/demo1.vue new file mode 100644 index 00000000..40fb300f --- /dev/null +++ b/components/number-keyboard/demo/cases/demo1.vue @@ -0,0 +1,54 @@ + + + + + \ No newline at end of file diff --git a/components/number-keyboard/demo/cases/demo2.vue b/components/number-keyboard/demo/cases/demo2.vue new file mode 100644 index 00000000..705ccf37 --- /dev/null +++ b/components/number-keyboard/demo/cases/demo2.vue @@ -0,0 +1,53 @@ + + + + + \ No newline at end of file diff --git a/components/number-keyboard/demo/index.vue b/components/number-keyboard/demo/index.vue new file mode 100644 index 00000000..ea3be7ab --- /dev/null +++ b/components/number-keyboard/demo/index.vue @@ -0,0 +1,18 @@ + + + diff --git a/components/number-keyboard/index.vue b/components/number-keyboard/index.vue new file mode 100644 index 00000000..0330123b --- /dev/null +++ b/components/number-keyboard/index.vue @@ -0,0 +1,119 @@ + + + + + diff --git a/components/number-keyboard/keyboard.vue b/components/number-keyboard/keyboard.vue new file mode 100644 index 00000000..aa0b0ee6 --- /dev/null +++ b/components/number-keyboard/keyboard.vue @@ -0,0 +1,187 @@ + + + + + diff --git a/components/number-keyboard/test/index.spec.js b/components/number-keyboard/test/index.spec.js new file mode 100644 index 00000000..75b86113 --- /dev/null +++ b/components/number-keyboard/test/index.spec.js @@ -0,0 +1,56 @@ +import NumberKeyboard from '../index' +import {mount} from 'avoriaz' + +describe('NumberKeyboard', () => { + let wrapper + + afterEach(() => { + wrapper && wrapper.destroy() + }) + + it('create a number-keyboard', done => { + wrapper = mount(NumberKeyboard) + + const eventStub = sinon.stub(wrapper.vm, '$emit') + const numberBtn = wrapper.find('.keyboard-number-item')[1] + + wrapper.vm.disorder = true + wrapper.vm.value = true + setTimeout(() => { + numberBtn.trigger('click') + expect(eventStub.calledWith('enter')).to.be.true + + wrapper.vm.isKeyboardShow = false + expect(eventStub.calledWith('input')).to.be.true + done() + }, 300) + }) + + it('number-keyboard delete', done => { + wrapper = mount(NumberKeyboard) + + const eventStub = sinon.stub(wrapper.vm, '$emit') + const deleteBtn = wrapper.find('.keyboard-operate-item')[0] + + wrapper.vm.value = true + setTimeout(() => { + deleteBtn.trigger('click') + expect(eventStub.calledWith('delete')).to.be.true + done() + }, 300) + }) + + it('number-keyboard confirm', done => { + wrapper = mount(NumberKeyboard) + + const eventStub = sinon.stub(wrapper.vm, '$emit') + const confirmBtn = wrapper.find('.keyboard-operate-item')[1] + + wrapper.vm.value = true + setTimeout(() => { + confirmBtn.trigger('click') + expect(eventStub.calledWith('confirm')).to.be.true + done() + }, 300) + }) +}) diff --git a/components/picker/README.md b/components/picker/README.md new file mode 100644 index 00000000..455ce35a --- /dev/null +++ b/components/picker/README.md @@ -0,0 +1,172 @@ +--- +title: Picker 选择器 +preview: https://didi.github.io/mand-mobile/examples/picker +--- + +滚动多列选择 + +### 引入 + +```javascript +import { Picker } from 'mand-mobile' + +Vue.component(Picker.name, Picker) +``` + +### 代码演示 + + +### API + +#### Picker Props +|属性 | 说明 | 类型 | 默认值 | 备注| +|----|-----|------|------|------| +|v-model|选择器是否可见|Boolean|`false`|-| +|data|数据源|Array<{value, lable, ...}>[]|`[]`|-| +|cols|数据列数|Number|`1`|-| +|default-index|选择器各列初始选中项索引|Array|`[]`|-| +|invalid-index|选择器各列不可用选项索引|Array|`[]`|某列多个不可用项使用数组,单个使用数字, 如`[[1,2], 2]`| +|is-view|是否内嵌在页面内展示,否则以弹层形式|Boolean|`false`|-| +|is-cascade|各列数据是否级联|Boolean|`false`|级联数据格式见附录| +|title|选择器标题|String|-|-| +|ok-text|选择器确认文案|String|`确认`|-| +|cancel-text|选择器取消文案|String|`取消`|-| + +#### Picker Methods + +##### refresh(callback, startColumnIndex) +重新初始化选择器,如更新`data`, `default-index`或`invalid-index` + +|参数 | 说明 | 类型| +|----|-----|------| +|callback|初始化完成回调|Function| +|startColumnIndex|从某列开始重置,默认为0|Function| + +##### getColumnValue(index): activeItemValue +获取某列当前选中项的值,需在`initialed`事件触发之后或异步调用 + +|参数 | 说明 | 类型| +|----|-----|------| +|index|列索引|Number| + +返回 + +|属性 | 说明 | 类型| +|----|-----|------| +|activeItemValue|选中项的值|Object: {value, lable, ...}| + +##### getColumnValues(): columnsValue +获取所有列选中项的值,需在`initialed`事件触发之后或异步调用 + +返回 + +|属性 | 说明 | 类型| +|----|-----|------| +|columnsValue|所有列选中项的值|Array<{value, lable, ...}>| + +##### getColumnIndex(index): activeItemIndex +获取某列当前选中项的索引值,需在`initialed`事件触发之后或异步调用 + +|参数 | 说明 | 类型| +|----|-----|------| +|index|列索引|Number| + +返回 + +|属性 | 说明 | 类型| +|----|-----|------| +|activeItemIndex|选中项的索引值|Number| + +##### getColumnIndexs(): columnsIndex +获取所有列选中项的索引值,需在`initialed`事件触发之后或异步调用 + +返回 + +|属性 | 说明 | 类型| +|----|-----|------| +|columnsIndex|所有列选中项的索引值|Array| + +##### setColumnValues(index, values, callback) +设置某列数据 + +|参数 | 说明 | 类型| +|----|-----|------| +|index|列索引|Number| +|values|列数据|Array<{value, lable, ...}>| +|callback|列数据设置完成回调|Function| + +#### Picker Events + +##### @initialed() +选择器数据初始化完成事件,可调用`getColumnIndex`, `getColumnIndexs`, `getColumnValue`, `getColumnValues`方法 + +##### @change(columnIndex, itemIndex, value) +选择器选中项更改事件 + +|参数 | 说明 | 类型| +|----|-----|------| +|columnIndex|更改列的索引值|Number| +|itemIndex|更改列选中项的索引值|Number| +|value|更改列选中项的的值|Object: {value, lable, ...}| + +##### @confirm(columnsValue) +选择器确认选择事件(仅`is-view`为`false`) + +|参数 | 说明 | 类型| +|----|-----|------| +|columnsValue|所有列选中项的值|Array<{value, lable, ...}>| + +##### @cancel() +选择器取消选择事件(仅`is-view`为`false`) + +##### @show() +选择器弹层展示事件(仅`is-view`为`false`) + +##### @hide() +选择器弹层隐藏事件(仅`is-view`为`false`) + +### 附录 + +* 非级联数据源数据格式 + +```javascript +[ + [ + { + // 选项展示文案 + "text": "", + // 以下自定义字段 + "value": "" + }, + // ... + ], + // ... +] +``` + +* 级联数据源数据格式 + +```javascript +[ + [ + { + // 选项展示文案 + "text": "", + // 第二列对应数据 + "children": [ + { + "text": "", + // 第三列对应数据 + "children": [ + // ... + ] + }, + // ... + ] + // 以下自定义字段 + "value": "" + }, + // ... + ] +] +``` diff --git a/components/picker/cascade.js b/components/picker/cascade.js new file mode 100644 index 00000000..b226ffdb --- /dev/null +++ b/components/picker/cascade.js @@ -0,0 +1,37 @@ +import {warn} from '../_util' + +const defaultOptions = { + currentLevel: 0, + maxLevel: 0, + values: [], + defaultIndex: [], +} + +/** + * cascade column by set value of following columns + * @param {*} picker instance of picker-column + * @param {*} options { currentLevel, maxLevel, values } + * @param {*} fn + */ +export default function(picker, options = {}, fn) { + // options = {...defaultOptions, ...options} + options = Object.assign({}, defaultOptions, options) + + /* istanbul ignore if */ + if (!picker) { + warn('cascade: picker is undefined') + return + } + + let values = options.values + + /* istanbul ignore next */ + for (let i = options.currentLevel + 1; i < options.maxLevel; i++) { + const activeIndex = options.defaultIndex[i] || 0 + const columnValues = values.children || [] + picker.setColumnValues(i, columnValues) + values = columnValues[activeIndex] || [] + } + + fn && fn() +} diff --git a/components/picker/component.js b/components/picker/component.js new file mode 100644 index 00000000..09e997a5 --- /dev/null +++ b/components/picker/component.js @@ -0,0 +1,7 @@ +export default { + 'name': 'picker', + 'text': '选择器', + 'category': 'feedback', + 'description': '', + 'author': 'xuxiaoyan' +} diff --git a/components/picker/demo/cases/demo0.vue b/components/picker/demo/cases/demo0.vue new file mode 100644 index 00000000..f424d954 --- /dev/null +++ b/components/picker/demo/cases/demo0.vue @@ -0,0 +1,63 @@ + + + \ No newline at end of file diff --git a/components/picker/demo/cases/demo1.vue b/components/picker/demo/cases/demo1.vue new file mode 100644 index 00000000..5b49dcae --- /dev/null +++ b/components/picker/demo/cases/demo1.vue @@ -0,0 +1,74 @@ + + + \ No newline at end of file diff --git a/components/picker/demo/cases/demo2.vue b/components/picker/demo/cases/demo2.vue new file mode 100644 index 00000000..d135e541 --- /dev/null +++ b/components/picker/demo/cases/demo2.vue @@ -0,0 +1,54 @@ + + + \ No newline at end of file diff --git a/components/picker/demo/data/district.js b/components/picker/demo/data/district.js new file mode 100644 index 00000000..0b34f06e --- /dev/null +++ b/components/picker/demo/data/district.js @@ -0,0 +1,1261 @@ +export default [[{ + "value": "340000", + "label": "安徽省", + "children": [{ + "value": "341500", + "label": "六安市", + "children": [{ + "value": "341522", + "label": "霍邱县", + "children": [] + }, { + "value": "341502", + "label": "金安区", + "children": [] + }, { + "value": "341524", + "label": "金寨县", + "children": [] + }, { + "value": "341526", + "label": "其它区", + "children": [] + }, { + "value": "341521", + "label": "寿县", + "children": [] + }, { + "value": "341523", + "label": "舒城县", + "children": [] + }, { + "value": "341503", + "label": "裕安区", + "children": [] + }] + }, { + "value": "340500", + "label": "马鞍山市", + "children": [{ + "value": "340506", + "label": "博望区", + "children": [] + }] + }, { + "value": "341800", + "label": "宣城市", + "children": [{ + "value": "341822", + "label": "广德县", + "children": [] + }, { + "value": "341824", + "label": "绩溪县", + "children": [] + }, { + "value": "341825", + "label": "旌德县", + "children": [] + }] + }] +}, { + "value": "820000", + "label": "澳门特别行政区", + "children": [{ + "value": "820100", + "label": "澳门半岛", + "children": [] + }, { + "value": "820200", + "label": "离岛", + "children": [] + }] +}, { + "value": "110000", + "label": "北京", + "children": [{ + "value": "110100", + "label": "北京市", + "children": [{ + "value": "110114", + "label": "昌平区", + "children": [] + }, { + "value": "110105", + "label": "朝阳区", + "children": [] + }, { + "value": "110103", + "label": "崇文区", + "children": [] + }, { + "value": "110115", + "label": "大兴区", + "children": [] + }, { + "value": "110101", + "label": "东城区", + "children": [] + }, { + "value": "110111", + "label": "房山区", + "children": [] + }, { + "value": "110106", + "label": "丰台区", + "children": [] + }, { + "value": "110108", + "label": "海淀区", + "children": [] + }, { + "value": "110116", + "label": "怀柔区", + "children": [] + }, { + "value": "110109", + "label": "门头沟区", + "children": [] + }, { + "value": "110228", + "label": "密云县", + "children": [] + }, { + "value": "110117", + "label": "平谷区", + "children": [] + }, { + "value": "110230", + "label": "其它区", + "children": [] + }, { + "value": "110107", + "label": "石景山区", + "children": [] + }, { + "value": "110113", + "label": "顺义区", + "children": [] + }, { + "value": "110112", + "label": "通州区", + "children": [] + }, { + "value": "110102", + "label": "西城区", + "children": [] + }, { + "value": "110104", + "label": "宣武区", + "children": [] + }, { + "value": "110229", + "label": "延庆县", + "children": [] + }] + }] +}, { + "value": "450000", + "label": "广西壮族自治区", + "children": [{ + "value": "450500", + "label": "北海市", + "children": [{ + "value": "450502", + "label": "海城区", + "children": [] + }, { + "value": "450521", + "label": "合浦县", + "children": [] + }, { + "value": "450522", + "label": "其它区", + "children": [] + }, { + "value": "450512", + "label": "铁山港区", + "children": [] + }, { + "value": "450503", + "label": "银海区", + "children": [] + }] + }, { + "value": "451000", + "label": "百色市", + "children": [{ + "value": "451024", + "label": "德保县", + "children": [] + }, { + "value": "451025", + "label": "靖西县", + "children": [] + }, { + "value": "451028", + "label": "乐业县", + "children": [] + }, { + "value": "451027", + "label": "凌云县", + "children": [] + }, { + "value": "451031", + "label": "隆林各族自治县", + "children": [] + }, { + "value": "451026", + "label": "那坡县", + "children": [] + }, { + "value": "451023", + "label": "平果县", + "children": [] + }, { + "value": "451032", + "label": "其它区", + "children": [] + }, { + "value": "451022", + "label": "田东县", + "children": [] + }, { + "value": "451029", + "label": "田林县", + "children": [] + }, { + "value": "451021", + "label": "田阳县", + "children": [] + }, { + "value": "451030", + "label": "西林县", + "children": [] + }, { + "value": "451002", + "label": "右江区", + "children": [] + }] + }, { + "value": "451400", + "label": "崇左市", + "children": [{ + "value": "451424", + "label": "大新县", + "children": [] + }, { + "value": "451421", + "label": "扶绥县", + "children": [] + }, { + "value": "451402", + "label": "江州区", + "children": [] + }, { + "value": "451423", + "label": "龙州县", + "children": [] + }, { + "value": "451422", + "label": "宁明县", + "children": [] + }, { + "value": "451481", + "label": "凭祥市", + "children": [] + }, { + "value": "451482", + "label": "其它区", + "children": [] + }, { + "value": "451425", + "label": "天等县", + "children": [] + }] + }, { + "value": "450600", + "label": "防城港市", + "children": [{ + "value": "450681", + "label": "东兴市", + "children": [] + }, { + "value": "450603", + "label": "防城区", + "children": [] + }, { + "value": "450602", + "label": "港口区", + "children": [] + }, { + "value": "450682", + "label": "其它区", + "children": [] + }, { + "value": "450621", + "label": "上思县", + "children": [] + }] + }, { + "value": "450800", + "label": "贵港市", + "children": [{ + "value": "450802", + "label": "港北区", + "children": [] + }, { + "value": "450803", + "label": "港南区", + "children": [] + }, { + "value": "450881", + "label": "桂平市", + "children": [] + }, { + "value": "450821", + "label": "平南县", + "children": [] + }, { + "value": "450882", + "label": "其它区", + "children": [] + }, { + "value": "450804", + "label": "覃塘区", + "children": [] + }] + }, { + "value": "450300", + "label": "桂林市", + "children": [{ + "value": "450303", + "label": "叠彩区", + "children": [] + }, { + "value": "450332", + "label": "恭城瑶族自治县", + "children": [] + }, { + "value": "450327", + "label": "灌阳县", + "children": [] + }, { + "value": "450331", + "label": "荔浦县", + "children": [] + }, { + "value": "450322", + "label": "临桂区", + "children": [] + }, { + "value": "450323", + "label": "灵川县", + "children": [] + }, { + "value": "450328", + "label": "龙胜各族自治县", + "children": [] + }, { + "value": "450330", + "label": "平乐县", + "children": [] + }, { + "value": "450333", + "label": "其它区", + "children": [] + }, { + "value": "450305", + "label": "七星区", + "children": [] + }, { + "value": "450324", + "label": "全州县", + "children": [] + }, { + "value": "450304", + "label": "象山区", + "children": [] + }, { + "value": "450325", + "label": "兴安县", + "children": [] + }, { + "value": "450302", + "label": "秀峰区", + "children": [] + }, { + "value": "450311", + "label": "雁山区", + "children": [] + }, { + "value": "450321", + "label": "阳朔县", + "children": [] + }, { + "value": "450326", + "label": "永福县", + "children": [] + }, { + "value": "450329", + "label": "资源县", + "children": [] + }] + }, { + "value": "451200", + "label": "河池市", + "children": [{ + "value": "451227", + "label": "巴马瑶族自治县", + "children": [] + }, { + "value": "451229", + "label": "大化瑶族自治县", + "children": [] + }, { + "value": "451224", + "label": "东兰县", + "children": [] + }, { + "value": "451228", + "label": "都安瑶族自治县", + "children": [] + }, { + "value": "451223", + "label": "凤山县", + "children": [] + }, { + "value": "451226", + "label": "环江毛南族自治县", + "children": [] + }, { + "value": "451202", + "label": "金城江区", + "children": [] + }, { + "value": "451225", + "label": "罗城仫佬族自治县", + "children": [] + }, { + "value": "451221", + "label": "南丹县", + "children": [] + }, { + "value": "451282", + "label": "其它区", + "children": [] + }, { + "value": "451222", + "label": "天峨县", + "children": [] + }, { + "value": "451281", + "label": "宜州市", + "children": [] + }] + }, { + "value": "451100", + "label": "贺州市", + "children": [{ + "value": "451102", + "label": "八步区", + "children": [] + }, { + "value": "451123", + "label": "富川瑶族自治县", + "children": [] + }, { + "value": "451119", + "label": "平桂管理区", + "children": [] + }, { + "value": "451124", + "label": "其它区", + "children": [] + }, { + "value": "451121", + "label": "昭平县", + "children": [] + }, { + "value": "451122", + "label": "钟山县", + "children": [] + }] + }, { + "value": "451300", + "label": "来宾市", + "children": [{ + "value": "451381", + "label": "合山市", + "children": [] + }, { + "value": "451324", + "label": "金秀瑶族自治县", + "children": [] + }, { + "value": "451382", + "label": "其它区", + "children": [] + }, { + "value": "451323", + "label": "武宣县", + "children": [] + }, { + "value": "451322", + "label": "象州县", + "children": [] + }, { + "value": "451321", + "label": "忻城县", + "children": [] + }, { + "value": "451302", + "label": "兴宾区", + "children": [] + }] + }, { + "value": "450200", + "label": "柳州市", + "children": [{ + "value": "450202", + "label": "城中区", + "children": [] + }, { + "value": "450205", + "label": "柳北区", + "children": [] + }, { + "value": "450222", + "label": "柳城县", + "children": [] + }, { + "value": "450221", + "label": "柳江县", + "children": [] + }, { + "value": "450204", + "label": "柳南区", + "children": [] + }, { + "value": "450223", + "label": "鹿寨县", + "children": [] + }, { + "value": "450227", + "label": "其它区", + "children": [] + }, { + "value": "450224", + "label": "融安县", + "children": [] + }, { + "value": "450225", + "label": "融水苗族自治县", + "children": [] + }, { + "value": "450226", + "label": "三江侗族自治县", + "children": [] + }, { + "value": "450203", + "label": "鱼峰区", + "children": [] + }] + }, { + "value": "450100", + "label": "南宁市", + "children": [{ + "value": "450126", + "label": "宾阳县", + "children": [] + }, { + "value": "450127", + "label": "横县", + "children": [] + }, { + "value": "450105", + "label": "江南区", + "children": [] + }, { + "value": "450108", + "label": "良庆区", + "children": [] + }, { + "value": "450123", + "label": "隆安县", + "children": [] + }, { + "value": "450124", + "label": "马山县", + "children": [] + }, { + "value": "450128", + "label": "其它区", + "children": [] + }, { + "value": "450103", + "label": "青秀区", + "children": [] + }, { + "value": "450125", + "label": "上林县", + "children": [] + }, { + "value": "450122", + "label": "武鸣区", + "children": [] + }, { + "value": "450107", + "label": "西乡塘区", + "children": [] + }, { + "value": "450102", + "label": "兴宁区", + "children": [] + }, { + "value": "450109", + "label": "邕宁区", + "children": [] + }] + }, { + "value": "450700", + "label": "钦州市", + "children": [{ + "value": "450721", + "label": "灵山县", + "children": [] + }, { + "value": "450722", + "label": "浦北县", + "children": [] + }, { + "value": "450723", + "label": "其它区", + "children": [] + }, { + "value": "450703", + "label": "钦北区", + "children": [] + }, { + "value": "450702", + "label": "钦南区", + "children": [] + }] + }, { + "value": "450400", + "label": "梧州市", + "children": [{ + "value": "450421", + "label": "苍梧县", + "children": [] + }, { + "value": "450481", + "label": "岑溪市", + "children": [] + }, { + "value": "450405", + "label": "长洲区", + "children": [] + }, { + "value": "450404", + "label": "蝶山区", + "children": [] + }, { + "value": "450406", + "label": "龙圩区", + "children": [] + }, { + "value": "450423", + "label": "蒙山县", + "children": [] + }, { + "value": "450482", + "label": "其它区", + "children": [] + }, { + "value": "450422", + "label": "藤县", + "children": [] + }, { + "value": "450403", + "label": "万秀区", + "children": [] + }] + }, { + "value": "450900", + "label": "玉林市", + "children": [{ + "value": "450981", + "label": "北流市", + "children": [] + }, { + "value": "450923", + "label": "博白县", + "children": [] + }, { + "value": "450903", + "label": "福绵区", + "children": [] + }, { + "value": "450922", + "label": "陆川县", + "children": [] + }, { + "value": "450982", + "label": "其它区", + "children": [] + }, { + "value": "450921", + "label": "容县", + "children": [] + }, { + "value": "450924", + "label": "兴业县", + "children": [] + }, { + "value": "450902", + "label": "玉州区", + "children": [] + }] + }] +}, { + "value": "810000", + "label": "香港特别行政区", + "children": [{ + "value": "810200", + "label": "九龙", + "children": [{ + "value": "810205", + "label": "观塘区", + "children": [] + }, { + "value": "810204", + "label": "黄大仙区", + "children": [] + }, { + "value": "810201", + "label": "九龙城区", + "children": [] + }, { + "value": "810203", + "label": "深水埗区", + "children": [] + }, { + "value": "810202", + "label": "油尖旺区", + "children": [] + }] + }, { + "value": "810100", + "label": "香港岛", + "children": [{ + "value": "810103", + "label": "东区", + "children": [] + }, { + "value": "810104", + "label": "南区", + "children": [] + }, { + "value": "810102", + "label": "湾仔", + "children": [] + }, { + "value": "810101", + "label": "中西区", + "children": [] + }] + }, { + "value": "810300", + "label": "新界", + "children": [{ + "value": "810301", + "label": "北区", + "children": [] + }, { + "value": "810302", + "label": "大埔区", + "children": [] + }, { + "value": "810308", + "label": "葵青区", + "children": [] + }, { + "value": "810309", + "label": "离岛区", + "children": [] + }, { + "value": "810307", + "label": "荃湾区", + "children": [] + }, { + "value": "810303", + "label": "沙田区", + "children": [] + }, { + "value": "810306", + "label": "屯门区", + "children": [] + }, { + "value": "810304", + "label": "西贡区", + "children": [] + }, { + "value": "810305", + "label": "元朗区", + "children": [] + }] + }] +}, { + "value": "330000", + "label": "浙江省", + "children": [{ + "value": "330100", + "label": "杭州市", + "children": [{ + "value": "330108", + "label": "滨江区", + "children": [] + }, { + "value": "330127", + "label": "淳安县", + "children": [] + }, { + "value": "330183", + "label": "富阳区", + "children": [] + }, { + "value": "330105", + "label": "拱墅区", + "children": [] + }, { + "value": "330182", + "label": "建德市", + "children": [] + }, { + "value": "330104", + "label": "江干区", + "children": [] + }, { + "value": "330185", + "label": "临安市", + "children": [] + }, { + "value": "330186", + "label": "其它区", + "children": [] + }, { + "value": "330102", + "label": "上城区", + "children": [] + }, { + "value": "330122", + "label": "桐庐县", + "children": [] + }, { + "value": "330106", + "label": "西湖区", + "children": [] + }, { + "value": "330103", + "label": "下城区", + "children": [] + }, { + "value": "330109", + "label": "萧山区", + "children": [] + }, { + "value": "330110", + "label": "余杭区", + "children": [] + }] + }, { + "value": "330500", + "label": "湖州市", + "children": [{ + "value": "330523", + "label": "安吉县", + "children": [] + }, { + "value": "330522", + "label": "长兴县", + "children": [] + }, { + "value": "330521", + "label": "德清县", + "children": [] + }, { + "value": "330503", + "label": "南浔区", + "children": [] + }, { + "value": "330524", + "label": "其它区", + "children": [] + }, { + "value": "330502", + "label": "吴兴区", + "children": [] + }] + }, { + "value": "330400", + "label": "嘉兴市", + "children": [{ + "value": "330481", + "label": "海宁市", + "children": [] + }, { + "value": "330424", + "label": "海盐县", + "children": [] + }, { + "value": "330421", + "label": "嘉善县", + "children": [] + }, { + "value": "330402", + "label": "南湖区", + "children": [] + }, { + "value": "330482", + "label": "平湖市", + "children": [] + }, { + "value": "330484", + "label": "其它区", + "children": [] + }, { + "value": "330483", + "label": "桐乡市", + "children": [] + }, { + "value": "330411", + "label": "秀洲区", + "children": [] + }] + }, { + "value": "330700", + "label": "金华市", + "children": [{ + "value": "330783", + "label": "东阳市", + "children": [] + }, { + "value": "330703", + "label": "金东区", + "children": [] + }, { + "value": "330781", + "label": "兰溪市", + "children": [] + }, { + "value": "330727", + "label": "磐安县", + "children": [] + }, { + "value": "330726", + "label": "浦江县", + "children": [] + }, { + "value": "330785", + "label": "其它区", + "children": [] + }, { + "value": "330702", + "label": "婺城区", + "children": [] + }, { + "value": "330723", + "label": "武义县", + "children": [] + }, { + "value": "330782", + "label": "义乌市", + "children": [] + }, { + "value": "330784", + "label": "永康市", + "children": [] + }] + }, { + "value": "331100", + "label": "丽水市", + "children": [{ + "value": "331122", + "label": "缙云县", + "children": [] + }, { + "value": "331127", + "label": "景宁畲族自治县", + "children": [] + }, { + "value": "331102", + "label": "莲都区", + "children": [] + }, { + "value": "331181", + "label": "龙泉市", + "children": [] + }, { + "value": "331182", + "label": "其它区", + "children": [] + }, { + "value": "331121", + "label": "青田县", + "children": [] + }, { + "value": "331126", + "label": "庆元县", + "children": [] + }, { + "value": "331124", + "label": "松阳县", + "children": [] + }, { + "value": "331123", + "label": "遂昌县", + "children": [] + }, { + "value": "331125", + "label": "云和县", + "children": [] + }] + }, { + "value": "330200", + "label": "宁波市", + "children": [{ + "value": "330206", + "label": "北仑区", + "children": [] + }, { + "value": "330282", + "label": "慈溪市", + "children": [] + }, { + "value": "330283", + "label": "奉化市", + "children": [] + }, { + "value": "330203", + "label": "海曙区", + "children": [] + }, { + "value": "330205", + "label": "江北区", + "children": [] + }, { + "value": "330204", + "label": "江东区", + "children": [] + }, { + "value": "330226", + "label": "宁海县", + "children": [] + }, { + "value": "330284", + "label": "其它区", + "children": [] + }, { + "value": "330225", + "label": "象山县", + "children": [] + }, { + "value": "330212", + "label": "鄞州区", + "children": [] + }, { + "value": "330281", + "label": "余姚市", + "children": [] + }, { + "value": "330211", + "label": "镇海区", + "children": [] + }] + }, { + "value": "330800", + "label": "衢州市", + "children": [{ + "value": "330822", + "label": "常山县", + "children": [] + }, { + "value": "330881", + "label": "江山市", + "children": [] + }, { + "value": "330824", + "label": "开化县", + "children": [] + }, { + "value": "330802", + "label": "柯城区", + "children": [] + }, { + "value": "330825", + "label": "龙游县", + "children": [] + }, { + "value": "330882", + "label": "其它区", + "children": [] + }, { + "value": "330803", + "label": "衢江区", + "children": [] + }] + }, { + "value": "330600", + "label": "绍兴市", + "children": [{ + "value": "330621", + "label": "柯桥区", + "children": [] + }, { + "value": "330684", + "label": "其它区", + "children": [] + }, { + "value": "330682", + "label": "上虞区", + "children": [] + }, { + "value": "330683", + "label": "嵊州市", + "children": [] + }, { + "value": "330624", + "label": "新昌县", + "children": [] + }, { + "value": "330602", + "label": "越城区", + "children": [] + }, { + "value": "330681", + "label": "诸暨市", + "children": [] + }] + }, { + "value": "331000", + "label": "台州市", + "children": [{ + "value": "331003", + "label": "黄岩区", + "children": [] + }, { + "value": "331002", + "label": "椒江区", + "children": [] + }, { + "value": "331082", + "label": "临海市", + "children": [] + }, { + "value": "331004", + "label": "路桥区", + "children": [] + }, { + "value": "331083", + "label": "其它区", + "children": [] + }, { + "value": "331022", + "label": "三门县", + "children": [] + }, { + "value": "331023", + "label": "天台县", + "children": [] + }, { + "value": "331081", + "label": "温岭市", + "children": [] + }, { + "value": "331024", + "label": "仙居县", + "children": [] + }, { + "value": "331021", + "label": "玉环县", + "children": [] + }] + }, { + "value": "330300", + "label": "温州市", + "children": [{ + "value": "330327", + "label": "苍南县", + "children": [] + }, { + "value": "330322", + "label": "洞头县", + "children": [] + }, { + "value": "330303", + "label": "龙湾区", + "children": [] + }, { + "value": "330302", + "label": "鹿城区", + "children": [] + }, { + "value": "330304", + "label": "瓯海区", + "children": [] + }, { + "value": "330326", + "label": "平阳县", + "children": [] + }, { + "value": "330383", + "label": "其它区", + "children": [] + }, { + "value": "330381", + "label": "瑞安市", + "children": [] + }, { + "value": "330329", + "label": "泰顺县", + "children": [] + }, { + "value": "330328", + "label": "文成县", + "children": [] + }, { + "value": "330324", + "label": "永嘉县", + "children": [] + }, { + "value": "330382", + "label": "乐清市", + "children": [] + }] + }, { + "value": "330900", + "label": "舟山市", + "children": [{ + "value": "330921", + "label": "岱山县", + "children": [] + }, { + "value": "330902", + "label": "定海区", + "children": [] + }, { + "value": "330903", + "label": "普陀区", + "children": [] + }, { + "value": "330923", + "label": "其它区", + "children": [] + }, { + "value": "330922", + "label": "嵊泗县", + "children": [] + }] + }] +}]] \ No newline at end of file diff --git a/components/picker/demo/data/simple.js b/components/picker/demo/data/simple.js new file mode 100644 index 00000000..65653d5d --- /dev/null +++ b/components/picker/demo/data/simple.js @@ -0,0 +1,20 @@ +export default [ + [ + {text: '2015', value: 1}, + {text: '2016', value: 2}, + {text: '2017', value: 3}, + {text: '2018', value: 4}, + {text: '2019', value: 5}, + {text: '2020', value: 6}, + {text: '2021', value: 2}, + {text: '2022', value: 3}, + {text: '2023', value: 2}, + {text: '2024', value: 3}, + {text: '2025', value: 2}, + {text: '2026', value: 3}, + {text: '2027', value: 2}, + {text: '2028', value: 3}, + {text: '2029', value: 2}, + {text: '2030', value: 3}, + ], +] diff --git a/components/picker/demo/index.vue b/components/picker/demo/index.vue new file mode 100644 index 00000000..6d4f968d --- /dev/null +++ b/components/picker/demo/index.vue @@ -0,0 +1,19 @@ + + + diff --git a/components/picker/index.vue b/components/picker/index.vue new file mode 100644 index 00000000..57cacf0c --- /dev/null +++ b/components/picker/index.vue @@ -0,0 +1,285 @@ + + + + + diff --git a/components/picker/picker-column.vue b/components/picker/picker-column.vue new file mode 100644 index 00000000..e6f913ca --- /dev/null +++ b/components/picker/picker-column.vue @@ -0,0 +1,478 @@ + + + + + diff --git a/components/picker/test/index.spec.js b/components/picker/test/index.spec.js new file mode 100644 index 00000000..2673d0cb --- /dev/null +++ b/components/picker/test/index.spec.js @@ -0,0 +1,95 @@ +import Picker from '../index' +import PickerColumn from '../picker-column' +import triggerTouch from '../../popup/test/touch-trigger' +import simple from '../demo/data/simple' +import district from '../demo/data/district' +import {mount} from 'avoriaz' + +describe('Picker', () => { + let wrapper + + afterEach(() => { + wrapper && wrapper.destroy() + }) + + it('create a picker', done => { + wrapper = mount(Picker) + expect(wrapper.vm.data.length).to.equal(0) + + const eventStub = sinon.stub(wrapper.vm, '$emit') + + wrapper.vm.data = simple + wrapper.vm.value = true + setTimeout(() => { + expect(eventStub.calledWith('show')).to.be.true + expect(wrapper.vm.isPickerShow).to.be.true + expect(!!wrapper.vm.column).to.be.true + expect(wrapper.find('.column-item').length).to.equal(16) + const cancelmBtn = wrapper.find('.md-popup-cancel')[0] + cancelmBtn.trigger('click') + wrapper.vm.value = false + expect(eventStub.calledWith('cancel')).to.be.true + setTimeout(() => { + expect(eventStub.calledWith('hide')).to.be.true + done() + }, 300) + }, 300) + }) + + it('create a picker column', done => { + wrapper = mount(PickerColumn) + expect(wrapper.vm.data.length).to.equal(0) + expect(wrapper.vm.defaultIndex.length).to.equal(0) + expect(wrapper.vm.defaultValue.length).to.equal(0) + + const eventStub = sinon.stub(wrapper.vm, '$emit') + + wrapper.vm.data = simple + wrapper.vm.validIndex = [3] + wrapper.vm.defaultIndex = [2] + wrapper.vm.refresh(() => { + expect(wrapper.vm.getColumnIndex(0)).to.equal(2) + expect(wrapper.vm.getColumnIndexs()[0]).to.equal(2) + expect(wrapper.vm.getColumnValue(0).text).to.equal('2017') + + const hook = wrapper.find('.md-picker-column-hook')[0] + + triggerTouch(hook.element, 'touchstart', 0, 0) + triggerTouch(hook.element, 'touchmove', 0, 108) + triggerTouch(hook.element, 'touchend') + // expect(eventStub.calledWith('change')).to.be.true + + wrapper.vm.setColumnValues(0, simple[0]) + done() + }) + }) + + it('picker defalut index', done => { + wrapper = mount(Picker) + expect(wrapper.vm.data.length).to.equal(0) + + wrapper.vm.defaultIndex = [1] + wrapper.vm.data = simple + wrapper.vm.value = true + wrapper.vm.$nextTick(() => { + expect(wrapper.find('.column-item').length).to.equal(16) + done() + }) + }) + + it('picker cascade', done => { + wrapper = mount(Picker, { + propsData: { + isCascade: true, + }, + }) + wrapper.vm.data = district + const eventStub = sinon.stub(wrapper.vm, '$emit') + wrapper.vm.$nextTick(() => { + const confirmBtn = wrapper.find('.md-popup-confirm')[0] + confirmBtn.trigger('click') + expect(eventStub.calledWith('confirm')).to.be.true + done() + }) + }) +}) diff --git a/components/popup-title-bar/index.vue b/components/popup-title-bar/index.vue new file mode 100644 index 00000000..2760e1a8 --- /dev/null +++ b/components/popup-title-bar/index.vue @@ -0,0 +1,4 @@ + + diff --git a/components/popup/README.md b/components/popup/README.md new file mode 100644 index 00000000..73c26288 --- /dev/null +++ b/components/popup/README.md @@ -0,0 +1,60 @@ +--- +title: Popup 弹出层 +preview: https://didi.github.io/mand-mobile/examples/popup +--- + +由其他控件触发,屏幕滑出或弹出一块自定义内容区域 + +### 引入 + +```javascript +import { Popup, PopupTitleBar } from 'mand-mobile' + +Vue.component(Popup.name, Popup) +Vue.component(PopupTitleBar.name, PopupTitleBar) +``` + +### 代码演示 + + +### API + +#### Popup Props +|属性 | 说明 | 类型 | 默认值| 备注| +|----|-----|------|------|------| +|v-model|弹出层是否可见|Boolean|`false`|-| +|has-mask|是否有蒙层|Boolean|`true`|-| +|mask-closable|点击蒙层是否可关闭弹出层|Boolean|`true`|-| +|position|弹出层位置|String|`center`|`center`, `top`, `bottom`, `left`, `right`| +|transition|弹出层过度动画|String|`fade, slide-up/down/left/right`|-| +|prevent-scroll|是否禁止滚动穿透|Boolean|`false`|-| +|prevent-scroll-exclude|禁止滚动的排除元素|String/HTMLElement|-|-| + +#### PopupTitleBar Props +|属性 | 说明 | 类型 | 默认值 | 备注| +|----|-----|------|------|------| +|title|标题|String|-|-| +|ok-text|确认按钮文案|String|-|为空则没有确认按钮| +|cancel-text|取消按钮文案|String|-|为空则没有取消按钮| + +#### Popup Events + +#### @beforeShow() +弹出层即将展示事件 + +#### @show() +弹出层展示事件 + +#### @beforeHide() +弹出层即将隐藏事件 + +#### @hide() +弹出层隐藏事件 + +#### PopupTitleBar Events + +##### @confirm() +确认选择事件 + +##### @cancel() +取消选择事件 diff --git a/components/popup/component.js b/components/popup/component.js new file mode 100644 index 00000000..76a1923c --- /dev/null +++ b/components/popup/component.js @@ -0,0 +1,7 @@ +export default { + 'name': 'popup', + 'text': '弹出层', + 'category': 'feedback', + 'description': '', + 'author': 'xuxiaoyan' +} diff --git a/components/popup/demo/cases/demo0.vue b/components/popup/demo/cases/demo0.vue new file mode 100644 index 00000000..a16ec39b --- /dev/null +++ b/components/popup/demo/cases/demo0.vue @@ -0,0 +1,128 @@ + + + + + \ No newline at end of file diff --git a/components/popup/demo/cases/demo1.vue b/components/popup/demo/cases/demo1.vue new file mode 100644 index 00000000..371a5632 --- /dev/null +++ b/components/popup/demo/cases/demo1.vue @@ -0,0 +1,124 @@ + + +mand-mobile +import {Popup, PopupTitleBar, Button, Icon} from 'mand-mobile' + +export default { + name: 'popup-demo', + title: '其他配置', + message: '防止滚动击穿请在移动设备中扫码预览', + height: 750, + components: { + [Popup.name]: Popup, + [PopupTitleBar.name]: PopupTitleBar, + [Button.name]: Button, + [Icon.name]: Icon, + }, + data() { + return { + isPopupShow: {}, + } + }, + methods: { + showPopUp(type) { + this.$set(this.isPopupShow, type, true) + }, + hidePopUp(type) { + this.$set(this.isPopupShow, type, false) + }, + }, +} + + + + \ No newline at end of file diff --git a/components/popup/demo/index.vue b/components/popup/demo/index.vue new file mode 100644 index 00000000..a1e8db88 --- /dev/null +++ b/components/popup/demo/index.vue @@ -0,0 +1,18 @@ + + + diff --git a/components/popup/index.vue b/components/popup/index.vue new file mode 100644 index 00000000..021a8165 --- /dev/null +++ b/components/popup/index.vue @@ -0,0 +1,275 @@ + + + + + diff --git a/components/popup/test/index.spec.js b/components/popup/test/index.spec.js new file mode 100644 index 00000000..d19fdc00 --- /dev/null +++ b/components/popup/test/index.spec.js @@ -0,0 +1,107 @@ +import Popup from '../index' +import PopupTitleBar from '../title-bar' +import {mount} from 'avoriaz' +import triggerTouch from './touch-trigger' + +describe('Popup', () => { + let wrapper + + afterEach(() => { + wrapper && wrapper.destroy() + }) + + it('create a popup', done => { + wrapper = mount(Popup, { + propsData: { + value: true, + }, + slots: { + default: PopupTitleBar, + }, + }) + + wrapper.vm.$nextTick(() => { + expect(wrapper.vm.isPopupShow).to.be.true + expect(wrapper.vm.isPopupBoxShow).to.be.true + wrapper.vm.value = false + done() + }) + }) + + it('create a popup with position center', done => { + wrapper = mount(Popup, { + propsData: { + position: 'center', + }, + }) + expect(wrapper.vm.transition).to.equal('fade') + wrapper.vm.value = true + setTimeout(() => { + const popupBox = wrapper.find('.md-popup-box')[0] + expect(wrapper.hasClass('md-popup') && wrapper.hasClass('center') && popupBox.hasClass('fade')).to.be.true + done() + }, 300) + }) + + it('popup with transition', done => { + wrapper = mount(Popup, { + propsData: { + position: 'bottom', + }, + }) + const popupBox = wrapper.find('.md-popup-box')[0] + + expect(wrapper.vm.transition).to.equal('slide-up') + + wrapper.vm.transition = 'slide-for-test' + // expect(popupBox.hasClass('slide-for-test')).to.be.true + done() + }) + + it('popup without mask', done => { + wrapper = mount(Popup, { + propsData: { + hasMask: false, + }, + }) + + wrapper.vm.value = true + setTimeout(() => { + expect(wrapper.contains('.md-popup-mask')).to.be.true + done() + }, 300) + }) + + it('popup mask is closable', done => { + wrapper = mount(Popup) + expect(wrapper.hasClass('with-mask')).to.be.true + + wrapper.vm.value = true + setTimeout(() => { + const popupMask = wrapper.find('.md-popup-mask')[0] + popupMask.trigger('click') + expect(wrapper.vm.isPopupBoxShow).to.be.false + done() + }, 300) + }) + + it('popup prevent scroll', done => { + wrapper = mount(Popup, { + propsData: { + preventScroll: true, + value: true, + }, + }) + + const popupBox = wrapper.find('.md-popup-box')[0] + setTimeout(() => { + document.body.style.height = '10000px' + triggerTouch(popupBox.element, 'touchstart', 0, 0) + triggerTouch(popupBox.element, 'touchmove', 0, 100) + triggerTouch(document, 'touchstart', 0, 0) + triggerTouch(document, 'touchmove', 0, 100) + document.body.style.height = '' + done() + }, 300) + }) +}) diff --git a/components/popup/test/touch-trigger.js b/components/popup/test/touch-trigger.js new file mode 100644 index 00000000..70cb2268 --- /dev/null +++ b/components/popup/test/touch-trigger.js @@ -0,0 +1,23 @@ +export default function(element, eventName, x, y, keyCode) { + const touch = { + identifier: Date.now(), + target: element, + pageX: x, + pageY: y, + clientX: x, + clientY: y, + radiusX: 2.5, + radiusY: 2.5, + rotationAngle: 10, + force: 0.5, + } + + const event = document.createEvent('CustomEvent') + event.initCustomEvent(eventName, true, true, {}) + event.touches = [touch] + event.targetTouches = [touch] + event.changedTouches = [touch] + event.keyCode = keyCode + + element.dispatchEvent(event) +} diff --git a/components/popup/title-bar.vue b/components/popup/title-bar.vue new file mode 100644 index 00000000..b9ebae02 --- /dev/null +++ b/components/popup/title-bar.vue @@ -0,0 +1,107 @@ + + + + + diff --git a/components/radio/README.md b/components/radio/README.md new file mode 100644 index 00000000..7848b229 --- /dev/null +++ b/components/radio/README.md @@ -0,0 +1,89 @@ +--- +title: Radio 单选框 +preview: https://didi.github.io/mand-mobile/examples/radio +--- + +可自定义或编辑单选框 + +### 引入 + +```javascript +import { Radio } from 'mand-mobile' + +Vue.component(Radio.name, Radio) +``` + +### 代码演示 + + +### API + +#### Radio Props +|属性 | 说明 | 类型 | 默认值 | 备注| +|----|-----|------|------|------| +|v-model|选中项的`value`|String|-|如果数据源中没有`value`, 则为`text`或`label`| +|options|选项数据源|Array<{text, value, disabled, ...}>|`[]`|`disabled`为选项是否禁用| +|default-index|默认选择项索引|Number|`-1`|`v-model`有初始值时无效| +|invalid-index|禁用选择项索引|Number/Array|`-1`|作用等同于`options`元素中的属性`disabled`| +|has-input-option|是否具有可编辑项|Boolean|`false`|-| +|input-option-label|可编辑项的名称|String|-|仅用于`has-input-option`为`true`| +|input-option-placeholder|可编辑项的占位提示|String|-|仅用于`has-input-option`为`true`| +|icon|选中项的图标|String|`right`|-| +|icon-inverse|非选中项的图标|String|-|-| +|icon-size|图标大小|String|`sm`|-| +|icon-position|图标位置|String|`right`|`left`, `right`| +|option-render|返回各选项自定义渲染内容|Function({text, value, disabled, ...}): String|-|`vue 2.1.0+`可使用`slot-scope`,见附录| +|is-slot-scope|是否强制使用或不使用`slot-scope`|Boolean|-|某些情况下需要根据业务逻辑动态确定是否使用| + +#### Radio Methods + +##### getSelectedValue(): option +获取当前选中项 + +返回 + +|属性 | 说明 | 类型| +|----|-----|------| +|option|选中项的数据|`Object:{text, value, disabled, ...}`,如果选中为可编辑项,则为`String`| + +##### getSelectedIndex(): index +获取当前选中项索引值 + +返回 + +|属性 | 说明 | 类型| +|----|-----|------| +|index|选中项索引值|Number| + +##### selectByIndex(index) +设置选中项 + +|参数 | 说明 | 类型| +|----|-----|------| +|index|选中项索引值|Number| + +#### Component Events + +##### @change(option, index) +切换选中项事件 + +|属性 | 说明 | 类型| +|----|-----|------| +|option|选中项的数据|`Object:{text, value, disabled, ...}`,如果选中为可编辑项,则为`String`| +|index|选中项索引值|Number| + +#### 附录 + +```html + +``` diff --git a/components/radio/component.js b/components/radio/component.js new file mode 100644 index 00000000..a76587c6 --- /dev/null +++ b/components/radio/component.js @@ -0,0 +1,7 @@ +export default { + 'name': 'radio', + 'text': '单选框', + 'category': 'form', + 'description': '', + 'author': 'xuxiaoyan' +} diff --git a/components/radio/demo/cases/demo0.vue b/components/radio/demo/cases/demo0.vue new file mode 100644 index 00000000..419ec15f --- /dev/null +++ b/components/radio/demo/cases/demo0.vue @@ -0,0 +1,48 @@ + + + \ No newline at end of file diff --git a/components/radio/demo/cases/demo1.vue b/components/radio/demo/cases/demo1.vue new file mode 100644 index 00000000..a12ec844 --- /dev/null +++ b/components/radio/demo/cases/demo1.vue @@ -0,0 +1,48 @@ + + + \ No newline at end of file diff --git a/components/radio/demo/cases/demo2.vue b/components/radio/demo/cases/demo2.vue new file mode 100644 index 00000000..e3f8b2b5 --- /dev/null +++ b/components/radio/demo/cases/demo2.vue @@ -0,0 +1,39 @@ + + + + + \ No newline at end of file diff --git a/components/radio/demo/cases/demo3.vue b/components/radio/demo/cases/demo3.vue new file mode 100644 index 00000000..d7a2b5bf --- /dev/null +++ b/components/radio/demo/cases/demo3.vue @@ -0,0 +1,40 @@ + + + + + diff --git a/components/radio/demo/cases/demo4.vue b/components/radio/demo/cases/demo4.vue new file mode 100644 index 00000000..b9a72fce --- /dev/null +++ b/components/radio/demo/cases/demo4.vue @@ -0,0 +1,29 @@ + + + \ No newline at end of file diff --git a/components/radio/demo/index.vue b/components/radio/demo/index.vue new file mode 100644 index 00000000..588eb27c --- /dev/null +++ b/components/radio/demo/index.vue @@ -0,0 +1,21 @@ + + + diff --git a/components/radio/index.vue b/components/radio/index.vue new file mode 100644 index 00000000..0b1b3e0d --- /dev/null +++ b/components/radio/index.vue @@ -0,0 +1,293 @@ + + + + + + diff --git a/components/radio/test/index.spec.js b/components/radio/test/index.spec.js new file mode 100644 index 00000000..0c0cb67e --- /dev/null +++ b/components/radio/test/index.spec.js @@ -0,0 +1,95 @@ +import Radio from '../index' +import {mount} from 'avoriaz' + +describe('Radio', () => { + let wrapper + + afterEach(() => { + wrapper && wrapper.destroy() + }) + + it('create a radio', done => { + wrapper = mount(Radio, { + propsData: { + options: [{text: '选项1', disabled: true}, {text: '选项2'}, {text: '选项3'}], + defaultIndex: 1, + }, + }) + + expect(wrapper.find('.md-field-item').length).to.equal(3) + expect(wrapper.vm.selectedIndex).to.equal(1) + + wrapper.vm.options = [{text: '选项1'}, {text: '选项2'}] + wrapper.vm.$nextTick(() => { + expect(wrapper.find('.md-field-item').length).to.equal(2) + done() + }) + }) + + it('create a radio with initial value', done => { + wrapper = mount(Radio, { + propsData: { + options: [{text: '选项1', disabled: true}, {text: '选项2'}, {text: '选项3'}], + invalidIndex: 1, + }, + }) + + const eventStub = sinon.stub(wrapper.vm, '$emit') + + wrapper.vm.value = '选项2' + wrapper.vm.$nextTick(() => { + expect(eventStub.calledWith('input')).to.be.true + wrapper.vm.value = '选项1' + wrapper.vm.$nextTick(() => { + expect(wrapper.vm.selectedIndex).to.equal(0) + + wrapper.instance().selectByIndex(4) + wrapper.instance().selectByIndex(1) + expect(wrapper.vm.selectedIndex).to.equal(0) + wrapper.instance().selectByIndex(2) + expect(wrapper.vm.selectedIndex).to.equal(2) + done() + }) + }) + }) + + it('create a radio with input', done => { + wrapper = mount(Radio, { + propsData: { + options: [{text: '选项1', disabled: true}, {text: '选项2'}, {text: '选项3'}], + hasInputOption: true, + }, + }) + + const eventStub = sinon.stub(wrapper.vm, '$emit') + + const option = wrapper.find('.md-input-item')[0] + expect(!!option).to.be.true + option.find('input')[0].trigger('focus') + wrapper.vm.inputOptionValue = '123' + expect(eventStub.calledWith('input')).to.be.true + option.find('input')[0].trigger('blur') + expect(wrapper.vm.selectedIndex).to.equal(3) + expect(wrapper.instance().getSelectedIndex()).to.equal(3) + expect(wrapper.instance().getSelectedValue()).to.equal('123') + wrapper.instance().selectByIndex(3) + done() + }) + + it('radio option choose', done => { + wrapper = mount(Radio, { + propsData: { + options: [{text: '选项1'}, {text: '选项2'}, {text: '选项3'}], + invalidIndex: 1, + }, + }) + + const options = wrapper.find('.md-field-item') + options[0].trigger('click') + options[1].trigger('click') + expect(wrapper.vm.selectedIndex).to.equal(0) + expect(wrapper.instance().getSelectedValue().text).to.equal('选项1') + expect(options[1].hasClass('disabled')).to.be.true + done() + }) +}) diff --git a/components/result-page/README.md b/components/result-page/README.md new file mode 100644 index 00000000..014325c8 --- /dev/null +++ b/components/result-page/README.md @@ -0,0 +1,39 @@ +--- +title: ResultPage 结果页 +preview: https://didi.github.io/mand-mobile/examples/result-page +--- + +用于展示流程结束页面的控件 + +### 引入 + +```javascript +import { ResultPage } from 'mand-mobile' + +Vue.component(ResultPage.name, ResultPage) +``` + +### 使用指南 + +建议将组建的父元素设置填满视窗,以达到居中的效果。页面上的图片会根据`type`设置相应的默认值 + +### 代码演示 + + +### API + +#### ResultPage Props +|属性 | 说明 | 类型 | 默认值 | 备注| +|----|-----|------|------|------| +|type | 页面类别 | String | `empty` | type可取`lost`, `network`和`empty`三个值,分别代表页面丢失、网络出错和空信息。根据类别不同,组件会拥有不同的默认图片和文案| +|img-url | 图片链接 | String | 空信息图片 | 根据类别不同,组件会拥有不同的默认图片 | +|text | 主文案 | String | `暂无信息` | 根据类别不同,组件会拥有不同的默认主文案 | +|subtext | 副文案 | String | - | 以更小的字体和更淡的颜色显示在主文案下方 | +|buttons | 按钮列表 | Array | - | 按钮对象数组,按钮对象结构可参考Button Props表| + +#### Button Props +|属性 | 说明 | 类型 | 默认值 | 备注| +|----|-----|------|------|------| +|text | 按钮文字 | String | - | - | +|type | 按钮样式类别 | String | `ghost` | 还可以选择`ghost-primary`,可参考`Button`控件 | +|handler | 点击操作 | Function | - | 点击按钮后调用的方法 | diff --git a/components/result-page/component.js b/components/result-page/component.js new file mode 100644 index 00000000..b16dff52 --- /dev/null +++ b/components/result-page/component.js @@ -0,0 +1,7 @@ +export default { + 'name': 'result-page', + 'text': '结果页', + 'category': 'business', + 'description': '用于展示流程终点页面的组件。', + 'author': 'zhaozhe' +} diff --git a/components/result-page/demo/cases/demo0.vue b/components/result-page/demo/cases/demo0.vue new file mode 100644 index 00000000..99a0f45d --- /dev/null +++ b/components/result-page/demo/cases/demo0.vue @@ -0,0 +1,24 @@ + + + + + \ No newline at end of file diff --git a/components/result-page/demo/cases/demo1.vue b/components/result-page/demo/cases/demo1.vue new file mode 100644 index 00000000..6ece03eb --- /dev/null +++ b/components/result-page/demo/cases/demo1.vue @@ -0,0 +1,25 @@ + + + + + \ No newline at end of file diff --git a/components/result-page/demo/cases/demo2.vue b/components/result-page/demo/cases/demo2.vue new file mode 100644 index 00000000..df18fc2e --- /dev/null +++ b/components/result-page/demo/cases/demo2.vue @@ -0,0 +1,43 @@ + + + + + \ No newline at end of file diff --git a/components/result-page/demo/cases/demo3.vue b/components/result-page/demo/cases/demo3.vue new file mode 100644 index 00000000..c67188c3 --- /dev/null +++ b/components/result-page/demo/cases/demo3.vue @@ -0,0 +1,28 @@ + + + + + \ No newline at end of file diff --git a/components/result-page/demo/index.vue b/components/result-page/demo/index.vue new file mode 100644 index 00000000..0bba7dd6 --- /dev/null +++ b/components/result-page/demo/index.vue @@ -0,0 +1,26 @@ + + + + + \ No newline at end of file diff --git a/components/result-page/index.vue b/components/result-page/index.vue new file mode 100644 index 00000000..3050a94c --- /dev/null +++ b/components/result-page/index.vue @@ -0,0 +1,105 @@ + + + + + diff --git a/components/result-page/test/index.spec.js b/components/result-page/test/index.spec.js new file mode 100644 index 00000000..84135849 --- /dev/null +++ b/components/result-page/test/index.spec.js @@ -0,0 +1,16 @@ +import ResultPage from '../index' +import {mount} from 'avoriaz' + +describe('ResultPage', () => { + let wrapper + + afterEach(() => { + wrapper && wrapper.destroy() + }) + + it('create a simple result-page', () => { + wrapper = mount(ResultPage) + + expect(wrapper.hasClass('md-result-page')).to.be.true + }) +}) diff --git a/components/selector/README.md b/components/selector/README.md new file mode 100644 index 00000000..be813d2f --- /dev/null +++ b/components/selector/README.md @@ -0,0 +1,51 @@ +--- +title: Selector 列表选择器 +preview: https://didi.github.io/mand-mobile/examples/selector +--- + +用于弹出列表中选择一项 + +### 引入 + +```javascript +import { Selector } from 'mand-mobile' + +Vue.component(Selector.name, Selector) +``` + +### 代码演示 + + + +### API + +#### Selector Props +|属性 | 说明 | 类型 | 默认值 | 备注| +|----|-----|------|------|------| +|v-model|选择器是否可见|Boolean|false|-| +|data|数据源|Array<{value,text,...}>|`[]`|`label`可为`html`片段| +|default-index|选择器初始选中项索引|Number|-|-| +|invalid-index|选择器不可用选项索引|Number|-|-| +|title|选择器标题|String|-|-| +|ok-text|选择器确认文案|String|-|若为空则为`确认模式`,即点击选项直接选择| +|cancel-text|选择器取消文案|String|`取消`|-| +|is-check|是否有`check`图标|Boolean|`false`|仅`确认模式`| +|option-render|返回各选项渲染内容|Function({value, text ,...}):String|-|`vue 2.1.0+`可使用`slot-scope`,参考`Radio`| + + +#### Selector Events + +#### @choose({value, text, ...}) +选择器选中某选项事件 + +#### @confirm({value, text, ...}) +选择器确认选中事件 + +#### @cancel() +选择器取消选中事件 + +#### @show() +选择器展示事件 + +#### @hide() +选择器隐藏事件 diff --git a/components/selector/component.js b/components/selector/component.js new file mode 100644 index 00000000..7b687ba5 --- /dev/null +++ b/components/selector/component.js @@ -0,0 +1,7 @@ +export default { + 'name': 'selector', + 'text': '列表选择器', + 'category': 'feedback', + 'description': '', + 'author': 'xuxiaoyan' +} diff --git a/components/selector/demo/cases/demo0.vue b/components/selector/demo/cases/demo0.vue new file mode 100644 index 00000000..1ea2e1dc --- /dev/null +++ b/components/selector/demo/cases/demo0.vue @@ -0,0 +1,67 @@ + + + \ No newline at end of file diff --git a/components/selector/demo/cases/demo1.vue b/components/selector/demo/cases/demo1.vue new file mode 100644 index 00000000..ddd730bc --- /dev/null +++ b/components/selector/demo/cases/demo1.vue @@ -0,0 +1,80 @@ + + + + + \ No newline at end of file diff --git a/components/selector/demo/cases/demo2.vue b/components/selector/demo/cases/demo2.vue new file mode 100644 index 00000000..c5957249 --- /dev/null +++ b/components/selector/demo/cases/demo2.vue @@ -0,0 +1,67 @@ + + + \ No newline at end of file diff --git a/components/selector/demo/cases/demo3.vue b/components/selector/demo/cases/demo3.vue new file mode 100644 index 00000000..e6e3523d --- /dev/null +++ b/components/selector/demo/cases/demo3.vue @@ -0,0 +1,68 @@ + + + \ No newline at end of file diff --git a/components/selector/demo/index.vue b/components/selector/demo/index.vue new file mode 100644 index 00000000..3c74598a --- /dev/null +++ b/components/selector/demo/index.vue @@ -0,0 +1,30 @@ + + + + + \ No newline at end of file diff --git a/components/selector/index.vue b/components/selector/index.vue new file mode 100644 index 00000000..90970c3a --- /dev/null +++ b/components/selector/index.vue @@ -0,0 +1,229 @@ + + + + + diff --git a/components/selector/test/index.spec.js b/components/selector/test/index.spec.js new file mode 100644 index 00000000..ac27f324 --- /dev/null +++ b/components/selector/test/index.spec.js @@ -0,0 +1,180 @@ +// import Vue from 'vue' +import Selector from '../index' +import {mount} from 'avoriaz' +import {setTimeout} from 'timers' + +describe('Selector', () => { + let wrapper + + afterEach(() => { + wrapper && wrapper.destroy() + }) + + it('create a selector', done => { + wrapper = mount(Selector) + + expect(wrapper.hasClass('md-selector')).to.be.true + expect(wrapper.vm.data.length).to.equal(0) + + wrapper.vm.value = true + wrapper.vm.data = [ + { + text: '选项一', + }, + { + text: '选项二', + }, + { + text: '选项三', + }, + { + text: '选项四', + }, + ] + + wrapper.vm.$nextTick(() => { + expect(wrapper.find('.md-radio-item').length).to.equal(4) + done() + }) + }) + + it('create a selector as check mode', () => { + wrapper = mount(Selector, { + propsData: { + okText: '确认', + isCheck: true, + }, + }) + + expect(wrapper.hasClass('is-check')).to.be.true + expect(wrapper.vm.isNeedConfirm).to.equal(true) + }) + + it('create a selector with default and invalid', done => { + wrapper = mount(Selector) + + wrapper.vm.value = true + wrapper.vm.defaultIndex = 1 + wrapper.vm.invalidIndex = 2 + wrapper.vm.data = [ + { + text: '选项一', + }, + { + text: '选项二', + }, + { + text: '选项三', + }, + { + text: '选项四', + }, + ] + + wrapper.vm.$nextTick(() => { + expect(wrapper.find('.md-radio-item')[1].hasClass('selected')).to.equal(true) + expect(wrapper.find('.md-radio-item')[2].hasClass('disabled')).to.equal(true) + done() + }) + }) + + it('selector events choose', done => { + wrapper = mount(Selector) + + const eventStub = sinon.stub(wrapper.vm, '$emit') + + wrapper.vm.value = true + wrapper.vm.data = [ + { + text: '选项一', + }, + { + text: '选项二', + }, + { + text: '选项三', + }, + { + text: '选项四', + }, + ] + + wrapper.vm.$nextTick(() => { + wrapper.find('.md-radio-item')[0].trigger('click') + expect(wrapper.vm.tmpActiveIndex).equal(0) + expect(wrapper.vm.activeIndex).equal(0) + expect(eventStub.calledWith('choose')).to.be.true + done() + }) + }) + + it('selector events confirm', done => { + wrapper = mount(Selector) + + const eventStub = sinon.stub(wrapper.vm, '$emit') + + wrapper.vm.value = true + wrapper.vm.data = [ + { + text: '选项一', + }, + { + text: '选项二', + }, + { + text: '选项三', + }, + { + text: '选项四', + }, + ] + wrapper.vm.okText = 'ok' + wrapper.vm.$nextTick(() => { + const item = wrapper.find('.md-radio-item')[0] + const confirmBtn = wrapper.find('.md-popup-confirm')[0] + item.trigger('click') + expect(wrapper.vm.tmpActiveIndex).equal(0) + + confirmBtn.trigger('click') + expect(wrapper.vm.activeIndex).equal(0) + expect(eventStub.calledWith('confirm')).to.be.true + done() + }) + }) + + it('selector events cancel', done => { + wrapper = mount(Selector, { + propsData: { + data: [ + [ + { + text: '选项一', + }, + { + text: '选项二', + }, + { + text: '选项三', + }, + { + text: '选项四', + }, + ], + ], + okText: 'ok', + cancelText: 'cancel', + defaultIndex: 2, + }, + }) + + const eventStub = sinon.stub(wrapper.vm, '$emit') + + wrapper.vm.value = true + wrapper.vm.$nextTick(() => { + const cancelBtn = wrapper.find('.md-popup-cancel')[0] + cancelBtn.trigger('click') + expect(eventStub.calledWith('cancel')).to.be.true + done() + }) + }) +}) diff --git a/components/stepper/README.md b/components/stepper/README.md new file mode 100644 index 00000000..b188ba55 --- /dev/null +++ b/components/stepper/README.md @@ -0,0 +1,34 @@ +--- +title: Stepper 步进器 +preview: https://didi.github.io/mand-mobile/examples/stepper +--- + +增加,减少或修改当前数值 + +### 引入 + +```javascript +import { Stepper } from 'mand-mobile' + +Vue.component(Stepper.name, Stepper) +``` + +### 代码演示 + + +### API + +#### Stepper Props +属性 | 说明 | 类型 | 默认值 +---------|------|--------|---- +default-value |默认值| Number|- +step|每次改变步数,可以为小数|Number|`1` +min|最小值|Number|`-Infinity` +max|最大值|Number|`Infinity` +disabled|禁用| Boolean|`false` +read-only|只读| Boolean|`false` + +#### Stepper Events + +##### @change() +值发生变化事件 diff --git a/components/stepper/component.js b/components/stepper/component.js new file mode 100644 index 00000000..859a86cf --- /dev/null +++ b/components/stepper/component.js @@ -0,0 +1,7 @@ +export default { + 'name': 'stepper', + 'text': '步进器', + 'category': 'basic', + 'description': '步进器', + 'author': 'linyufei' +} diff --git a/components/stepper/demo/cases/demo0.vue b/components/stepper/demo/cases/demo0.vue new file mode 100644 index 00000000..a138dd7a --- /dev/null +++ b/components/stepper/demo/cases/demo0.vue @@ -0,0 +1,30 @@ + + + \ No newline at end of file diff --git a/components/stepper/demo/cases/demo1.vue b/components/stepper/demo/cases/demo1.vue new file mode 100644 index 00000000..5fd70f5d --- /dev/null +++ b/components/stepper/demo/cases/demo1.vue @@ -0,0 +1,30 @@ + + + \ No newline at end of file diff --git a/components/stepper/demo/cases/demo2.vue b/components/stepper/demo/cases/demo2.vue new file mode 100644 index 00000000..2a47c776 --- /dev/null +++ b/components/stepper/demo/cases/demo2.vue @@ -0,0 +1,31 @@ + + + \ No newline at end of file diff --git a/components/stepper/demo/cases/demo3.vue b/components/stepper/demo/cases/demo3.vue new file mode 100644 index 00000000..850594d4 --- /dev/null +++ b/components/stepper/demo/cases/demo3.vue @@ -0,0 +1,31 @@ + + + \ No newline at end of file diff --git a/components/stepper/demo/cases/demo4.vue b/components/stepper/demo/cases/demo4.vue new file mode 100644 index 00000000..b15543a7 --- /dev/null +++ b/components/stepper/demo/cases/demo4.vue @@ -0,0 +1,30 @@ + + + \ No newline at end of file diff --git a/components/stepper/demo/cases/demo5.vue b/components/stepper/demo/cases/demo5.vue new file mode 100644 index 00000000..5f25883e --- /dev/null +++ b/components/stepper/demo/cases/demo5.vue @@ -0,0 +1,30 @@ + + + \ No newline at end of file diff --git a/components/stepper/demo/index.vue b/components/stepper/demo/index.vue new file mode 100644 index 00000000..5c043c3b --- /dev/null +++ b/components/stepper/demo/index.vue @@ -0,0 +1,28 @@ + + + + + \ No newline at end of file diff --git a/components/stepper/index.vue b/components/stepper/index.vue new file mode 100644 index 00000000..d664ed28 --- /dev/null +++ b/components/stepper/index.vue @@ -0,0 +1,231 @@ + + + + + diff --git a/components/stepper/test/index.spec.js b/components/stepper/test/index.spec.js new file mode 100644 index 00000000..7049aec6 --- /dev/null +++ b/components/stepper/test/index.spec.js @@ -0,0 +1,111 @@ +import Stepper from '../index' +import {mount} from 'avoriaz' + +describe('Stepper', () => { + let wrapper + + afterEach(() => { + wrapper && wrapper.destroy() + }) + + it('create a simple stepper', () => { + wrapper = mount(Stepper) + + expect(wrapper.hasClass('md-stepper')).to.be.true + expect(wrapper.vm.defaultValue).to.equal(0) + expect(wrapper.vm.step).to.equal(1) + expect(wrapper.vm.disabled).to.equal(false) + expect(wrapper.vm.readOnly).to.equal(false) + }) + + it('change stepper default props', () => { + wrapper = mount(Stepper, { + propsData: { + defaultValue: 2, + step: 2, + disabled: true, + readOnly: true, + }, + }) + + expect(wrapper.vm.defaultValue).to.equal(2) + expect(wrapper.vm.step).to.equal(2) + expect(wrapper.vm.disabled).to.equal(true) + expect(wrapper.vm.readOnly).to.equal(true) + }) + + it('stepper method reduce', () => { + wrapper = mount(Stepper) + wrapper.vm.currentNum = 1 + wrapper.vm.step = 2 + wrapper.find('.md-stepper-button-reduce')[0].trigger('click') + expect(Number(wrapper.vm.currentNum)).to.equal(-1) + }) + + it('stepper method add', () => { + wrapper = mount(Stepper) + wrapper.vm.currentNum = 1 + wrapper.vm.step = 2 + wrapper.find('.md-stepper-button-add')[0].trigger('click') + expect(wrapper.vm.currentNum).to.equal(3) + }) + + it('stepper method getCurrentNum', () => { + wrapper = mount(Stepper) + wrapper.vm.defaultValue = 2 + wrapper.vm.min = 3 + expect(wrapper.vm.$_getCurrentNum()).to.equal(wrapper.vm.min) + + wrapper.vm.defaultValue = 4 + wrapper.vm.max = 3 + expect(wrapper.vm.$_getCurrentNum()).to.equal(wrapper.vm.max) + }) + + it('stepper method checkMinMax', () => { + wrapper = mount(Stepper) + wrapper.vm.max = 5 + wrapper.vm.min = 3 + expect(wrapper.vm.$_checkMinMax()).to.equal(true) + + wrapper.vm.max = 3 + wrapper.vm.min = 5 + expect(wrapper.vm.$_checkMinMax()).to.equal(false) + }) + + it('stepper method checkStatus', () => { + wrapper = mount(Stepper) + wrapper.vm.min = 2 + wrapper.vm.currentNum = 3 + wrapper.vm.step = 2 + wrapper.vm.$_checkStatus() + expect(wrapper.vm.isMin).to.equal(true) + }) + + it('stepper method reset', () => { + wrapper = mount(Stepper) + wrapper.vm.currentNum = null + wrapper.vm.min = 5 + wrapper.vm.$_reset() + expect(wrapper.vm.currentNum).to.equal(wrapper.vm.min) + }) + + it('stepper method onChange', () => { + wrapper = mount(Stepper) + wrapper.vm.currentNum = 2 + wrapper.vm.min = 3 + wrapper.vm.$_onChange() + // currentNum < min + expect(wrapper.vm.currentNum).to.equal(wrapper.vm.min) + + wrapper.vm.currentNum = 6 + wrapper.vm.max = 5 + wrapper.vm.$_onChange() + // currentNum > max + expect(wrapper.vm.currentNum).to.equal(wrapper.vm.max) + + wrapper.vm.currentNum = 4 + wrapper.vm.$_onChange() + // min < currentNum < max + expect(wrapper.vm.currentNum).to.equal(4) + }) +}) diff --git a/components/steps/README.md b/components/steps/README.md new file mode 100644 index 00000000..1a0988dd --- /dev/null +++ b/components/steps/README.md @@ -0,0 +1,25 @@ +--- +title: Steps 步骤条 +preview: https://didi.github.io/mand-mobile/examples/steps +--- + +用于引导用户按照流程完成任务的导航条,显示当前所在步骤 + +### 引入 + +```javascript +import { Steps } from 'mand-mobile' + +Vue.component(Steps.name, Steps) +``` + +### 代码演示 + + +### API + +#### Tabs Props +属性 | 说明 | 类型 | 默认值 | 备注 +----|-----|------|------|------ +steps | 步骤信息数组 | Array | - | 数组中每个元素须包含`name`属性,会作为步骤名称显示 +current | 当前步骤 | Number | `0` | 可通过修改该值动态改变当前所在步骤 diff --git a/components/steps/component.js b/components/steps/component.js new file mode 100644 index 00000000..5644b311 --- /dev/null +++ b/components/steps/component.js @@ -0,0 +1,7 @@ +export default { + 'name': 'steps', + 'text': '步骤条', + 'category': 'basic', + 'description': '展示当前所在步骤位置的组件。', + 'author': 'zhaozhe' +} diff --git a/components/steps/demo/cases/demo0.vue b/components/steps/demo/cases/demo0.vue new file mode 100644 index 00000000..2b6abfd8 --- /dev/null +++ b/components/steps/demo/cases/demo0.vue @@ -0,0 +1,34 @@ + + + \ No newline at end of file diff --git a/components/steps/demo/cases/demo1.vue b/components/steps/demo/cases/demo1.vue new file mode 100644 index 00000000..cd3cc4af --- /dev/null +++ b/components/steps/demo/cases/demo1.vue @@ -0,0 +1,37 @@ + + + \ No newline at end of file diff --git a/components/steps/demo/cases/demo2.vue b/components/steps/demo/cases/demo2.vue new file mode 100644 index 00000000..dcd826bb --- /dev/null +++ b/components/steps/demo/cases/demo2.vue @@ -0,0 +1,42 @@ + + + \ No newline at end of file diff --git a/components/steps/demo/cases/demo3.vue b/components/steps/demo/cases/demo3.vue new file mode 100644 index 00000000..40709f23 --- /dev/null +++ b/components/steps/demo/cases/demo3.vue @@ -0,0 +1,38 @@ + + + \ No newline at end of file diff --git a/components/steps/demo/cases/demo4.vue b/components/steps/demo/cases/demo4.vue new file mode 100644 index 00000000..0f8de28b --- /dev/null +++ b/components/steps/demo/cases/demo4.vue @@ -0,0 +1,44 @@ + + + \ No newline at end of file diff --git a/components/steps/demo/cases/demo5.vue b/components/steps/demo/cases/demo5.vue new file mode 100644 index 00000000..f7ba67b6 --- /dev/null +++ b/components/steps/demo/cases/demo5.vue @@ -0,0 +1,52 @@ + + + + + diff --git a/components/steps/demo/cases/demo6.vue b/components/steps/demo/cases/demo6.vue new file mode 100644 index 00000000..28b51ed3 --- /dev/null +++ b/components/steps/demo/cases/demo6.vue @@ -0,0 +1,52 @@ + + + + + diff --git a/components/steps/demo/index.vue b/components/steps/demo/index.vue new file mode 100644 index 00000000..9c3e649b --- /dev/null +++ b/components/steps/demo/index.vue @@ -0,0 +1,28 @@ + + + + + \ No newline at end of file diff --git a/components/steps/index.vue b/components/steps/index.vue new file mode 100644 index 00000000..733fef4e --- /dev/null +++ b/components/steps/index.vue @@ -0,0 +1,138 @@ + + + + + diff --git a/components/steps/test/index.spec.js b/components/steps/test/index.spec.js new file mode 100644 index 00000000..e00a3fe6 --- /dev/null +++ b/components/steps/test/index.spec.js @@ -0,0 +1,16 @@ +import Steps from '../index' +import {mount} from 'avoriaz' + +describe('Steps', () => { + let wrapper + + afterEach(() => { + wrapper && wrapper.destroy() + }) + + it('create a simple steps', () => { + wrapper = mount(Steps) + + expect(wrapper.hasClass('md-steps')).to.be.true + }) +}) diff --git a/components/swiper-item/index.vue b/components/swiper-item/index.vue new file mode 100644 index 00000000..37d7d7fc --- /dev/null +++ b/components/swiper-item/index.vue @@ -0,0 +1,4 @@ + + diff --git a/components/swiper/README.md b/components/swiper/README.md new file mode 100644 index 00000000..6d155513 --- /dev/null +++ b/components/swiper/README.md @@ -0,0 +1,104 @@ +--- +title: Swiper 轮播 +preview: https://didi.github.io/mand-mobile/examples/swiper +--- + +走马灯,用于一组图片或卡片轮播 + +### 引入 + +```javascript +import { Swiper, SwiperItem } from 'mand-mobile' + +Vue.component(Swiper.name, Swiper) +Vue.component(SwiperItem.name, SwiperItem) +``` + +### 代码演示 + + +### API + +#### Swiper Props + +|属性|说明|类型|默认值|可选值| +|---|---|---|---|---| +|autoplay|自动切换间隔时长(毫秒), 禁用可设置为`0`|Number|`3000`|`0`, `[500, +Int.Max)`| +|transition|面板切换动画效果|String|`slide`|`slide`, `slideY`, `fade`, `fade`| +|default-index|第一屏面板索引值|Number|`0`|`[0, length - 1]`| +|has-dots|控制面板指示点|Boolean|`true`|`true`, `false`| +|is-prevent|是否阻止默认的事件,如页面滚动事件|Boolean|`true`|`true`, `false`| +|is-loop|是否循环播放|Boolean|`true`|`true`, `false`| +|dragable|是否禁用触摸滑动|Boolean|`true`|`true`, `false`| + +#### Swiper Methods + +##### play(autoplay) +打开自动切换 + +|参数|说明|类型|默认值|可选值| +|---|---|---|---|---| +|autoplay|自动切换间隔时长(毫秒)|Number|`3000`|`[500, +Int.Max)`| + +```js +vm.$refs.swiper.play(5000) +``` + +##### stop() +停止自动切换 + +```js +vm.$refs.swiper.stop() +``` + +##### pre() +前一个item + +```js +vm.$refs.swiper.pre() +``` + +##### next() +后一个item + +```js +vm.$refs.swiper.next() +``` + +##### goto(index) +切换到某一个index + +|参数|说明|类型|默认值|可选值| +|---|---|---|---|---| +|index|第一屏面板索引值|Number|`0`|`[0, length - 1]`| +```js +vm.$refs.swiper.goto(2) +``` + +##### getIndex() +获取当前显示的index + +|参数|说明|类型| +|---|---|---| +|index|当前显示的index|Number| + +```js +var index = vm.$refs.swiper.getIndex() +``` + +#### Swiper Events +##### @beforeChange(from, to) +轮播器将要切换前的事件 + +|参数 | 说明 | 类型 | +|----|-----|------| +| from | 轮播器当前展示的索引值 | Number | +| to | 轮播器下一屏展示的索引值 | Number | + +##### @afterChange(from, to) +轮播器切换完成时的事件 + +|参数 | 说明 | 类型 | +|----|-----|------| +| from | 轮播器当前展示的索引值 | Number | +| to | 轮播器下一屏展示的索引值 | Number | diff --git a/components/swiper/component.js b/components/swiper/component.js new file mode 100644 index 00000000..c93d640a --- /dev/null +++ b/components/swiper/component.js @@ -0,0 +1,7 @@ +export default { + 'name': 'swiper', + 'text': '轮播', + 'category': 'basic', + 'description': '轮播', + 'author': 'huangbinxing' +} diff --git a/components/swiper/demo/cases/demo0.vue b/components/swiper/demo/cases/demo0.vue new file mode 100644 index 00000000..bbfa23d3 --- /dev/null +++ b/components/swiper/demo/cases/demo0.vue @@ -0,0 +1,89 @@ + + + + + diff --git a/components/swiper/demo/cases/demo1.vue b/components/swiper/demo/cases/demo1.vue new file mode 100644 index 00000000..0ad5af5e --- /dev/null +++ b/components/swiper/demo/cases/demo1.vue @@ -0,0 +1,70 @@ + + + + + diff --git a/components/swiper/demo/cases/demo2.vue b/components/swiper/demo/cases/demo2.vue new file mode 100644 index 00000000..c248d38a --- /dev/null +++ b/components/swiper/demo/cases/demo2.vue @@ -0,0 +1,75 @@ + + + + + diff --git a/components/swiper/demo/cases/demo3.vue b/components/swiper/demo/cases/demo3.vue new file mode 100644 index 00000000..dd6fe20f --- /dev/null +++ b/components/swiper/demo/cases/demo3.vue @@ -0,0 +1,81 @@ + + + + + diff --git a/components/swiper/demo/data/mulit-item.js b/components/swiper/demo/data/mulit-item.js new file mode 100644 index 00000000..9154bfa8 --- /dev/null +++ b/components/swiper/demo/data/mulit-item.js @@ -0,0 +1,82 @@ +var colors = [ + [ + { + color: '#4390EE', + text: '引力波', + }, + { + color: '#CA4040', + text: '智子', + }, + { + color: '#FF8604', + text: '水滴', + }, + { + color: '#00CC00', + text: '二向箔', + }, + { + color: '#0066CC', + text: '飞刃', + }, + { + color: '#99CCCC', + text: '碎星', + }, + ], + [ + { + color: '#990033', + text: '危机', + }, + { + color: '#CCFF66', + text: '威摄', + }, + { + color: '#FF9900', + text: '威摄后', + }, + { + color: '#FF9933', + text: '广播', + }, + { + color: '#99CC33', + text: '掩体', + }, + { + color: '#CC6699', + text: '银河', + }, + ], + [ + { + color: '#0099CC', + text: '猜疑链', + }, + { + color: '#CCCCCC', + text: '技术爆炸', + }, + { + color: '#FF6666', + text: '黑暗森林', + }, + { + color: '#99CCCC', + text: '地球', + }, + { + color: '#FFCC99', + text: '三体', + }, + { + color: '#FFCCCC', + text: '歌者', + }, + ], +] + +export default colors diff --git a/components/swiper/demo/data/simple.js b/components/swiper/demo/data/simple.js new file mode 100644 index 00000000..b81bc9be --- /dev/null +++ b/components/swiper/demo/data/simple.js @@ -0,0 +1,16 @@ +var colors = [ + { + color: '#4390EE', + text: '给时光以生命,给岁月以文明。', + }, + { + color: '#364d79', + text: '你的无畏来源于无知。', + }, + { + color: '#CA4040', + text: '一切都将逝去,只有死神永生。', + }, +] + +export default colors diff --git a/components/swiper/demo/index.vue b/components/swiper/demo/index.vue new file mode 100644 index 00000000..09fa0264 --- /dev/null +++ b/components/swiper/demo/index.vue @@ -0,0 +1,35 @@ + + + + + \ No newline at end of file diff --git a/components/swiper/index.vue b/components/swiper/index.vue new file mode 100644 index 00000000..ca085971 --- /dev/null +++ b/components/swiper/index.vue @@ -0,0 +1,675 @@ + + + + + diff --git a/components/swiper/swiper-item.vue b/components/swiper/swiper-item.vue new file mode 100644 index 00000000..77db5a3e --- /dev/null +++ b/components/swiper/swiper-item.vue @@ -0,0 +1,68 @@ + + + + + diff --git a/components/swiper/test/index.spec.js b/components/swiper/test/index.spec.js new file mode 100644 index 00000000..0f43cbe2 --- /dev/null +++ b/components/swiper/test/index.spec.js @@ -0,0 +1,392 @@ +import Swiper from '../index' +import SwiperItem from '../swiper-item' +import triggerTouch from '../../popup/test/touch-trigger' +import { mount, shallow } from 'avoriaz' + +describe('Swiper', () => { + let wrapper + + afterEach(() => { + wrapper && wrapper.destroy() + }) + + it('create a simple swiper', done => { + wrapper = mount(Swiper) + + expect(wrapper.hasClass('md-swiper')).to.be.true + + expect(wrapper.vm.autoplay).to.equal(3000) + expect(wrapper.vm.transition).to.equal('slide') + expect(wrapper.vm.defaultIndex).to.equal(0) + expect(wrapper.vm.hasDots).to.equal(true) + expect(wrapper.vm.isPrevent).to.equal(true) + expect(wrapper.vm.isLoop).to.equal(true) + expect(wrapper.vm.dragable).to.equal(true) + done() + }) + + it('change swiper default props', () => { + wrapper = mount(Swiper, { + propsData: { + autoplay: 5000, + transition: 'slideY', + defaultIndex: 1, + hasDots: false, + isPrevent: false, + isLoop: false, + dragable: false + } + }) + + expect(wrapper.vm.autoplay).to.equal(5000) + expect(wrapper.vm.transition).to.equal('slideY') + expect(wrapper.vm.defaultIndex).to.equal(1) + expect(wrapper.vm.hasDots).to.be.false + expect(wrapper.vm.isPrevent).to.be.false + expect(wrapper.vm.isLoop).to.be.false + expect(wrapper.vm.dragable).to.be.false + + expect(wrapper.vm.isVertical).to.be.true + }) + + // it('set ill props for swiper ', () => { + // wrapper = mount(Swiper, { + // propsData: { + // autoplay: 300, + // transition: 'slideYZ', + // defaultIndex: -1, + // hasDots: 'false', + // isPrevent: 'false', + // isLoop: 'false', + // dragable: 'false' + // } + // }) + // }) + + it('create a swiper item', () => { + wrapper = mount(Swiper, { + slots: { + 'default': [SwiperItem, SwiperItem, SwiperItem] + } + }) + expect(wrapper.find('.md-swiper-item')[0].hasStyle('height', 'auto')).to.be.true + }) + + it('create a vertical swiper item', () => { + wrapper = mount(Swiper, { + propsData: { + transition: 'slideY' + }, + slots: { + 'default': SwiperItem + } + }) + expect(wrapper.find('.md-swiper-item')[0].hasStyle('width', 'auto')).to.be.true + }) + + it('swiper method play', done => { + wrapper = mount(Swiper, { + slots: { + 'default': [SwiperItem, SwiperItem, SwiperItem] + } + }) + + setTimeout(() => { + wrapper.vm.play(5000) + expect(wrapper.vm.autoplay).to.equal(5000) + done() + }, 1000) + }) + + it('swiper method stop', done => { + wrapper = mount(Swiper, { + slots: { + 'default': [SwiperItem, SwiperItem, SwiperItem] + } + }) + + setTimeout(() => { + wrapper.vm.stop() + expect(wrapper.vm.timer).to.equal(null) + done() + }, 1000) + }) + + it('swiper method pre', done => { + wrapper = mount(Swiper, { + slots: { + 'default': [SwiperItem, SwiperItem, SwiperItem] + } + }) + setTimeout(() => { + wrapper.vm.prev() + expect(wrapper.vm.getIndex()).to.equal(2) + done() + }, 500) + }) + + it('swiper method next', done => { + wrapper = mount(Swiper, { + slots: { + 'default': [SwiperItem, SwiperItem, SwiperItem] + } + }) + + setTimeout(() => { + wrapper.vm.next() + expect(wrapper.vm.getIndex()).to.equal(1) + done() + }, 500) + }) + + it('swiper method goto', done => { + wrapper = mount(Swiper, { + slots: { + 'default': [SwiperItem, SwiperItem, SwiperItem] + } + }) + + setTimeout(() => { + wrapper.vm.goto('a') // ill + expect(wrapper.vm.getIndex()).to.equal(0) + + wrapper.vm.goto(-1) // ill + expect(wrapper.vm.getIndex()).to.equal(0) + + wrapper.vm.goto(3) // ill + expect(wrapper.vm.getIndex()).to.equal(0) + + wrapper.vm.goto(2) + expect(wrapper.vm.getIndex()).to.equal(2) + done() + }, 500) + }) + + it('swiper method getIndex', () => { + wrapper = mount(Swiper) + + expect(wrapper.vm.getIndex()).to.equal(wrapper.vm.index) + }) + + + it('drag swiper', done => { + wrapper = mount(Swiper, { + propsData: { + autoplay: 0 + }, + slots: { + 'default': [SwiperItem, SwiperItem, SwiperItem] + } + }) + + // wrapper.vm.$nextTick(() => { + expect(wrapper.vm.getIndex()).to.equal(0) + setTimeout(() => { + const hook = wrapper.vm.$el + const eventStub = sinon.stub(wrapper.vm, '$emit') + + expect(wrapper.vm.getIndex()).to.equal(0) + triggerTouch(hook, 'touchstart', 0, 0) + triggerTouch(hook, 'touchmove', -100, 0) + triggerTouch(hook, 'touchend') + expect(eventStub.calledWith('before-change', 0, 1)).to.be.true + setTimeout(() => { + expect(wrapper.vm.getIndex()).to.equal(1) + // expect(eventStub.calledWith('after-change', 0, 1)).to.be.true + done() + }, 400) + }, 500) + // }) + }) + + it('drag swiper with a litter distance', done => { + wrapper = mount(Swiper, { + propsData: { + autoplay: 0 + }, + slots: { + 'default': [SwiperItem, SwiperItem, SwiperItem] + } + }) + expect(wrapper.vm.getIndex()).to.equal(0) + setTimeout(() => { + const hook = wrapper.vm.$el + const eventStub = sinon.stub(wrapper.vm, '$emit') + + expect(wrapper.vm.getIndex()).to.equal(0) + triggerTouch(hook, 'touchstart', 0, 0) + triggerTouch(hook, 'touchmove', -4, 0) + triggerTouch(hook, 'touchend') + expect(eventStub.calledWith('before-change')).to.be.false + setTimeout(() => { + expect(wrapper.vm.getIndex()).to.equal(0) + done() + }, 300) + }, 500) + }) + + it('drag swiper with single item', done => { + wrapper = mount(Swiper, { + propsData: { + autoplay: 0, + isLoop: false + }, + slots: { + 'default': SwiperItem + } + }) + expect(wrapper.vm.getIndex()).to.equal(0) + setTimeout(() => { + const hook = wrapper.vm.$el + const eventStub = sinon.stub(wrapper.vm, '$emit') + + expect(wrapper.vm.getIndex()).to.equal(0) + triggerTouch(hook, 'touchstart', 0, 0) + triggerTouch(hook, 'touchmove', -100, 0) + triggerTouch(hook, 'touchend') + expect(eventStub.calledWith('before-change')).to.be.false + setTimeout(() => { + expect(wrapper.vm.getIndex()).to.equal(0) + done() + }, 300) + }, 500) + }) + + it('drag swiper at first item to last item in disloop mode', done => { + wrapper = mount(Swiper, { + propsData: { + autoplay: 0, + isLoop: false + }, + slots: { + 'default': [SwiperItem, SwiperItem, SwiperItem] + } + }) + expect(wrapper.vm.getIndex()).to.equal(0) + setTimeout(() => { + const hook = wrapper.vm.$el + const eventStub = sinon.stub(wrapper.vm, '$emit') + + expect(wrapper.vm.getIndex()).to.equal(0) + triggerTouch(hook, 'touchstart', 0, 0) + triggerTouch(hook, 'touchmove', 100, 0) + triggerTouch(hook, 'touchend') + expect(eventStub.calledWith('before-change')).to.be.false + setTimeout(() => { + expect(wrapper.vm.getIndex()).to.equal(0) + done() + }, 300) + }, 500) + }) + + it('drag swiper at edge (0 -> 2)', done => { + wrapper = mount(Swiper, { + propsData: { + autoplay: 0 + }, + slots: { + 'default': [SwiperItem, SwiperItem, SwiperItem] + } + }) + + setTimeout(() => { + const hook = wrapper.vm.$el + triggerTouch(hook, 'touchstart', 0, 0) + triggerTouch(hook, 'touchmove', 100, 0) + triggerTouch(hook, 'touchend') + setTimeout(() => { + expect(wrapper.vm.getIndex()).to.equal(2) + done() + }, 1000) + }, 500) + }) + + it('drag swiper at edge (2 -> 0)', done => { + wrapper = mount(Swiper, { + propsData: { + autoplay: 0, + defaultIndex: 2 + }, + slots: { + 'default': [SwiperItem, SwiperItem, SwiperItem] + } + }) + + setTimeout(() => { + const hook = wrapper.vm.$el + triggerTouch(hook, 'touchstart', 0, 0) + triggerTouch(hook, 'touchmove', -100, 0) + triggerTouch(hook, 'touchend') + setTimeout(() => { + expect(wrapper.vm.getIndex()).to.equal(0) + done() + }, 1000) + }, 500) + }) + + it('drag swiper in disdrag mode', done => { + wrapper = mount(Swiper, { + propsData: { + autoplay: 0, + transition: 'slideY', + isLoop: false + }, + slots: { + 'default': [SwiperItem, SwiperItem, SwiperItem] + } + }) + // 初始 index 为 0 + expect(wrapper.vm.getIndex()).to.equal(0) + setTimeout(() => { + const hook = wrapper.vm.$el + + // 初始化后 index 为 0 + expect(wrapper.vm.getIndex()).to.equal(0) + triggerTouch(hook, 'touchstart', 0, 0) + triggerTouch(hook, 'touchmove', 0, -50) + triggerTouch(hook, 'touchend') + setTimeout(() => { + // 向右滑动后,index 为 1 + expect(wrapper.vm.getIndex()).to.equal(1) + done() + }, 300) + }, 500) + }) + + it('set transition by fade', done => { + wrapper = mount(Swiper, { + propsData: { + autoplay: 1000, + transition: 'fade' + }, + slots: { + 'default': [SwiperItem, SwiperItem, SwiperItem] + } + }) + + setTimeout(() => { + // 1500 ms 后渐变到第二屏 + expect(wrapper.vm.getIndex()).to.equal(1) + done() + }, 1500) + }) + + // it('swiper destroye', done => { + // wrapper = mount(Swiper, { + // slots: { + // 'default': [SwiperItem, SwiperItem, SwiperItem] + // } + // }) + + // setTimeout(() => { + // // 1500 ms 后渐变到第二屏 + // wrapper.destroy() + // expect(wrapper.vm.timer).to.equal(null) + // expect(wrapper.vm.reInitTimer).to.equal(null) + // done() + // }, 1500) + // }) + + + +}) diff --git a/components/switch/README.md b/components/switch/README.md new file mode 100644 index 00000000..197c1272 --- /dev/null +++ b/components/switch/README.md @@ -0,0 +1,34 @@ +--- +title: Switch 开关 +preview: https://didi.github.io/mand-mobile/examples/switch +--- + +开关按钮,用于表示开关状态/两种状态之间的切换 + +### 引入 + +```javascript +import { Switch } from 'md-mobile' + +Vue.component(Switch.name, Switch) +``` + +### 代码演示 + + +### API + +#### Switch Props +|属性 | 说明 | 类型 | 默认值| +|----|-----|------|------| +|v-model|打开或者关闭|Boolean|`false`| +|disabled|是否禁用|Boolean|`false`| + +#### Switch Events + +##### @change(isActive) +事件说明 + +|属性 | 说明 | 类型 | +|----|-----|------| +|isActive|开关状态,打开或者关闭|Boolean| diff --git a/components/switch/component.js b/components/switch/component.js new file mode 100644 index 00000000..00d7452c --- /dev/null +++ b/components/switch/component.js @@ -0,0 +1,7 @@ +export default { + 'name': 'switch', + 'text': '滑动开关', + 'category': 'basic', + 'description': '', + 'author': 'chengyanjing' +} diff --git a/components/switch/demo/cases/demo0.vue b/components/switch/demo/cases/demo0.vue new file mode 100644 index 00000000..6af01210 --- /dev/null +++ b/components/switch/demo/cases/demo0.vue @@ -0,0 +1,29 @@ + + + \ No newline at end of file diff --git a/components/switch/demo/cases/demo1.vue b/components/switch/demo/cases/demo1.vue new file mode 100644 index 00000000..f242c055 --- /dev/null +++ b/components/switch/demo/cases/demo1.vue @@ -0,0 +1,29 @@ + + + \ No newline at end of file diff --git a/components/switch/demo/cases/demo2.vue b/components/switch/demo/cases/demo2.vue new file mode 100644 index 00000000..a8f59d9b --- /dev/null +++ b/components/switch/demo/cases/demo2.vue @@ -0,0 +1,30 @@ + + + \ No newline at end of file diff --git a/components/switch/demo/cases/demo3.vue b/components/switch/demo/cases/demo3.vue new file mode 100644 index 00000000..0b7db0b9 --- /dev/null +++ b/components/switch/demo/cases/demo3.vue @@ -0,0 +1,30 @@ + + + \ No newline at end of file diff --git a/components/switch/demo/index.vue b/components/switch/demo/index.vue new file mode 100644 index 00000000..7e595708 --- /dev/null +++ b/components/switch/demo/index.vue @@ -0,0 +1,24 @@ + + + + + \ No newline at end of file diff --git a/components/switch/index.vue b/components/switch/index.vue new file mode 100644 index 00000000..b1eaa633 --- /dev/null +++ b/components/switch/index.vue @@ -0,0 +1,76 @@ + + + + + diff --git a/components/switch/test/index.spec.js b/components/switch/test/index.spec.js new file mode 100644 index 00000000..1df67ccf --- /dev/null +++ b/components/switch/test/index.spec.js @@ -0,0 +1,52 @@ +import Switch from '../index' +import {mount} from 'avoriaz' + +describe('Switch', () => { + let wrapper + + afterEach(() => { + wrapper && wrapper.destroy() + }) + + it('create a simple switch', () => { + wrapper = mount(Switch) + expect(wrapper.hasClass('md-switch')).to.be.true + }) + + it('create a simple active switch', () => { + wrapper = mount(Switch, { + propsData: { + value: true, + }, + }) + expect(wrapper.hasClass('active')).to.be.true + + const eventStub = sinon.stub(wrapper.vm, '$emit') + wrapper.trigger('click') + expect(eventStub.calledWith('change')).to.be.true + }) + + it('create a simple inactive switch', done => { + wrapper = mount(Switch, { + propsData: { + value: false, + }, + }) + expect(wrapper.hasClass('active')).to.be.false + + wrapper.vm.value = true + setTimeout(() => { + expect(wrapper.hasClass('active')).to.be.true + done() + }, 300) + }) + + it('create a disabled switch', () => { + wrapper = mount(Switch, { + propsData: { + disabled: true, + }, + }) + expect(wrapper.hasClass('disabled')).to.be.true + }) +}) diff --git a/components/tab-bar/README.md b/components/tab-bar/README.md new file mode 100644 index 00000000..110ef9a0 --- /dev/null +++ b/components/tab-bar/README.md @@ -0,0 +1,47 @@ +--- +title: TabBar 标签栏 +preview: https://didi.github.io/mand-mobile/examples/tab-bar +--- + +用于创建不含内容区域的标签栏 + +### 引入 + +```javascript +import { TabBar } from 'mand-mobile' + +Vue.component(TabBar.name, TabBar) +``` + +### 代码演示 + + +### API + +#### TabBar Props +|属性 | 说明 | 类型 | 默认值 | 备注| +|----|-----|------|------|------| +| titles | 标签标题数组 | Array | - | 传入该数组会直接根据数组内容渲染组件,也可以不使用该属性,直接在控件中插入定制的标题按钮。在不使用scope-slot时,该值为字符串数组;在使用scope-slot时,该值为对象数组,每个对象会作为props供父组件使用 | +| show-ink-bar | 是否显示下划线 | Boolean | true | - | +| ink-bar-length | 下划线宽度 | Number | 70 | 该数值为下划线占标签按钮宽度的百分比,须在0-100之间 | +| ink-bar-animate | 是否启用下划线动画 | Boolean | true | - | +| default-index | 默认激活的标签索引 | Number | 0 | - | + +#### TabBar Methods + +##### selectTab(index) +选择某一标签 + +|属性 | 说明 | 类型 | 默认值| +|----|-----|------|------| +| index | 标签索引 | Number | - | + +#### TabBar Events + +##### @indexChanged(index, preIndex) +标签索引发生变化 + +|属性 | 说明 | 类型 | +|----|-----|------| +| index | 改变后的标签索引 | Number | +| preIndex | 改变前的标签索引 | Number | diff --git a/components/tab-bar/component.js b/components/tab-bar/component.js new file mode 100644 index 00000000..edbf83d8 --- /dev/null +++ b/components/tab-bar/component.js @@ -0,0 +1,7 @@ +export default { + 'name': 'tab-bar', + 'text': '标签栏', + 'category': 'basic', + 'description': '用一组标签按钮控制另一区域内容切换的组件。', + 'author': 'zhaozhe' +} diff --git a/components/tab-bar/demo/cases/demo0.vue b/components/tab-bar/demo/cases/demo0.vue new file mode 100644 index 00000000..6778e1a2 --- /dev/null +++ b/components/tab-bar/demo/cases/demo0.vue @@ -0,0 +1,23 @@ + + + \ No newline at end of file diff --git a/components/tab-bar/demo/cases/demo1.vue b/components/tab-bar/demo/cases/demo1.vue new file mode 100644 index 00000000..2cfcef32 --- /dev/null +++ b/components/tab-bar/demo/cases/demo1.vue @@ -0,0 +1,24 @@ + + + \ No newline at end of file diff --git a/components/tab-bar/demo/cases/demo2.vue b/components/tab-bar/demo/cases/demo2.vue new file mode 100644 index 00000000..bb9ce248 --- /dev/null +++ b/components/tab-bar/demo/cases/demo2.vue @@ -0,0 +1,26 @@ + + + \ No newline at end of file diff --git a/components/tab-bar/demo/cases/demo3.vue b/components/tab-bar/demo/cases/demo3.vue new file mode 100644 index 00000000..0aad27f6 --- /dev/null +++ b/components/tab-bar/demo/cases/demo3.vue @@ -0,0 +1,25 @@ + + + \ No newline at end of file diff --git a/components/tab-bar/demo/cases/demo4.vue b/components/tab-bar/demo/cases/demo4.vue new file mode 100644 index 00000000..1f003567 --- /dev/null +++ b/components/tab-bar/demo/cases/demo4.vue @@ -0,0 +1,25 @@ + + + \ No newline at end of file diff --git a/components/tab-bar/demo/cases/demo5.vue b/components/tab-bar/demo/cases/demo5.vue new file mode 100644 index 00000000..65bcee54 --- /dev/null +++ b/components/tab-bar/demo/cases/demo5.vue @@ -0,0 +1,31 @@ + + + \ No newline at end of file diff --git a/components/tab-bar/demo/cases/demo6.vue b/components/tab-bar/demo/cases/demo6.vue new file mode 100644 index 00000000..9d548304 --- /dev/null +++ b/components/tab-bar/demo/cases/demo6.vue @@ -0,0 +1,29 @@ + + + \ No newline at end of file diff --git a/components/tab-bar/demo/cases/demo7.vue b/components/tab-bar/demo/cases/demo7.vue new file mode 100644 index 00000000..024cdbf0 --- /dev/null +++ b/components/tab-bar/demo/cases/demo7.vue @@ -0,0 +1,35 @@ + + + \ No newline at end of file diff --git a/components/tab-bar/demo/cases/demo8.vue b/components/tab-bar/demo/cases/demo8.vue new file mode 100644 index 00000000..d627c486 --- /dev/null +++ b/components/tab-bar/demo/cases/demo8.vue @@ -0,0 +1,43 @@ + + + \ No newline at end of file diff --git a/components/tab-bar/demo/cases/demo9.vue b/components/tab-bar/demo/cases/demo9.vue new file mode 100644 index 00000000..f6bf3ccc --- /dev/null +++ b/components/tab-bar/demo/cases/demo9.vue @@ -0,0 +1,28 @@ + + + + + \ No newline at end of file diff --git a/components/tab-bar/demo/index.vue b/components/tab-bar/demo/index.vue new file mode 100644 index 00000000..f24b86c9 --- /dev/null +++ b/components/tab-bar/demo/index.vue @@ -0,0 +1,30 @@ + + + + + \ No newline at end of file diff --git a/components/tab-bar/index.vue b/components/tab-bar/index.vue new file mode 100644 index 00000000..df449905 --- /dev/null +++ b/components/tab-bar/index.vue @@ -0,0 +1,180 @@ + + + diff --git a/components/tab-bar/test/index.spec.js b/components/tab-bar/test/index.spec.js new file mode 100644 index 00000000..50679a51 --- /dev/null +++ b/components/tab-bar/test/index.spec.js @@ -0,0 +1,74 @@ +import TabBar from '../index' +import {mount} from 'avoriaz' + +describe('TabBar', () => { + let wrapper + + afterEach(() => { + wrapper && wrapper.destroy() + }) + + it('create an empty tab-bar', () => { + wrapper = mount(TabBar) + + expect(wrapper.hasClass('md-tab-bar')).to.be.true + }) + + it('create a tab-bar with title list', () => { + wrapper = mount(TabBar, { + propsData: { + titles: ['标题一', '标题二', '标题三'], + }, + }) + + expect(wrapper.find('.md-tab-title').length).to.equal(3) + }) + + it('switch index by changing default index', done => { + wrapper = mount(TabBar, { + propsData: { + titles: ['标题一', '标题二', '标题三'], + }, + }) + + expect(wrapper.find('.md-tab-title')[0].hasClass('active')).to.be.true + + wrapper.vm.defaultIndex = 2 + wrapper.vm.$nextTick(() => { + expect(wrapper.find('.md-tab-title')[2].hasClass('active')).to.be.true + done() + }) + }) + + it('switch index by clicking', done => { + wrapper = mount(TabBar, { + propsData: { + titles: ['标题一', '标题二', '标题三'], + }, + }) + + expect(wrapper.find('.md-tab-title')[0].hasClass('active')).to.be.true + wrapper.find('.md-tab-title')[2].trigger('click') + wrapper.vm.$nextTick(() => { + expect(wrapper.find('.md-tab-title')[2].hasClass('active')).to.be.true + done() + }) + }) + + it('create a tab-bar with customized titles', () => { + wrapper = mount(TabBar, { + slots: { + default: [ + { + template: '
title A
', + }, + { + template: '
title B
', + }, + ], + }, + }) + + expect(wrapper.find('.md-tab-title').length).to.equal(2) + }) +}) diff --git a/components/tab-picker/README.md b/components/tab-picker/README.md new file mode 100644 index 00000000..7b182e76 --- /dev/null +++ b/components/tab-picker/README.md @@ -0,0 +1,197 @@ +--- +title: TabPicker 多级联动选择器 +preview: https://didi.github.io/mand-mobile/examples/tab-picker +--- + +底部级联选择、非级联选择的tab切换的面板 + +### 引入 + +```javascript +import { TabPicker } from 'mand-mobile' + +Vue.component(TabPicker.name, TabPicker) +``` + +### 使用指南 + +tab切换的title支持自定义渲染(通过slot-scope) +```html + +
+ 标签dom + {{ props.label }} +
+``` +异步级联面板支持传入slot +```html + +
loading内容
+ + +
数据异常
+``` + +### 代码演示 + + +### API + +#### TabPicker Props +|属性 | 说明 | 类型 | 默认值 | 备注| +|----|-----|------|------|------| +|v-model|控制显示或隐藏|Boolean|`false`| -| +|data|数据源|Array|`[]`|参数据格式考`附录`| +|data-struct|数据级联类型|String|`noCascade`|`normal`, `cascade`, `async`| +|default-index|初始选中项索引|Array|`[]`|-| +|option-render|返回各选项渲染内容|Array|`[]`|`vue 2.1.0+`可使用`slot-scope`,见附录| +|async-func|异步获取数据函数|Function(value, callBack)|-|-| +|title|弹窗标题|Boolean|-|-| +|ok-text|确认按钮文案|String|`确认`|-| +|cancel-text|取消按钮文案|String|`取消`|-| + + +#### TabPicker Methods + +#### getSelectedItem() +获取所有列选中项的值 + +返回 + +|属性 | 说明 | 类型| +|----|-----|------| +|columnsValue|所有列选中项的值|Array<{value, lable, ...}>| + +#### TabPicker Events + +#### @change(select) +底部弹窗选中事件 + +|属性 | 说明 | 类型| +|----|-----|------| +|select|各列选中项值|Object: {value,lable}| + +#### @confirm(selected) +底部弹窗确认选择事件 + +|属性 | 说明 | 类型 | +|----|-----|------| +|selected|各列选中项值|Array<{value,lable}>| + +#### @cancel() +底部弹窗取消选择事件 + +#### @show() +底部弹窗弹层展示事件 + +#### @hide() +底部弹窗弹层隐藏事件 + +### 附录 + +* 非级联数据源数据格式 + +```javascript +[ + { + // 选项展示文案 + "label": "", + // 以下自定义字段 + "value": "", + //该选项下的列表 + "children": [ + { + "label": "", + "value": "" + }, + // ... + ] + }, + // ... + // ... +] +``` + +* 级联数据源数据格式 + +```javascript +[ + { + // 选项展示文案 + "label": "", + // 选项值 + "value": "", + // 第二列对应数据 + "children": [ + { + "label": "", + "value": "", + "children": [ + //... + ] + } + ] + }, + //... +] +``` + +* 异步级联数据源数据格式 + +```javascript +{ + "options": [ + { + // 选项展示文案 + "label": "", + // 选项值 + "value": "" + }, + //... + ] + "asyncFunc": (value, callback) => { + callback(null, { + "options": [ + { + "label": '', + "value": '' + }, + //... + ], + "asyncFunc": (value, callback) => { + //... + } + }) + } +} +``` + +* 自定义渲染option + +```html + +``` diff --git a/components/tab-picker/component.js b/components/tab-picker/component.js new file mode 100644 index 00000000..2ddadfc2 --- /dev/null +++ b/components/tab-picker/component.js @@ -0,0 +1,7 @@ +export default { + 'name': 'tab-picker', + 'text': '底部多级联动选择器', + 'category': 'feedback', + 'description': '', + 'author': 'qiman' +} diff --git a/components/tab-picker/demo/cases/demo0.vue b/components/tab-picker/demo/cases/demo0.vue new file mode 100644 index 00000000..09d3cefd --- /dev/null +++ b/components/tab-picker/demo/cases/demo0.vue @@ -0,0 +1,55 @@ + + + \ No newline at end of file diff --git a/components/tab-picker/demo/cases/demo1.vue b/components/tab-picker/demo/cases/demo1.vue new file mode 100644 index 00000000..97bbf500 --- /dev/null +++ b/components/tab-picker/demo/cases/demo1.vue @@ -0,0 +1,63 @@ + + + \ No newline at end of file diff --git a/components/tab-picker/demo/cases/demo2.vue b/components/tab-picker/demo/cases/demo2.vue new file mode 100644 index 00000000..6b4e9d8e --- /dev/null +++ b/components/tab-picker/demo/cases/demo2.vue @@ -0,0 +1,108 @@ + + + \ No newline at end of file diff --git a/components/tab-picker/demo/data/cascade.js b/components/tab-picker/demo/data/cascade.js new file mode 100644 index 00000000..84c39917 --- /dev/null +++ b/components/tab-picker/demo/data/cascade.js @@ -0,0 +1,46 @@ +export default [ + { + label: '张三', + value: 1, + children: [ + { + label: '学生', + value: 2, + children: [ + { + label: '男', + value: 11, + children: '', + }, + { + label: '女', + value: 22, + children: '', + }, + ], + }, + ], + }, + { + label: '李四', + value: 2, + children: [ + { + label: '学生', + value: 2, + children: [ + { + label: '男', + value: 11, + children: '', + }, + { + label: '女', + value: 22, + children: '', + }, + ], + }, + ], + }, +] diff --git a/components/tab-picker/demo/data/no-cascade.js b/components/tab-picker/demo/data/no-cascade.js new file mode 100644 index 00000000..84755942 --- /dev/null +++ b/components/tab-picker/demo/data/no-cascade.js @@ -0,0 +1,80 @@ +export default [ + { + label: '第一选择项', + value: '0277', + children: [ + { + label: '武汉', + value: '027', + }, + { + label: '襄阳', + value: '027', + }, + { + label: '十堰', + value: '027', + }, + { + label: '武汉', + value: '027', + }, + { + label: '襄阳', + value: '027', + }, + { + label: '十堰', + value: '027', + }, + { + label: '武汉', + value: '027', + }, + { + label: '襄阳', + value: '027', + }, + { + label: '十堰', + value: '027', + }, + ], + }, + { + label: '第二选择项', + value: '0272', + children: [ + { + label: '成都', + value: '024', + }, + { + label: '汶川', + value: '021', + }, + { + label: '绵阳市', + value: '026', + }, + ], + }, + { + label: '第三选择项', + value: '0247', + children: [ + { + label: '长沙', + value: '0297', + }, + { + label: '株洲', + value: '0273', + }, + { + label: '岳阳', + value: '0207', + }, + ], + }, +] diff --git a/components/tab-picker/demo/index.vue b/components/tab-picker/demo/index.vue new file mode 100644 index 00000000..62661ff8 --- /dev/null +++ b/components/tab-picker/demo/index.vue @@ -0,0 +1,199 @@ + + + + + \ No newline at end of file diff --git a/components/tab-picker/index.vue b/components/tab-picker/index.vue new file mode 100644 index 00000000..55cf3ea0 --- /dev/null +++ b/components/tab-picker/index.vue @@ -0,0 +1,389 @@ + + + + + + + diff --git a/components/tab-picker/test/index.spec.js b/components/tab-picker/test/index.spec.js new file mode 100644 index 00000000..52b650b7 --- /dev/null +++ b/components/tab-picker/test/index.spec.js @@ -0,0 +1,159 @@ +import TabPicker from '../index' +import {mount} from 'avoriaz' +import cascade from '../demo/data/cascade' +import noCascade from '../demo/data/no-cascade' + +describe('TabPicker', () => { + let wrapper + + afterEach(() => { + wrapper && wrapper.destroy() + }) + + it('create a normal tab-picker', done => { + wrapper = mount(TabPicker, { + propsData: { + data: noCascade, + value: true, + }, + }) + expect(wrapper.vm.data.length).to.equal(3) + + const eventStub = sinon.stub(wrapper.vm, '$emit') + + wrapper.vm.$nextTick(() => { + expect(eventStub.calledWith('show')) + expect(wrapper.find('.md-tab-title').length).to.equal(3) + const cancelmBtn = wrapper.find('.md-popup-cancel')[0] + cancelmBtn.trigger('click') + expect(eventStub.calledWith('input')) + expect(eventStub.calledWith('cancel')).to.be.true + setTimeout(() => { + expect(wrapper.vm.isTabPickerShow).to.be.false + expect(eventStub.calledWith('hide')) + done() + }, 500) + }) + }) + + it('create a cascade tab-picker', done => { + wrapper = mount(TabPicker, { + propsData: { + dataStruct: 'cascade', + data: cascade, + value: true, + }, + }) + + const eventStub = sinon.stub(wrapper.vm, '$emit') + + wrapper.vm.$nextTick(() => { + expect(eventStub.calledWith('show')) + expect(wrapper.find('.md-tab-title').length).to.equal(1) + wrapper.find('.md-radio-item')[0].trigger('click') + expect(eventStub.calledWith('change')).to.be.true + setTimeout(() => { + expect(wrapper.vm.renderData.length).to.equal(2) + expect(wrapper.find('.md-tab-title').length).to.equal(2) + wrapper.find('.md-popup-confirm')[0].trigger('click') + expect(eventStub.calledWith('confirm')).to.be.true + done() + }, 500) + }) + }) + + it('create async tabPicker', done => { + wrapper = mount(TabPicker, { + propsData: { + value: true, + dataStruct: 'async', + asyncFunc: (value, callback) => { + setTimeout(() => { + callback(null, { + options: [ + { + label: '一级选项一', + value: '0271', + }, + { + label: '一级选项二', + value: '0272', + }, + ], + asyncFunc: (value, callback) => { + callback(null, { + options: [ + { + label: '二级选项一', + value: '0271', + }, + { + label: '二级选项二', + value: '0272', + }, + ], + asyncFunc: (value, callback) => { + callback(null, { + options: [ + { + label: '三级选项一', + value: '0271', + }, + { + label: '三级选项二', + value: '0272', + }, + ], + }) + }, + }) + }, + }) + }, 500) + }, + }, + }) + + const eventStub = sinon.stub(wrapper.vm, '$emit') + + wrapper.vm.$nextTick(() => { + setTimeout(() => { + expect(wrapper.vm.renderData.length).to.equal(1) + expect(wrapper.find('.md-tab-title').length).to.equal(1) + wrapper.find('.md-radio-item')[0].trigger('click') + expect(eventStub.calledWith('change')).to.be.true + setTimeout(() => { + expect(wrapper.vm.renderData.length).to.equal(2) + expect(wrapper.find('.md-tab-title').length).to.equal(2) + const cancelmBtn = wrapper.find('.md-popup-cancel')[0] + cancelmBtn.trigger('click') + expect(eventStub.calledWith('input')).to.be.false + expect(eventStub.calledWith('cancel')).to.be.true + done() + }, 800) + }, 800) + }) + }) + + it('tabPicker defalut index and casacde', done => { + wrapper = mount(TabPicker, { + propsData: { + data: cascade, + value: true, + dataStruct: 'cascade', + defaultIndex: [0, 0, 0], + }, + }) + + const eventStub = sinon.stub(wrapper.vm, '$emit') + + wrapper.vm.$nextTick(() => { + expect(wrapper.vm.renderData.length).to.equal(3) + expect(wrapper.find('.md-tab-title').length).to.equal(3) + const confirmBtn = wrapper.find('.md-popup-confirm')[0] + confirmBtn.trigger('click') + expect(eventStub.calledWith('confirm')).to.be.true + done() + }) + }) +}) diff --git a/components/tabs/README.md b/components/tabs/README.md new file mode 100644 index 00000000..cd8f3ed9 --- /dev/null +++ b/components/tabs/README.md @@ -0,0 +1,48 @@ +--- +title: Tabs 标签页 +preview: https://didi.github.io/mand-mobile/examples/tabs +--- + +用于创建包含内容区域的标签页 + +### 引入 + +```javascript +import { Tabs } from 'mand-mobile' + +Vue.component(Tabs.name, Tabs) +``` + +### 代码演示 + + +### API + +#### Tabs Props +|属性 | 说明 | 类型 | 默认值 | 备注| +|----|-----|------|------|------| +|titles|标签标题数组|Array|-|传入该数组会直接根据数组内容渲染组件,也可以不使用该属性,直接在控件中插入定制的标题按钮。在不使用scope-slot时,该值为字符串数组;在使用scope-slot时,该值为对象数组,每个对象会作为props供父组件使用| +|show-ink-bar|是否显示下划线|Boolean|true|-| +|ink-bar-length|下划线宽度|Number|`70`|该数值为下划线占标签按钮宽度的百分比,须在0-100之间| +|ink-bar-animate|是否启用下划线动画|Boolean|`true`|-| +|default-index|默认激活的标签索引|Number|`0`|-| +|noslide|动画样式|Boolean|`false`|如果为真,则不显示滑动动画| + +#### Tabs Methods + +##### selectTab(index) +选择某一标签 + +|属性 | 说明 | 类型 | +|----|-----|------| +|index|标签索引|Number| + +#### Tabs Events + +##### @change(index, preIndex) +标签索引发生变化事件 + +|属性 | 说明 | 类型| +|----|-----|------| +|index|改变后的标签索引|Number| +|preIndex|改变前的标签索引|Number| diff --git a/components/tabs/component.js b/components/tabs/component.js new file mode 100644 index 00000000..4f83596f --- /dev/null +++ b/components/tabs/component.js @@ -0,0 +1,7 @@ +export default { + 'name': 'tabs', + 'text': '标签页', + 'category': 'basic', + 'description': '用于在内部固定区域切换展示内容的组件。', + 'author': 'zhaozhe' +} diff --git a/components/tabs/demo/cases/demo0.vue b/components/tabs/demo/cases/demo0.vue new file mode 100644 index 00000000..94344bc1 --- /dev/null +++ b/components/tabs/demo/cases/demo0.vue @@ -0,0 +1,27 @@ + + + \ No newline at end of file diff --git a/components/tabs/demo/cases/demo1.vue b/components/tabs/demo/cases/demo1.vue new file mode 100644 index 00000000..5117271e --- /dev/null +++ b/components/tabs/demo/cases/demo1.vue @@ -0,0 +1,30 @@ + + +mand-mobile +import {Tabs} from 'mand-mobile' + +export default { + name: 'tab-bar-demo', + title: '不带下划线', + components: { + [Tabs.name]: Tabs, + }, + data() { + return { + titles: ['第一', '第二', '第三', '第四'], + } + }, +} + + \ No newline at end of file diff --git a/components/tabs/demo/cases/demo10.vue b/components/tabs/demo/cases/demo10.vue new file mode 100644 index 00000000..793c57de --- /dev/null +++ b/components/tabs/demo/cases/demo10.vue @@ -0,0 +1,47 @@ + + + \ No newline at end of file diff --git a/components/tabs/demo/cases/demo11.vue b/components/tabs/demo/cases/demo11.vue new file mode 100644 index 00000000..c724e0e7 --- /dev/null +++ b/components/tabs/demo/cases/demo11.vue @@ -0,0 +1,35 @@ + + +mand-mobile +import {Tabs} from 'mand-mobile' + +export default { + name: 'tab-bar-demo', + title: '设置标题栏边距', + components: { + [Tabs.name]: Tabs, + }, + data() { + return { + titles: ['第一', '第二', '第三', '第四'], + } + }, +} + + + + \ No newline at end of file diff --git a/components/tabs/demo/cases/demo2.vue b/components/tabs/demo/cases/demo2.vue new file mode 100644 index 00000000..ae9d30e5 --- /dev/null +++ b/components/tabs/demo/cases/demo2.vue @@ -0,0 +1,30 @@ + + + \ No newline at end of file diff --git a/components/tabs/demo/cases/demo3.vue b/components/tabs/demo/cases/demo3.vue new file mode 100644 index 00000000..ddb513e1 --- /dev/null +++ b/components/tabs/demo/cases/demo3.vue @@ -0,0 +1,31 @@ + + +mand-mobile +import {Tabs} from 'mand-mobile' + +export default { + name: 'tab-bar-demo', + title: '指定下划线长度', + components: { + [Tabs.name]: Tabs, + }, + data() { + return { + titles: ['第一', '第二', '第三', '第四'], + } + }, +} + + \ No newline at end of file diff --git a/components/tabs/demo/cases/demo4.vue b/components/tabs/demo/cases/demo4.vue new file mode 100644 index 00000000..88258143 --- /dev/null +++ b/components/tabs/demo/cases/demo4.vue @@ -0,0 +1,29 @@ + + + \ No newline at end of file diff --git a/components/tabs/demo/cases/demo5.vue b/components/tabs/demo/cases/demo5.vue new file mode 100644 index 00000000..566dcdf2 --- /dev/null +++ b/components/tabs/demo/cases/demo5.vue @@ -0,0 +1,47 @@ + + + \ No newline at end of file diff --git a/components/tabs/demo/cases/demo6.vue b/components/tabs/demo/cases/demo6.vue new file mode 100644 index 00000000..4151aac9 --- /dev/null +++ b/components/tabs/demo/cases/demo6.vue @@ -0,0 +1,34 @@ + + + \ No newline at end of file diff --git a/components/tabs/demo/cases/demo7.vue b/components/tabs/demo/cases/demo7.vue new file mode 100644 index 00000000..8f248620 --- /dev/null +++ b/components/tabs/demo/cases/demo7.vue @@ -0,0 +1,34 @@ + + + \ No newline at end of file diff --git a/components/tabs/demo/cases/demo8.vue b/components/tabs/demo/cases/demo8.vue new file mode 100644 index 00000000..37e0edfa --- /dev/null +++ b/components/tabs/demo/cases/demo8.vue @@ -0,0 +1,28 @@ + + + \ No newline at end of file diff --git a/components/tabs/demo/cases/demo9.vue b/components/tabs/demo/cases/demo9.vue new file mode 100644 index 00000000..17a15260 --- /dev/null +++ b/components/tabs/demo/cases/demo9.vue @@ -0,0 +1,28 @@ + + + \ No newline at end of file diff --git a/components/tabs/demo/index.vue b/components/tabs/demo/index.vue new file mode 100644 index 00000000..fa8d7f91 --- /dev/null +++ b/components/tabs/demo/index.vue @@ -0,0 +1,38 @@ + + + + + \ No newline at end of file diff --git a/components/tabs/index.vue b/components/tabs/index.vue new file mode 100644 index 00000000..3b539828 --- /dev/null +++ b/components/tabs/index.vue @@ -0,0 +1,194 @@ + + + diff --git a/components/tabs/test/index.spec.js b/components/tabs/test/index.spec.js new file mode 100644 index 00000000..f0af5fe8 --- /dev/null +++ b/components/tabs/test/index.spec.js @@ -0,0 +1,82 @@ +import Tabs from '../index' +import {mount} from 'avoriaz' + +describe('Tabs', () => { + let wrapper + + afterEach(() => { + wrapper && wrapper.destroy() + }) + + it('create an empty tabs', () => { + wrapper = mount(Tabs) + + expect(wrapper.hasClass('md-tabs')).to.be.true + }) + + it('create a tabs with title list', () => { + wrapper = mount(Tabs, { + propsData: { + titles: ['标题一', '标题二', '标题三'], + }, + }) + + expect(wrapper.find('.md-tab-title').length).to.equal(3) + }) + + it('switch index by using selectTab method', done => { + wrapper = mount(Tabs, { + propsData: { + titles: ['标题一', '标题二', '标题三'], + }, + }) + + expect(wrapper.find('.md-tab-title')[0].hasClass('active')).to.be.true + + wrapper.vm.selectTab(2) + wrapper.vm.$nextTick(() => { + expect(wrapper.find('.md-tab-title')[2].hasClass('active')).to.be.true + done() + }) + }) + + it('switch index by clicking', done => { + wrapper = mount(Tabs, { + propsData: { + titles: ['标题一', '标题二', '标题三'], + }, + }) + + expect(wrapper.find('.md-tab-title')[0].hasClass('active')).to.be.true + wrapper.find('.md-tab-title')[2].trigger('click') + wrapper.vm.$nextTick(() => { + expect(wrapper.find('.md-tab-title')[2].hasClass('active')).to.be.true + done() + }) + }) + + it('create a tabs with customized titles and contents', () => { + wrapper = mount(Tabs, { + slots: { + default: [ + { + template: '
content A
', + }, + { + template: '
content B
', + }, + ], + title: [ + { + template: '
title A
', + }, + { + template: '
title B
', + }, + ], + }, + }) + + expect(wrapper.find('.md-tab-title').length).to.equal(2) + }) +}) diff --git a/components/tag/README.md b/components/tag/README.md new file mode 100644 index 00000000..bb65da57 --- /dev/null +++ b/components/tag/README.md @@ -0,0 +1,29 @@ +--- +title: Tag 标签 +preview: https://didi.github.io/mand-mobile/examples/tag +--- + +用于表示区域的状态的标签 + +### 引入 + +```javascript +import { Tag } from 'mand-mobile' + +Vue.component(Tag.name, Tag) +``` + +### 代码演示 + + +### API + +#### Tag Props +|属性 | 说明 | 类型 | 默认值 |可选值| +|----|-----|------|------|------| +|size|标签大小|String|`large`|`tiny`, `small`, `large`| +|shape|标签形状|String|`square`|`square`, `circle`, `fillet`| +|type|标签样式|String|`ghost`|`fill`(填充), `ghost`(线框)| +|fill-color|标签颜色`rgba` or `hex number`|String|`rgba(0,0,0,0)`|-| +|font-weight|字体粗细|String|`normal`|`normal`, `bold`, `bolder`| +|font-color|字体颜色`rgba` or `hex number`|String|`#fc9153`|-| diff --git a/components/tag/component.js b/components/tag/component.js new file mode 100644 index 00000000..44bf5613 --- /dev/null +++ b/components/tag/component.js @@ -0,0 +1,7 @@ +export default { + 'name': 'tag', + 'text': '标签', + 'category': 'basic', + 'description': '小标签', + 'author': 'guoyunlong ' +} diff --git a/components/tag/demo/cases/demo0.vue b/components/tag/demo/cases/demo0.vue new file mode 100644 index 00000000..4e4d3190 --- /dev/null +++ b/components/tag/demo/cases/demo0.vue @@ -0,0 +1,19 @@ + + + \ No newline at end of file diff --git a/components/tag/demo/cases/demo1.vue b/components/tag/demo/cases/demo1.vue new file mode 100644 index 00000000..0a1f2f24 --- /dev/null +++ b/components/tag/demo/cases/demo1.vue @@ -0,0 +1,18 @@ + + + \ No newline at end of file diff --git a/components/tag/demo/cases/demo2.vue b/components/tag/demo/cases/demo2.vue new file mode 100644 index 00000000..b0672f27 --- /dev/null +++ b/components/tag/demo/cases/demo2.vue @@ -0,0 +1,17 @@ + + + \ No newline at end of file diff --git a/components/tag/demo/cases/demo3.vue b/components/tag/demo/cases/demo3.vue new file mode 100644 index 00000000..f0636877 --- /dev/null +++ b/components/tag/demo/cases/demo3.vue @@ -0,0 +1,17 @@ + + + \ No newline at end of file diff --git a/components/tag/demo/index.vue b/components/tag/demo/index.vue new file mode 100644 index 00000000..02da4ded --- /dev/null +++ b/components/tag/demo/index.vue @@ -0,0 +1,30 @@ + + + + + \ No newline at end of file diff --git a/components/tag/index.vue b/components/tag/index.vue new file mode 100644 index 00000000..2255f8df --- /dev/null +++ b/components/tag/index.vue @@ -0,0 +1,121 @@ + + + + + diff --git a/components/tag/test/index.spec.js b/components/tag/test/index.spec.js new file mode 100644 index 00000000..d0fb3b9f --- /dev/null +++ b/components/tag/test/index.spec.js @@ -0,0 +1,44 @@ +import Tag from '../index' +import {mount, shallow} from 'avoriaz' + +describe('Tag', () => { + let wrapper + + afterEach(() => { + wrapper && wrapper.destroy() + }) + + it('create a simple tag', () => { + wrapper = mount(Tag) + + expect(wrapper.hasClass('md-tag')).to.be.true + }) + + it('create a circle tag', done => { + wrapper = mount(Tag) + wrapper.setProps({ + shape: 'circle', + fontColor: '#fff', + }) + wrapper.vm.$nextTick(function() { + expect(wrapper.contains('.shape-circle')).to.be.true + done() + }) + }) + it('create a fill tag', () => { + wrapper = mount(Tag) + wrapper.setProps({ + type: 'fill', + fontColor: '#fff', + }) + expect(wrapper.contains('.type-fill')).to.be.true + }) + it('create a ghost tag', () => { + wrapper = mount(Tag) + wrapper.setProps({ + type: 'ghost', + fontColor: '#fff', + }) + expect(wrapper.contains('.type-ghost')).to.be.true + }) +}) diff --git a/components/tip/README.md b/components/tip/README.md new file mode 100644 index 00000000..f66ca23f --- /dev/null +++ b/components/tip/README.md @@ -0,0 +1,29 @@ +--- +title: Tip 气泡提示 +preview: https://didi.github.io/mand-mobile/examples/tip +--- + +### 引入 + +```javascript +import { Tip } from 'mand-mobile' + +Vue.component(Tip.name, Tip) +``` + +### 代码演示 + + +### API + +#### Tips Props +|属性 | 说明 | 类型 | 默认值|备注| +|----|-----|------|------|------| +|content|提示文本内容|String|-|-| +|placement|位置|String|`top`|`top`, `left`, `bottom`, `right`| + +#### Tip@show() +提示框显示后触发的事件 + +#### Tip@hide() +提示框隐藏后触发的事件 diff --git a/components/tip/component.js b/components/tip/component.js new file mode 100644 index 00000000..f1df301e --- /dev/null +++ b/components/tip/component.js @@ -0,0 +1,7 @@ +export default { + 'name': 'tip', + 'text': '轻提示', + 'category': 'feedback', + 'description': '弹出式轻提示', + 'author': 'liuxinyumichael' +} diff --git a/components/tip/demo/cases/demo0.vue b/components/tip/demo/cases/demo0.vue new file mode 100644 index 00000000..61296bd4 --- /dev/null +++ b/components/tip/demo/cases/demo0.vue @@ -0,0 +1,19 @@ + + + diff --git a/components/tip/demo/cases/demo1.vue b/components/tip/demo/cases/demo1.vue new file mode 100644 index 00000000..60363553 --- /dev/null +++ b/components/tip/demo/cases/demo1.vue @@ -0,0 +1,19 @@ + + + diff --git a/components/tip/demo/cases/demo2.vue b/components/tip/demo/cases/demo2.vue new file mode 100644 index 00000000..461af31e --- /dev/null +++ b/components/tip/demo/cases/demo2.vue @@ -0,0 +1,19 @@ + + + \ No newline at end of file diff --git a/components/tip/demo/cases/demo3.vue b/components/tip/demo/cases/demo3.vue new file mode 100644 index 00000000..81b48bfa --- /dev/null +++ b/components/tip/demo/cases/demo3.vue @@ -0,0 +1,19 @@ + + + diff --git a/components/tip/demo/cases/demo4.vue b/components/tip/demo/cases/demo4.vue new file mode 100644 index 00000000..5f016bb2 --- /dev/null +++ b/components/tip/demo/cases/demo4.vue @@ -0,0 +1,31 @@ + + + diff --git a/components/tip/demo/index.vue b/components/tip/demo/index.vue new file mode 100644 index 00000000..31361f63 --- /dev/null +++ b/components/tip/demo/index.vue @@ -0,0 +1,27 @@ + + + + + diff --git a/components/tip/index.js b/components/tip/index.js new file mode 100644 index 00000000..d1763439 --- /dev/null +++ b/components/tip/index.js @@ -0,0 +1,195 @@ +import Vue from 'vue' +import TipOptions from './tip' +const Tip = Vue.extend(TipOptions) + +export default { + name: 'md-tip', + + props: { + placement: { + type: String, + default: 'top', + }, + content: { + type: String, + default: '', + }, + }, + + mounted() { + this.wrapperEl = this.$_getFirstScrollWrapper(this.$el) + }, + + beforeDestroy() { + if (this.$_tipVM) { + const el = this.$_tipVM.$el + const parent = el.parentNode + if (parent) { + parent.removeChild(el) + } + this.$_tipVM.$destroy() + } + }, + + /** + * Only render the first node of slots + * and add tip tirgger handler on it + */ + render() { + // eslint-disable-line no-unused-vars + if (!this.$slots.default || !this.$slots.default.length) { + return this.$slots.default + } + + const firstNode = this.$slots.default[0] + + const on = (firstNode.data.on = firstNode.data.on || {}) + const nativeOn = (firstNode.data.nativeOn = firstNode.data.nativeOn || {}) + + on.click = this.$_addEventHandle(on.click, this.show) + nativeOn.click = this.$_addEventHandle(nativeOn.click, this.show) + + return firstNode + }, + + methods: { + /** + * Add extra tip trigger handler + * without overwriting the old ones + */ + $_addEventHandle(old, fn) { + if (!old) { + return fn + } else if (Array.isArray(old)) { + return old.indexOf(fn) > -1 ? old : old.concat(fn) + } else { + return old === fn ? old : [old, fn] + } + }, + + /** + * Get the first scrollable parent, + * so we can append the tip element to + * the right parent container + */ + $_getFirstScrollWrapper(node) { + if (node === null || node === document.body) { + return node + } + + const overflowY = window.getComputedStyle(node).overflowY + const isScrollable = overflowY !== 'visible' && overflowY !== 'hidden' + + if (isScrollable && node.scrollHeight > node.clientHeight) { + return node + } else { + return this.$_getFirstScrollWrapper(node.parentNode) + } + }, + + /** + * Get the relative position of an element + * inside a wrapper + */ + $_getPosition(node, wrapper) { + let x = 0 + let y = 0 + let el = node + + while (el) { + x += el.offsetLeft + y += el.offsetTop + + if (el === wrapper || el === document.body || el === null) { + break + } + + el = el.offsetParent + } + + return {x, y} + }, + + /** + * Lazy create tip element + */ + $_getOrNewTip() { + if (this.$_tipVM) { + return this.$_tipVM + } + + const tipVM = (this.$_tipVM = new Tip({ + propsData: { + placement: this.placement, + content: this.content, + }, + }).$mount()) + + tipVM.$on('close', this.hide) + + return tipVM + }, + + /** + * Calculate the position of tip, + * and relayout it's position + */ + layout() { + if (!this.$_tipVM) { + return + } + + const tipElRect = this.$_tipVM.$el.getBoundingClientRect() + const referenceElRect = this.$el.getBoundingClientRect() + const delta = this.$_getPosition(this.$el, this.wrapperEl) + + switch (this.placement) { + case 'left': + delta.y += (referenceElRect.height - tipElRect.height) / 2 + delta.x -= tipElRect.width + 10 + break + + case 'right': + delta.y += (referenceElRect.height - tipElRect.height) / 2 + delta.x += referenceElRect.width + 10 + break + + case 'bottom': + delta.y += referenceElRect.height + 10 + delta.x += (referenceElRect.width - tipElRect.width) / 2 + break + + default: + delta.y -= tipElRect.height + 10 + delta.x += (referenceElRect.width - tipElRect.width) / 2 + break + } + + this.$_tipVM.$el.style.cssText = `position: absolute; top: ${delta.y}px; left: ${delta.x}px;` + }, + + /** + * Do the magic, show me your tip + */ + show() { + const tipVM = this.$_getOrNewTip() + + if (tipVM.$el.parentNode !== this.wrapperEl) { + this.wrapperEl.appendChild(tipVM.$el) + } + + this.layout() + this.$emit('show') + }, + + /** + * Hide tip + */ + hide() { + if (this.$_tipVM && this.$_tipVM.$el.parentNode !== null) { + this.$_tipVM.$el.parentNode.removeChild(this.$_tipVM.$el) + this.$emit('hide') + } + }, + }, +} diff --git a/components/tip/test/index.spec.js b/components/tip/test/index.spec.js new file mode 100644 index 00000000..599a3952 --- /dev/null +++ b/components/tip/test/index.spec.js @@ -0,0 +1,58 @@ +import TipContent from '../tip.vue' +import Button from '../../button/index.vue' +import sinon from 'sinon' +import {mount} from 'avoriaz' + +describe('Tip', () => { + let wrapper + + afterEach(() => { + wrapper && wrapper.destroy() + }) + + it('create a tip float over left', () => { + wrapper = mount(TipContent, { + propsData: { + content: 'Hello, Earth!', + placement: 'left', + }, + }) + + expect(wrapper.hasClass('is-left')).to.be.true + }) + + it('create a tip float over bottom', () => { + wrapper = mount(TipContent, { + propsData: { + content: 'Hello, Earth!', + placement: 'bottom', + }, + }) + + expect(wrapper.hasClass('is-bottom')).to.be.true + }) + + it('create a tip float over right', () => { + wrapper = mount(TipContent, { + propsData: { + content: 'Hello, Earth!', + placement: 'right', + }, + }) + + expect(wrapper.hasClass('is-right')).to.be.true + }) + + it('click close to hide', () => { + wrapper = mount(TipContent, { + propsData: { + content: 'Hello, Earth!', + }, + }) + + const eventStub = sinon.stub(wrapper.vm, '$emit') + + wrapper.first('.md-icon-cross').trigger('click') + expect(eventStub.calledWith('close')).to.be.true + }) +}) diff --git a/components/tip/tip.vue b/components/tip/tip.vue new file mode 100644 index 00000000..49941999 --- /dev/null +++ b/components/tip/tip.vue @@ -0,0 +1,96 @@ + + + + + + diff --git a/components/toast/README.md b/components/toast/README.md new file mode 100644 index 00000000..8f180976 --- /dev/null +++ b/components/toast/README.md @@ -0,0 +1,73 @@ +--- +title: Toast 轻提示 +preview: https://didi.github.io/mand-mobile/examples/toast +--- + +弹出式消息提示 + +### 引入 + +```javascript +import { Toast } from 'mand-mobile' + +Toast.succeed('操作成功') +``` + +### 代码演示 + + +### API + +#### Toast({content, icon, duration, hasMask, parentNode}) +显示自定义提示 + +|属性 | 说明 | 类型 | 默认值|备注| +|----|-----|------|------|------| +| icon | Icon组件图标名称 | String | - |如需自定义图标, 请查看`Icon`组件 | +| content | 提示内容文本 | String | - |- | +| duration | 显示多少毫秒后自动消失, 若为`0`则一直显示 | Number | `1000` | - | +| hasMask | 是否显示透明遮罩, 以此防止用户点击 | Boolean | `false` | - | +| parentNode | 组件挂载节点 | HTMLElement | `document.body`|- | + +#### Toast.info(content, duration, hasMask, parentNode) +显示纯文本提示 + +|属性 | 说明 | 类型 | 默认值| +|----|-----|------|------| +| content | 提示内容文本 | String | - | +| duration | 显示多少毫秒后自动消失, 若为`0`则一直显示 | Number | `1000` | +| hasMask | 是否显示透明遮罩, 以此防止用户点击 | Boolean | `false` | +| parentNode | 组件挂载节点 | HTMLElement | `document.body` | + +#### Toast.succeed(content, duration, hasMask, parentNode) +显示成功提示 + +|属性 | 说明 | 类型 | 默认值| +|----|-----|------|------| +| content | 提示内容文本 | String | - | +| duration | 显示多少毫秒后自动消失, 若为`0`则一直显示 | Number | `1000` | +| hasMask | 是否显示透明遮罩, 以此防止用户点击 | Boolean | `false` | +| parentNode | 组件挂载节点 | HTMLElement | `document.body` | + +#### Toast.failed(content, duration, hasMask, parentNode) +显示失败提示 + +|属性 | 说明 | 类型 | 默认值| +|----|-----|------|------| +| content | 提示内容文本 | String | - | +| duration | 显示多少毫秒后自动消失, 若为`0`则一直显示 | Number | `1000` | +| hasMask | 是否显示透明遮罩, 以此防止用户点击 | Boolean | `false` | +| parentNode | 组件挂载节点 | HTMLElement | `document.body`| + +#### Toast.loading(content, duration, hasMask, parentNode) +显示载入提示 + +|属性 | 说明 | 类型 | 默认值| +|----|-----|------|------| +| content | 提示内容文本 | String | - | +| duration | 显示多少毫秒后自动消失, 若为`0`则一直显示 | Number | `0` | +| hasMask | 是否显示透明遮罩, 以此防止用户点击 | Boolean | `true` | +| parentNode | 组件挂载节点 | HTMLElement | `document.body`| + +#### Toast.hide() +隐藏提示 diff --git a/components/toast/component.js b/components/toast/component.js new file mode 100644 index 00000000..fe20894b --- /dev/null +++ b/components/toast/component.js @@ -0,0 +1,7 @@ +export default { + 'name': 'toast', + 'text': '提示', + 'category': 'feedback', + 'description': '弹出式提示', + 'author': 'liuxinyumichael' +} diff --git a/components/toast/demo/cases/demo0.vue b/components/toast/demo/cases/demo0.vue new file mode 100644 index 00000000..e20996a0 --- /dev/null +++ b/components/toast/demo/cases/demo0.vue @@ -0,0 +1,21 @@ + + + diff --git a/components/toast/demo/cases/demo1.vue b/components/toast/demo/cases/demo1.vue new file mode 100644 index 00000000..7c566f85 --- /dev/null +++ b/components/toast/demo/cases/demo1.vue @@ -0,0 +1,21 @@ + + + diff --git a/components/toast/demo/cases/demo2.vue b/components/toast/demo/cases/demo2.vue new file mode 100644 index 00000000..e061db08 --- /dev/null +++ b/components/toast/demo/cases/demo2.vue @@ -0,0 +1,21 @@ + + + diff --git a/components/toast/demo/cases/demo3.vue b/components/toast/demo/cases/demo3.vue new file mode 100644 index 00000000..c21c1b47 --- /dev/null +++ b/components/toast/demo/cases/demo3.vue @@ -0,0 +1,24 @@ + + + diff --git a/components/toast/demo/cases/demo4.vue b/components/toast/demo/cases/demo4.vue new file mode 100644 index 00000000..08d94ceb --- /dev/null +++ b/components/toast/demo/cases/demo4.vue @@ -0,0 +1,27 @@ + + + diff --git a/components/toast/demo/cases/demo5.vue b/components/toast/demo/cases/demo5.vue new file mode 100644 index 00000000..c7244b09 --- /dev/null +++ b/components/toast/demo/cases/demo5.vue @@ -0,0 +1,21 @@ + + + diff --git a/components/toast/demo/index.vue b/components/toast/demo/index.vue new file mode 100644 index 00000000..3edbdde6 --- /dev/null +++ b/components/toast/demo/index.vue @@ -0,0 +1,22 @@ + + + diff --git a/components/toast/index.js b/components/toast/index.js new file mode 100644 index 00000000..40d91e72 --- /dev/null +++ b/components/toast/index.js @@ -0,0 +1,116 @@ +import Vue from 'vue' +import ToastOptions from './toast' +const ToastConstructor = Vue.extend(ToastOptions) + +function Toast({content = '', icon = '', duration = 3000, hasMask = false, parentNode = document.body}) { + let vm = Toast._instance + + if (!vm) { + vm = Toast._instance = new ToastConstructor({ + propsData: { + content, + icon, + duration, + hasMask, + }, + }).$mount() + parentNode.appendChild(vm.$el) + } + + vm.content = content + vm.icon = icon + vm.duration = duration + vm.hasMask = hasMask + vm.visible = true + + return vm +} + +// There is only one toast singleton +Toast._instance = null + +/** + * Hide toast + */ +Toast.hide = () => { + if (Toast._instance instanceof ToastConstructor && Toast._instance.visible) { + Toast._instance.hide() + } +} + +/** + * Show info toast + * @param {string} content + * @param {number=} [duration=3000] + * @param {boolean=} [hasMask=false] + * @param {node=} [parentNode=document.body] + * @returns {Toast} + */ + +Toast.info = (content = '', duration = 3000, hasMask = false, parentNode = document.body) => { + return Toast({ + icon: '', + content, + duration, + hasMask, + parentNode, + }) +} + +/** + * Show succeed toast + * @param {string} content + * @param {number=} [duration=3000] + * @param {boolean=} [hasMask=false] + * @param {node=} [parentNode=document.body] + * @returns {Toast} + */ + +Toast.succeed = (content = '', duration = 3000, hasMask = false, parentNode = document.body) => { + return Toast({ + icon: 'circle-right', + content, + duration, + hasMask, + parentNode, + }) +} + +/** + * Show failed toast + * @param {string} content + * @param {number=} [duration=3000] + * @param {boolean=} [hasMask=true] + * @param {node=} [parentNode=document.body] + * @returns {Toast} + */ + +Toast.failed = (content = '', duration = 3000, hasMask = false, parentNode = document.body) => { + return Toast({ + icon: 'circle-cross', + content, + duration, + hasMask, + parentNode, + }) +} + +/** + * Show loading toast + * @param {string} content + * @param {number=} [duration=0] + * @param {boolean=} [hasMask=false] + * @param {node=} [parentNode=document.body] + * @returns {Toast} + */ +Toast.loading = (content = '', duration = 0, hasMask = true, parentNode = document.body) => { + return Toast({ + icon: 'spinner', + content, + duration, + hasMask, + parentNode, + }) +} + +export default Toast diff --git a/components/toast/test/index.spec.js b/components/toast/test/index.spec.js new file mode 100644 index 00000000..8cd0291f --- /dev/null +++ b/components/toast/test/index.spec.js @@ -0,0 +1,96 @@ +import Toast from '../toast.vue' +import sinon from 'sinon' +import {mount} from 'avoriaz' + +describe('Toast', () => { + let wrapper + + afterEach(() => { + wrapper && wrapper.destroy() + }) + + it('create simple toast', () => { + wrapper = mount(Toast, { + propsData: { + content: 'Hello, Earth!', + }, + }) + + expect(wrapper.hasClass('md-toast')).to.equal(true) + }) + + it('has content', () => { + wrapper = mount(Toast, { + propsData: { + content: 'Hello, Earth!', + }, + }) + + expect( + wrapper + .first('.md-toast-content span') + .text() + .trim(), + ).to.equal('Hello, Earth!') + }) + + it('create icon toast', () => { + wrapper = mount(Toast, { + propsData: { + icon: 'cross', + content: 'Hello, Earth!', + }, + }) + + expect(wrapper.contains('.md-icon')).to.be.true + }) + + it('should update timer after state changed', done => { + wrapper = mount(Toast, { + propsData: { + icon: 'spinner', + content: 'Hello, Earth!', + duration: 1000, + }, + }) + setTimeout(() => { + wrapper.setProps({icon: 'circle-right'}) + setTimeout(function() { + expect(wrapper.vm.visible).to.be.true + done() + }, 500) + }, 800) + }) + + it('auto hide', done => { + wrapper = mount(Toast, { + propsData: { + icon: 'spinner', + content: 'Hello, Earth!', + duration: 1000, + }, + }) + setTimeout(() => { + expect(wrapper.vm.visible).to.be.false + done() + }, 1100) + }) + + // it('emit hide event', done => { + // wrapper = mount(Toast, { + // propsData: { + // icon: 'spinner', + // content: 'Hello, Earth!', + // duration: 0 + // } + // }) + + // const eventStub = sinon.stub(wrapper.vm, '$emit') + // wrapper.vm.hide() + // setTimeout(() => { + // console.log(wrapper.vm.$el) + // expect(eventStub.calledWith('hide')).to.be.true + // done() + // }, 500) + // }) +}) diff --git a/components/toast/toast.vue b/components/toast/toast.vue new file mode 100644 index 00000000..c0416279 --- /dev/null +++ b/components/toast/toast.vue @@ -0,0 +1,111 @@ + + + + + diff --git a/config/dev.env.js b/config/dev.env.js new file mode 100644 index 00000000..1e22973a --- /dev/null +++ b/config/dev.env.js @@ -0,0 +1,7 @@ +'use strict' +const merge = require('webpack-merge') +const prodEnv = require('./prod.env') + +module.exports = merge(prodEnv, { + NODE_ENV: '"development"' +}) diff --git a/config/index.js b/config/index.js new file mode 100644 index 00000000..e1070793 --- /dev/null +++ b/config/index.js @@ -0,0 +1,61 @@ + +'use strict' +// Template version: 1.1.1 +// see http://vuejs-templates.github.io/webpack for documentation. + +const path = require('path') + +module.exports = { + build: { + env: require('./prod.env'), + assetsRoot: path.resolve(__dirname, '../lib'), + assetsSubDirectory: '', + assetsPublicPath: '/', + productionSourceMap: false, + // Gzip off by default as many popular static hosts such as + // Surge or Netlify already gzip all static assets for you. + // Before setting to `true`, make sure to: + // npm install --save-dev compression-webpack-plugin + productionGzip: false, + productionGzipExtensions: ['js', 'css'], + // Run the build command with an extra argument to + // View the bundle analyzer report after build finishes: + // `npm run build --report` + // Set to `true` or `false` to always turn it on or off + bundleAnalyzerReport: process.env.npm_config_report + }, + example: { + env: require('./prod.env'), + index: path.resolve(__dirname, '../output/example/index.html'), + assetsRoot: path.resolve(__dirname, '../docs/examples'), + assetsSubDirectory: 'example', + assetsPublicPath: '//static.ins.xiaojukeji.com/static/manhattan/mand-mobile/', + productionSourceMap: false, + // Gzip off by default as many popular static hosts such as + // Surge or Netlify already gzip all static assets for you. + // Before setting to `true`, make sure to: + // npm install --save-dev compression-webpack-plugin + productionGzip: false, + productionGzipExtensions: ['js', 'css'], + // Run the build command with an extra argument to + // View the bundle analyzer report after build finishes: + // `npm run build --report` + // Set to `true` or `false` to always turn it on or off + bundleAnalyzerReport: process.env.npm_config_report + }, + dev: { + env: require('./dev.env'), + port: process.env.PORT || 4000, + index: 'index.html', + assetsRoot: '/', + assetsSubDirectory: 'static', + assetsPublicPath: '/', + proxyTable: {}, + // CSS Sourcemaps off by default because relative paths are "buggy" + // with this option, according to the CSS-Loader README + // (https://github.com/webpack/css-loader#sourcemaps) + // In our experience, they generally work as expected, + // just be aware of this issue when enabling this option. + cssSourceMap: false + } +} diff --git a/config/prod.env.js b/config/prod.env.js new file mode 100644 index 00000000..a6f99761 --- /dev/null +++ b/config/prod.env.js @@ -0,0 +1,4 @@ +'use strict' +module.exports = { + NODE_ENV: '"production"' +} diff --git a/config/test.env.js b/config/test.env.js new file mode 100644 index 00000000..c2824a30 --- /dev/null +++ b/config/test.env.js @@ -0,0 +1,7 @@ +'use strict' +const merge = require('webpack-merge') +const devEnv = require('./dev.env') + +module.exports = merge(devEnv, { + NODE_ENV: '"testing"' +}) diff --git a/examples/App.vue b/examples/App.vue new file mode 100644 index 00000000..94f1bea5 --- /dev/null +++ b/examples/App.vue @@ -0,0 +1,147 @@ + + + + + + diff --git a/examples/assets/images/bank-zs.svg b/examples/assets/images/bank-zs.svg new file mode 100644 index 00000000..d0cde1cd --- /dev/null +++ b/examples/assets/images/bank-zs.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/assets/images/cashier-icon-1.png b/examples/assets/images/cashier-icon-1.png new file mode 100644 index 00000000..bee32c2a Binary files /dev/null and b/examples/assets/images/cashier-icon-1.png differ diff --git a/examples/assets/images/cashier-icon-2.png b/examples/assets/images/cashier-icon-2.png new file mode 100644 index 00000000..5a1c9df1 Binary files /dev/null and b/examples/assets/images/cashier-icon-2.png differ diff --git a/examples/assets/images/cashier-icon-3.png b/examples/assets/images/cashier-icon-3.png new file mode 100644 index 00000000..fea3fcd0 Binary files /dev/null and b/examples/assets/images/cashier-icon-3.png differ diff --git a/examples/assets/images/cashier-icon-4.png b/examples/assets/images/cashier-icon-4.png new file mode 100644 index 00000000..4ccdb19a Binary files /dev/null and b/examples/assets/images/cashier-icon-4.png differ diff --git a/examples/assets/images/cashier-icon-5.png b/examples/assets/images/cashier-icon-5.png new file mode 100644 index 00000000..48908024 Binary files /dev/null and b/examples/assets/images/cashier-icon-5.png differ diff --git a/examples/assets/responsive.js b/examples/assets/responsive.js new file mode 100644 index 00000000..c59e1da4 --- /dev/null +++ b/examples/assets/responsive.js @@ -0,0 +1,26 @@ +(function (window, document) { + + function resize(){ + var ww = window.innerWidth; + if (ww > window.screen.width) { + window.requestAnimationFrame(resize); + } + else{ + if (ww > 720) { + ww = 720 + } + document.documentElement.style.fontSize = ww * 0.13333333333333333 + 'px'; + document.body.style.opacity = 1; + } + } + + if (document.readyState !== 'loading') { + resize(); + } + else { + document.addEventListener('DOMContentLoaded', resize); + } + + window.addEventListener('resize', resize); + + })(window, document) diff --git a/examples/category.vue b/examples/category.vue new file mode 100644 index 00000000..bf48d15b --- /dev/null +++ b/examples/category.vue @@ -0,0 +1,187 @@ + + + + + diff --git a/examples/components.json b/examples/components.json new file mode 100644 index 00000000..b52cdfcd --- /dev/null +++ b/examples/components.json @@ -0,0 +1 @@ +[{"category":"basic","name":"Basic","text":"基础","list":[{"name":"ActionBar","path":"/action-bar","icon":"action-bar","text":"操作栏"},{"path":"/button","name":"Button","icon":"button","text":"按钮"},{"name":"DropMenu","path":"/drop-menu","icon":"drop-menu","text":"下拉菜单"},{"path":"/icon","name":"Icon","icon":"icon","text":"图标"},{"name":"ImageReader","path":"/image-reader","icon":"image-reader","text":"图片选择器"},{"name":"ImageViewer","path":"/image-viewer","icon":"image-viewer","text":"图片浏览器"},{"name":"NoticeBar","path":"/notice-bar","icon":"notice-bar","text":"通知栏"},{"name":"Stepper","path":"/stepper","icon":"stepper","text":"步进器"},{"name":"Steps","path":"/steps","icon":"steps","text":"步骤条"},{"name":"Swiper","path":"/swiper","icon":"swiper","text":"轮播"},{"name":"TabBar","path":"/tab-bar","icon":"tab-bar","text":"标签栏"},{"name":"Tabs","path":"/tabs","icon":"tabs","text":"标签页"},{"name":"Tag","path":"/tag","icon":"tag","text":"标签"}]},{"category":"business","name":"Business","text":"业务相关","list":[{"name":"Captcha","path":"/captcha","icon":"captcha","text":"验证码窗口"},{"name":"Cashier","path":"/cashier","icon":"cashier","text":"收银台"},{"name":"Chart","path":"/chart","icon":"chart","text":"折线图表"},{"name":"Landscape","path":"/landscape","icon":"landscape","text":"压屏窗"},{"name":"ResultPage","path":"/result-page","icon":"result-page","text":"结果页"}]},{"category":"feedback","name":"Feedback","text":"操作反馈","list":[{"name":"ActionSheet","path":"/action-sheet","icon":"action-sheet","text":"动作面板"},{"name":"DatePicker","path":"/date-picker","icon":"date-picker","text":"日期选择器"},{"name":"Dialog","path":"/dialog","icon":"dialog","text":"模态窗"},{"name":"Picker","path":"/picker","icon":"picker","text":"选择器"},{"name":"Popup","path":"/popup","icon":"popup","text":"弹出层"},{"name":"Selector","path":"/selector","icon":"selector","text":"列表选择器"},{"name":"TabPicker","path":"/tab-picker","icon":"tab-picker","text":"多频道选择器"},{"name":"Tip","path":"/tip","icon":"tip","text":"气泡提示"},{"name":"Toast","path":"/toast","icon":"toast","text":"轻提示"}]},{"category":"form","name":"Form","text":"表单相关","list":[{"name":"Agree","path":"/agree","icon":"agree","text":"勾选按钮"},{"name":"Codebox","path":"/codebox","icon":"codebox","text":"字符码输入框"},{"name":"Field","path":"/field","icon":"field","text":"区域列表组合"},{"name":"InputItem","path":"/input-item","icon":"input-item","text":"输入框"},{"name":"NumberKeyboard","path":"/number-keyboard","icon":"number-keyboard","text":"数字键盘"},{"name":"Radio","path":"/radio","icon":"radio","text":"单选框"},{"name":"Switch","path":"/switch","icon":"switch","text":"开关"}]}] \ No newline at end of file diff --git a/examples/create-demo-module.js b/examples/create-demo-module.js new file mode 100644 index 00000000..c00f1671 --- /dev/null +++ b/examples/create-demo-module.js @@ -0,0 +1,10 @@ +export default function(name = '', demos = []) { + return { + name: `${name}-demo`, + data() { + return { + demos, + } + }, + } +} diff --git a/examples/demo-index.indemand.js b/examples/demo-index.indemand.js new file mode 100644 index 00000000..bc3ad7ca --- /dev/null +++ b/examples/demo-index.indemand.js @@ -0,0 +1,39 @@ +export {default as Home} from './home.indemand.vue' +export {default as Category} from './category' + +export {default as Button} from '../components/button/demo' +export {default as Icon} from '../components/icon/demo' +export {default as Popup} from '../components/popup/demo' +export {default as ActionBar} from '../components/action-bar/demo' +export {default as DropMenu} from '../components/drop-menu/demo' +export {default as Picker} from '../components/picker/demo' +export {default as TabBar} from '../components/tab-bar/demo' +export {default as Swiper} from '../components/swiper/demo' +export {default as Toast} from '../components/toast/demo' +export {default as Dialog} from '../components/dialog/demo' +export {default as Tip} from '../components/tip/demo' +export {default as Tabs} from '../components/tabs/demo' +export {default as Tag} from '../components/tag/demo' +export {default as InputItem} from '../components/input-item/demo' +export {default as NumberKeyboard} from '../components/number-keyboard/demo' +export {default as Stepper} from '../components/stepper/demo' +export {default as Steps} from '../components/steps/demo' +export {default as NoticeBar} from '../components/notice-bar/demo' +export {default as ResultPage} from '../components/result-page/demo' +export {default as ActionSheet} from '../components/action-sheet/demo' +export {default as Selector} from '../components/selector/demo' +export {default as Landscape} from '../components/landscape/demo' +export {default as ImageViewer} from '../components/image-viewer/demo' +export {default as ImageReader} from '../components/image-reader/demo' +export {default as TabPicker} from '../components/tab-picker/demo' +export {default as Field} from '../components/field/demo' +export {default as Switch} from '../components/switch/demo' +export {default as Agree} from '../components/agree/demo' +export {default as Radio} from '../components/radio/demo' +export {default as DatePicker} from '../components/date-picker/demo' +export {default as Captcha} from '../components/captcha/demo' +export {default as Codebox} from '../components/codebox/demo' +export {default as Cashier} from '../components/cashier/demo' +export {default as Chart} from '../components/chart/demo' + +/* @init<%export {default as ${componentNameUpper}} = '../components/${componentName}/demo' */ diff --git a/examples/demo-index.js b/examples/demo-index.js new file mode 100644 index 00000000..6e3caf12 --- /dev/null +++ b/examples/demo-index.js @@ -0,0 +1,38 @@ +export {default as Home} from './home.vue' + +export const Category = r => require.ensure([], () => r(require('./category')), 'category') +export const Button = r => require.ensure([], () => r(require('../components/button/demo')), 'button') +export const Icon = r => require.ensure([], () => r(require('../components/icon/demo')), 'icon') +export const Popup = r => require.ensure([], () => r(require('../components/popup/demo')), 'popup') +export const ActionBar = r => require.ensure([], () => r(require('../components/action-bar/demo')), 'action-bar') +export const DropMenu = r => require.ensure([], () => r(require('../components/drop-menu/demo')), 'drop-menu') +export const Picker = r => require.ensure([], () => r(require('../components/picker/demo')), 'picker') +export const TabBar = r => require.ensure([], () => r(require('../components/tab-bar/demo')), 'tab-bar') +export const Swiper = r => require.ensure([], () => r(require('../components/swiper/demo')), 'swiper') +export const Toast = r => require.ensure([], () => r(require('../components/toast/demo')), 'toast') +export const Dialog = r => require.ensure([], () => r(require('../components/dialog/demo')), 'dialog') +export const Tip = r => require.ensure([], () => r(require('../components/tip/demo')), 'tip') +export const Tabs = r => require.ensure([], () => r(require('../components/tabs/demo')), 'tabs') +export const Tag = r => require.ensure([], () => r(require('../components/tag/demo')), 'tag') +export const InputItem = r => require.ensure([], () => r(require('../components/input-item/demo')), 'input-item') +export const NumberKeyboard = r => + require.ensure([], () => r(require('../components/number-keyboard/demo')), 'number-keyboard') +export const Stepper = r => require.ensure([], () => r(require('../components/stepper/demo')), 'stepper') +export const Steps = r => require.ensure([], () => r(require('../components/steps/demo')), 'steps') +export const NoticeBar = r => require.ensure([], () => r(require('../components/notice-bar/demo')), 'notice-bar') +export const ResultPage = r => require.ensure([], () => r(require('../components/result-page/demo')), 'result-page') +export const ActionSheet = r => require.ensure([], () => r(require('../components/action-sheet/demo')), 'action-sheet') +export const Selector = r => require.ensure([], () => r(require('../components/selector/demo')), 'selector') +export const Landscape = r => require.ensure([], () => r(require('../components/landscape/demo')), 'landscape') +export const ImageViewer = r => require.ensure([], () => r(require('../components/image-viewer/demo')), 'image-viewer') +export const ImageReader = r => require.ensure([], () => r(require('../components/image-reader/demo')), 'image-reader') +export const TabPicker = r => require.ensure([], () => r(require('../components/tab-picker/demo')), 'tab-picker') +export const Field = r => require.ensure([], () => r(require('../components/field/demo')), 'field') +export const Switch = r => require.ensure([], () => r(require('../components/switch/demo')), 'switch') +export const Agree = r => require.ensure([], () => r(require('../components/agree/demo')), 'agree') +export const Radio = r => require.ensure([], () => r(require('../components/radio/demo')), 'radio') +export const DatePicker = r => require.ensure([], () => r(require('../components/date-picker/demo')), 'date-picker') +export const Captcha = r => require.ensure([], () => r(require('../components/captcha/demo')), 'captcha') +export const Codebox = r => require.ensure([], () => r(require('../components/codebox/demo')), 'codebox') +export const Cashier = r => require.ensure([], () => r(require('../components/cashier/demo')), 'cashier') +export const Chart = r => require.ensure([], () => r(require('../components/chart/demo')), 'chart') /* @init<%export const ${componentNameUpper} = r => require.ensure([], () => r(require('../components/${componentName}/demo')), '${componentName}')%> */ diff --git a/examples/home.indemand.vue b/examples/home.indemand.vue new file mode 100644 index 00000000..accfe1a6 --- /dev/null +++ b/examples/home.indemand.vue @@ -0,0 +1,94 @@ + + + + + diff --git a/examples/home.vue b/examples/home.vue new file mode 100644 index 00000000..bce49535 --- /dev/null +++ b/examples/home.vue @@ -0,0 +1,95 @@ + + + + + diff --git a/examples/index.html b/examples/index.html new file mode 100644 index 00000000..1923ce7e --- /dev/null +++ b/examples/index.html @@ -0,0 +1,31 @@ + + + + Mand Mobile + + + + + + + + + + + + +
+ + + + + + + + diff --git a/examples/main.indemand.js b/examples/main.indemand.js new file mode 100644 index 00000000..38ec7d3f --- /dev/null +++ b/examples/main.indemand.js @@ -0,0 +1,32 @@ +// The Vue build version to load with the `import` command +// (runtime-only or standalone) has been set in webpack.base.conf with an alias. +import Vue from 'vue' +import VueRouter from 'vue-router' +import routes from './route.indemand' +import App from './App' + +import '../components/_style/global.styl' +import './theme.custom.styl' + +Vue.config.productionTip = false + +Vue.use(VueRouter) + +const isProd = process.env.NODE_ENV === 'production' + +const router = new VueRouter({ + mode: 'history', + base: isProd ? '/mand-mobile/examples' : '', + routes, +}) + +router.afterEach(route => { + document.title = route.name ? `${route.name}-Mand Mobile` : 'Mand Mobile' +}) + +/* eslint-disable no-new */ +new Vue({ + el: '#app', + render: h => h(App), + router, +}) diff --git a/examples/main.js b/examples/main.js new file mode 100644 index 00000000..9bc5ae7d --- /dev/null +++ b/examples/main.js @@ -0,0 +1,36 @@ +// The Vue build version to load with the `import` command +// (runtime-only or standalone) has been set in webpack.base.conf with an alias. +import Vue from 'vue' +import VueRouter from 'vue-router' +import routes from './route' +import App from './App' +import FastClick from 'fastclick' +import '../components/_style/global.styl' +import './theme.custom.styl' + +if ('ontouchstart' in window) { + FastClick.attach(document.body) +} + +Vue.config.productionTip = false + +Vue.use(VueRouter) + +const isProd = process.env.NODE_ENV === 'production' + +const router = new VueRouter({ + mode: 'history', + base: isProd ? '/mand-mobile/examples' : '', + routes, +}) + +router.afterEach(route => { + document.title = route.name ? `${route.name}-Mand Mobile` : 'Mand Mobile' +}) + +/* eslint-disable no-new */ +new Vue({ + el: '#app', + render: h => h(App), + router, +}) diff --git a/examples/route.indemand.js b/examples/route.indemand.js new file mode 100644 index 00000000..6f46da43 --- /dev/null +++ b/examples/route.indemand.js @@ -0,0 +1,46 @@ + + +import components from './components.json' +import * as demo from './demo-index.indemand' + +const traverseComponents = (data, fn) => { + data.map(item => + item.list && item.list.map(subItem => + fn(subItem) + ) + ) +} + +const registerRoute = (components) => { + const routes = [] + traverseComponents(components, (component) => { + routes.push({ + name: component.name, + path: component.path, + // require(`../components${component.path}/demo`).default + component: demo[component.name] || {}, + meta: { + title: component.name || '', + description: component.text || '' + } + }) + }) + return routes +} + +const routes = registerRoute(components) + +routes.push({ + path: '/home', + component: demo['Home'] +}) +routes.push({ + path: '/category', + component: demo['Category'] +}) +routes.push({ + path: '/', + redirect: '/home' +}) + +export default routes diff --git a/examples/route.js b/examples/route.js new file mode 100644 index 00000000..8105b66b --- /dev/null +++ b/examples/route.js @@ -0,0 +1,44 @@ +import components from './components.json' +import * as demo from './demo-index' + +const traverseComponents = (data, fn) => { + data.map(item => + item.list && item.list.map(subItem => + fn(subItem) + ) + ) +} + +const registerRoute = (components) => { + const routes = [] + traverseComponents(components, (component) => { + routes.push({ + name: component.name, + path: component.path, + // require(`../components${component.path}/demo`).default + component: demo[component.name] || {}, + meta: { + title: component.name || '', + description: component.text || '' + } + }) + }) + return routes +} + +const routes = registerRoute(components) + +routes.push({ + path: '/home', + component: demo['Home'] +}) +routes.push({ + path: '/category', + component: demo['Category'] +}) +routes.push({ + path: '/', + redirect: '/home' +}) + +export default routes diff --git a/examples/single-component-app.vue b/examples/single-component-app.vue new file mode 100644 index 00000000..2fcd5485 --- /dev/null +++ b/examples/single-component-app.vue @@ -0,0 +1,126 @@ + + + + + + diff --git a/examples/single-component-main.js b/examples/single-component-main.js new file mode 100644 index 00000000..e16efdab --- /dev/null +++ b/examples/single-component-main.js @@ -0,0 +1,16 @@ +// The Vue build version to load with the `import` command +// (runtime-only or standalone) has been set in webpack.base.conf with an alias. +import Vue from 'vue' +import VueRouter from 'vue-router' +import App from './single-component-app' +import '../components/_style/global.styl' + +Vue.config.productionTip = false + +const isProd = process.env.NODE_ENV === 'production' + +/* eslint-disable no-new */ +new Vue({ + el: '#app', + render: h => h(App), +}) diff --git a/examples/theme.custom.styl b/examples/theme.custom.styl new file mode 100644 index 00000000..0a9abad3 --- /dev/null +++ b/examples/theme.custom.styl @@ -0,0 +1 @@ +// color-primary = #1AAD19 \ No newline at end of file diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 00000000..5850c869 --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,172 @@ +'use strict' +const path = require('path') +const gulp = require('gulp') +const gulpBase64 = require('gulp-base64') +const preprocess = require('gulp-preprocess') +const babel = require('gulp-babel') +const gutil = require('gulp-util') +const compiler = require('vueify').compiler +const stylus = require('stylus') +const gulpStylus = require('gulp-stylus') +const through = require('through2') +const merge2 = require('merge2') +const colors = require('colors') +const pkg = require('./package.json') +const components = require('./examples/components.json') +// const execSync = require('child_process').execSync + +colors.setTheme({ + info: 'green', + warn: 'yellow', + error: 'red', + bold: 'bold' +}) + +const componentList = generateComponentsList(components) +const uncompliedComponentList = [...componentList] +const mixins = [ + path.join(__dirname, './components/_style/mixin/*.styl'), + path.join(__dirname, './node_modules/nib/lib/nib/vendor'), + path.join(__dirname, './node_modules/nib/lib/nib/gradients'), + path.join(__dirname, './node_modules/nib/lib/nib/flex') +] + +let succNum = 0 +let failNum = 0 + +function generateComponentsList (components) { + let list = [] + components.map(nav => + nav.list.map(item => + list.push(item.path.substr(1)) + ) + ) + return list +} + +function compilingComponentLog () { + return through.obj((file, encode, callback) => { + if (file.path) { + const res = file.content ? 0 : -1 + file.path.replace(/lib\/((\S)+)/ig, (s, v) => { + if (!v) return + if (res < 0) { + succNum++ + console.log(`${pkg.name}/lib/${v} ✔`.info) + } else { + failNum++ + console.log(`${pkg.name}/lib/${v} ✘ (complie error)`.error) + } + uncompliedComponentList.splice(uncompliedComponentList.indexOf(v), 1) + }) + } + callback() + }) +} + +function compileVueStylus (content, cb, compiler, filePath) { + stylus(content) + .import(mixins[0]) + .import(mixins[1]) + .import(mixins[2]) + .import(mixins[3]) + .render((err, css) => { + if (err) throw err + cb(null, css) + }) +} + +function gulpVueify (options) { + return through.obj(function (file, encode, callback) { + if (file.isNull()) { + return callback(null, file) + } + if (file.isStream()) { + this.emit('error', new gutil.PluginError('gulp-vueify', 'Streams are not supported')) + return callback() + } + if (options) { + compiler.applyConfig(options) + } + const isExactCss = options.isExactCss + let styleContent = '' + const styleCb = res => { + if (res.style) { + styleContent = res.style + } + } + if (isExactCss) { + compiler.on('style', styleCb) + } + compiler.compile(file.contents.toString(), file.path, (err, result) => { + if (err) { + this.emit('error', new gutil.PluginError('gulp-vueify', + `In file ${path.relative(process.cwd(), file.path)}:\n${err.message}`)) + return callback() + } + if (isExactCss) { + // 仅提取css + file.path = gutil.replaceExtension(file.path, '.css') + file.contents = Buffer.from(styleContent) + compiler.removeListener('style', styleCb) + } else { + // js & css 集成至一个文件 + file.path = gutil.replaceExtension(file.path, '.js') + file.contents = Buffer.from(result) + } + callback(null, file) + }) + }) +} + +gulp.task('compile', () => { + const streamCompiledVue = gulp.src('./components/*/*.vue') + .pipe(gulpVueify({ + extractCSS: true, + customCompilers: { + stylus: compileVueStylus + } + })) + .pipe(gulp.dest('./lib')) + .pipe(compilingComponentLog()) + + const streamCompiledCss = gulp.src('./components/*/*.vue') + .pipe(gulpVueify({ + isExactCss: true, + customCompilers: { + stylus: compileVueStylus + } + })) + .pipe(gulpBase64()) + .pipe(gulp.dest('./lib/style')) + + const streamCompiledStylus = gulp.src('./components/_style/*.styl') + .pipe(gulpStylus({ + compress: true, + import: mixins + })) + .pipe(gulp.dest('./lib/style')) + + const streamCompiledJs = gulp.src('./components/**/!(test)/!(component).js') + .pipe(preprocess({ + context: { + 'NODE_ENV': 'production', + 'MAN_VERSION': `'${pkg.version}' //` + }})) + .pipe(babel()) + .pipe(gulp.dest('./lib')) + + return merge2([streamCompiledVue, streamCompiledCss, streamCompiledStylus, streamCompiledJs]) +}) + +gulp.task('build', ['compile'], () => { + uncompliedComponentList.map(item => { + failNum++ + console.log(`${pkg.name}/lib/${item} ✘ (not found)`.error) + }) + console.log( + `\n${pkg.name}(${pkg.version}) `.warn.bold + + `build ${componentList.length} components : `.warn.bold + + `${succNum} successed / ${failNum} failed\n`.warn.bold + ) +}) diff --git a/package.json b/package.json new file mode 100644 index 00000000..d1d1de2e --- /dev/null +++ b/package.json @@ -0,0 +1,212 @@ +{ + "name": "mand-mobile", + "version": "0.5.0-alpha.1", + "description": "A Vue.js 2.0 Mobile UI Toolkit", + "main": "lib/mand-mobile.cjs.js", + "style": "lib/mand-mobile.css", + "module": "lib/mand-mobile.esm.js", + "unpkg": "lib/index.js", + "typings": "types/index.d.ts", + "files": [ + "lib", + "components", + "types" + ], + "scripts": { + "dev:webpack": "webpack-dashboard -m -- node build/webpack/dev-server", + "dev": "node build/rollup/dev-server.rollup", + "dev:site": "cd site && npm start", + "create": "node build/component-init.js", + "cz": "git-cz", + "build": "rm -rf ./lib && npm run build:components && npm run build:mand-mobile", + "build:site": "cd site && npm run generate && npm run build", + "prebuild:example": "npm run build", + "build:example": "cross-env NODE_ENV=production BUILD_TYPE=example node build/rollup/build-example.rollup", + "build:mand-mobile": "cross-env NODE_ENV=production node build/rollup/build-mand-mobile.rollup", + "build:components": "cross-env NODE_ENV=production && node build/rollup/build-component.rollup", + "build:analysis": "cross-env NODE_ENV=production node build/webpack/build-mand-mobile --analysis", + "test:webpack": "cross-env NODE_ENV=testing BABEL_ENV=test karma start test/unit/karma.conf.js", + "test": "cross-env NODE_ENV=testing BABEL_ENV=test karma start test/unit/rollup.karma.conf.js --auto-watch", + "lint": "eslint --ext .js,.vue components test/unit/specs test/e2e/specs", + "precommit": "lint-staged", + "build:webpack": "rm -rf ./lib && npm run build:webpack:mand-mobile && npm run build:webpack:components", + "build:webpack:example": "cross-env NODE_ENV=production node build/webpack/build-example", + "build:webpack:mand-mobile": "cross-env NODE_ENV=production node build/webpack/build-mand-mobile", + "build:webpack:components": "cross-env NODE_ENV=production gulp build --gulpfile gulpfile.js && node build/webpack/build-style-entry" + }, + "license": "Apache", + "config": { + "commitizen": { + "path": "./build/mand-change-log.js" + } + }, + "lint-staged": { + "components/**/*.{vue,js,json}": [ + "aesir lint -s", + "aesir format -S", + "git add" + ] + }, + "devDependencies": { + "@types/babel-generator": "^6.25.1", + "@types/babel-types": "^7.0.0", + "@types/babylon": "^6.16.2", + "@types/eslint-plugin-prettier": "^2.2.0", + "@types/glob": "^5.0.35", + "@types/prettier": "^1.8.0", + "@types/vue": "^2.0.0", + "aesir-cli": "^0.0.5", + "autoprefixer": "^7.1.2", + "avoriaz": "^6.0.1", + "babel-core": "^6.22.1", + "babel-eslint": "^7.1.1", + "babel-generator": "^6.26.0", + "babel-helper-vue-jsx-merge-props": "^2.0.2", + "babel-loader": "^7.1.1", + "babel-plugin-import": "^1.6.2", + "babel-plugin-istanbul": "^4.1.1", + "babel-plugin-transform-runtime": "^6.22.0", + "babel-preset-env": "^1.6.1", + "babel-preset-es2015": "^6.24.1", + "babel-preset-stage-2": "^6.22.0", + "babel-register": "^6.22.0", + "babel-types": "^6.26.0", + "babelrc-rollup": "^3.0.0", + "babylon": "^6.18.0", + "bluebird": "^3.5.1", + "chai": "^4.1.2", + "chalk": "^2.0.1", + "chromedriver": "^2.27.2", + "commander": "^2.12.2", + "commitizen": "^2.9.6", + "connect-history-api-fallback": "^1.3.0", + "copy-webpack-plugin": "^4.0.1", + "cross-env": "^5.0.1", + "cross-spawn": "^5.0.1", + "css-loader": "^0.28.0", + "cssnano": "^3.10.0", + "dependency-tree": "^5.12.0", + "eslint": "^4.4.1", + "eslint-config-aesir-mandatory": "0.0.3", + "eslint-config-aesir-recommand": "^0.0.2", + "eslint-config-standard": "^10.2.1", + "eslint-friendly-formatter": "^3.0.0", + "eslint-loader": "^1.7.1", + "eslint-plugin-html": "^3.2.0", + "eslint-plugin-import": "^2.7.0", + "eslint-plugin-json": "^1.2.0", + "eslint-plugin-node": "^5.1.1", + "eslint-plugin-prettier": "^2.2.0", + "eslint-plugin-promise": "^3.5.0", + "eslint-plugin-standard": "^3.0.1", + "eventsource-polyfill": "^0.9.6", + "express": "^4.14.1", + "extract-text-webpack-plugin": "^3.0.0", + "fastclick": "^1.0.6", + "file-loader": "^1.1.4", + "friendly-errors-webpack-plugin": "^1.6.1", + "fs-extra": "^4.0.2", + "git-user-email": "^0.2.2", + "git-user-name": "^2.0.0", + "glob": "^7.1.2", + "gulp": "^3.9.1", + "gulp-babel": "^7.0.0", + "gulp-base64": "^0.1.3", + "gulp-preprocess": "^2.0.0", + "gulp-stylus": "^2.6.0", + "gulp-util": "^3.0.8", + "html-webpack-plugin": "^2.30.1", + "http-proxy-middleware": "^0.17.3", + "husky": "^0.14.3", + "inject-loader": "^3.0.0", + "inquirer": "^3.3.0", + "karma": "^1.4.1", + "karma-chrome-launcher": "^2.2.0", + "karma-coverage": "^1.1.1", + "karma-mocha": "^1.3.0", + "karma-phantomjs-launcher": "^1.0.2", + "karma-phantomjs-shim": "^1.4.0", + "karma-rollup-preprocessor": "^5.1.1", + "karma-sinon-chai": "^1.3.1", + "karma-sourcemap-loader": "^0.3.7", + "karma-spec-reporter": "0.0.31", + "karma-webpack": "^2.0.2", + "lint-staged": "^5.0.0", + "livereload": "^0.7.0", + "merge2": "^1.2.0", + "mime-types": "^2.1.17", + "mocha": "^3.2.0", + "moment": "^2.19.1", + "needle": "^2.0.1", + "nib": "^1.1.2", + "nightwatch": "^0.9.12", + "opn": "^5.1.0", + "optimize-css-assets-webpack-plugin": "^3.2.0", + "ora": "^1.3.0", + "phantomjs-prebuilt": "^2.1.16", + "portfinder": "^1.0.13", + "postcss-loader": "^2.0.8", + "postcss-pxtorem": "^4.0.1", + "postcss-url": "^7.3.1", + "poststylus": "^1.0.0", + "prettier": "^1.5.3", + "progress-bar-webpack-plugin": "^1.10.0", + "recursive-copy": "^2.0.8", + "rimraf": "^2.6.0", + "rollup": "^0.54.0", + "rollup-plugin-alias": "^1.4.0", + "rollup-plugin-babel": "^3.0.3", + "rollup-plugin-commonjs": "^8.2.6", + "rollup-plugin-css-only": "^0.2.0", + "rollup-plugin-filesize": "^1.5.0", + "rollup-plugin-glob-import": "^0.1.4", + "rollup-plugin-image": "^1.0.2", + "rollup-plugin-import-alias": "^1.0.4", + "rollup-plugin-inject": "^2.0.0", + "rollup-plugin-json": "^2.3.0", + "rollup-plugin-node-builtins": "^2.1.2", + "rollup-plugin-node-globals": "1.1.0", + "rollup-plugin-node-resolve": "^3.0.2", + "rollup-plugin-postcss": "^1.2.9", + "rollup-plugin-progress": "^0.4.0", + "rollup-plugin-replace": "^2.0.0", + "rollup-plugin-require-context": "0.0.2", + "rollup-plugin-stylus-css-modules": "^1.5.0", + "rollup-plugin-template-html": "^0.0.3", + "rollup-plugin-uglify": "^3.0.0", + "rollup-plugin-url": "^1.3.0", + "rollup-plugin-vue": "^3.0.0", + "rollup-pluginutils": "^2.0.1", + "selenium-server": "^3.0.1", + "semver": "^5.4.1", + "shelljs": "^0.7.8", + "sinon": "^4.0.0", + "sinon-chai": "^2.8.0", + "stylus": "^0.54.5", + "stylus-loader": "^3.0.1", + "svg-sprite-loader": "^0.3.1", + "url-loader": "^0.5.8", + "vue": "^2.4.2", + "vue-loader": "^13.0.4", + "vue-router": "^3.0.1", + "vue-style-loader": "^3.0.1", + "vue-template-compiler": "^2.4.2", + "vueify": "^9.4.1", + "webpack": "^3.6.0", + "webpack-bundle-analyzer": "^2.9.0", + "webpack-dashboard": "^1.0.2", + "webpack-dev-middleware": "^1.12.0", + "webpack-hot-middleware": "^2.18.2", + "webpack-merge": "^4.1.0", + "word-wrap": "^1.2.3" + }, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + }, + "browserslist": [ + "> 1%", + "last 2 versions", + "not ie <= 8" + ] +} diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 00000000..db061a43 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,10 @@ + +// https://github.com/michael-ciniawsky/postcss-load-config + +module.exports = (ctx) => ({ + plugins: { + 'postcss-pxtorem': ctx.env !== 'production' ? { rootValue: 100, propWhiteList: [] } : false, + 'postcss-url': {url: 'inline'}, + 'cssnano': { zindex: false, mergeIdents: false, discardUnused: false, autoprefixer: false}, + } +}) diff --git a/site/.babelrc b/site/.babelrc new file mode 100644 index 00000000..0c4cce4b --- /dev/null +++ b/site/.babelrc @@ -0,0 +1,24 @@ +{ + // "presets": [ + // ["env", { + // "modules": false, + // "targets": { + // "browsers": ["> 1%", "last 2 versions", "not ie <= 8"] + // } + // }], + // "stage-2" + // ], + "presets": [ + "env", + "stage-2" + ], + "plugins": [ + ["import", { "libraryName": "mand-mobile", "style": true }] + ], + "env": { + "test": { + "presets": ["env", "stage-2"], + "plugins": ["istanbul"] + } + } +} diff --git a/site/.eslintignore b/site/.eslintignore new file mode 100644 index 00000000..266d451e --- /dev/null +++ b/site/.eslintignore @@ -0,0 +1,3 @@ +build/*.js +config/*.js +public/* diff --git a/site/.eslintrc.js b/site/.eslintrc.js new file mode 100644 index 00000000..e0dd62d9 --- /dev/null +++ b/site/.eslintrc.js @@ -0,0 +1,27 @@ +// https://eslint.org/docs/user-guide/configuring + +module.exports = { + root: true, + parser: 'babel-eslint', + parserOptions: { + sourceType: 'module' + }, + env: { + browser: true, + }, + // https://github.com/standard/standard/blob/master/docs/RULES-en.md + extends: 'standard', + // required to lint *.vue files + plugins: [ + 'html' + ], + // add your custom rules here + 'rules': { + // allow paren-less arrow functions + 'arrow-parens': 0, + // allow async-await + 'generator-star-spacing': 0, + // allow debugger during development + 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0 + } +} diff --git a/site/.gitignore b/site/.gitignore new file mode 100644 index 00000000..ca370260 --- /dev/null +++ b/site/.gitignore @@ -0,0 +1,18 @@ +.DS_Store +node_modules/ +dist/ +public/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +test/unit/coverage +test/e2e/reports +selenium-debug.log + +# Editor directories and files +.idea +.vscode +*.suo +*.ntvs* +*.njsproj +*.sln diff --git a/site/.postcssrc.js b/site/.postcssrc.js new file mode 100644 index 00000000..b4d062c1 --- /dev/null +++ b/site/.postcssrc.js @@ -0,0 +1,8 @@ +// https://github.com/michael-ciniawsky/postcss-load-config + +module.exports = { + "plugins": { + // to edit target browsers: use "browserslist" field in package.json + // "autoprefixer": {} + } +} diff --git a/site/build/bin/default.mfe.blog.config.js b/site/build/bin/default.mfe.blog.config.js new file mode 100644 index 00000000..c2632a2d --- /dev/null +++ b/site/build/bin/default.mfe.blog.config.js @@ -0,0 +1,26 @@ +const path = require('path') + +function resolve(dir) { + return path.join(__dirname, '../..', dir) +} + +module.exports = { + title: '', + subtitle: '', + logo: '', + favicon: '', + theme: 'default', + hasHome: true, + source: [], + markdownBoundary: { + '': '
', + }, + output: resolve('public'), + defaultTemplate: resolve('theme/default/components/Doc.vue'), + links: [], + copyRight: '', + produceBy: '', + powerBy: '', + routePrefix: '', + staticPrefix: '', +} diff --git a/site/build/bin/gen-indices.js b/site/build/bin/gen-indices.js new file mode 100644 index 00000000..e34c5009 --- /dev/null +++ b/site/build/bin/gen-indices.js @@ -0,0 +1,84 @@ +const fs = require('fs') +const path = require('path') +const algoliasearch = require('algoliasearch') +const key = require('./algolia-key') + +const client = algoliasearch('4GDUUWIAWB', key) + +const index = client.initIndex('mand') + +index.clearIndex(err => { + if (err) { + return + } + + generateIndices('../../docs') + + fs.readdir(path.resolve(__dirname, '../../../components'), (err, dirs) => { + if (err) { + console.log(err) + return + } + + dirs.forEach(dir => { + if (dir.indexOf('.') < 0) { + generateIndices(`../../../components/${ dir }`, dir) + } + }) + }) +}) + +function generateIndices(filePath, component) { + fs.readdir(path.resolve(__dirname, filePath), (err, files) => { + if (err) { + console.log(err) + return + } + + let indices = [] + files.forEach(file => { + if (file.indexOf('.md') < 0) { + return + } + + const content = fs.readFileSync(path.resolve(__dirname, `${ filePath }/${ file }`), 'utf8') + + if (content.indexOf('##') < 0) { + return + } + + const matches = content + .replace(/:::[\s\S]*?:::/g, '') + .replace(/```[\s\S]*?```/g, '') + .match(/#{2,5}[^#]*/g) + .map(match => match.replace(/\n+/g, '\n').split('\n').filter(part => !!part)) + .map(match => { + const length = match.length + if (length > 2) { + const desc = match.slice(1, length).join('') + return [match[0], desc] + } + return match + }) + + indices = indices.concat(matches.map(match => { + const isComponent = match[0].indexOf('###') < 0 + const title = match[0].replace(/#{2,5}/, '').trim() + let index + + index = { + component: component || file.replace('.md', ''), + title, + } + + index.ranking = isComponent ? 2 : 1 + index.content = (match[1] || title).replace(/<[^>]+>/g, '') + return index + })) + }) + + index.addObjects(indices, (err, res) => { + console.log(err, res) + }) + }) +} \ No newline at end of file diff --git a/site/build/bin/markdown.js b/site/build/bin/markdown.js new file mode 100644 index 00000000..474cee57 --- /dev/null +++ b/site/build/bin/markdown.js @@ -0,0 +1,49 @@ +'use strict' +const marked = require('marked') +const fm = require('front-matter') +const highlightCore = require('highlight.js') +const renderer = new marked.Renderer() + +let toc = [] // toc html fragment + +/** + * Generate toc during parse heading elements + * @param {string} text content of heading element + * @param {number} level level of heading element + */ +renderer.heading = function(text, level) { + const link = (isShowText = true) => { + return `${isShowText ? text : '#'}` + } + toc.push(link()) + return `${text}${link(false)}` +} + +const highlight = function(code) { + return highlightCore.highlightAuto(code).value +} + +marked.setOptions({ + renderer: renderer, + highlight: highlight, +}) + +/** + * Parse Markdown content to html fragment & get info from markdown + * @param {string} content markdown content + * @return {object} {info, body, toc} + * info {object} markdown info + * body {string} html fragment of markdown content + * toc {string} html fragment of toc + */ +const markdown = function(content) { + const res = fm(content) // get markdown info from front matter + toc = [] + return { + info: res.attributes, + body: JSON.stringify(marked(res.body)), + toc: JSON.stringify(toc.join('')), + } +} + +module.exports = {markdown, highlight} diff --git a/site/build/bin/mfe-blog-dev.js b/site/build/bin/mfe-blog-dev.js new file mode 100644 index 00000000..a8e61d8b --- /dev/null +++ b/site/build/bin/mfe-blog-dev.js @@ -0,0 +1,47 @@ +const path = require('path') +const nodemon = require('nodemon') +const {mbConfig, traverseSource, info} = require('./utils') + +/** + * Get path of files which should be monitored by nodemon + */ +function getWatchPath() { + const watchList = [path.join(__dirname, '../..', 'mfe.blog.config.js')] + + function handlerSingleSource(item) { + const markdownPath = item.markdown + const templatePath = item.template || mbConfig.defaultTemplate + const demoPath = item.demo + + markdownPath && watchList.push(markdownPath) + templatePath && watchList.push(templatePath) + demoPath && watchList.push(demoPath) + } + + traverseSource(mbConfig.source, handlerSingleSource) + + return watchList +} + +/** + * Start Parsing + */ +function startDev() { + nodemon({ + script: path.join(__dirname, 'mfe-blog-generate.js'), + ext: 'js vue md css styl', + stdout: true, + watch: getWatchPath(), + ignore: ['public/*', 'build/*', 'node_modules/*'], + }) + + nodemon + .on('quit', function() { + process.exit() + }) + .on('restart', function(files) { + info(`Parsing ${files}`) + }) +} + +startDev() diff --git a/site/build/bin/mfe-blog-generate.js b/site/build/bin/mfe-blog-generate.js new file mode 100644 index 00000000..18a35c21 --- /dev/null +++ b/site/build/bin/mfe-blog-generate.js @@ -0,0 +1,212 @@ +const fs = require('fs') +const path = require('path') +const mkdirp = require('mkdirp') +const highlight = require('./markdown').highlight +const markdown = require('./markdown').markdown +const {mbConfig, traverseSource, kebabToCamel, info, warn, error} = require('./utils') + +let views, routes + +function resolve(dir) { + return path.join(__dirname, '../../../', dir) +} +/** + * Transform markdown to html fragment && + * Generate entry(index.data.js) of markdown data + * @param {string} markdownPath + * @param {string} outputPath + */ +function generateSourceData(markdownPath, outputPath) { + let markDownContent = '' + + if (fs.existsSync(markdownPath)) { + markDownContent = fs.readFileSync(markdownPath).toString() + markDownContent = parseBoundarySymbolic(markDownContent) + markDownContent = makeJavascriptModule({}, markdown(markDownContent)) + } else { + error(`markdown is not exist : ${markdownPath}`) + } + + fs.writeFileSync(`${outputPath}/index.data.js`, markDownContent) +} + +/** + * Generate entry(index.demo.js) of Demo + * @param {string} markdownPath + * @param {string} outputPath + */ +function generateDemoVue(outputPath, demoPath = []) { + const imports = {} + const exports = [] + + for (let index = 0, len = demoPath.length; index < len; index++) { + const demo = demoPath[index] + const demoName = `demo${index}` + const demoFileName = `${demoName}.vue` + + if (!fs.existsSync(demo)) { + error(`demo is not exist : ${demo}`) + continue + } + + const demoContent = fs.readFileSync(demo).toString() + imports[demoName] = `./${demoFileName}` + exports[index] = `{ component: ${demoName}, code: ${JSON.stringify(highlight(demoContent))} }` + + fs.writeFileSync(`${outputPath}/${demoFileName}`, demoContent) + } + + fs.writeFileSync( `${outputPath}/index.demo.js`, makeJavascriptModule(imports, { demos: `[${exports.join(',')}]`, })) +} + +/** + * Generate entry(index.vue) of Doc + * @param {string} templatePath doc template + * @param {string} outputPath + */ +function generateDocVue(templatePath, outputPath) { + templatePath = templatePath || mbConfig.defaultTemplate + + let template = '' + + if (fs.existsSync(templatePath)) { + template = fs.readFileSync(templatePath).toString() + } else { + error(`template is not exist : ${templatePath}`) + } + + fs.writeFileSync(`${outputPath}/index.vue`, template) +} + +function generateConfig(config) { + const reg = new RegExp(resolve(''), 'ig') + fs.writeFileSync( `${config.output}/config.js`, `window.mbConfig=${JSON.stringify(config)}`.replace(reg, '')) +} + +/** + * Generate routes enttry(route.js) of Doc + * @param {string} outputPath + */ +function generateRoutepath(outputPath) { + fs.writeFileSync( `${outputPath}/route.js`, views.join('') + makeJavascriptModule( {}, { default: `[${routes.join(',')}]`})) +} + +/** + * Generate code for Import Doc vue & Export Doc Route + * @param {object} source + * @param {string} routePath + * @param {array} views + * @param {array} routes + */ +function saveRoutepath(source, routePath, views, routes) { + const name = kebabToCamel(routePath.replace(/\//g, '-')) + const text = source.text + const src = source.src + const entryPath = `${mbConfig.output}${routePath}/index.vue` + const meta = `meta: { text: '${text}', src: '${src || ''}', markdown: '${!!source.markdown || ''}'}` + + let view, route + + if (fs.existsSync(entryPath)) { + view = `const ${name} = r => require.ensure([], () => r(require('.${routePath}')), '${name}');\n` + views.push(view) + route = `{ path: '${routePath}', component: ${name}, ${meta} }` + } else { + if (src && (src.indexOf('//') < 0)) { + route = `{ path: '${routePath}', redirect: '${src}', ${meta} }` + } else { + route = `{ path: '${routePath}', ${meta} }` + } + } + routes.push(route) +} + +/** + * Parse boundary symbolic in marddown content + * @param {string} content + * @return {string} content + */ +function parseBoundarySymbolic(content) { + const boundary = mbConfig.markdownBoundary + for (const key in boundary) { + if (boundary.hasOwnProperty(key)) { + content = content.replace(key, boundary[key]) + } + } + return content +} + +/** + * Generate Js Module + * @param {object} imports + * @param {object} exports + * @return {string} code + */ +function makeJavascriptModule(imports = {}, exports = {}) { + let content = '' + + for (const key in imports) { + if (imports.hasOwnProperty(key)) { + // import [key] from [conent] + content += `import ${key} from ${JSON.stringify(imports[key])};\n` + } + } + + for (const key in exports) { + if (exports.hasOwnProperty(key)) { + const singleExport = typeof exports[key] === 'string' ? exports[key] : JSON.stringify(exports[key]) + // export default [conent] or export const [key] = [conent] + content += key === 'default' ? `export default ${singleExport};\n` : `export const ${key} = ${singleExport};\n` + } + } + + return content +} + +function startGenerate() { + const source = mbConfig.source + const startTmp = Date.now() + + views = [] + routes = [] + + info('Start processing\n') + + if (!source || !source.length) { + warn('No source found!') + end() + return + } + + function end() { + const endTmp = Date.now() + console.log('\n') + info(`Parse completed! Files loaded in ${(endTmp - startTmp) / 1000}s\n`) + } + + function handlerSingleSource(item, path) { + const markdownPath = item.markdown + const templatePath = item.template + const demoPath = item.demo + const outputPath = `${mbConfig.output}/${path.join('/')}` + + mkdirp.sync(outputPath) + + if (markdownPath) { + generateSourceData(markdownPath, outputPath) + generateDemoVue(outputPath, demoPath) + generateDocVue(templatePath, outputPath) + info(`${markdownPath}`) + } + saveRoutepath(item, `/${path.join('/')}`, views, routes) + } + + traverseSource(source, handlerSingleSource) + + generateRoutepath(mbConfig.output) + generateConfig(mbConfig) + end() +} + +// bootstrap +startGenerate() diff --git a/site/build/bin/utils.js b/site/build/bin/utils.js new file mode 100644 index 00000000..a20e07e4 --- /dev/null +++ b/site/build/bin/utils.js @@ -0,0 +1,66 @@ +const colors = require('colors') +const defaultMbConfig = require('./default.mfe.blog.config') + +// Mfe template blog config info +const mbConfig = Object.assign(defaultMbConfig, require('../../mfe.blog.config')) + +colors.setTheme({ + info: 'green', + warn: 'yellow', + error: 'red', + bold: 'bold', +}) + +function log(msg) { + if (process.argv.indexOf('--log') >= 0) { + console.log(msg) + } +} + +function info(msg) { + log('[MTB INFO]'.info.bold + ` ${msg}`) +} + +function warn(msg) { + log('[MTB WARN]'.warn.bold + ` ${msg}`) +} + +function error(msg) { + log('[MTB ERROR]'.error.bold + ` ${msg}`) +} + +// Traverse "source" and do sth with each item +function traverseSource(source, fn, path = [], level = 0) { + for (let i = 0, len = source.length; i < len; i++) { + const item = source[i] + path[level] = item.name + + if (item.menu && Array.isArray(item.menu)) { + const tmpPath = [...path] + level++ + traverseSource(item.menu, fn, path, level) + path = [...tmpPath] + level-- + } + + fn && fn(item, path) + } +} + +// Transform kebab-case to camelCase +function kebabToCamel (str) { + const parts = str.split('-') + let newStr = '' + + for (let i = 0, len = parts.length; i < len; i++) { + const part = parts[i] + if (!part) { + continue + } + newStr += part[0].toLocaleUpperCase() + part.substr(1) + } + + return newStr +} + +module.exports = {mbConfig, traverseSource, kebabToCamel, info, warn, error} diff --git a/site/build/build.js b/site/build/build.js new file mode 100644 index 00000000..4dfd1516 --- /dev/null +++ b/site/build/build.js @@ -0,0 +1,41 @@ +require('./check-versions')() + +const rm = require('rimraf') +const path = require('path') +const chalk = require('chalk') +const webpack = require('webpack') +const config = require('../config') +const webpackConfig = require('./webpack.prod.conf') + +rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => { + if (err) { + throw err + } + 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')) + 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" + ) + ) + }) +}) diff --git a/site/build/check-versions.js b/site/build/check-versions.js new file mode 100644 index 00000000..7e211aff --- /dev/null +++ b/site/build/check-versions.js @@ -0,0 +1,48 @@ +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) + } +} diff --git a/site/build/dev-client.js b/site/build/dev-client.js new file mode 100644 index 00000000..66eb6b0c --- /dev/null +++ b/site/build/dev-client.js @@ -0,0 +1,9 @@ +/* eslint-disable */ +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() + } +}) diff --git a/site/build/dev-server.js b/site/build/dev-server.js new file mode 100644 index 00000000..9a3462e2 --- /dev/null +++ b/site/build/dev-server.js @@ -0,0 +1,95 @@ +require('./check-versions')() + +const config = require('../config') +const {info} = require('./bin/utils') + +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') +const 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 devMiddleware = require('webpack-dev-middleware')(compiler, { + publicPath: webpackConfig.output.publicPath, + quiet: true, +}) + +const hotMiddleware = require('webpack-hot-middleware')(compiler, { + log: false, + heartbeat: 2000, +}) + +// 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) + +// 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 + +info('Starting dev server...') +devMiddleware.waitUntilValid(() => { + portfinder.getPort((err, port) => { + if (err) { + _reject(err) + } + process.env.PORT = port + var uri = 'http://localhost:' + port + info('Listening at ' + uri.warn.bold + '\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() + }, +} diff --git a/site/build/utils.js b/site/build/utils.js new file mode 100644 index 00000000..06300518 --- /dev/null +++ b/site/build/utils.js @@ -0,0 +1,83 @@ +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 = [ + resolve('../components/_style/mixin/theme.styl'), + resolve('../components/_style/mixin/util.styl'), + resolve('theme/default/assets/css/mixin.styl') + ] + + return { + css: generateLoaders('postcss'), + postcss: generateLoaders('postcss'), + less: generateLoaders('less'), + sass: generateLoaders('sass', {indentedSyntax: true}), + scss: generateLoaders('sass'), + 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 +} diff --git a/site/build/vue-loader.conf.js b/site/build/vue-loader.conf.js new file mode 100644 index 00000000..20cf2a6d --- /dev/null +++ b/site/build/vue-loader.conf.js @@ -0,0 +1,16 @@ +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', + }, +} diff --git a/site/build/webpack.base.conf.js b/site/build/webpack.base.conf.js new file mode 100644 index 00000000..417c98c7 --- /dev/null +++ b/site/build/webpack.base.conf.js @@ -0,0 +1,71 @@ +'use strict' +const path = require('path') +const utils = require('./utils') +const config = require('../config') +const vueLoaderConfig = require('./vue-loader.conf') +const HtmlWebpackPlugin = require('html-webpack-plugin') +const { mbConfig } = require('./bin/utils') + +function resolve (dir) { + return path.join(__dirname, '..', dir) +} + +module.exports = { + entry: { + app: [`${mbConfig.output}/config.js`, `./theme/${mbConfig.theme}/main.js`] + }, + output: { + path: config.build.assetsRoot, + filename: '[name].js', + chunkFilename: '[name].js', + publicPath: process.env.NODE_ENV === 'production' + ? config.build.assetsPublicPath + : process.env.NODE_ENV === 'testing' + ? '/mand-mobile/' + : config.dev.assetsPublicPath + }, + resolve: { + extensions: ['.js', '.vue', '.json'], + alias: { + 'vue$': 'vue/dist/vue.esm.js', + '@examples': resolve('../examples'), + } + }, + module: { + rules: [ + { + test: /\.vue$/, + loader: 'vue-loader', + options: vueLoaderConfig + }, + { + test: /\.js$/, + loader: 'babel-loader?cacheDirectory', + include: [resolve('theme'), mbConfig.output] + }, + { + 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('../examples/assets/images')] + } + ] + }, + plugins: [ + new HtmlWebpackPlugin({ + filename: 'index.html', + template: `index.html`, + title: mbConfig.title, + subtitle: mbConfig.subtitle, + logo: mbConfig.favicon, + chunksSortMode: 'dependency' + }) + ] +} diff --git a/site/build/webpack.dev.conf.js b/site/build/webpack.dev.conf.js new file mode 100644 index 00000000..bf10b210 --- /dev/null +++ b/site/build/webpack.dev.conf.js @@ -0,0 +1,40 @@ +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 HtmlWebpackPlugin = require('html-webpack-plugin') +const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') + +const pxtoremConfig = pxtorem({ rootValue: 100, propWhiteList: [] }) +// 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]) +}) + +module.exports = merge(baseWebpackConfig, { + 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 webpack.DefinePlugin({ + 'process.env': config.dev.env, + }), + // 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(), + new FriendlyErrorsPlugin(), + ], +}) diff --git a/site/build/webpack.prod.conf.js b/site/build/webpack.prod.conf.js new file mode 100644 index 00000000..bdbfa970 --- /dev/null +++ b/site/build/webpack.prod.conf.js @@ -0,0 +1,73 @@ +const path = require('path') +const utils = require('./utils') +const webpack = require('webpack') +const config = require('../config') +const merge = require('webpack-merge') +const baseWebpackConfig = require('./webpack.base.conf') +const ProgressBarPlugin = require('progress-bar-webpack-plugin') +const ExtractTextPlugin = require('extract-text-webpack-plugin') +const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin') +const MinifyPlugin = require("babel-minify-webpack-plugin") + +const env = config.build.env + +const webpackConfig = merge(baseWebpackConfig, { + module: { + rules: utils.styleLoaders({ + sourceMap: config.build.productionSourceMap, + extract: true, + }), + }, + devtool: false, + output: { + path: config.build.assetsRoot, + filename: utils.assetsPath('js/[name].[chunkhash:8].js'), + chunkFilename: utils.assetsPath('js/[name].[chunkhash:8].js'), + }, + plugins: [ + new ProgressBarPlugin({ + format: ' BUILD SITE [:bar] :percent (:elapsed seconds)', + clear: false + }), + // http://vuejs.github.io/vue-loader/en/workflow/production.html + new webpack.DefinePlugin({ + 'process.env': env, + }), + new MinifyPlugin({}, { + sourceMap: true, + }), + // extract css into its own file + new ExtractTextPlugin({ + filename: utils.assetsPath('css/[name].[contenthash:8].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, + }, + }), + // 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'], + }), + ], +}) + +module.exports = webpackConfig diff --git a/site/config/dev.env.js b/site/config/dev.env.js new file mode 100644 index 00000000..2772aa32 --- /dev/null +++ b/site/config/dev.env.js @@ -0,0 +1,6 @@ +const merge = require('webpack-merge') +const prodEnv = require('./prod.env') + +module.exports = merge(prodEnv, { + NODE_ENV: '"development"', +}) diff --git a/site/config/index.js b/site/config/index.js new file mode 100644 index 00000000..ef0b2bcc --- /dev/null +++ b/site/config/index.js @@ -0,0 +1,41 @@ +const {mbConfig} = require('../build/bin/utils') +// Template version: 1.1.3 +// see http://vuejs-templates.github.io/webpack for documentation. + +const path = require('path') + +module.exports = { + build: { + env: require('./prod.env'), + index: path.resolve(__dirname, '../dist/index.html'), + assetsRoot: path.resolve(__dirname, '../../docs'), + assetsSubDirectory: 'static', + assetsPublicPath: mbConfig.staticPrefix ? mbConfig.staticPrefix : '/', + productionSourceMap: true, + // Gzip off by default as many popular static hosts such as + // Surge or Netlify already gzip all static assets for you. + // Before setting to `true`, make sure to: + // npm install --save-dev compression-webpack-plugin + productionGzip: false, + productionGzipExtensions: ['js', 'css'], + // Run the build command with an extra argument to + // View the bundle analyzer report after build finishes: + // `npm run build --report` + // Set to `true` or `false` to always turn it on or off + bundleAnalyzerReport: process.env.npm_config_report, + }, + dev: { + env: require('./dev.env'), + port: process.env.PORT || 8080, + autoOpenBrowser: true, + assetsSubDirectory: 'static', + assetsPublicPath: '/', + proxyTable: {}, + // CSS Sourcemaps off by default because relative paths are "buggy" + // with this option, according to the CSS-Loader README + // (https://github.com/webpack/css-loader#sourcemaps) + // In our experience, they generally work as expected, + // just be aware of this issue when enabling this option. + cssSourceMap: false, + }, +} diff --git a/site/config/prod.env.js b/site/config/prod.env.js new file mode 100644 index 00000000..1d1c74fd --- /dev/null +++ b/site/config/prod.env.js @@ -0,0 +1,3 @@ +module.exports = { + NODE_ENV: '"production"', +} diff --git a/site/index.html b/site/index.html new file mode 100644 index 00000000..f4e77743 --- /dev/null +++ b/site/index.html @@ -0,0 +1,34 @@ + + + + + + + + + + + <%= htmlWebpackPlugin.options.title %> + + + + + + +
+ + + + + + + + diff --git a/site/mfe.blog.config.js b/site/mfe.blog.config.js new file mode 100644 index 00000000..5cbc0812 --- /dev/null +++ b/site/mfe.blog.config.js @@ -0,0 +1,153 @@ +const fs = require('fs') +const path = require('path') +const components = require('../examples/components.json') + +function resolve(dir) { + return path.join(__dirname, '..', dir) +} + +function generateDemos (name) { + const demoPath = resolve(`components/${name}/demo/cases`) + + if (fs.existsSync(demoPath)) { + const files = fs.readdirSync(demoPath) + return files.map(file => { + return `${demoPath}/${file}` + }) + } else { + return [] + } +} + +function generateSource () { + const menus = [] + components.forEach(category => { + const list = category.list + const subMenus = [] + + list && list.forEach(component => { + subMenus.push({ + name: component.path.substr(1), + text: `${component.name} ${component.text}`, + markdown: resolve(`components${component.path}/README.md`), + demo: generateDemos(component.path.substr(1)) + }) + }) + menus.push({ + name: category.category, + text: category.name, + menu: subMenus + }) + }) + + return menus +} + +module.exports = { + title: 'Mand Mobile', + subtitle: 'Manhattan Design Mobile', + logo: '//manhattan.didistatic.com/static/manhattan/mand/docs/mand-logo-black.svg', + favicon: '//static.galileo.xiaojukeji.com/static/tms/other/mand-mobile-logo.png', + source: [ + { + name: 'design', + text: '设计', + src: '/design/resource', + menu: [ + { + name: 'resource', + text: '设计资源', + markdown: resolve('site/docs/resource.md'), + } + ] + }, + { + name: 'docs', + text: '组件', + src: '/docs/introduce', + menu: [ + { + name: 'introduce', + text: 'Mand Mobile', + markdown: resolve('site/docs/introduce.md'), + }, + { + name: 'started', + text: '快速上手', + markdown: resolve('site/docs/started.md'), + }, + { + name: 'development', + text: '开发指南', + markdown: resolve('site/docs/development.md'), + }, + { + name: 'changelog', + text: '更新日志', + markdown: resolve('CHANGELOG.md'), + }, + { + name: 'theme', + text: '主题定制', + markdown: resolve('site/docs/theme.md'), + }, + { + name: 'preview', + text: '组件概览', + template: resolve('site/theme/default/Preview.vue'), + markdown: resolve('README.md'), + }, + { + name: 'components', + text: 'Components', + menu: generateSource() + }, + ], + }, + ], + components: generateSource(), + markdownBoundary: { + '': '
', + '': + '', + }, + links: [ + { + title: '链接', + link: [ + { + text: 'GitHub', + src: 'https://github.com/didi/mand-mobile', + }, + { + text: '更新日志', + src: '//didi.github.io/mand-mobile/docs/changelog', + }, + { + text: '贡献指南', + src: 'https://github.com/didi/mand-mobile/blob/master/CONTRIBUTING.md', + }, + { + text: '问题反馈', + src: 'https://github.com/didi/mand-mobile/issues', + } + ], + }, + { + title: '更多产品', + link: [ + { + text: 'cube-ui - Vue.js组件库', + src: 'https://didi.github.io/cube-ui', + }, + { + text: 'VirtualAPK - Android 插件化框架', + src: 'https://didi.github.io/virtual-apk.html', + } + ], + }, + ], + copyRight: '2012-2018 Didi Chuxing. All Rights Reserved', + routePrefix: '/mfe/mand-mobile', + // staticPrefix: '//manhattan.didistatic.com/static/manhattan/mand-mobile', +} diff --git a/site/package.json b/site/package.json new file mode 100644 index 00000000..5ff13f52 --- /dev/null +++ b/site/package.json @@ -0,0 +1,90 @@ +{ + "name": "mand-mobile-site", + "version": "1.0.0", + "description": "Document Site For Mand Mobile", + "author": "xuxiaoyan ", + "private": true, + "scripts": { + "dev": "node ./build/dev-server --log", + "generate": "node ./build/bin/mfe-blog-generate --log", + "start": "node ./build/bin/mfe-blog-dev --log & npm run dev", + "build:test": "cross-env NODE_ENV=testing node build/build", + "build": "cross-env NODE_ENV=production node build/build", + "indices": "node ./build/bin/gen-indices --log" + }, + "dependencies": { + "vue": "^2.5.2", + "vue-qrcode-component": "^2.1.1", + "vue-router": "^3.0.1" + }, + "devDependencies": { + "algoliasearch": "^3.25.1", + "autoprefixer": "^7.1.2", + "babel-core": "^6.22.1", + "babel-eslint": "^7.1.1", + "babel-loader": "^7.1.1", + "babel-minify-webpack-plugin": "^0.3.0", + "babel-plugin-istanbul": "^4.1.1", + "babel-plugin-transform-runtime": "^6.22.0", + "babel-preset-env": "^1.3.2", + "babel-preset-stage-2": "^6.22.0", + "babel-register": "^6.22.0", + "colors": "^1.1.2", + "connect-history-api-fallback": "^1.3.0", + "copy-webpack-plugin": "^4.0.1", + "cross-env": "^5.0.1", + "cross-spawn": "^5.0.1", + "css-loader": "^0.28.0", + "eslint": "^3.19.0", + "eslint-config-standard": "^10.2.1", + "eslint-friendly-formatter": "^3.0.0", + "eslint-loader": "^1.7.1", + "eslint-plugin-html": "^3.0.0", + "eslint-plugin-import": "^2.7.0", + "eslint-plugin-node": "^5.2.0", + "eslint-plugin-promise": "^3.4.0", + "eslint-plugin-standard": "^3.0.1", + "eventsource-polyfill": "^0.9.6", + "express": "^4.14.1", + "extract-text-webpack-plugin": "^3.0.0", + "file-loader": "^1.1.4", + "friendly-errors-webpack-plugin": "^1.6.1", + "front-matter": "^2.3.0", + "highlight.js": "^9.12.0", + "html-webpack-plugin": "^2.30.1", + "http-proxy-middleware": "^0.17.3", + "inject-loader": "^3.0.0", + "mand-mobile": "^0.4.21", + "marked": "^0.3.6", + "nightwatch": "^0.9.12", + "nodemon": "^1.12.1", + "opn": "^5.1.0", + "optimize-css-assets-webpack-plugin": "^3.2.0", + "ora": "^1.2.0", + "portfinder": "^1.0.13", + "rimraf": "^2.6.0", + "selenium-server": "^3.0.1", + "semver": "^5.3.0", + "shelljs": "^0.7.6", + "stylus": "^0.54.5", + "stylus-loader": "^3.0.1", + "url-loader": "^0.5.8", + "vue-loader": "^13.3.0", + "vue-style-loader": "^3.0.1", + "vue-template-compiler": "^2.5.2", + "webpack": "^3.6.0", + "webpack-bundle-analyzer": "^2.9.0", + "webpack-dev-middleware": "^1.12.0", + "webpack-hot-middleware": "^2.18.2", + "webpack-merge": "^4.1.0" + }, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + }, + "browserslist": [ + "> 1%", + "last 2 versions", + "not ie <= 8" + ] +} diff --git a/site/theme/default/App.vue b/site/theme/default/App.vue new file mode 100644 index 00000000..1bd30854 --- /dev/null +++ b/site/theme/default/App.vue @@ -0,0 +1,87 @@ + + + + + + diff --git a/site/theme/default/Error.vue b/site/theme/default/Error.vue new file mode 100644 index 00000000..14796cf5 --- /dev/null +++ b/site/theme/default/Error.vue @@ -0,0 +1,60 @@ + + + + + \ No newline at end of file diff --git a/site/theme/default/Home.vue b/site/theme/default/Home.vue new file mode 100644 index 00000000..b900f9ef --- /dev/null +++ b/site/theme/default/Home.vue @@ -0,0 +1,409 @@ + + + + + diff --git a/site/theme/default/Preview.vue b/site/theme/default/Preview.vue new file mode 100644 index 00000000..56f5f6c7 --- /dev/null +++ b/site/theme/default/Preview.vue @@ -0,0 +1,106 @@ + + + + + diff --git a/site/theme/default/assets/css/demo.styl b/site/theme/default/assets/css/demo.styl new file mode 100644 index 00000000..122536e3 --- /dev/null +++ b/site/theme/default/assets/css/demo.styl @@ -0,0 +1,158 @@ +.md-example-child + box-sizing border-box + +.md-example-child-button, .md-example-child-landscape, .md-example-child-action-sheet, .md-example-child-date-picker + , .md-example-child-icon, .md-example-child-reader, .md-example-child-notice-bar, .md-example-child-number-keyboard + , .md-example-child-picker, .md-example-child-popup, .md-example-child-result-page, .md-example-child-tab-picker + , .md-example-child-toast, .md-example-child-dialog, .md-example-child-cashier, .md-example-chart-child + clearfix() + padding 20px + +.md-example-child-button + .md-button + float left + margin-bottom 10px +.md-example-child-agree + margin-top 70px + padding 0 h-gap-lg + font-size font-minor-large +.md-example-child-drop-menu + position absolute + width 100% + height 100% + .md-drop-menu-bar + height drop-menu-height !important +.md-example-child-field + .md-input-item + background #FFF + padding 0 32px + .strong-tip + font-size 24px + color color-text-hightlight +.md-example-child-icon + &.md-example-child-icon-1 + display flex + align-items center + height 200px + .md-example-item, .md-example-item-s + float left + width 33% + padding 15px 0 + color #333 + text-align center + p + text-align center + font-size font-body-normal + color #666 + .md-example-item-s + width 25% +.md-example-child-popup + .md-popup.with-mask, .md-popup-box + position absolute !important +.md-example-child-radio + .md-field-item-custom-title + font-weight font-weight-medium + .md-field-item-custom-brief + font-size font-minor-normal +.md-example-child-selector + .md-popup.with-mask + position absolute !important +.md-example-child-steps + position relative + top 40px +.md-example-child-switch + display flex + align-items center + justify-content center + height 194px +.md-example-child-tabs + .md-tab-content + background #f + height 100px + line-height 100px + text-align center +.md-example-child-tag + display flex + align-items center + justify-content center + height 194px + .md-tag + margin-right h-gap-sm +.md-example-child-tab-picker + .md-popup.with-mask + position absolute !important +.md-example-child-dialog + .md-button + margin-bottom 20px +.md-example-child-tip + zoom 1 !important + display flex + align-items center + justify-content center + height 194px +.md-example-child-stepper + display flex + align-items center + height 194px + .md-field + flex 1 +.md-example-child-notice-bar + display flex + align-items center + height 194px + .md-notice-bar + flex 1 +.md-example-child-codebox + display flex + align-items center + justify-content center + height 194px +.md-example-chart-child + svg + width 100% !important + +.md-image-viewer + position absolute !important +.md-action-bar + position absolute !important +.md-action-sheet + .md-popup + position absolute !important +.md-date-picker + .md-popup + position absolute !important +.md-drop-menu + position absolute !important + height 100% !important + z-index 100 + .md-drop-menu-bar + height drop-menu-height + .md-popup + position absolute !important +.md-dialog .md-popup-box, .md-toast + zoom .6 +.md-number-keyboard + .md-popup-box + left 50% !important + max-width 800px !important + margin-left -400px !important + z-index 1104 !important + box-shadow 0 4px 20px rgba(0, 0, 0, .1) +.md-captcha + z-index 1402 !important + .md-number-keyboard .md-popup-box + position fixed !important + zoom .6 + .md-dialog .md-popup-box + top 30% !important +.md-landscape + .md-popup.with-mask, .md-popup-box, .close + position absolute !important + .close + bottom 5% !important +.md-picker + .md-popup + position absolute !important +.md-cashier + .md-popup.with-mask + position absolute !important diff --git a/site/theme/default/assets/css/global.styl b/site/theme/default/assets/css/global.styl new file mode 100644 index 00000000..9e898545 --- /dev/null +++ b/site/theme/default/assets/css/global.styl @@ -0,0 +1,17 @@ +.doc-demo-title a + margin-right 5px + background color-primary-tap + color #fff + padding 5px 10px + border-radius 4px + font-size 16px + text-decoration none +.doc-demo-box-priview ul>li + list-style none !important + +.doc-content-qrcode + img + display inline-block + width 100% + margin-top 10px + opacity .8 \ No newline at end of file diff --git a/site/theme/default/assets/css/hightlight.css b/site/theme/default/assets/css/hightlight.css new file mode 100644 index 00000000..35ca8f87 --- /dev/null +++ b/site/theme/default/assets/css/hightlight.css @@ -0,0 +1,17 @@ +.hljs{display:block;overflow-x:auto;padding:.5em;background:#f8f8f8;color:#333} +.hljs-comment,.hljs-quote{color:#999;font-style:italic} +.hljs-keyword,.hljs-selector-tag,.hljs-subst{color:#333;font-weight:500} +.hljs-literal,.hljs-number,.hljs-tag .hljs-attr,.hljs-template-variable,.hljs-variable{color:teal} +.hljs-doctag,.hljs-string{color:#d14} +.hljs-section,.hljs-selector-id,.hljs-title{color:#900;font-weight:500} +.hljs-subst{font-weight:400} +.hljs-class .hljs-title,.hljs-type{color:#458;font-weight:500} +.hljs-attribute,.hljs-name,.hljs-tag{color:#256ea2;font-weight:400} +.hljs-link,.hljs-regexp{color:#009926} +.hljs-bullet,.hljs-symbol{color:#990073} +.hljs-built_in,.hljs-builtin-name{color:#0086b3} +.hljs-meta{color:#999;font-weight:500} +.hljs-deletion{background:#fdd} +.hljs-addition{background:#dfd} +.hljs-emphasis{font-style:italic} +.hljs-strong{font-weight:500} diff --git a/site/theme/default/assets/css/markdown.styl b/site/theme/default/assets/css/markdown.styl new file mode 100644 index 00000000..a865df01 --- /dev/null +++ b/site/theme/default/assets/css/markdown.styl @@ -0,0 +1,451 @@ +.doc-content-paragraph, .doc-demo-box-code{ + * { + font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace + } + h1, h2, h3, h4 { + color: #333; + font-weight: 500; + margin-top: 1.5em; + } + + h1, h2, h3, h4, h5, p , dl{ + margin-bottom: 16px; + padding: 0; + line-height: 1.2; + -webkit-font-smoothing: antialiased; + } + h1, h2, h3, h4, h5{ + a{ + margin-left: 10px; + display: none; + text-decoration: none; + font-weight: bold; + } + &:hover a{ + display: inline; + } + } + h1 { + font-size: 32px; + } + h2 { + font-size: 28px; + } + h1, h2 { + padding-bottom: 10px; + } + h3 { + font-size: 22px; + } + h4 { + font-size: 18px; + } + h5 { + font-size: 16px; + font-weight: 400; + margin-top: 1em; + } + a { + color: #3ca0e6; + margin: 0; + padding: 0; + vertical-align: baseline; + margin: 0 5px; + text-decoration: none; + } + a:hover { + text-decoration: none; + color: #ff6600; + } + a:visited { + /*color: purple;*/ + } + ul, ol { + padding: 0; + padding-left: 24px; + margin-top: 10px; + } + li { + margin-bottom: 5px; + line-height: 24px; + list-style: circle; + } + p, ul, ol { + font-size: 14px; + line-height: 24px; + } + + ol ol, ul ol { + list-style-type: lower-roman; + } + + p { + color: #5e6d82; + } + + code, pre { + border-radius: 3px; + color: inherit; + } + + code { + font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace !important; + // -webkit-font-smoothing: antialiased; + margin: 0 2px; + padding: 0 5px; + border: solid 1px #f0f0f0; + background-color:#f2f4f5; + } + + code * { + font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace !important; + font-size: 14px; + } + + pre { + position: relative; + margin-bottom: 1em; + line-height: 1.7em; + overflow: auto; + padding: 15px 30px; + box-sizing: border-box; + background-color:#f2f4f5; + } + + pre > code { + border: 0; + display: block; + max-width: initial; + padding: 0; + margin: 0; + overflow: auto; + line-height: inherit; + font-size: 14px; + white-space: pre; + background: 0 0; + } + + code { + color: #666555; + } + code:after{ + content: ''; + position: absolute; + top: 5px; + right: 5px; + color: #ddd; + font-size: 12px; + } + code.lang-vue:after{ + content: 'Vue'; + } + code.lang-css:after{ + content: 'Css'; + } + code.lang-javascript:after{ + content: 'Js'; + } + code.lang-shell:after{ + content: 'Shell'; + } + code.lang-stylus:after{ + content: 'Stylus'; + } + + /** markdown preview plus 对于代码块的处理有些问题, 所以使用统一的颜色 */ + /*code .keyword { + color: #8959a8; + } + + code .number { + color: #f5871f; + } + + code .comment { + color: #998 + }*/ + + aside { + display: block; + float: right; + width: 390px; + } + blockquote { + border-left:.3em solid #048EFA; + padding: 1em; + margin-left:0; + margin-bottom: 1em; + background: rgba(252, 145, 83, 0.05); + border-radius: 4px; + } + blockquote cite { + font-size:14px; + line-height:20px; + color:#bfbfbf; + } + blockquote cite:before { + content: '\2014 \00A0'; + } + + blockquote p { + margin: 0; + color: #666; + } + hr { + text-align: left; + color: #999; + height: 2px; + padding: 0; + margin: 16px 0; + background-color: #e7e7e7; + border: 0 none; + } + + dl { + padding: 0; + } + + dl dt { + padding: 10px 0; + margin-top: 16px; + font-size: 1em; + font-style: italic; + font-weight: bold; + } + + dl dd { + padding: 0 16px; + margin-bottom: 16px; + } + + dd { + margin-left: 0; + } + + /* Code below this line is copyright Twitter Inc. */ + + button, + input, + select, + textarea { + font-size: 100%; + margin: 0; + vertical-align: baseline; + *vertical-align: middle; + } + button, input { + line-height: normal; + *overflow: visible; + } + button::-moz-focus-inner, input::-moz-focus-inner { + border: 0; + padding: 0; + } + button, + input[type="button"], + input[type="reset"], + input[type="submit"] { + cursor: pointer; + -webkit-appearance: button; + } + input[type=checkbox], input[type=radio] { + cursor: pointer; + } + /* override default chrome & firefox settings */ + input:not([type="image"]), textarea { + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; + } + + input[type="search"] { + -webkit-appearance: textfield; + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; + } + input[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; + } + label, + input, + select, + textarea { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 13px; + font-weight: normal; + line-height: normal; + margin-bottom: 18px; + } + input[type=checkbox], input[type=radio] { + cursor: pointer; + margin-bottom: 0; + } + input[type=text], + input[type=password], + textarea, + select { + display: inline-block; + width: 210px; + padding: 4px; + font-size: 13px; + font-weight: normal; + line-height: 18px; + height: 18px; + color: #808080; + border: 1px solid #ccc; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; + } + select, input[type=file] { + height: 27px; + line-height: 27px; + } + textarea { + height: auto; + } + /* grey out placeholders */ + :-moz-placeholder { + color: #bfbfbf; + } + ::-webkit-input-placeholder { + color: #bfbfbf; + } + input[type=text], + input[type=password], + select, + textarea { + -webkit-transition: border linear 0.2s, box-shadow linear 0.2s; + -moz-transition: border linear 0.2s, box-shadow linear 0.2s; + transition: border linear 0.2s, box-shadow linear 0.2s; + -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1); + -moz-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1); + box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1); + } + input[type=text]:focus, input[type=password]:focus, textarea:focus { + outline: none; + border-color: rgba(82, 168, 236, 0.8); + -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1), 0 0 8px rgba(82, 168, 236, 0.6); + -moz-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1), 0 0 8px rgba(82, 168, 236, 0.6); + box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1), 0 0 8px rgba(82, 168, 236, 0.6); + } + /* buttons */ + button { + display: inline-block; + padding: 4px 14px; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 13px; + line-height: 18px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + background-color: #0064cd; + background-repeat: repeat-x; + background-image: -khtml-gradient(linear, left top, left bottom, from(#049cdb), to(#0064cd)); + background-image: -moz-linear-gradient(top, #049cdb, #0064cd); + background-image: -ms-linear-gradient(top, #049cdb, #0064cd); + background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #049cdb), color-stop(100%, #0064cd)); + background-image: -webkit-linear-gradient(top, #049cdb, #0064cd); + background-image: -o-linear-gradient(top, #049cdb, #0064cd); + background-image: linear-gradient(top, #049cdb, #0064cd); + color: #fff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + border: 1px solid #004b9a; + border-bottom-color: #003f81; + -webkit-transition: 0.1s linear all; + -moz-transition: 0.1s linear all; + transition: 0.1s linear all; + border-color: #0064cd #0064cd #003f81; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + } + button:hover { + color: #fff; + background-position: 0 -15px; + text-decoration: none; + } + button:active { + -webkit-box-shadow: inset 0 3px 7px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 3px 7px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 3px 7px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + } + button::-moz-focus-inner { + padding: 0; + border: 0; + } + table { + border-collapse: collapse; + width: 100%; + background-color: #fafafa; + font-size: 14px; + margin-bottom: 45px; + line-height: 1.5em; + } + + table td, table th { + border-bottom: .5px solid #ebebeb; + padding: 15px; + max-width: 250px; + font-size: 13px; + code { + font-size: 12px; + } + } + + table th { + text-align: left; + white-space: nowrap; + color: #666; + font-weight: 400; + border: none; + background: #f0f0f0; + } + + // table td:first-child, table th:first-child { + // border-left: solid 1px #ebebeb; + // } + + // table td:last-child, table th:last-child { + // border-right: solid 1px #ebebeb; + // } + + table tr:last-child td { + border: none; + } + + table th:first-child { + -moz-border-radius: 6px 0 0 0; + -webkit-border-radius: 6px 0 0 0; + border-radius: 6px 0 0 0; + border: none; + } + table th:last-child { + padding-right: 1px; + -moz-border-radius: 0 6px 0 0; + -webkit-border-radius: 0 6px 0 0; + border-radius: 0 6px 0 0; + border: none; + } + table th:only-child{ + -moz-border-radius: 6px 6px 0 0; + -webkit-border-radius: 6px 6px 0 0; + border-radius: 6px 6px 0 0; + } + table tr:last-child td:first-child { + -moz-border-radius: 0 0 0 6px; + -webkit-border-radius: 0 0 0 6px; + border-radius: 0 0 0 6px; + } + table tr:last-child td:last-child { + -moz-border-radius: 0 0 6px 0; + -webkit-border-radius: 0 0 6px 0; + border-radius: 0 0 6px 0; + } + img { + vertical-align: middle; + max-width: 100%; + } +} \ No newline at end of file diff --git a/site/theme/default/assets/css/mixin.styl b/site/theme/default/assets/css/mixin.styl new file mode 100644 index 00000000..ffa05d22 --- /dev/null +++ b/site/theme/default/assets/css/mixin.styl @@ -0,0 +1,9 @@ +block() + float left + width 100% + +clearfix() + &:after + content "" + clear both + display table \ No newline at end of file diff --git a/site/theme/default/assets/css/toc.styl b/site/theme/default/assets/css/toc.styl new file mode 100644 index 00000000..e39ad93c --- /dev/null +++ b/site/theme/default/assets/css/toc.styl @@ -0,0 +1,46 @@ + .default-doc-toc + position absolute + top 0 + right 0 + width 150px + border-left solid 1px #f0f1f2 + &.is-stricky + position fixed + top 32px + right 0 + .mfe-blog-toc-item + position relative + block() + padding 5px 15px + box-sizing border-box + text-decoration none + color #333 + white-space nowrap + overflow hidden + text-overflow ellipsis + &.mfe-blog-toc-item-h1 + font-size 18px + &.mfe-blog-toc-item-h2 + font-size 16px + &.mfe-blog-toc-item-h3 + // color #333 + font-size 14px + &.mfe-blog-toc-item-h4 + // color #999 + font-size 12px + &.mfe-blog-toc-item-h5 + // color #ccc + font-size 12px + font-weight 300 + &.active::before + content "" + position absolute + left 0 + top 0 + width 2px + height 100% + background-color #fc9153 + +@media (max-width: 750px) + .mfe-blog-theme-default-doc .default-doc-toc + display none \ No newline at end of file diff --git a/site/theme/default/assets/js/home.config.js b/site/theme/default/assets/js/home.config.js new file mode 100644 index 00000000..18c07fa8 --- /dev/null +++ b/site/theme/default/assets/js/home.config.js @@ -0,0 +1,86 @@ +import MfeTable from '../../components/Table' + +const qrcodeTableView = { + name: 'qrcodeTable', + components: { + MfeTable + }, + data () { + return { + qrcodeTableShow: false + } + }, + template: '' +} + +export default [ + { + title: 'Mand Mobile', + describe: '一个基于Vue的移动端UI组件库,丰富、灵活、实用,快速搭建优质的金融类产品,让复杂的金融场景变简单。', + buttons: [{ + type: 'link', + text: '开始使用', + src: '/docs', + theme: 'start' + }, { + type: 'handler', + text: '扫码体验', + click (ref) { + this.$refs[ref][0].qrcodeTableShow = true + }, + theme: 'demo', + slots: qrcodeTableView + }, { + htmls: '' + }], + animations: { + bg: '//manhattan.didistatic.com/static/manhattan/mand/docs/mand-doc-home-0.svg', + content: '//manhattan.didistatic.com/static/manhattan/mand/docs/mand-doc-home-0.png' + } + }, + { + title: '用户体验', + describe: '基于「合理、好用」设计价值观,从交互操作、视觉抽象、图形可视等角度共同解决问题。 ', + animations: { + bg: '//manhattan.didistatic.com/static/manhattan/mand/docs/mand-doc-home-1.svg', + content: '//manhattan.didistatic.com/static/manhattan/mand/docs/mand-doc-home-1.png' + }, + decorate: '//manhattan.didistatic.com/static/manhattan/mand/docs/mand-doc-home-cirlce.svg' + }, + { + title: '敏捷支持', + describe: '汲取最前沿技术,组件化轻量化实现,兼顾稳定和品质,努力实现金融场景的全覆盖。', + animations: [ + { + icon: '//manhattan.didistatic.com/static/manhattan/mand/docs/mand-doc-home-2-0.svg', + title: '丰富的组件', + describe: '30+的基础组件,覆盖金融场景', + }, + { + icon: '//manhattan.didistatic.com/static/manhattan/mand/docs/mand-doc-home-2-1.svg', + title: '极高的易用性', + describe: '组件均有详细说明文档、案例演示', + }, + { + icon: '//manhattan.didistatic.com/static/manhattan/mand/docs/mand-doc-home-2-2.svg', + title: '轻量的Bundle', + describe: '支持babel-plugin-import自动化按需加载代码,减小bundle体积', + } + ] + }, + { + title: '共享资源', + describe: '提供相关资源的下载,输出规范,助力快速搭建优质产品页面原型或高保真视觉稿。', + buttons: [{ + type: 'link', + text: '设计资源', + src: '/design/resource', + theme: 'demo' + }], + animations: { + bg: '//manhattan.didistatic.com/static/manhattan/mand/docs/mand-doc-home-3.svg', + content: '//manhattan.didistatic.com/static/manhattan/mand/docs/mand-doc-home-3.png' + }, + decorate: '//manhattan.didistatic.com/static/manhattan/mand/docs/mand-doc-home-rectangle.svg' + }, +] \ No newline at end of file diff --git a/site/theme/default/assets/js/responsive.js b/site/theme/default/assets/js/responsive.js new file mode 100644 index 00000000..c59e1da4 --- /dev/null +++ b/site/theme/default/assets/js/responsive.js @@ -0,0 +1,26 @@ +(function (window, document) { + + function resize(){ + var ww = window.innerWidth; + if (ww > window.screen.width) { + window.requestAnimationFrame(resize); + } + else{ + if (ww > 720) { + ww = 720 + } + document.documentElement.style.fontSize = ww * 0.13333333333333333 + 'px'; + document.body.style.opacity = 1; + } + } + + if (document.readyState !== 'loading') { + resize(); + } + else { + document.addEventListener('DOMContentLoaded', resize); + } + + window.addEventListener('resize', resize); + + })(window, document) diff --git a/site/theme/default/assets/js/util.js b/site/theme/default/assets/js/util.js new file mode 100644 index 00000000..29b407b5 --- /dev/null +++ b/site/theme/default/assets/js/util.js @@ -0,0 +1,34 @@ +export function findMenu(list, nav) { + let sublist + list.forEach(item => { + if (item.name === nav) { + return (sublist = item.menu) + } + }) + return sublist +} + + +export function setScale (scale) { + const viewPort = document.querySelector('meta[name=viewport]') + + if (!viewPort) { + return + } + + const viewPortContent = viewPort.getAttribute('content') + const viewPortContentParts = viewPortContent.split(',') + + let newViewPortContent = '' + + for (let i = 0, len = viewPortContentParts.length; i < len; i++) { + const attr = viewPortContentParts[i] + if ((attr.indexOf('initial-scale') >= 0) || (attr.indexOf('maximum-scale') >= 0) || (attr.indexOf('minimum-scale') >= 0)) { + continue + } else { + newViewPortContent += `${attr},` + } + } + newViewPortContent += `initial-scale=${scale}, maximum-scale=${scale}, minimum-scale=${scale}` + viewPort.setAttribute('content', newViewPortContent) +} \ No newline at end of file diff --git a/site/theme/default/components/Doc.vue b/site/theme/default/components/Doc.vue new file mode 100644 index 00000000..faebb9e4 --- /dev/null +++ b/site/theme/default/components/Doc.vue @@ -0,0 +1,501 @@ + + + + + diff --git a/site/theme/default/components/Footer.vue b/site/theme/default/components/Footer.vue new file mode 100644 index 00000000..ed6aced6 --- /dev/null +++ b/site/theme/default/components/Footer.vue @@ -0,0 +1,141 @@ + + + + + diff --git a/site/theme/default/components/Header.vue b/site/theme/default/components/Header.vue new file mode 100644 index 00000000..e3cee25b --- /dev/null +++ b/site/theme/default/components/Header.vue @@ -0,0 +1,377 @@ + + + + + diff --git a/site/theme/default/components/Menu.vue b/site/theme/default/components/Menu.vue new file mode 100644 index 00000000..804405d6 --- /dev/null +++ b/site/theme/default/components/Menu.vue @@ -0,0 +1,116 @@ + + + + + diff --git a/site/theme/default/components/Table.vue b/site/theme/default/components/Table.vue new file mode 100644 index 00000000..f3403f93 --- /dev/null +++ b/site/theme/default/components/Table.vue @@ -0,0 +1,159 @@ + + + + + + + + diff --git a/site/theme/default/main.js b/site/theme/default/main.js new file mode 100644 index 00000000..46de4a33 --- /dev/null +++ b/site/theme/default/main.js @@ -0,0 +1,19 @@ +// The Vue build version to load with the `import` command +// (runtime-only or standalone) has been set in webpack.base.conf with an alias. +import Vue from 'vue' +import App from './App' +import router from './router' +import { setScale } from './assets/js/util' + +Vue.config.productionTip = false + +if ($(window).width() > 750) { + setScale(0.5) +} + +/* eslint-disable no-new */ +new Vue({ + el: '#app', + render: h => h(App), + router, +}) diff --git a/site/theme/default/router/index.js b/site/theme/default/router/index.js new file mode 100644 index 00000000..30e0f4cc --- /dev/null +++ b/site/theme/default/router/index.js @@ -0,0 +1,37 @@ +import Vue from 'vue' +import Router from 'vue-router' +import Home from '../Home' +import Error from '../Error' +import Routes from '../../../public/route' + +Vue.use(Router) + +Routes.map((item, index) => { + item.meta = item.meta || {} + item.meta.index = index + return item +}) + +const routes = [ + ...Routes, + {path: '/home', component: Home, meta: {noMenu: true}}, + {path: '/', redirect: '/home'}, + {path: '*', component: Error, meta: {noMenu: true}}, +] + +const router = new Router({ + mode: 'history', + base: window.mbConfig.routePrefix, + routes +}) + +router.beforeEach((to, from, next) => { + document.title = (to.meta.text + ? `${to.meta.text}-${window.mbConfig.title}` + : window.mbConfig.title).replace(/<[^>]+>/g, '') + next() +}) + +window.$routes = Routes + +export default router diff --git a/static/.gitkeep b/static/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/static/animate.css b/static/animate.css new file mode 100644 index 00000000..3cff2ee3 --- /dev/null +++ b/static/animate.css @@ -0,0 +1,16 @@ +@charset "UTF-8";body{-webkit-backface-visibility:hidden;} +.animated{-webkit-animation-duration:1s;-moz-animation-duration:1s;-o-animation-duration:1s;animation-duration:1s;-webkit-animation-fill-mode:both;-moz-animation-fill-mode:both;-o-animation-fill-mode:both;animation-fill-mode:both;} +.animated.hinge{-webkit-animation-duration:2s;-moz-animation-duration:2s;-o-animation-duration:2s;animation-duration:2s;} +@-webkit-keyframes fadeInLeftBig{0%{opacity:0;-webkit-transform:translateX(-2000px);} +100%{opacity:1;-webkit-transform:translateX(0);} +} +@-moz-keyframes fadeInLeftBig{0%{opacity:0;-moz-transform:translateX(-2000px);} +100%{opacity:1;-moz-transform:translateX(0);} +} +@-o-keyframes fadeInLeftBig{0%{opacity:0;-o-transform:translateX(-2000px);} +100%{opacity:1;-o-transform:translateX(0);} +} +@keyframes fadeInLeftBig{0%{opacity:0;transform:translateX(-2000px);} +100%{opacity:1;transform:translateX(0);} +} +.fadeInLeftBig{-webkit-animation-name:fadeInLeftBig;-moz-animation-name:fadeInLeftBig;-o-animation-name:fadeInLeftBig;animation-name:fadeInLeftBig;} diff --git a/static/jquery.lettering.js b/static/jquery.lettering.js new file mode 100644 index 00000000..9124e9bd --- /dev/null +++ b/static/jquery.lettering.js @@ -0,0 +1 @@ +(function($){function injector(t,splitter,klass,after){var a=t.text().split(splitter),inject='';if(a.length){$(a).each(function(i,item){inject+=''+item+''+after});t.empty().append(inject)}}var methods={init:function(){return this.each(function(){injector($(this),'','char','')})},words:function(){return this.each(function(){injector($(this),' ','word',' ')})},lines:function(){return this.each(function(){var r="eefec303079ad17405c889e092e105b0";injector($(this).children("br").replaceWith(r).end(),r,'line','')})}};$.fn.lettering=function(method){if(method&&methods[method]){return methods[method].apply(this,[].slice.call(arguments,1))}else if(method==='letters'||!method){return methods.init.apply(this,[].slice.call(arguments,0))}$.error('Method '+method+' does not exist on jQuery.lettering');return this}})(window.jQuery||window.Zepto); \ No newline at end of file diff --git a/static/jquery.textillate.js b/static/jquery.textillate.js new file mode 100644 index 00000000..45b528b3 --- /dev/null +++ b/static/jquery.textillate.js @@ -0,0 +1 @@ +(function ($) { "use strict"; function isInEffect (effect) { return /In/.test(effect) || $.inArray(effect, $.fn.textillate.defaults.inEffects) >= 0; }; function isOutEffect (effect) { return /Out/.test(effect) || $.inArray(effect, $.fn.textillate.defaults.outEffects) >= 0; }; function stringToBoolean(str) { if (str !== "true" && str !== "false"){ return str; }; return (str === "true"); } function getData (node) { var attrs = node.attributes || [] , data = {}; if (!attrs.length){ return data; } $.each(attrs, function (i, attr) { var nodeName = attr.nodeName.replace(/delayscale/, 'delayScale'); if (/^data-in-*/.test(nodeName)) { data.in = data.in || {}; data.in[nodeName.replace(/data-in-/, '')] = stringToBoolean(attr.nodeValue); } else if (/^data-out-*/.test(nodeName)) { data.out = data.out || {}; data.out[nodeName.replace(/data-out-/, '')] =stringToBoolean(attr.nodeValue); } else if (/^data-*/.test(nodeName)) { data[nodeName.replace(/data-/, '')] = stringToBoolean(attr.nodeValue); } }); return data; } function shuffle (o) { for (var j, x, i = o.length; i; j = parseInt(Math.random() * i), x = o[--i], o[i] = o[j], o[j] = x); return o; } function animate ($t, effect, cb) { $t.addClass('animated ' + effect) .css('visibility', 'visible') .show(); $t.one('animationend webkitAnimationEnd oAnimationEnd', function () { $t.removeClass('animated ' + effect); cb && cb(); }); } function animateTokens ($tokens, options, cb) { var that = this , count = $tokens.length; if (!count) { cb && cb(); return; } if (options.shuffle){ $tokens = shuffle($tokens); } if (options.reverse){ $tokens = $tokens.toArray().reverse(); } $.each($tokens, function (i, t) { var $token = $(t); function complete () { if (isInEffect(options.effect)) { $token.css('visibility', 'visible'); } else if (isOutEffect(options.effect)) { $token.css('visibility', 'hidden'); } count -= 1; if (!count && cb){ cb(); } } var delay = options.sync ? options.delay : options.delay * i * options.delayScale; $token.text() ? setTimeout(function () { animate($token, options.effect, complete) }, delay) : complete(); }); }; var Textillate = function (element, options) { var base = this, $element = $(element); base.init = function () { base.$texts = $element.find(options.selector); if (!base.$texts.length) { base.$texts = $('
  • ' + $element.html() + '
'); $element.html(base.$texts); } base.$texts.hide(); base.$current = $('') .html(base.$texts.find(':first-child').html()) .prependTo($element); if (isInEffect(options.in.effect)) { base.$current.css('visibility', 'hidden'); } else if (isOutEffect(options.out.effect)) { base.$current.css('visibility', 'visible'); } base.setOptions(options); base.timeoutRun = null; setTimeout(function () { base.options.autoStart && base.start(); }, base.options.initialDelay) }; base.setOptions = function (options) { base.options = options; }; base.triggerEvent = function (name) { var e = $.Event(name + '.tlt'); $element.trigger(e, base); return e; }; base.in = function (index, cb) { index = index || 0; var $elem = base.$texts.find(':nth-child(' + ((index||0) + 1) + ')') , options = $.extend(true, {}, base.options, $elem.length ? getData($elem[0]) : {}) , $tokens; $elem.addClass('current'); base.triggerEvent('inAnimationBegin'); base.$current .html($elem.html()) .lettering('words'); if (base.options.type == "char") { base.$current.find('[class^="word"]') .css({ 'display': 'inline-block', '-webkit-transform': 'translate3d(0,0,0)', '-moz-transform': 'translate3d(0,0,0)', '-o-transform': 'translate3d(0,0,0)', 'transform': 'translate3d(0,0,0)' }) .each(function () { $(this).lettering() }); } $tokens = base.$current .find('[class^="' + base.options.type + '"]') .css('display', 'inline-block'); if (isInEffect(options.in.effect)) { $tokens.css('visibility', 'hidden'); } else if (isOutEffect(options.in.effect)) { $tokens.css('visibility', 'visible'); } base.currentIndex = index; animateTokens($tokens, options.in, function () { base.triggerEvent('inAnimationEnd'); if (options.in.callback) { options.in.callback(); } if (cb) { cb(base); } }); }; base.out = function (cb) { var $elem = base.$texts.find(':nth-child(' + ((base.currentIndex||0) + 1) + ')') , $tokens = base.$current.find('[class^="' + base.options.type + '"]') , options = $.extend(true, {}, base.options, $elem.length ? getData($elem[0]) : {}); base.triggerEvent('outAnimationBegin'); animateTokens($tokens, options.out, function () { $elem.removeClass('current'); base.triggerEvent('outAnimationEnd'); if (options.out.callback) { options.out.callback(); } if (cb) { cb(base); } }); }; base.start = function (index) { setTimeout(function () { base.triggerEvent('start'); (function run (index) { base.in(index, function () { var length = base.$texts.children().length; index += 1; if (!base.options.loop && index >= length) { if (base.options.callback) base.options.callback(); base.triggerEvent('end'); } else { index = index % length; base.timeoutRun = setTimeout(function () { base.out(function () { run(index) }); }, base.options.minDisplayTime); } }); }(index || 0)); }, base.options.initialDelay); }; base.stop = function () { if (base.timeoutRun) { clearInterval(base.timeoutRun); base.timeoutRun = null; } }; base.init(); }; $.fn.textillate = function (settings, args) { return this.each(function () { var $this = $(this) , data = $this.data('textillate') , options = $.extend(true, {}, $.fn.textillate.defaults, getData(this), typeof settings == 'object' && settings); if (!data) { $this.data('textillate', (data = new Textillate(this, options))); } else if (typeof settings == 'string') { data[settings].apply(data, [].concat(args)); } else { data.setOptions.call(data, options); } }) }; $.fn.textillate.defaults = { selector: '.texts', loop: false, minDisplayTime: 2000, initialDelay: 0, in: { effect: 'fadeInLeftBig', delayScale: 1.5, delay: 50, sync: false, reverse: false, shuffle: false, callback: function () {} }, out: { effect: 'hinge', delayScale: 1.5, delay: 50, sync: false, reverse: false, shuffle: false, callback: function () {} }, autoStart: true, inEffects: [], outEffects: [ 'hinge' ], callback: function () {}, type: 'char' }; }(window.jQuery||window.Zepto)); \ No newline at end of file diff --git a/static/pace.css b/static/pace.css new file mode 100644 index 00000000..b2dfca60 --- /dev/null +++ b/static/pace.css @@ -0,0 +1 @@ +.pace{-webkit-pointer-events:none;pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.pace-inactive{display:none}.pace .pace-progress{position:fixed;top:0;right:100%;z-index:2000;width:100%;height:2px;background:#fc9153;opacity:1}.pace .pace-progress-inner{position:absolute;right:0;display:block;width:75pt;height:100%;opacity:1;-webkit-transform:rotate(3deg) translate(0,-4px);transform:rotate(3deg) translate(0,-4px);-ms-transform:rotate(3deg) translate(0,-4px)}.pace .pace-activity{position:fixed;top:10px;right:10px;z-index:2000;display:block;width:10px;height:10px;border:2px solid transparent;border-radius:10px;border-top-color:#fc9153;border-left-color:#fc9153;-webkit-animation:pace-spinner .4s linear infinite;animation:pace-spinner .4s linear infinite}@-webkit-keyframes pace-spinner{0%{-webkit-transform:rotate(0);transform:rotate(0)}to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes pace-spinner{0%{transform:rotate(0)}to{transform:rotate(360deg)}} \ No newline at end of file diff --git a/static/pace.js b/static/pace.js new file mode 100644 index 00000000..c47d6e5a --- /dev/null +++ b/static/pace.js @@ -0,0 +1,2 @@ +/*! pace 1.0.0 */ +(function(){var a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X=[].slice,Y={}.hasOwnProperty,Z=function(a,b){function c(){this.constructor=a}for(var d in b)Y.call(b,d)&&(a[d]=b[d]);return c.prototype=b.prototype,a.prototype=new c,a.__super__=b.prototype,a},$=[].indexOf||function(a){for(var b=0,c=this.length;c>b;b++)if(b in this&&this[b]===a)return b;return-1};for(u={catchupTime:100,initialRate:.03,minTime:250,ghostTime:100,maxProgressPerFrame:20,easeFactor:1.25,startOnPageLoad:!0,restartOnPushState:!0,restartOnRequestAfter:500,target:"body",elements:{checkInterval:100,selectors:["body"]},eventLag:{minSamples:10,sampleCount:3,lagThreshold:3},ajax:{trackMethods:["GET"],trackWebSockets:!0,ignoreURLs:[]}},C=function(){var a;return null!=(a="undefined"!=typeof performance&&null!==performance&&"function"==typeof performance.now?performance.now():void 0)?a:+new Date},E=window.requestAnimationFrame||window.mozRequestAnimationFrame||window.webkitRequestAnimationFrame||window.msRequestAnimationFrame,t=window.cancelAnimationFrame||window.mozCancelAnimationFrame,null==E&&(E=function(a){return setTimeout(a,50)},t=function(a){return clearTimeout(a)}),G=function(a){var b,c;return b=C(),(c=function(){var d;return d=C()-b,d>=33?(b=C(),a(d,function(){return E(c)})):setTimeout(c,33-d)})()},F=function(){var a,b,c;return c=arguments[0],b=arguments[1],a=3<=arguments.length?X.call(arguments,2):[],"function"==typeof c[b]?c[b].apply(c,a):c[b]},v=function(){var a,b,c,d,e,f,g;for(b=arguments[0],d=2<=arguments.length?X.call(arguments,1):[],f=0,g=d.length;g>f;f++)if(c=d[f])for(a in c)Y.call(c,a)&&(e=c[a],null!=b[a]&&"object"==typeof b[a]&&null!=e&&"object"==typeof e?v(b[a],e):b[a]=e);return b},q=function(a){var b,c,d,e,f;for(c=b=0,e=0,f=a.length;f>e;e++)d=a[e],c+=Math.abs(d),b++;return c/b},x=function(a,b){var c,d,e;if(null==a&&(a="options"),null==b&&(b=!0),e=document.querySelector("[data-pace-"+a+"]")){if(c=e.getAttribute("data-pace-"+a),!b)return c;try{return JSON.parse(c)}catch(f){return d=f,"undefined"!=typeof console&&null!==console?console.error("Error parsing inline pace options",d):void 0}}},g=function(){function a(){}return a.prototype.on=function(a,b,c,d){var e;return null==d&&(d=!1),null==this.bindings&&(this.bindings={}),null==(e=this.bindings)[a]&&(e[a]=[]),this.bindings[a].push({handler:b,ctx:c,once:d})},a.prototype.once=function(a,b,c){return this.on(a,b,c,!0)},a.prototype.off=function(a,b){var c,d,e;if(null!=(null!=(d=this.bindings)?d[a]:void 0)){if(null==b)return delete this.bindings[a];for(c=0,e=[];cQ;Q++)K=U[Q],D[K]===!0&&(D[K]=u[K]);i=function(a){function b(){return V=b.__super__.constructor.apply(this,arguments)}return Z(b,a),b}(Error),b=function(){function a(){this.progress=0}return a.prototype.getElement=function(){var a;if(null==this.el){if(a=document.querySelector(D.target),!a)throw new i;this.el=document.createElement("div"),this.el.className="pace pace-active",document.body.className=document.body.className.replace(/pace-done/g,""),document.body.className+=" pace-running",this.el.innerHTML='
\n
\n
\n
',null!=a.firstChild?a.insertBefore(this.el,a.firstChild):a.appendChild(this.el)}return this.el},a.prototype.finish=function(){var a;return a=this.getElement(),a.className=a.className.replace("pace-active",""),a.className+=" pace-inactive",document.body.className=document.body.className.replace("pace-running",""),document.body.className+=" pace-done"},a.prototype.update=function(a){return this.progress=a,this.render()},a.prototype.destroy=function(){try{this.getElement().parentNode.removeChild(this.getElement())}catch(a){i=a}return this.el=void 0},a.prototype.render=function(){var a,b,c,d,e,f,g;if(null==document.querySelector(D.target))return!1;for(a=this.getElement(),d="translate3d("+this.progress+"%, 0, 0)",g=["webkitTransform","msTransform","transform"],e=0,f=g.length;f>e;e++)b=g[e],a.children[0].style[b]=d;return(!this.lastRenderedProgress||this.lastRenderedProgress|0!==this.progress|0)&&(a.children[0].setAttribute("data-progress-text",""+(0|this.progress)+"%"),this.progress>=100?c="99":(c=this.progress<10?"0":"",c+=0|this.progress),a.children[0].setAttribute("data-progress",""+c)),this.lastRenderedProgress=this.progress},a.prototype.done=function(){return this.progress>=100},a}(),h=function(){function a(){this.bindings={}}return a.prototype.trigger=function(a,b){var c,d,e,f,g;if(null!=this.bindings[a]){for(f=this.bindings[a],g=[],d=0,e=f.length;e>d;d++)c=f[d],g.push(c.call(this,b));return g}},a.prototype.on=function(a,b){var c;return null==(c=this.bindings)[a]&&(c[a]=[]),this.bindings[a].push(b)},a}(),P=window.XMLHttpRequest,O=window.XDomainRequest,N=window.WebSocket,w=function(a,b){var c,d,e,f;f=[];for(d in b.prototype)try{e=b.prototype[d],f.push(null==a[d]&&"function"!=typeof e?a[d]=e:void 0)}catch(g){c=g}return f},A=[],j.ignore=function(){var a,b,c;return b=arguments[0],a=2<=arguments.length?X.call(arguments,1):[],A.unshift("ignore"),c=b.apply(null,a),A.shift(),c},j.track=function(){var a,b,c;return b=arguments[0],a=2<=arguments.length?X.call(arguments,1):[],A.unshift("track"),c=b.apply(null,a),A.shift(),c},J=function(a){var b;if(null==a&&(a="GET"),"track"===A[0])return"force";if(!A.length&&D.ajax){if("socket"===a&&D.ajax.trackWebSockets)return!0;if(b=a.toUpperCase(),$.call(D.ajax.trackMethods,b)>=0)return!0}return!1},k=function(a){function b(){var a,c=this;b.__super__.constructor.apply(this,arguments),a=function(a){var b;return b=a.open,a.open=function(d,e){return J(d)&&c.trigger("request",{type:d,url:e,request:a}),b.apply(a,arguments)}},window.XMLHttpRequest=function(b){var c;return c=new P(b),a(c),c};try{w(window.XMLHttpRequest,P)}catch(d){}if(null!=O){window.XDomainRequest=function(){var b;return b=new O,a(b),b};try{w(window.XDomainRequest,O)}catch(d){}}if(null!=N&&D.ajax.trackWebSockets){window.WebSocket=function(a,b){var d;return d=null!=b?new N(a,b):new N(a),J("socket")&&c.trigger("request",{type:"socket",url:a,protocols:b,request:d}),d};try{w(window.WebSocket,N)}catch(d){}}}return Z(b,a),b}(h),R=null,y=function(){return null==R&&(R=new k),R},I=function(a){var b,c,d,e;for(e=D.ajax.ignoreURLs,c=0,d=e.length;d>c;c++)if(b=e[c],"string"==typeof b){if(-1!==a.indexOf(b))return!0}else if(b.test(a))return!0;return!1},y().on("request",function(b){var c,d,e,f,g;return f=b.type,e=b.request,g=b.url,I(g)?void 0:j.running||D.restartOnRequestAfter===!1&&"force"!==J(f)?void 0:(d=arguments,c=D.restartOnRequestAfter||0,"boolean"==typeof c&&(c=0),setTimeout(function(){var b,c,g,h,i,k;if(b="socket"===f?e.readyState<2:0<(h=e.readyState)&&4>h){for(j.restart(),i=j.sources,k=[],c=0,g=i.length;g>c;c++){if(K=i[c],K instanceof a){K.watch.apply(K,d);break}k.push(void 0)}return k}},c))}),a=function(){function a(){var a=this;this.elements=[],y().on("request",function(){return a.watch.apply(a,arguments)})}return a.prototype.watch=function(a){var b,c,d,e;return d=a.type,b=a.request,e=a.url,I(e)?void 0:(c="socket"===d?new n(b):new o(b),this.elements.push(c))},a}(),o=function(){function a(a){var b,c,d,e,f,g,h=this;if(this.progress=0,null!=window.ProgressEvent)for(c=null,a.addEventListener("progress",function(a){return h.progress=a.lengthComputable?100*a.loaded/a.total:h.progress+(100-h.progress)/2},!1),g=["load","abort","timeout","error"],d=0,e=g.length;e>d;d++)b=g[d],a.addEventListener(b,function(){return h.progress=100},!1);else f=a.onreadystatechange,a.onreadystatechange=function(){var b;return 0===(b=a.readyState)||4===b?h.progress=100:3===a.readyState&&(h.progress=50),"function"==typeof f?f.apply(null,arguments):void 0}}return a}(),n=function(){function a(a){var b,c,d,e,f=this;for(this.progress=0,e=["error","open"],c=0,d=e.length;d>c;c++)b=e[c],a.addEventListener(b,function(){return f.progress=100},!1)}return a}(),d=function(){function a(a){var b,c,d,f;for(null==a&&(a={}),this.elements=[],null==a.selectors&&(a.selectors=[]),f=a.selectors,c=0,d=f.length;d>c;c++)b=f[c],this.elements.push(new e(b))}return a}(),e=function(){function a(a){this.selector=a,this.progress=0,this.check()}return a.prototype.check=function(){var a=this;return document.querySelector(this.selector)?this.done():setTimeout(function(){return a.check()},D.elements.checkInterval)},a.prototype.done=function(){return this.progress=100},a}(),c=function(){function a(){var a,b,c=this;this.progress=null!=(b=this.states[document.readyState])?b:100,a=document.onreadystatechange,document.onreadystatechange=function(){return null!=c.states[document.readyState]&&(c.progress=c.states[document.readyState]),"function"==typeof a?a.apply(null,arguments):void 0}}return a.prototype.states={loading:0,interactive:50,complete:100},a}(),f=function(){function a(){var a,b,c,d,e,f=this;this.progress=0,a=0,e=[],d=0,c=C(),b=setInterval(function(){var g;return g=C()-c-50,c=C(),e.push(g),e.length>D.eventLag.sampleCount&&e.shift(),a=q(e),++d>=D.eventLag.minSamples&&a=100&&(this.done=!0),b===this.last?this.sinceLastUpdate+=a:(this.sinceLastUpdate&&(this.rate=(b-this.last)/this.sinceLastUpdate),this.catchup=(b-this.progress)/D.catchupTime,this.sinceLastUpdate=0,this.last=b),b>this.progress&&(this.progress+=this.catchup*a),c=1-Math.pow(this.progress/100,D.easeFactor),this.progress+=c*this.rate*a,this.progress=Math.min(this.lastProgress+D.maxProgressPerFrame,this.progress),this.progress=Math.max(0,this.progress),this.progress=Math.min(100,this.progress),this.lastProgress=this.progress,this.progress},a}(),L=null,H=null,r=null,M=null,p=null,s=null,j.running=!1,z=function(){return D.restartOnPushState?j.restart():void 0},null!=window.history.pushState&&(T=window.history.pushState,window.history.pushState=function(){return z(),T.apply(window.history,arguments)}),null!=window.history.replaceState&&(W=window.history.replaceState,window.history.replaceState=function(){return z(),W.apply(window.history,arguments)}),l={ajax:a,elements:d,document:c,eventLag:f},(B=function(){var a,c,d,e,f,g,h,i;for(j.sources=L=[],g=["ajax","elements","document","eventLag"],c=0,e=g.length;e>c;c++)a=g[c],D[a]!==!1&&L.push(new l[a](D[a]));for(i=null!=(h=D.extraSources)?h:[],d=0,f=i.length;f>d;d++)K=i[d],L.push(new K(D));return j.bar=r=new b,H=[],M=new m})(),j.stop=function(){return j.trigger("stop"),j.running=!1,r.destroy(),s=!0,null!=p&&("function"==typeof t&&t(p),p=null),B()},j.restart=function(){return j.trigger("restart"),j.stop(),j.start()},j.go=function(){var a;return j.running=!0,r.render(),a=C(),s=!1,p=G(function(b,c){var d,e,f,g,h,i,k,l,n,o,p,q,t,u,v,w;for(l=100-r.progress,e=p=0,f=!0,i=q=0,u=L.length;u>q;i=++q)for(K=L[i],o=null!=H[i]?H[i]:H[i]=[],h=null!=(w=K.elements)?w:[K],k=t=0,v=h.length;v>t;k=++t)g=h[k],n=null!=o[k]?o[k]:o[k]=new m(g),f&=n.done,n.done||(e++,p+=n.tick(b));return d=p/e,r.update(M.tick(b,d)),r.done()||f||s?(r.update(100),j.trigger("done"),setTimeout(function(){return r.finish(),j.running=!1,j.trigger("hide")},Math.max(D.ghostTime,Math.max(D.minTime-(C()-a),0)))):c()})},j.start=function(a){v(D,a),j.running=!0;try{r.render()}catch(b){i=b}return document.querySelector(".pace")?(j.trigger("start"),j.go()):setTimeout(j.start,50)},"function"==typeof define&&define.amd?define(function(){return j}):"object"==typeof exports?module.exports=j:D.startOnPageLoad&&j.start()}).call(this); \ No newline at end of file diff --git a/test/unit/.eslintrc b/test/unit/.eslintrc new file mode 100644 index 00000000..959a4f4b --- /dev/null +++ b/test/unit/.eslintrc @@ -0,0 +1,9 @@ +{ + "env": { + "mocha": true + }, + "globals": { + "expect": true, + "sinon": true + } +} diff --git a/test/unit/index.js b/test/unit/index.js new file mode 100644 index 00000000..59c1a417 --- /dev/null +++ b/test/unit/index.js @@ -0,0 +1,8 @@ +import Vue from 'vue' + +/* eslint-disable no-undef */ +Vue.config.productionTip = false + +// require all test files (files that ends with .spec.js) +const testsContext = require.context('../../components', true, /\.spec$/) +testsContext.keys().forEach(testsContext) diff --git a/test/unit/index.rollup.js b/test/unit/index.rollup.js new file mode 100644 index 00000000..3cd88815 --- /dev/null +++ b/test/unit/index.rollup.js @@ -0,0 +1 @@ +import '../../components/*/test/*.spec.js' \ No newline at end of file diff --git a/test/unit/karma.conf.js b/test/unit/karma.conf.js new file mode 100644 index 00000000..6e598f0f --- /dev/null +++ b/test/unit/karma.conf.js @@ -0,0 +1,47 @@ +// This is a karma config file. For more details see +// http://karma-runner.github.io/0.13/config/configuration-file.html +// we are also using it with karma-webpack +// https://github.com/webpack/karma-webpack +const webpackConfig = require('../../build/webpack/webpack.test.conf') +const merge = require('webpack-merge') +const webpack = require('webpack') +const scope = process.argv[4] || '' +module.exports = function(config) { + config.set({ + // to run in additional browsers: + // 1. install corresponding karma launcher + // http://karma-runner.github.io/0.13/config/browsers.html + // 2. add it to the `browsers` array below. + browsers: ['PhantomJS'], + frameworks: ['mocha', 'sinon-chai'], + reporters: ['spec', 'coverage'], + files: ['./index.js'], + preprocessors: { + './index.js': ['webpack', 'sourcemap'], + [`components/${scope}/!(demo)/*.vue`]: ['coverage'], + 'components/!(_util)/*.js': ['coverage'], + }, + concurrency: 1, + webpack: merge( + { + plugins: [ + new webpack.DefinePlugin({ + 'process.env': { + NODE_ENV: '"testing"', + SCOPE: `"${scope}"`, + }, + }), + ], + }, + webpackConfig + ), + webpackMiddleware: { + // noInfo: true + }, + coverageReporter: { + dir: '../../output/coverage', + reporters: [{type: 'lcov', subdir: '.'}, {type: 'text'}], + includeAllSources: false, + }, + }) +} diff --git a/test/unit/rollup.karma.conf.js b/test/unit/rollup.karma.conf.js new file mode 100644 index 00000000..29ddc0d4 --- /dev/null +++ b/test/unit/rollup.karma.conf.js @@ -0,0 +1,52 @@ +const { rollupPlugin } = require('../../build/rollup/rollup-plugin-config') +// This is a karma config file. For more details see +// http://karma-runner.github.io/0.13/config/configuration-file.html +// we are also using it with karma-webpack +// https://github.com/webpack/karma-webpack +const scope = process.argv[5] || '' +let file = './index.rollup.js' +if (scope) { + file = `../../components/${scope}/test/*.spec.js` +} + +module.exports = function(config) { + config.set({ + // to run in additional browsers: + // 1. install corresponding karma launcher + // http://karma-runner.github.io/0.13/config/browsers.html + // 2. add it to the `browsers` array below. + singleRun: true, + browsers: ['PhantomJS'], + // basePath: '../../', + frameworks: ['mocha', 'sinon-chai'], + reporters: ['spec', 'coverage'], + files: [{ + pattern: file, + watched: false, + }], + preprocessors: { + // './index.js': ['webpack', 'sourcemap'], + // 'components/*/test/*.spec.js': ['rollup'], + [file]: ['rollup'] + // [`components/${scope}/!(demo)/*.vue`]: ['coverage'], + // 'components/!(_util)/*.js': ['coverage'], + }, + client: { + clearContext: true, + }, + rollupPreprocessor: { + output: { + // file: 'bundle.js', + format: 'iife', + name: 'mand', + // sourcemap: 'inline', + }, + plugins: rollupPlugin, + }, + coverageReporter: { + dir: '../../docs/coverage', + reporters: [{type: 'lcov', subdir: '.'}, {type: 'text'}], + includeAllSources: false, + }, + }) +} diff --git a/types/component.d.ts b/types/component.d.ts new file mode 100644 index 00000000..5545653b --- /dev/null +++ b/types/component.d.ts @@ -0,0 +1,5 @@ +import Vue from 'vue' + +export class MandComponent extends Vue { + static name: string +} \ No newline at end of file diff --git a/types/dialog.d.ts b/types/dialog.d.ts new file mode 100644 index 00000000..6a1a2052 --- /dev/null +++ b/types/dialog.d.ts @@ -0,0 +1,27 @@ +import Vue from 'vue' + +export type DialogOptions = { + title?: string + content?: string + confirmText?: string + onConfirm?: () => void +} + +export type DialogAlertOptions = { + icon?: string +} & DialogOptions + + +export type DialogConfirmOptions = { + cancelText?: string +} & DialogAlertOptions + +export interface Dialog { + confirm(options: DialogConfirmOptions): Vue + alert(options: DialogAlertOptions): Vue + succeed(options: DialogOptions): Vue + failed(options: DialogOptions): Vue + close(): void +} + +export const Dialog: Dialog \ No newline at end of file diff --git a/types/image-processor.d.ts b/types/image-processor.d.ts new file mode 100644 index 00000000..ed4358a6 --- /dev/null +++ b/types/image-processor.d.ts @@ -0,0 +1,15 @@ +export interface ImageProcessorOptions { + dataUrl: string + width?: number + height?: number + quality: number +} + +export interface ImageProcessorData { + dataUrl: string + blob: Blob +} + +export declare function ImageProcessor( + options: ImageProcessorOptions, + callback?: (data: ImageProcessorData) => any): Promise diff --git a/types/index.d.ts b/types/index.d.ts new file mode 100644 index 00000000..7d82dc49 --- /dev/null +++ b/types/index.d.ts @@ -0,0 +1,59 @@ +import Vue from 'vue' +import { MandComponent } from './component' +import { Toast } from './toast' +import imageProcessor from './image-processor' +import { Dialog } from './dialog' + + +export function install(vue: typeof Vue): void + +export class ActionBar extends MandComponent { } +export class ActionSheet extends MandComponent { } +export class Agree extends MandComponent { } +export class Button extends MandComponent { } +export class Captcha extends MandComponent { } +export class Cashier extends MandComponent { } +export class Chart extends MandComponent { } +export class Codebox extends MandComponent { } +export class DatePicker extends MandComponent { } +export class DropMenu extends MandComponent { } +export class Field extends MandComponent { } +export class FieldItem extends MandComponent { } +export class Icon extends MandComponent { } +export class ImageReader extends MandComponent { } +export class ImageViewer extends MandComponent { } +export class InputItem extends MandComponent { } +export class Landscape extends MandComponent { } +export class NoticeBar extends MandComponent { } +export class NumberKeyboard extends MandComponent { } +export class Picker extends MandComponent { } +export class Popup extends MandComponent { } +export class PopupTitleBar extends MandComponent { } +export class Radio extends MandComponent { } +export class ResultPage extends MandComponent { } +export class Selector extends MandComponent { } +export class Stepper extends MandComponent { } +export class Steps extends MandComponent { } +export class Swiper extends MandComponent { } +export class SwiperItem extends MandComponent { } +export class Switch extends MandComponent { } +export class TabBar extends MandComponent { } +export class TabPicker extends MandComponent { } +export class Tabs extends MandComponent { } +export class Tag extends MandComponent { } +export class Tip extends MandComponent { } + + +// declare module 'mand-mobile/lib/image-reader/image-processor' { +// export = imageProcessor +// /** +// * export image processor options +// */ +// export interface ImageProcessorOptions extends imageProcessor.ImageProcessorOptions { } +// export interface ImageProcessorData extends imageProcessor.ImageProcessorData { } +// } + +export { + Toast, + Dialog, +} \ No newline at end of file diff --git a/types/toast.d.ts b/types/toast.d.ts new file mode 100644 index 00000000..eeb5ab72 --- /dev/null +++ b/types/toast.d.ts @@ -0,0 +1,19 @@ +export type ToastOptions = { + content: string + duration: number + parentNode: Element +} + +export type ToastConstructorOptions = { + icon: string +} & ToastOptions + +export interface Toast { + (options?: ToastConstructorOptions): void + succeed(options?: ToastOptions): void + failed(options?: ToastOptions): void + loading(options?: ToastOptions): void + hide(): void +} + +export const Toast: Toast \ No newline at end of file