diff --git a/docs/examples-docs/password-input.md b/docs/examples-docs/password-input.md
new file mode 100644
index 000000000..440e96475
--- /dev/null
+++ b/docs/examples-docs/password-input.md
@@ -0,0 +1,88 @@
+
+
+## PasswordInput 密码输入框
+密码输入框组件通常与 [数字键盘](/zanui/vue/component/number-keyboard) 组件配合使用
+
+### 使用指南
+``` javascript
+import { PasswordInput, NumberKeyBoard } from 'vant';
+
+Vue.component(PasswordInput.name, PasswordInput);
+Vue.component(NumberKeyBoard.name, NumberKeyBoard);
+```
+
+### 代码演示
+
+#### 基础用法
+
+:::demo 基础用法
+```html
+
+
+
+
+
+```
+
+```javascript
+export default {
+ data() {
+ return {
+ value: '',
+ showKeyboard: true
+ }
+ },
+
+ methods: {
+ onInput(key) {
+ this.value = (this.value + key).slice(0, 6);
+ },
+ onDelete() {
+ this.value = this.value.slice(0, this.value.length - 1);
+ }
+ }
+}
+```
+:::
+
+### API
+
+| 参数 | 说明 | 类型 | 默认值 | 可选值 |
+|-----------|-----------|-----------|-------------|-------------|
+| value | 密码值 | `String` | `''` | - |
+| length | 密码长度 | `Number` | `6` | - |
+| info | 输入框下方提示 | `String` | - | - |
+| errorInfo | 输入框下方错误提示 | `String` | - | - |
+
+### Event
+
+| 事件名 | 说明 | 参数 |
+|-----------|-----------|-----------|
+| focus | 输入框聚焦时触发 | - |
diff --git a/docs/src/doc.config.js b/docs/src/doc.config.js
index 2451493c9..0e42554c6 100644
--- a/docs/src/doc.config.js
+++ b/docs/src/doc.config.js
@@ -147,6 +147,10 @@ module.exports = {
"path": "/number-keyboard",
"title": "NumberKeyboard 数字键盘"
},
+ {
+ "path": "/password-input",
+ "title": "PasswordInput 密码输入框"
+ },
{
"path": "/radio",
"title": "Radio 单选框"
diff --git a/packages/index.js b/packages/index.js
index 831cd0d82..f617e8870 100644
--- a/packages/index.js
+++ b/packages/index.js
@@ -26,6 +26,7 @@ import NavBar from './nav-bar';
import NoticeBar from './notice-bar';
import NumberKeyboard from './number-keyboard';
import Panel from './panel';
+import PasswordInput from './password-input';
import Picker from './picker';
import Popup from './popup';
import Progress from './progress';
@@ -78,6 +79,7 @@ const components = [
NoticeBar,
NumberKeyboard,
Panel,
+ PasswordInput,
Picker,
Popup,
Progress,
@@ -146,6 +148,7 @@ export {
NoticeBar,
NumberKeyboard,
Panel,
+ PasswordInput,
Picker,
Popup,
Progress,
diff --git a/packages/password-input/index.vue b/packages/password-input/index.vue
new file mode 100644
index 000000000..2f16aed3a
--- /dev/null
+++ b/packages/password-input/index.vue
@@ -0,0 +1,43 @@
+
+
+
+
+
diff --git a/packages/vant-css/src/index.css b/packages/vant-css/src/index.css
index 9585bd21a..dc819043f 100644
--- a/packages/vant-css/src/index.css
+++ b/packages/vant-css/src/index.css
@@ -34,6 +34,7 @@
@import './radio.css';
@import './switch.css';
@import './uploader.css';
+@import './password-input.css';
@import './number-keyboard.css';
/* action components */
diff --git a/packages/vant-css/src/password-input.css b/packages/vant-css/src/password-input.css
new file mode 100644
index 000000000..eb6a64982
--- /dev/null
+++ b/packages/vant-css/src/password-input.css
@@ -0,0 +1,59 @@
+@import "./common/var.css";
+
+.van-password-input {
+ margin: 0 15px;
+ user-select: none;
+ position: relative;
+
+ &:focus {
+ outline: none;
+ }
+
+ &__info,
+ &__error-info {
+ font-size: 14px;
+ margin-top: 15px;
+ text-align: center;
+ }
+
+ &__info {
+ color: $gray-dark;
+ }
+
+ &__error-info {
+ color: $red;
+ }
+
+ &__security {
+ width: 100%;
+ height: 50px;
+ display: flex;
+ background-color: $white;
+
+ &::after {
+ border-radius: 6px;
+ }
+
+ li {
+ flex: 1;
+ height: 100%;
+ position: relative;
+
+ &:not(:first-of-type)::after {
+ border-left-width: 1px;
+ }
+ }
+
+ i {
+ position: absolute;
+ left: 50%;
+ top: 50%;
+ width: 10px;
+ height: 10px;
+ margin: -5px 0 0 -5px;
+ visibility: hidden;
+ border-radius: 100%;
+ background-color: $black;
+ }
+ }
+}
diff --git a/test/unit/specs/password-input.spec.js b/test/unit/specs/password-input.spec.js
new file mode 100644
index 000000000..e5ce480d5
--- /dev/null
+++ b/test/unit/specs/password-input.spec.js
@@ -0,0 +1,56 @@
+import PasswordInput from 'packages/password-input';
+import { mount } from 'avoriaz';
+
+describe('PasswordInput', () => {
+ let wrapper;
+ afterEach(() => {
+ wrapper && wrapper.destroy();
+ });
+
+ it('create a PasswordInput', () => {
+ wrapper = mount(PasswordInput, {});
+ expect(wrapper.find('.van-password-input').length).to.equal(1);
+ });
+
+ it('create a PasswordInput with value && info', (done) => {
+ wrapper = mount(PasswordInput, {
+ propsData: {
+ value: '000',
+ info: '测试info'
+ }
+ });
+
+ expect(wrapper.find('.van-password-input i')[2].hasStyle('visibility', 'visible')).to.be.true;
+ expect(wrapper.find('.van-password-input i')[3].hasStyle('visibility', 'visible')).to.be.false;
+ expect(wrapper.find('.van-password-input__info')[0].text()).to.equal('测试info');
+
+ wrapper.vm.value = '0000';
+ wrapper.vm.errorInfo = '测试errorInfo';
+ wrapper.vm.$nextTick(() => {
+ expect(wrapper.find('.van-password-input i')[3].hasStyle('visibility', 'visible')).to.be.true;
+ expect(wrapper.find('.van-password-input__info').length).to.equal(0);
+ expect(wrapper.find('.van-password-input__error-info')[0].text()).to.equal('测试errorInfo');
+ done();
+ });
+ });
+
+ it('listen to focus event', () => {
+ wrapper = mount(PasswordInput, {});
+
+ const focus = sinon.spy();
+ wrapper.vm.$on('focus', focus);
+ wrapper.find('.van-password-input__security')[0].trigger('touchstart');
+
+ expect(focus.calledOnce).to.be.true;
+ });
+
+ it('change password length', () => {
+ wrapper = mount(PasswordInput, {
+ propsData: {
+ length: 2
+ }
+ });
+
+ expect(wrapper.find('.van-password-input i').length).to.equal(2);
+ });
+});