diff --git a/src-next/step/index.js b/src-next/step/index.js
new file mode 100644
index 000000000..5998e2d51
--- /dev/null
+++ b/src-next/step/index.js
@@ -0,0 +1,88 @@
+import { createNamespace } from '../utils';
+import { BORDER } from '../utils/constant';
+import { ChildrenMixin } from '../mixins/relation';
+import Icon from '../icon';
+
+const [createComponent, bem] = createNamespace('step');
+
+export default createComponent({
+ mixins: [ChildrenMixin('vanSteps')],
+
+ computed: {
+ status() {
+ if (this.index < this.parent.active) {
+ return 'finish';
+ }
+ if (this.index === +this.parent.active) {
+ return 'process';
+ }
+ },
+
+ active() {
+ return this.status === 'process';
+ },
+
+ lineStyle() {
+ if (this.status === 'finish' && this.parent.activeColor) {
+ return {
+ background: this.parent.activeColor,
+ };
+ }
+ },
+ },
+
+ methods: {
+ genCircle() {
+ const { activeIcon, activeColor, inactiveIcon } = this.parent;
+
+ if (this.active) {
+ return this.$slots['active-icon'] ? (
+ this.$slots['active-icon']()
+ ) : (
+
+ );
+ }
+
+ if (inactiveIcon || this.$slots['inactive-icon']) {
+ return this.$slots['inactive-icon'] ? (
+ this.$slots['inactive-icon']()
+ ) : (
+
+ );
+ }
+
+ return ;
+ },
+
+ onClickStep() {
+ this.parent.$emit('click-step', this.index);
+ },
+ },
+
+ render() {
+ const { status, active } = this;
+ const { activeColor, direction } = this.parent;
+
+ const titleStyle = active && { color: activeColor };
+
+ return (
+
+
+ {this.$slots.default?.()}
+
+
+ {this.genCircle()}
+
+
+
+ );
+ },
+});
diff --git a/src-next/step/index.less b/src-next/step/index.less
new file mode 100644
index 000000000..8ecc8ca13
--- /dev/null
+++ b/src-next/step/index.less
@@ -0,0 +1,151 @@
+@import '../style/var';
+
+.van-step {
+ position: relative;
+ flex: 1;
+ color: @step-text-color;
+ font-size: @step-font-size;
+
+ &__circle {
+ display: block;
+ width: @step-circle-size;
+ height: @step-circle-size;
+ background-color: @step-circle-color;
+ border-radius: 50%;
+ }
+
+ &__line {
+ position: absolute;
+ background-color: @step-line-color;
+ transition: background-color @animation-duration-base;
+ }
+
+ &--horizontal {
+ float: left;
+
+ &:first-child {
+ .van-step__title {
+ margin-left: 0;
+ transform: none;
+ }
+ }
+
+ &:last-child {
+ position: absolute;
+ right: 1px;
+ width: auto;
+
+ .van-step__title {
+ margin-left: 0;
+ transform: none;
+ }
+
+ .van-step__circle-container {
+ right: -9px;
+ left: auto;
+ }
+ }
+
+ .van-step__circle-container {
+ position: absolute;
+ top: 30px;
+ left: -@padding-xs;
+ z-index: 1;
+ padding: 0 @padding-xs;
+ background-color: @white;
+ transform: translateY(-50%);
+ }
+
+ .van-step__title {
+ display: inline-block;
+ margin-left: 3px;
+ font-size: @step-horizontal-title-font-size;
+ transform: translateX(-50%);
+
+ @media (max-width: 321px) {
+ font-size: @step-horizontal-title-font-size - 1px;
+ }
+ }
+
+ .van-step__line {
+ top: 30px;
+ left: 0;
+ width: 100%;
+ height: 1px;
+ }
+
+ .van-step__icon {
+ display: block;
+ font-size: @step-icon-size;
+ }
+
+ .van-step--process {
+ color: @step-process-text-color;
+ }
+ }
+
+ &--vertical {
+ display: block;
+ float: none;
+ padding: 10px 10px 10px 0;
+ line-height: 18px;
+
+ &:not(:last-child)::after {
+ border-bottom-width: 1px;
+ }
+
+ &:first-child {
+ &::before {
+ position: absolute;
+ top: 0;
+ left: -15px;
+ z-index: 1;
+ width: 1px;
+ height: 20px;
+ background-color: @white;
+ content: '';
+ }
+ }
+
+ .van-step__circle-container {
+ position: absolute;
+ top: 19px;
+ left: -15px;
+ z-index: 2;
+ font-size: @step-icon-size;
+ line-height: 1;
+ transform: translate(-50%, -50%);
+ }
+
+ .van-step__line {
+ top: 16px;
+ left: -15px;
+ width: 1px;
+ height: 100%;
+ }
+ }
+
+ &:last-child {
+ .van-step__line {
+ width: 0;
+ }
+ }
+
+ &--finish {
+ color: @step-finish-text-color;
+
+ .van-step__circle,
+ .van-step__line {
+ background-color: @step-finish-line-color;
+ }
+ }
+
+ &__icon,
+ &__title {
+ transition: color @animation-duration-base;
+
+ &--active {
+ color: @step-active-color;
+ }
+ }
+}
diff --git a/src-next/steps/README.md b/src-next/steps/README.md
new file mode 100644
index 000000000..0f2863655
--- /dev/null
+++ b/src-next/steps/README.md
@@ -0,0 +1,89 @@
+# Steps
+
+### Install
+
+```js
+import Vue from 'vue';
+import { Step, Steps } from 'vant';
+
+Vue.use(Step);
+Vue.use(Steps);
+```
+
+## Usage
+
+### Basic Usage
+
+```html
+
+ Step1
+ Step2
+ Step3
+ Step4
+
+```
+
+```js
+export default {
+ data() {
+ return {
+ active: 1,
+ };
+ },
+};
+```
+
+### Custom Style
+
+```html
+
+ Step1
+ Step2
+ Step3
+ Step4
+
+```
+
+### Vertical Steps
+
+```html
+
+
+ 【City】Status1
+ 2016-07-12 12:40
+
+
+ 【City】Status2
+ 2016-07-11 10:00
+
+
+ 【City】Status3
+ 2016-07-10 09:30
+
+
+```
+
+## API
+
+### Steps Props
+
+| Attribute | Description | Type | Default |
+| ------------- | ------------------------ | ------------------ | ------------ |
+| active | Active step | _number \| string_ | `0` |
+| direction | Can be set to `vertical` | _string_ | `horizontal` |
+| active-color | Active step color | _string_ | `#07c160` |
+| active-icon | Active icon name | _string_ | `checked` |
+| inactive-icon | Active icon name | _string_ | - |
+
+### Step Slots
+
+| Name | Description |
+| ------------- | -------------------- |
+| active-icon | Custom active icon |
+| inactive-icon | Custom inactive icon |
+
+### Steps Events
+
+| Event | Description | Arguments |
+| --- | --- | --- |
+| click-step `v2.5.9` | Triggered when a step's title or icon is clicked | _index: number_ |
diff --git a/src-next/steps/README.zh-CN.md b/src-next/steps/README.zh-CN.md
new file mode 100644
index 000000000..d1a93a0f1
--- /dev/null
+++ b/src-next/steps/README.zh-CN.md
@@ -0,0 +1,95 @@
+# Steps 步骤条
+
+### 引入
+
+```js
+import Vue from 'vue';
+import { Step, Steps } from 'vant';
+
+Vue.use(Step);
+Vue.use(Steps);
+```
+
+## 代码演示
+
+### 基础用法
+
+`active`属性表示当前步骤的索引,从 0 起计
+
+```html
+
+ 买家下单
+ 商家接单
+ 买家提货
+ 交易完成
+
+```
+
+```js
+export default {
+ data() {
+ return {
+ active: 1,
+ };
+ },
+};
+```
+
+### 自定义样式
+
+可以通过`active-icon`和`active-color`属性设置激活状态下的图标和颜色
+
+```html
+
+ 买家下单
+ 商家接单
+ 买家提货
+ 交易完成
+
+```
+
+### 竖向步骤条
+
+可以通过设置`direction`属性来改变步骤条的显示方向
+
+```html
+
+
+ 【城市】物流状态1
+ 2016-07-12 12:40
+
+
+ 【城市】物流状态2
+ 2016-07-11 10:00
+
+
+ 快件已发货
+ 2016-07-10 09:30
+
+
+```
+
+## API
+
+### Steps Props
+
+| 参数 | 说明 | 类型 | 默认值 |
+| --- | --- | --- | --- |
+| active | 当前步骤 | _number \| string_ | `0` |
+| direction | 显示方向,可选值为 `vertical` | _string_ | `horizontal` |
+| active-color | 激活状态颜色 | _string_ | `#07c160` |
+| active-icon | 激活状态底部图标,可选值见 [Icon 组件](#/zh-CN/icon) | _string_ | `checked` |
+| inactive-icon | 未激活状态底部图标,可选值见 [Icon 组件](#/zh-CN/icon) | _string_ | - |
+
+### Step Slots
+
+| 名称 | 说明 |
+| ------------- | -------------------- |
+| active-icon | 自定义激活状态图标 |
+| inactive-icon | 自定义未激活状态图标 |
+
+### Steps Events
+
+| 事件名 | 说明 | 回调参数 |
+| ------------------- | -------------------------- | --------------- |
+| click-step `v2.5.9` | 点击步骤的标题或图标时触发 | _index: number_ |
diff --git a/src-next/steps/demo/index.vue b/src-next/steps/demo/index.vue
new file mode 100644
index 000000000..1d0a6236b
--- /dev/null
+++ b/src-next/steps/demo/index.vue
@@ -0,0 +1,116 @@
+
+
+
+
+ {{ t('step1') }}
+ {{ t('step2') }}
+ {{ t('step3') }}
+ {{ t('step4') }}
+
+
+ {{ t('nextStep') }}
+
+
+
+
+ {{ t('step1') }}
+ {{ t('step2') }}
+ {{ t('step3') }}
+ {{ t('step4') }}
+
+
+
+
+
+
+ {{ t('status1') }}
+ 2016-07-12 12:40
+
+
+ {{ t('status2') }}
+ 2016-07-11 10:00
+
+
+ {{ t('status3') }}
+ 2016-07-10 09:30
+
+
+
+
+
+
+
+
+
diff --git a/src-next/steps/index.js b/src-next/steps/index.js
new file mode 100644
index 000000000..45b0ad84b
--- /dev/null
+++ b/src-next/steps/index.js
@@ -0,0 +1,35 @@
+import { createNamespace } from '../utils';
+import { ParentMixin } from '../mixins/relation';
+
+const [createComponent, bem] = createNamespace('steps');
+
+export default createComponent({
+ mixins: [ParentMixin('vanSteps')],
+
+ props: {
+ activeColor: String,
+ inactiveIcon: String,
+ active: {
+ type: [Number, String],
+ default: 0,
+ },
+ direction: {
+ type: String,
+ default: 'horizontal',
+ },
+ activeIcon: {
+ type: String,
+ default: 'checked',
+ },
+ },
+
+ emits: ['click-step'],
+
+ render() {
+ return (
+
+
{this.$slots.default?.()}
+
+ );
+ },
+});
diff --git a/src-next/steps/index.less b/src-next/steps/index.less
new file mode 100644
index 000000000..51d94d716
--- /dev/null
+++ b/src-next/steps/index.less
@@ -0,0 +1,21 @@
+@import '../style/var';
+
+.van-steps {
+ overflow: hidden;
+ background-color: @steps-background-color;
+
+ &--horizontal {
+ padding: 10px 10px 0;
+
+ .van-steps__items {
+ position: relative;
+ display: flex;
+ margin: 0 0 10px;
+ padding-bottom: 22px;
+ }
+ }
+
+ &--vertical {
+ padding: 0 0 0 @padding-xl;
+ }
+}
diff --git a/src-next/steps/test/__snapshots__/demo.spec.js.snap b/src-next/steps/test/__snapshots__/demo.spec.js.snap
new file mode 100644
index 000000000..98a80d91e
--- /dev/null
+++ b/src-next/steps/test/__snapshots__/demo.spec.js.snap
@@ -0,0 +1,96 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`renders demo correctly 1`] = `
+
+
+
+
+
+
+
+
+
【城市】物流状态1
+
2016-07-12 12:40
+
+
+
+
+
+
+
+
【城市】物流状态
+
2016-07-11 10:00
+
+
+
+
+
+
+
快件已发货
+
2016-07-10 09:30
+
+
+
+
+
+
+
+
+`;
diff --git a/src-next/steps/test/__snapshots__/index.spec.js.snap b/src-next/steps/test/__snapshots__/index.spec.js.snap
new file mode 100644
index 000000000..232fc1a04
--- /dev/null
+++ b/src-next/steps/test/__snapshots__/index.spec.js.snap
@@ -0,0 +1,29 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`icon slot 1`] = `
+
+
+
+
+ A
+
+
Custim Inactive Icon
+
+
+
+
+ B
+
+
Custim Active Icon
+
+
+
+
+ C
+
+
Custim Inactive Icon
+
+
+
+
+`;
diff --git a/src-next/steps/test/demo.spec.js b/src-next/steps/test/demo.spec.js
new file mode 100644
index 000000000..5c70922b5
--- /dev/null
+++ b/src-next/steps/test/demo.spec.js
@@ -0,0 +1,4 @@
+import Demo from '../demo';
+import { snapshotDemo } from '../../../test/demo';
+
+snapshotDemo(Demo);
diff --git a/src-next/steps/test/index.spec.js b/src-next/steps/test/index.spec.js
new file mode 100644
index 000000000..2521175f1
--- /dev/null
+++ b/src-next/steps/test/index.spec.js
@@ -0,0 +1,49 @@
+import { mount } from '../../../test';
+
+test('icon slot', () => {
+ const wrapper = mount({
+ template: `
+
+
+ Custim Inactive Icon
+ A
+
+
+ Custim Active Icon
+ B
+
+
+ Custim Inactive Icon
+ C
+
+
+ `,
+ });
+ expect(wrapper).toMatchSnapshot();
+});
+
+test('click-step event', () => {
+ const onClickStep = jest.fn();
+ const wrapper = mount({
+ template: `
+
+ A
+ B
+ C
+
+ `,
+ methods: {
+ onClickStep,
+ },
+ });
+
+ wrapper.find('.van-step').trigger('click');
+ expect(onClickStep).toHaveBeenCalledTimes(0);
+
+ wrapper.find('.van-step__title').trigger('click');
+ expect(onClickStep).toHaveBeenCalledWith(0);
+
+ wrapper.findAll('.van-step__circle-container').at(2).trigger('click');
+ expect(onClickStep).toHaveBeenCalledTimes(2);
+ expect(onClickStep).toHaveBeenLastCalledWith(2);
+});
diff --git a/vant.config.js b/vant.config.js
index 8bc31a38e..0c9d2eb25 100644
--- a/vant.config.js
+++ b/vant.config.js
@@ -265,10 +265,10 @@ module.exports = {
path: 'skeleton',
title: 'Skeleton 骨架屏',
},
- // {
- // path: 'steps',
- // title: 'Steps 步骤条',
- // },
+ {
+ path: 'steps',
+ title: 'Steps 步骤条',
+ },
// {
// path: 'sticky',
// title: 'Sticky 粘性布局',
@@ -599,10 +599,10 @@ module.exports = {
path: 'skeleton',
title: 'Skeleton',
},
- // {
- // path: 'steps',
- // title: 'Steps',
- // },
+ {
+ path: 'steps',
+ title: 'Steps',
+ },
// {
// path: 'sticky',
// title: 'Sticky',