diff --git a/example/app.json b/example/app.json
index ff94fcde..276a6e51 100644
--- a/example/app.json
+++ b/example/app.json
@@ -45,7 +45,8 @@
"pages/overlay/index",
"pages/circle/index",
"pages/grid/index",
- "pages/dropdown-menu/index"
+ "pages/dropdown-menu/index",
+ "pages/index-bar/index"
],
"window": {
"navigationBarBackgroundColor": "#f8f8f8",
@@ -110,6 +111,8 @@
"van-picker": "./dist/picker/index",
"van-overlay": "./dist/overlay/index",
"van-circle": "./dist/circle/index",
+ "van-index-bar": "./dist/index-bar/index",
+ "van-index-anchor": "./dist/index-anchor/index",
"van-grid": "./dist/grid/index",
"van-grid-item": "./dist/grid-item/index",
"van-dropdown-menu": "./dist/dropdown-menu/index",
diff --git a/example/config.js b/example/config.js
index d9123b5c..2b87364f 100644
--- a/example/config.js
+++ b/example/config.js
@@ -183,6 +183,10 @@ export default [
path: '/grid',
title: 'Grid 宫格'
},
+ {
+ path: '/index-bar',
+ title: 'IndexBar 索引栏'
+ },
{
path: '/sidebar',
title: 'Sidebar 侧边导航'
diff --git a/example/pages/index-bar/index.js b/example/pages/index-bar/index.js
new file mode 100644
index 00000000..1189fb35
--- /dev/null
+++ b/example/pages/index-bar/index.js
@@ -0,0 +1,28 @@
+import Page from '../../common/page';
+
+const indexList = [];
+const charCodeOfA = 'A'.charCodeAt(0);
+for (let i = 0; i < 26; i++) {
+ indexList.push(String.fromCharCode(charCodeOfA + i));
+}
+
+Page({
+ data: {
+ activeTab: 0,
+ indexList,
+ customIndexList: [1, 2, 3, 4, 5, 6, 8, 9, 10],
+ scrollTop: 0,
+ },
+
+ onChange(event) {
+ this.setData({
+ activeTab: event.detail.name
+ });
+ },
+
+ onPageScroll(event) {
+ this.setData({
+ scrollTop: event.scrollTop
+ });
+ }
+});
diff --git a/example/pages/index-bar/index.json b/example/pages/index-bar/index.json
new file mode 100644
index 00000000..4ee5f343
--- /dev/null
+++ b/example/pages/index-bar/index.json
@@ -0,0 +1,3 @@
+{
+ "navigationBarTitleText": "IndexBar 索引栏"
+}
diff --git a/example/pages/index-bar/index.wxml b/example/pages/index-bar/index.wxml
new file mode 100644
index 00000000..60e94f7e
--- /dev/null
+++ b/example/pages/index-bar/index.wxml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 标题{{ item }}
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/example/pages/index-bar/index.wxss b/example/pages/index-bar/index.wxss
new file mode 100644
index 00000000..e69de29b
diff --git a/packages/common/style/var.less b/packages/common/style/var.less
index cfbd7656..d148b8d0 100644
--- a/packages/common/style/var.less
+++ b/packages/common/style/var.less
@@ -540,6 +540,20 @@
@dropdown-menu-title-line-height: 18px;
@dropdown-menu-option-active-color: @blue;
+// IndexAnchor
+@index-anchor-padding: 0 @padding-md;
+@index-anchor-text-color: @text-color;
+@index-anchor-font-weight: 500;
+@index-anchor-font-size: @font-size-md;
+@index-anchor-line-height: 32px;
+@index-anchor-background-color: transparent;
+@index-anchor-active-background-color: @white;
+@index-anchor-active-text-color: @green;
+
+// IndexBar
+@index-bar-index-font-size: @font-size-xs;
+@index-bar-index-line-height: 14px;
+
// skeleton
@skeleton-padding: 0 @padding-md;
@skeleton-row-height: 16px;
diff --git a/packages/index-anchor/index.json b/packages/index-anchor/index.json
new file mode 100644
index 00000000..467ce294
--- /dev/null
+++ b/packages/index-anchor/index.json
@@ -0,0 +1,3 @@
+{
+ "component": true
+}
diff --git a/packages/index-anchor/index.less b/packages/index-anchor/index.less
new file mode 100644
index 00000000..829f6b28
--- /dev/null
+++ b/packages/index-anchor/index.less
@@ -0,0 +1,18 @@
+@import '../common/style/var.less';
+@import '../common/style/theme.less';
+
+.van-index-anchor {
+ .theme(padding, '@index-anchor-padding');
+ .theme(color, '@index-anchor-text-color');
+ .theme(font-weight, '@index-anchor-font-weight');
+ .theme(font-size, '@index-anchor-font-size');
+ .theme(line-height, '@index-anchor-line-height');
+ .theme(background-color, '@index-anchor-background-color');
+
+ &--active {
+ right: 0;
+ left: 0;
+ .theme(color, '@index-anchor-active-text-color');
+ .theme(background-color, '@index-anchor-active-background-color');
+ }
+}
diff --git a/packages/index-anchor/index.ts b/packages/index-anchor/index.ts
new file mode 100644
index 00000000..f4d5d409
--- /dev/null
+++ b/packages/index-anchor/index.ts
@@ -0,0 +1,25 @@
+import { VantComponent } from '../common/component';
+
+VantComponent({
+ relation: {
+ name: 'index-bar',
+ type: 'ancestor',
+ linked(target) {
+ this.parent = target;
+ },
+ unlinked() {
+ this.parent = null;
+ }
+ },
+
+ props: {
+ useSlot: Boolean,
+ index: null
+ },
+
+ data: {
+ active: false,
+ wrapperStyle: '',
+ anchorStyle: ''
+ }
+});
diff --git a/packages/index-anchor/index.wxml b/packages/index-anchor/index.wxml
new file mode 100644
index 00000000..49affa7c
--- /dev/null
+++ b/packages/index-anchor/index.wxml
@@ -0,0 +1,14 @@
+
+
+
+
+ {{ index }}
+
+
+
diff --git a/packages/index-bar/README.md b/packages/index-bar/README.md
new file mode 100644
index 00000000..fd6972ff
--- /dev/null
+++ b/packages/index-bar/README.md
@@ -0,0 +1,110 @@
+# IndexBar 索引栏
+
+### 引入
+
+在`app.json`或`index.json`中引入组件,详细介绍见[快速上手](#/quickstart#yin-ru-zu-jian)
+
+```json
+"usingComponents": {
+ "van-index-bar": "path/to/vant-weapp/dist/van-index-bar/index",
+ "van-index-anchor": "path/to/vant-weapp/dist/van-index-anchor/index"
+}
+```
+
+## 代码演示
+
+### 基础用法
+
+点击索引栏时,会自动跳转到对应的`IndexAnchor`锚点位置
+
+```html
+
+
+
+
+
+
+
+
+
+
+
+ ...
+
+```
+
+```javascript
+Page({
+ onPageScroll(event) {
+ this.setData({
+ scrollTop: event.scrollTop
+ });
+ }
+});
+```
+
+### 自定义索引列表
+
+可以通过`index-list`属性自定义展示的索引字符列表,
+
+```html
+
+ 标题1
+
+
+
+
+ 标题2
+
+
+
+
+ ...
+
+```
+
+```javascript
+Page({
+ data: {
+ indexList: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
+ },
+
+ onPageScroll(event) {
+ this.setData({
+ scrollTop: event.scrollTop
+ });
+ }
+});
+```
+
+## API
+
+### IndexBar Props
+
+| 参数 | 说明 | 类型 | 默认值 | 版本 |
+|------|------|------|------|------|
+| scroll-top | 当前滚动高度(自定义组件内部感知不到页面滚动,所以依赖接入方传入)| *Number* | 0 | - |
+| index-list | 索引字符列表 | *string[] \| number[]* | `A-Z` | - |
+| z-index | z-index 层级 | *number* | `1` | - |
+| sticky | 是否开启锚点自动吸顶 | *boolean* | `true` | - |
+| sticky-offset-top | 锚点自动吸顶时与顶部的距离 | *number* | `0` | - |
+| highlight-color | 索引字符高亮颜色 | *string* | `#07c160` | - |
+
+### IndexAnchor Props
+
+| 参数 | 说明 | 类型 | 默认值 | 版本 |
+|------|------|------|------|------|
+| use-slot | 是否使用自定义内容的插槽 | *boolean* | `false` | - |
+| index | 索引字符 | *string \| number* | - | - |
+
+### IndexBar Events
+
+| 事件名 | 说明 | 回调参数 |
+|------|------|------|
+| select | 选中字符时触发 | index: 索引字符 |
+
+### IndexAnchor Slots
+
+| 名称 | 说明 |
+|------|------|
+| default | 锚点位置显示内容,默认为索引字符 |
\ No newline at end of file
diff --git a/packages/index-bar/index.json b/packages/index-bar/index.json
new file mode 100644
index 00000000..467ce294
--- /dev/null
+++ b/packages/index-bar/index.json
@@ -0,0 +1,3 @@
+{
+ "component": true
+}
diff --git a/packages/index-bar/index.less b/packages/index-bar/index.less
new file mode 100644
index 00000000..8cd20846
--- /dev/null
+++ b/packages/index-bar/index.less
@@ -0,0 +1,24 @@
+@import '../common/style/var.less';
+@import '../common/style/theme.less';
+
+.van-index-bar {
+ position: relative;
+
+ &__sidebar {
+ position: fixed;
+ top: 50%;
+ right: 0;
+ display: flex;
+ flex-direction: column;
+ text-align: center;
+ transform: translateY(-50%);
+ user-select: none;
+ }
+
+ &__index {
+ font-weight: 500;
+ .theme(padding, '0 @padding-base 0 @padding-md');
+ .theme(font-size, '@index-bar-index-font-size');
+ .theme(line-height, '@index-bar-index-line-height');
+ }
+}
diff --git a/packages/index-bar/index.ts b/packages/index-bar/index.ts
new file mode 100644
index 00000000..08ef8f8d
--- /dev/null
+++ b/packages/index-bar/index.ts
@@ -0,0 +1,308 @@
+import { VantComponent } from '../common/component';
+import { GREEN } from '../common/color';
+
+const indexList = () => {
+ const indexList = [];
+ const charCodeOfA = 'A'.charCodeAt(0);
+
+ for (let i = 0; i < 26; i++) {
+ indexList.push(String.fromCharCode(charCodeOfA + i));
+ }
+
+ return indexList;
+};
+
+VantComponent({
+ relation: {
+ name: 'index-anchor',
+ type: 'descendant',
+ linked() {
+ this.updateData();
+ },
+ linkChanged() {
+ this.updateData();
+ },
+ unlinked() {
+ this.updateData();
+ }
+ },
+
+ props: {
+ sticky: {
+ type: Boolean,
+ value: true
+ },
+ zIndex: {
+ type: Number,
+ value: 1
+ },
+ highlightColor: {
+ type: String,
+ value: GREEN
+ },
+ scrollTop: {
+ type: Number,
+ value: 0,
+ observer: 'onScroll'
+ },
+ stickyOffsetTop: {
+ type: Number,
+ value: 0
+ },
+ indexList: {
+ type: Array,
+ value: indexList()
+ }
+ },
+
+ data: {
+ activeAnchorIndex: null,
+ showSidebar: false
+ },
+
+ methods: {
+ updateData() {
+ this.timer && clearTimeout(this.timer);
+
+ this.timer = setTimeout(() => {
+ this.children = this.getRelationNodes('../index-anchor/index');
+
+ this.setData({
+ showSidebar: !!this.children.length
+ });
+
+ this.setRect().then(() => {
+ this.onScroll();
+ });
+ }, 0);
+ },
+
+ setRect() {
+ return Promise.all([
+ this.setAnchorsRect(),
+ this.setListRect(),
+ this.setSiderbarRect()
+ ]);
+ },
+
+ setAnchorsRect() {
+ return Promise.all(
+ this.children.map(anchor => (
+ anchor.getRect('.van-index-anchor-wrapper').then(
+ (rect: WechatMiniprogram.BoundingClientRectCallbackResult) => {
+ Object.assign(anchor, {
+ height: rect.height,
+ top: rect.top + this.data.scrollTop
+ });
+ }
+ )
+ ))
+ );
+ },
+
+ setListRect() {
+ return this.getRect('.van-index-bar').then(
+ (rect: WechatMiniprogram.BoundingClientRectCallbackResult) => {
+ Object.assign(this, {
+ height: rect.height,
+ top: rect.top + this.data.scrollTop
+ });
+ }
+ );
+ },
+
+ setSiderbarRect() {
+ return this.getRect('.van-index-bar__sidebar').then(res => {
+ this.sidebar = {
+ height: res.height,
+ top: res.top
+ };
+ });
+ },
+
+ setDiffData({ target, data }) {
+ const diffData = {};
+
+ Object.keys(data).forEach(key => {
+ if (target.data[key] !== data[key]) {
+ diffData[key] = data[key];
+ }
+ });
+
+ if (Object.keys(diffData).length) {
+ target.setData(diffData);
+ }
+ },
+
+ getAnchorRect(anchor) {
+ return anchor.getRect('.van-index-anchor-wrapper').then(
+ (rect: WechatMiniprogram.BoundingClientRectCallbackResult) => (
+ {
+ height: rect.height,
+ top: rect.top
+ }
+ )
+ );
+ },
+
+ getActiveAnchorIndex() {
+ const { children } = this;
+ const {
+ sticky,
+ scrollTop,
+ stickyOffsetTop
+ } = this.data;
+
+ for (let i = this.children.length - 1; i >= 0; i--) {
+ const preAnchorHeight = i > 0 ? children[i - 1].height : 0;
+ const reachTop = sticky ? preAnchorHeight + stickyOffsetTop : 0;
+
+ if (reachTop + scrollTop >= children[i].top) {
+ return i;
+ }
+ }
+
+ return -1;
+ },
+
+ onScroll() {
+ const {
+ children = []
+ } = this;
+
+ if (!children.length) {
+ return;
+ }
+
+ const {
+ sticky,
+ stickyOffsetTop,
+ zIndex,
+ highlightColor,
+ scrollTop
+ } = this.data;
+
+ const active = this.getActiveAnchorIndex();
+
+ this.setDiffData({
+ target: this,
+ data: {
+ activeAnchorIndex: active
+ }
+ });
+
+ if (sticky) {
+ let isActiveAnchorSticky = false;
+
+ if (active !== -1) {
+ isActiveAnchorSticky = children[active].top <= stickyOffsetTop + scrollTop;
+ }
+
+ children.forEach((item, index) => {
+ if (index === active) {
+ let wrapperStyle = '';
+ let anchorStyle = `
+ color: ${highlightColor};
+ `;
+
+ if (isActiveAnchorSticky) {
+ wrapperStyle = `
+ height: ${children[index].height}px;
+ `;
+
+ anchorStyle = `
+ position: fixed;
+ top: ${stickyOffsetTop}px;
+ z-index: ${zIndex};
+ color: ${highlightColor};
+ `;
+ }
+
+ this.setDiffData({
+ target: item,
+ data: {
+ active: true,
+ anchorStyle,
+ wrapperStyle
+ }
+ });
+ } else if (index === active - 1) {
+ const currentAnchor = children[index];
+
+ const currentOffsetTop = currentAnchor.top;
+ const targetOffsetTop = index === children.length - 1
+ ? this.top
+ : children[index + 1].top;
+
+ const parentOffsetHeight = targetOffsetTop - currentOffsetTop;
+ const translateY = parentOffsetHeight - currentAnchor.height;
+
+ const anchorStyle = `
+ position: relative;
+ transform: translate3d(0, ${translateY}px, 0);
+ z-index: ${zIndex};
+ color: ${highlightColor};
+ `;
+
+ this.setDiffData({
+ target: item,
+ data: {
+ active: true,
+ anchorStyle
+ }
+ });
+ } else {
+ this.setDiffData({
+ target: item,
+ data: {
+ active: false,
+ anchorStyle: '',
+ wrapperStyle: '',
+ }
+ });
+ }
+ });
+ }
+ },
+
+ onClick(event) {
+ this.scrollToAnchor(event.target.dataset.index);
+ },
+
+ onTouchMove(event) {
+ const sidebarLength = this.children.length;
+ const touch = event.touches[0];
+ const itemHeight = this.sidebar.height / sidebarLength;
+ let index = Math.floor((touch.clientY - this.sidebar.top) / itemHeight);
+
+ if (index < 0) {
+ index = 0;
+ } else if (index > sidebarLength - 1) {
+ index = sidebarLength - 1;
+ }
+
+ this.scrollToAnchor(index);
+ },
+
+ onTouchStop() {
+ this.scrollToAnchorIndex = null;
+ },
+
+ scrollToAnchor(index) {
+ if (typeof index !== 'number' || this.scrollToAnchorIndex === index) {
+ return;
+ }
+
+ this.scrollToAnchorIndex = index;
+
+ const anchor = this.children.filter(item => item.data.index === this.data.indexList[index])[0];
+
+ this.$emit('select', anchor.data.index);
+
+ anchor && wx.pageScrollTo({
+ duration: 0,
+ scrollTop: anchor.top
+ });
+ }
+ }
+});
diff --git a/packages/index-bar/index.wxml b/packages/index-bar/index.wxml
new file mode 100644
index 00000000..19a59cf9
--- /dev/null
+++ b/packages/index-bar/index.wxml
@@ -0,0 +1,22 @@
+
+
+
+
+