IndexBar 索引栏
介绍
IndexBar 索引栏是一个用于列表索引分类显示和快速定位的导航组件。通过右侧的字母索引栏,用户可以快速跳转到对应的内容区域,常用于通讯录、城市列表、商品分类等需要按字母或分类快速检索的场景。组件支持吸顶效果、触摸滑动、自动高亮等功能,提供流畅的交互体验。
核心特性:
- 双组件配合 - IndexBar 容器组件配合 IndexAnchor 锚点组件使用,结构清晰
- 快速定位 - 点击或滑动右侧索引栏,内容区域快速滚动到对应位置
- 吸顶效果 - 支持锚点标题吸顶,滚动时当前分类标题固定在顶部
- 触摸交互 - 支持在索引栏上滑动手指连续切换索引,操作流畅自然
- 自动高亮 - 滚动内容时,右侧索引栏自动高亮当前所在位置的索引
- 位置计算 - 自动计算各个锚点的位置,实现精准的滚动定位
- 暗色主题 - 内置暗色模式支持,自动适配深色界面风格
参考: src/wd/components/wd-index-bar/wd-index-bar.vue:1-50
基本用法
基础使用
最简单的使用方式,IndexBar 包裹 IndexAnchor 锚点组件。
<template>
<view class="demo-index-bar">
<wd-index-bar>
<view v-for="letter in letters" :key="letter">
<wd-index-anchor :index="letter" />
<view v-for="item in getContactsByLetter(letter)" :key="item.id" class="contact-item">
<text class="contact-name">{{ item.name }}</text>
</view>
</view>
</wd-index-bar>
</view>
</template>
<script lang="ts" setup>
const letters = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']
// 模拟联系人数据
const contacts = [
{ id: 1, name: 'Allen', letter: 'A' },
{ id: 2, name: 'Amy', letter: 'A' },
{ id: 3, name: 'Bob', letter: 'B' },
{ id: 4, name: 'Bill', letter: 'B' },
{ id: 5, name: 'Chris', letter: 'C' },
{ id: 6, name: 'Cindy', letter: 'C' },
{ id: 7, name: 'David', letter: 'D' },
{ id: 8, name: 'Diana', letter: 'D' },
]
const getContactsByLetter = (letter: string) => {
return contacts.filter(c => c.letter === letter)
}
</script>
<style lang="scss" scoped>
.demo-index-bar {
height: 100vh;
}
.contact-item {
padding: 24rpx 32rpx;
border-bottom: 1rpx solid #e0e0e0;
.contact-name {
font-size: 28rpx;
color: #333;
}
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
使用说明:
wd-index-bar作为外层容器,控制滚动和索引wd-index-anchor标记每个分组的开始位置index属性设置锚点的索引值,显示在右侧索引栏- IndexBar 会自动收集所有 IndexAnchor 生成右侧索引栏
参考: src/wd/components/wd-index-bar/wd-index-bar.vue:2-34, src/wd/components/wd-index-anchor/wd-index-anchor.vue:6-14, 46
吸顶效果
启用吸顶效果后,滚动时当前分类标题会固定在顶部。
<template>
<view class="demo-index-bar">
<wd-index-bar :sticky="true">
<view v-for="letter in letters" :key="letter">
<wd-index-anchor :index="letter" />
<view v-for="item in getContactsByLetter(letter)" :key="item.id" class="contact-item">
<image class="contact-avatar" :src="item.avatar" mode="aspectFill" />
<view class="contact-info">
<text class="contact-name">{{ item.name }}</text>
<text class="contact-phone">{{ item.phone }}</text>
</view>
</view>
</view>
</wd-index-bar>
</view>
</template>
<script lang="ts" setup>
const letters = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J']
// 模拟联系人数据
const contacts = [
{ id: 1, name: 'Allen Wang', phone: '138****1234', avatar: '/static/avatar.png', letter: 'A' },
{ id: 2, name: 'Amy Chen', phone: '139****5678', avatar: '/static/avatar.png', letter: 'A' },
{ id: 3, name: 'Bob Li', phone: '136****9012', avatar: '/static/avatar.png', letter: 'B' },
{ id: 4, name: 'Bill Zhang', phone: '137****3456', avatar: '/static/avatar.png', letter: 'B' },
{ id: 5, name: 'Chris Liu', phone: '135****7890', avatar: '/static/avatar.png', letter: 'C' },
// ... 更多数据
]
const getContactsByLetter = (letter: string) => {
return contacts.filter(c => c.letter === letter)
}
</script>
<style lang="scss" scoped>
.demo-index-bar {
height: 100vh;
}
.contact-item {
display: flex;
align-items: center;
padding: 24rpx 32rpx;
border-bottom: 1rpx solid #e0e0e0;
.contact-avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
background: #f0f0f0;
}
.contact-info {
flex: 1;
margin-left: 24rpx;
display: flex;
flex-direction: column;
.contact-name {
font-size: 28rpx;
color: #333;
margin-bottom: 8rpx;
}
.contact-phone {
font-size: 24rpx;
color: #999;
}
}
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
使用说明:
sticky属性启用吸顶效果- 吸顶模式下,当前激活的锚点会固定在顶部
- 适用于内容较多、需要明确分类标识的场景
- 吸顶元素使用
position: sticky实现
参考: src/wd/components/wd-index-bar/wd-index-bar.vue:62, src/wd/components/wd-index-anchor/wd-index-anchor.vue:82-84, 150-156
自定义锚点内容
使用插槽自定义锚点的显示内容。
<template>
<view class="demo-index-bar">
<wd-index-bar :sticky="true">
<view v-for="group in cityGroups" :key="group.letter">
<wd-index-anchor :index="group.letter">
<view class="custom-anchor">
<wd-icon name="location" size="32" />
<text class="anchor-text">{{ group.name }}</text>
</view>
</wd-index-anchor>
<view v-for="city in group.cities" :key="city.id" class="city-item">
<text class="city-name">{{ city.name }}</text>
<text class="city-code">{{ city.code }}</text>
</view>
</view>
</wd-index-bar>
</view>
</template>
<script lang="ts" setup>
const cityGroups = [
{
letter: 'A',
name: 'A 字母城市',
cities: [
{ id: 1, name: '安庆', code: 'AQ' },
{ id: 2, name: '安阳', code: 'AY' },
],
},
{
letter: 'B',
name: 'B 字母城市',
cities: [
{ id: 3, name: '北京', code: 'BJ' },
{ id: 4, name: '保定', code: 'BD' },
],
},
{
letter: 'C',
name: 'C 字母城市',
cities: [
{ id: 5, name: '成都', code: 'CD' },
{ id: 6, name: '重庆', code: 'CQ' },
],
},
]
</script>
<style lang="scss" scoped>
.demo-index-bar {
height: 100vh;
}
.custom-anchor {
display: flex;
align-items: center;
gap: 12rpx;
padding: 20rpx 32rpx;
background: #f5f5f5;
.anchor-text {
font-size: 28rpx;
color: #333;
font-weight: bold;
}
}
.city-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx 32rpx;
border-bottom: 1rpx solid #e0e0e0;
.city-name {
font-size: 28rpx;
color: #333;
}
.city-code {
font-size: 24rpx;
color: #999;
}
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
使用说明:
- IndexAnchor 支持默认插槽自定义内容
- 不使用插槽时,默认显示
index属性的值 - 自定义内容可以包含图标、文字、样式等
- 适合需要丰富视觉效果的场景
参考: src/wd/components/wd-index-anchor/wd-index-anchor.vue:11-13
高级用法
通讯录列表
实现一个完整的通讯录功能,包括搜索和分组。
<template>
<view class="contact-list">
<!-- 搜索栏 -->
<view class="search-bar">
<wd-search v-model="searchQuery" placeholder="搜索联系人" @search="handleSearch" />
</view>
<!-- 索引栏 -->
<wd-index-bar :sticky="true">
<!-- 常用联系人 -->
<template v-if="!searchQuery && favoriteContacts.length > 0">
<wd-index-anchor index="★">
<view class="anchor-title">
<wd-icon name="star-fill" size="32" color="#ff9500" />
<text>常用联系人</text>
</view>
</wd-index-anchor>
<view
v-for="contact in favoriteContacts"
:key="contact.id"
class="contact-item"
@click="handleContactClick(contact)"
>
<image class="contact-avatar" :src="contact.avatar" mode="aspectFill" />
<view class="contact-info">
<text class="contact-name">{{ contact.name }}</text>
<text class="contact-company">{{ contact.company }}</text>
</view>
<wd-icon name="arrow-right" size="32" color="#999" />
</view>
</template>
<!-- 字母分组 -->
<view v-for="letter in displayLetters" :key="letter">
<wd-index-anchor :index="letter" />
<view
v-for="contact in getContactsByLetter(letter)"
:key="contact.id"
class="contact-item"
@click="handleContactClick(contact)"
>
<image class="contact-avatar" :src="contact.avatar" mode="aspectFill" />
<view class="contact-info">
<text class="contact-name">{{ contact.name }}</text>
<text class="contact-phone">{{ contact.phone }}</text>
</view>
<wd-icon name="arrow-right" size="32" color="#999" />
</view>
</view>
</wd-index-bar>
<!-- 空状态 -->
<view v-if="filteredContacts.length === 0" class="empty-state">
<wd-icon name="search" size="120" color="#ccc" />
<text class="empty-text">未找到联系人</text>
</view>
</view>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue'
interface Contact {
id: number
name: string
phone: string
avatar: string
company?: string
letter: string
isFavorite?: boolean
}
const searchQuery = ref('')
// 模拟联系人数据
const allContacts: Contact[] = [
{ id: 1, name: 'Allen Wang', phone: '138****1234', avatar: '/static/avatar.png', company: '科技公司', letter: 'A', isFavorite: true },
{ id: 2, name: 'Amy Chen', phone: '139****5678', avatar: '/static/avatar.png', letter: 'A' },
{ id: 3, name: 'Bob Li', phone: '136****9012', avatar: '/static/avatar.png', letter: 'B', isFavorite: true },
// ... 更多数据
]
// 常用联系人
const favoriteContacts = computed(() => {
return allContacts.filter(c => c.isFavorite)
})
// 过滤后的联系人
const filteredContacts = computed(() => {
if (!searchQuery.value) {
return allContacts
}
return allContacts.filter(c =>
c.name.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
c.phone.includes(searchQuery.value)
)
})
// 显示的字母索引
const displayLetters = computed(() => {
const letters = new Set(filteredContacts.value.map(c => c.letter))
return Array.from(letters).sort()
})
const getContactsByLetter = (letter: string) => {
return filteredContacts.value.filter(c => c.letter === letter)
}
const handleSearch = (value: string) => {
console.log('搜索:', value)
}
const handleContactClick = (contact: Contact) => {
uni.showToast({
title: `点击了 ${contact.name}`,
icon: 'none',
})
}
</script>
<style lang="scss" scoped>
.contact-list {
height: 100vh;
display: flex;
flex-direction: column;
}
.search-bar {
padding: 16rpx;
background: #fff;
border-bottom: 1rpx solid #e0e0e0;
}
.anchor-title {
display: flex;
align-items: center;
gap: 12rpx;
padding: 20rpx 32rpx;
background: #fff8e6;
font-size: 28rpx;
color: #333;
font-weight: bold;
}
.contact-item {
display: flex;
align-items: center;
padding: 24rpx 32rpx;
background: #fff;
border-bottom: 1rpx solid #e0e0e0;
&:active {
background: #f5f5f5;
}
.contact-avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
background: #f0f0f0;
}
.contact-info {
flex: 1;
margin-left: 24rpx;
display: flex;
flex-direction: column;
.contact-name {
font-size: 28rpx;
color: #333;
margin-bottom: 8rpx;
}
.contact-phone,
.contact-company {
font-size: 24rpx;
color: #999;
}
}
}
.empty-state {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 128rpx 32rpx;
.empty-text {
margin-top: 32rpx;
font-size: 28rpx;
color: #999;
}
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
使用说明:
- 结合搜索功能实现联系人筛选
- 常用联系人使用特殊索引标识(★)
- 点击联系人可以跳转到详情页
- 空状态提示用户未找到内容
城市选择器
实现一个城市选择功能,包括热门城市和字母索引。
<template>
<view class="city-selector">
<view class="header">
<text class="title">选择城市</text>
<wd-icon name="close" size="40" @click="handleClose" />
</view>
<wd-index-bar :sticky="true">
<!-- 当前定位 -->
<wd-index-anchor index="#">
<view class="section-title">当前定位</view>
</wd-index-anchor>
<view class="current-city" @click="handleCityClick(currentCity)">
<wd-icon name="location" size="32" color="#4d80f0" />
<text class="city-name">{{ currentCity.name }}</text>
</view>
<!-- 热门城市 -->
<wd-index-anchor index="🔥">
<view class="section-title">热门城市</view>
</wd-index-anchor>
<view class="hot-cities">
<view
v-for="city in hotCities"
:key="city.id"
class="hot-city-item"
@click="handleCityClick(city)"
>
{{ city.name }}
</view>
</view>
<!-- 字母分组 -->
<view v-for="letter in letters" :key="letter">
<wd-index-anchor :index="letter" />
<view
v-for="city in getCitiesByLetter(letter)"
:key="city.id"
class="city-item"
@click="handleCityClick(city)"
>
<text class="city-name">{{ city.name }}</text>
</view>
</view>
</wd-index-bar>
</view>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
interface City {
id: number
name: string
code: string
letter: string
}
const currentCity = ref<City>({
id: 0,
name: '北京',
code: 'BJ',
letter: 'B',
})
const hotCities: City[] = [
{ id: 1, name: '北京', code: 'BJ', letter: 'B' },
{ id: 2, name: '上海', code: 'SH', letter: 'S' },
{ id: 3, name: '广州', code: 'GZ', letter: 'G' },
{ id: 4, name: '深圳', code: 'SZ', letter: 'S' },
{ id: 5, name: '杭州', code: 'HZ', letter: 'H' },
{ id: 6, name: '成都', code: 'CD', letter: 'C' },
]
const cities: City[] = [
{ id: 7, name: '安庆', code: 'AQ', letter: 'A' },
{ id: 8, name: '安阳', code: 'AY', letter: 'A' },
{ id: 9, name: '保定', code: 'BD', letter: 'B' },
// ... 更多城市数据
]
const letters = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']
const getCitiesByLetter = (letter: string) => {
return cities.filter(c => c.letter === letter)
}
const handleCityClick = (city: City) => {
uni.showToast({
title: `已选择 ${city.name}`,
icon: 'success',
})
// 这里可以通过事件或路由传递选中的城市
setTimeout(() => {
handleClose()
}, 1000)
}
const handleClose = () => {
uni.navigateBack()
}
</script>
<style lang="scss" scoped>
.city-selector {
height: 100vh;
display: flex;
flex-direction: column;
background: #fff;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx 32rpx;
border-bottom: 1rpx solid #e0e0e0;
.title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
}
.section-title {
padding: 20rpx 32rpx;
background: #f5f5f5;
font-size: 28rpx;
color: #666;
font-weight: bold;
}
.current-city {
display: flex;
align-items: center;
gap: 12rpx;
padding: 24rpx 32rpx;
border-bottom: 1rpx solid #e0e0e0;
.city-name {
font-size: 28rpx;
color: #333;
}
}
.hot-cities {
display: flex;
flex-wrap: wrap;
padding: 24rpx 24rpx 12rpx;
gap: 16rpx;
border-bottom: 1rpx solid #e0e0e0;
.hot-city-item {
flex: 0 0 calc(33.333% - 12rpx);
padding: 20rpx 0;
text-align: center;
background: #f5f5f5;
border-radius: 8rpx;
font-size: 28rpx;
color: #333;
&:active {
background: #e0e0e0;
}
}
}
.city-item {
padding: 24rpx 32rpx;
border-bottom: 1rpx solid #e0e0e0;
&:active {
background: #f5f5f5;
}
.city-name {
font-size: 28rpx;
color: #333;
}
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
使用说明:
- 使用特殊字符作为索引(# 表示定位,🔥 表示热门)
- 热门城市使用宫格布局,方便快速选择
- 当前定位和热门城市不参与字母索引
- 选择城市后自动返回上一页
商品分类
实现商品分类列表,支持多级分类和图片展示。
<template>
<view class="product-category">
<wd-index-bar :sticky="true">
<view v-for="category in categories" :key="category.letter">
<wd-index-anchor :index="category.letter" />
<view
v-for="item in category.items"
:key="item.id"
class="category-item"
@click="handleCategoryClick(item)"
>
<image class="category-icon" :src="item.icon" mode="aspectFill" />
<view class="category-info">
<text class="category-name">{{ item.name }}</text>
<text class="category-count">{{ item.count }} 件商品</text>
</view>
<wd-icon name="arrow-right" size="32" color="#999" />
</view>
</view>
</wd-index-bar>
</view>
</template>
<script lang="ts" setup>
interface CategoryItem {
id: number
name: string
icon: string
count: number
}
interface Category {
letter: string
items: CategoryItem[]
}
const categories: Category[] = [
{
letter: 'A',
items: [
{ id: 1, name: 'Apple 苹果', icon: '/static/category/apple.png', count: 120 },
],
},
{
letter: 'B',
items: [
{ id: 2, name: 'Book 图书', icon: '/static/category/book.png', count: 800 },
{ id: 3, name: 'Beauty 美妆', icon: '/static/category/beauty.png', count: 350 },
],
},
{
letter: 'C',
items: [
{ id: 4, name: 'Computer 电脑', icon: '/static/category/computer.png', count: 200 },
{ id: 5, name: 'Clothing 服装', icon: '/static/category/clothing.png', count: 1500 },
],
},
]
const handleCategoryClick = (item: CategoryItem) => {
uni.navigateTo({
url: `/pages/category/list?id=${item.id}&name=${item.name}`,
})
}
</script>
<style lang="scss" scoped>
.product-category {
height: 100vh;
}
.category-item {
display: flex;
align-items: center;
padding: 24rpx 32rpx;
background: #fff;
border-bottom: 1rpx solid #e0e0e0;
&:active {
background: #f5f5f5;
}
.category-icon {
width: 80rpx;
height: 80rpx;
border-radius: 8rpx;
background: #f0f0f0;
}
.category-info {
flex: 1;
margin-left: 24rpx;
display: flex;
flex-direction: column;
.category-name {
font-size: 28rpx;
color: #333;
margin-bottom: 8rpx;
}
.category-count {
font-size: 24rpx;
color: #999;
}
}
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
使用说明:
- 商品分类按首字母分组
- 每个分类显示图标、名称和商品数量
- 点击分类跳转到对应的商品列表页
API
IndexBar Props
| 参数 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| sticky | 索引锚点是否吸顶 | boolean | false |
参考: src/wd/components/wd-index-bar/wd-index-bar.vue:60-63, 66-68
IndexAnchor Props
| 参数 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| customStyle | 自定义根节点样式 | string | '' |
| customClass | 自定义根节点样式类 | string | '' |
| index | 索引值,显示在右侧索引栏 | number | string | - |
参考: src/wd/components/wd-index-anchor/wd-index-anchor.vue:40-47, 64-68
IndexBar Slots
| 插槽名 | 说明 | 参数 |
|---|---|---|
| default | 默认插槽,放置 IndexAnchor 和内容 | - |
参考: src/wd/components/wd-index-bar/wd-index-bar.vue:13
IndexAnchor Slots
| 插槽名 | 说明 | 参数 |
|---|---|---|
| default | 自定义锚点内容,不使用时显示 index 值 | - |
参考: src/wd/components/wd-index-anchor/wd-index-anchor.vue:11-13
IndexAnchor 实例方法
| 方法名 | 说明 | 参数 | 返回值 |
|---|---|---|---|
| getInfo | 获取锚点位置信息 | () => void | - |
参考: src/wd/components/wd-index-anchor/wd-index-anchor.vue:52-61, 90-96, 106-111
类型定义
/**
* 锚点索引类型
*/
export type AnchorIndex = number | string
/**
* 索引栏组件属性接口
*/
export interface WdIndexBarProps {
/** 索引是否吸顶 */
sticky?: boolean
}
/**
* 索引锚点组件属性接口
*/
export interface WdIndexAnchorProps {
/** 自定义根节点样式 */
customStyle?: string
/** 自定义根节点样式类 */
customClass?: string
/** 索引值 */
index: AnchorIndex
}
/**
* 索引锚点组件暴露方法接口
*/
export interface WdIndexAnchorExpose {
/** 锚点距离顶部的位置 */
top: Ref<number>
/** 获取锚点位置信息的方法 */
getInfo: () => void
/** 锚点唯一ID */
indexAnchorId: Ref<string>
/** 是否处于吸顶状态(计算属性) */
isSticky: ComputedRef<boolean>
}
/** 索引锚点组件实例类型 */
export type WdIndexAnchorInstance = ComponentPublicInstance<WdIndexAnchorProps, WdIndexAnchorExpose>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
参考: src/wd/components/wd-index-bar/wd-index-bar.vue:55-63, src/wd/components/wd-index-anchor/wd-index-anchor.vue:22, 40-61, 114
主题定制
CSS 变量
IndexBar 组件提供了以下 CSS 变量用于主题定制:
// 索引栏容器
// (容器样式主要通过 position 和尺寸控制,无特定 CSS 变量)
// 右侧索引项
// 索引项样式(文字大小、颜色、内边距等)
// 索引锚点
$-color-gray-2: #f5f7fa; // 锚点背景色
$-color-title: #1d1d1f; // 锚点文字颜色
// 锚点字体大小: 28rpx
// 锚点内边距: 20rpx
// 右侧索引
// 索引字体大小: 24rpx
$-color-theme: #4d80f0; // 激活索引颜色
// 暗色主题
.wot-theme-dark {
$-color-white: #ffffff; // 索引文字颜色(暗色)
$-color-gray-8: #48484a; // 锚点背景色(暗色)
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
参考: src/wd/components/wd-index-bar/wd-index-bar.vue:233-276, src/wd/components/wd-index-anchor/wd-index-anchor.vue:117-157
自定义样式
基础样式定制:
<template>
<view class="custom-index-bar">
<wd-index-bar :sticky="true">
<view v-for="letter in letters" :key="letter">
<wd-index-anchor :index="letter" custom-class="custom-anchor" />
<view v-for="i in 3" :key="i" class="list-item">
项目 {{ letter }}{{ i }}
</view>
</view>
</wd-index-bar>
</view>
</template>
<script lang="ts" setup>
const letters = ['A', 'B', 'C', 'D', 'E']
</script>
<style lang="scss" scoped>
.custom-index-bar {
height: 100vh;
// 自定义锚点样式
:deep(.custom-anchor) {
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
color: #fff;
padding: 24rpx 32rpx;
font-size: 32rpx;
font-weight: bold;
}
// 自定义右侧索引栏
:deep(.wd-index-bar__sidebar) {
right: 16rpx;
}
:deep(.wd-index-bar__index) {
font-size: 28rpx;
padding: 12rpx 16rpx;
color: #4d80f0;
font-weight: bold;
&.is-active {
background: #4d80f0;
color: #fff;
border-radius: 50%;
}
}
}
.list-item {
padding: 24rpx 32rpx;
border-bottom: 1rpx solid #e0e0e0;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
深色主题:
<template>
<view class="dark-index-bar wot-theme-dark">
<wd-index-bar :sticky="true">
<view v-for="letter in letters" :key="letter">
<wd-index-anchor :index="letter" />
<view v-for="i in 3" :key="i" class="list-item">
项目 {{ letter }}{{ i }}
</view>
</view>
</wd-index-bar>
</view>
</template>
<script lang="ts" setup>
const letters = ['A', 'B', 'C', 'D', 'E']
</script>
<style lang="scss" scoped>
.dark-index-bar {
height: 100vh;
background: #1a1a1a;
}
.list-item {
padding: 24rpx 32rpx;
border-bottom: 1rpx solid #2c2c2e;
color: #e5e5e7;
background: #1a1a1a;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
参考: src/wd/components/wd-index-bar/wd-index-bar.vue:238-245, src/wd/components/wd-index-anchor/wd-index-anchor.vue:122-128
最佳实践
1. 合理设置索引值
推荐做法:
<script lang="ts" setup>
// ✅ 使用简洁明了的索引值
const letters = ['A', 'B', 'C', 'D', 'E', 'F', 'G']
// ✅ 特殊分组使用有意义的符号
const specialIndexes = {
location: '#', // 当前定位
hot: '🔥', // 热门
favorite: '★', // 收藏
recent: '⏱', // 最近
}
</script>
<template>
<wd-index-bar>
<!-- 特殊分组 -->
<wd-index-anchor :index="specialIndexes.hot" />
<!-- 字母分组 -->
<view v-for="letter in letters" :key="letter">
<wd-index-anchor :index="letter" />
</view>
</wd-index-bar>
</template>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
不推荐做法:
<script lang="ts" setup>
// ❌ 索引值过长
const indexes = ['Section A', 'Section B', 'Section C']
// ❌ 索引值无意义
const indexes2 = ['1', '2', '3', '4', '5']
</script>2
3
4
5
6
7
说明:
- 索引值应该简短,通常为 1-2 个字符
- 字母索引使用大写字母
- 特殊分组可以使用符号或 Emoji
- 避免使用过长或无意义的索引值
2. 优化大数据列表性能
推荐做法:
<template>
<wd-index-bar :sticky="true">
<view v-for="letter in letters" :key="letter">
<wd-index-anchor :index="letter" />
<!-- ✅ 使用虚拟列表渲染大量数据 -->
<view
v-for="item in getItemsByLetter(letter)"
:key="item.id"
class="list-item"
>
{{ item.name }}
</view>
</view>
</wd-index-bar>
</template>
<script lang="ts" setup>
import { computed } from 'vue'
// ✅ 使用 computed 缓存分组结果
const getItemsByLetter = (letter: string) => {
return computed(() => {
return allItems.filter(item => item.letter === letter)
}).value
}
</script>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
不推荐做法:
<template>
<wd-index-bar :sticky="true">
<!-- ❌ 直接渲染所有数据,不分组 -->
<view v-for="item in allItems" :key="item.id">
<wd-index-anchor :index="item.letter" />
<view class="list-item">{{ item.name }}</view>
</view>
</wd-index-bar>
</template>2
3
4
5
6
7
8
9
说明:
- 大数据列表使用 computed 缓存分组结果
- 按字母分组,避免重复渲染锚点
- 考虑使用虚拟列表优化性能
- 避免在列表项中执行复杂计算
3. 正确处理吸顶效果
推荐做法:
<template>
<view class="page">
<!-- ✅ 为 IndexBar 设置明确的高度 -->
<wd-index-bar :sticky="true" class="index-bar-container">
<view v-for="letter in letters" :key="letter">
<wd-index-anchor :index="letter" />
<view v-for="i in 5" :key="i" class="list-item">
项目 {{ letter }}{{ i }}
</view>
</view>
</wd-index-bar>
</view>
</template>
<style lang="scss" scoped>
.page {
height: 100vh;
overflow: hidden;
}
// ✅ 设置固定高度
.index-bar-container {
height: 100%;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
不推荐做法:
<style lang="scss" scoped>
// ❌ 没有设置高度,吸顶效果可能异常
.index-bar-container {
// 没有 height
}
</style>2
3
4
5
6
说明:
- IndexBar 必须设置明确的高度
- 吸顶效果依赖固定的容器高度
- 推荐使用
100vh或固定像素值 - 避免使用
auto或不设置高度
4. 结合搜索功能
推荐做法:
<template>
<view class="page">
<view class="search-bar">
<wd-search v-model="searchQuery" />
</view>
<!-- ✅ 搜索时隐藏索引栏 -->
<wd-index-bar v-if="!searchQuery" :sticky="true">
<view v-for="letter in letters" :key="letter">
<wd-index-anchor :index="letter" />
<view v-for="item in getItemsByLetter(letter)" :key="item.id">
{{ item.name }}
</view>
</view>
</wd-index-bar>
<!-- 搜索结果 -->
<view v-else class="search-results">
<view v-for="item in searchResults" :key="item.id" class="result-item">
{{ item.name }}
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue'
const searchQuery = ref('')
const searchResults = computed(() => {
if (!searchQuery.value) return []
return allItems.filter(item =>
item.name.toLowerCase().includes(searchQuery.value.toLowerCase())
)
})
</script>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
说明:
- 搜索时隐藏索引栏,避免布局冲突
- 搜索结果单独渲染,不使用索引分组
- 提供清晰的视觉切换反馈
5. 移动端触摸优化
推荐做法:
<template>
<wd-index-bar :sticky="true">
<view v-for="letter in letters" :key="letter">
<wd-index-anchor :index="letter" />
<!-- ✅ 列表项添加点击态 -->
<view
v-for="item in getItemsByLetter(letter)"
:key="item.id"
class="list-item"
@click="handleItemClick(item)"
>
{{ item.name }}
</view>
</view>
</wd-index-bar>
</template>
<style lang="scss" scoped>
.list-item {
padding: 24rpx 32rpx;
transition: background-color 0.2s;
// ✅ 添加点击态反馈
&:active {
background-color: #f5f5f5;
}
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
说明:
- 为列表项添加点击态,提升触摸反馈
- 索引栏支持滑动手势,无需额外处理
- 避免在触摸事件中阻止默认行为
常见问题
1. 为什么索引栏没有显示?
问题原因:
- IndexAnchor 组件未正确渲染
- index 属性未设置或重复
- IndexBar 容器高度为 0
解决方案:
<template>
<!-- ✅ 确保 IndexBar 有明确的高度 -->
<view class="page">
<wd-index-bar :sticky="true" class="index-bar-wrapper">
<view v-for="letter in letters" :key="letter">
<!-- ✅ 确保每个 IndexAnchor 都有唯一的 index -->
<wd-index-anchor :index="letter" />
<view class="content">内容</view>
</view>
</wd-index-bar>
</view>
</template>
<script lang="ts" setup>
// ✅ 确保 letters 数组不为空
const letters = ['A', 'B', 'C', 'D', 'E']
</script>
<style lang="scss" scoped>
.page {
height: 100vh;
}
.index-bar-wrapper {
height: 100%;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
说明:
- 检查 IndexBar 容器是否有高度
- 检查 IndexAnchor 的 index 是否唯一
- 确保 letters 数组有数据
参考: src/wd/components/wd-index-bar/wd-index-bar.vue:22-29, src/wd/components/wd-index-anchor/wd-index-anchor.vue:46
2. 吸顶效果不生效怎么办?
问题原因:
- sticky 属性未设置为 true
- 父容器没有滚动
- 浏览器不支持 position: sticky
解决方案:
<template>
<wd-index-bar :sticky="true">
<view v-for="letter in letters" :key="letter">
<wd-index-anchor :index="letter" />
<!-- 确保每个分组有足够的内容,能够滚动 -->
<view v-for="i in 10" :key="i" class="list-item">
项目 {{ letter }}{{ i }}
</view>
</view>
</wd-index-bar>
</template>
<style lang="scss" scoped>
// 确保列表有足够高度可以滚动
.list-item {
padding: 24rpx 32rpx;
min-height: 100rpx;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
说明:
- 确保
sticky属性设置为true - 确保内容足够多,可以滚动
- 吸顶依赖
position: sticky,部分旧浏览器不支持
参考: src/wd/components/wd-index-bar/wd-index-bar.vue:62, src/wd/components/wd-index-anchor/wd-index-anchor.vue:82-84, 150-156
3. 点击索引栏没有跳转?
问题原因:
- 锚点位置未正确计算
- 内容未正确渲染
- 索引值与锚点 index 不匹配
解决方案:
<template>
<wd-index-bar>
<view v-for="letter in letters" :key="letter">
<!-- ✅ 确保 index 值与右侧索引栏一致 -->
<wd-index-anchor :index="letter" />
<!-- ✅ 确保每个分组都有内容 -->
<view v-if="getItemsByLetter(letter).length > 0">
<view
v-for="item in getItemsByLetter(letter)"
:key="item.id"
class="list-item"
>
{{ item.name }}
</view>
</view>
</view>
</wd-index-bar>
</template>
<script lang="ts" setup>
import { onMounted } from 'vue'
// ✅ 页面加载后等待DOM渲染完成
onMounted(() => {
setTimeout(() => {
// 确保锚点位置计算完成
}, 300)
})
</script>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
说明:
- 确保每个索引对应的分组都有内容
- 确保 DOM 渲染完成后再计算位置
- 检查 index 值是否正确
参考: src/wd/components/wd-index-bar/wd-index-bar.vue:126-138, 211-218
4. 如何实现索引栏的自定义样式?
问题原因:
- 不清楚如何覆盖默认样式
- 深度选择器使用不正确
解决方案:
<template>
<view class="custom-wrapper">
<wd-index-bar :sticky="true">
<view v-for="letter in letters" :key="letter">
<wd-index-anchor :index="letter" custom-class="my-anchor" />
<view class="content">内容</view>
</view>
</wd-index-bar>
</view>
</template>
<style lang="scss" scoped>
.custom-wrapper {
height: 100vh;
// ✅ 自定义右侧索引栏样式
:deep(.wd-index-bar__sidebar) {
right: 24rpx; // 调整位置
}
:deep(.wd-index-bar__index) {
font-size: 28rpx; // 调整字体大小
color: #4d80f0; // 调整颜色
padding: 12rpx 16rpx; // 调整内边距
&.is-active {
background: #4d80f0;
color: #fff;
border-radius: 50%;
}
}
// ✅ 自定义锚点样式
:deep(.my-anchor) {
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
color: #fff;
padding: 28rpx 32rpx;
font-weight: bold;
}
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
说明:
- 使用
:deep()深度选择器修改组件内部样式 - 通过
custom-class为锚点添加自定义类名 - 注意样式优先级,必要时使用
!important
参考: src/wd/components/wd-index-anchor/wd-index-anchor.vue:42-44
5. 如何监听当前激活的索引?
问题原因:
- IndexBar 组件未提供 change 事件
- 需要通过其他方式获取当前索引
解决方案:
<template>
<wd-index-bar :sticky="true">
<view v-for="letter in letters" :key="letter">
<wd-index-anchor :index="letter" />
<!-- 使用 Intersection Observer 或滚动事件监听 -->
<view
v-for="item in getItemsByLetter(letter)"
:key="item.id"
class="list-item"
>
{{ item.name }}
</view>
</view>
</wd-index-bar>
<!-- 显示当前索引 -->
<view class="current-index">
当前: {{ currentIndex }}
</view>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue'
const currentIndex = ref('A')
// 方案1: 通过自定义逻辑跟踪当前索引
const handleScroll = (event: any) => {
// 根据滚动位置计算当前索引
// 这需要自己实现逻辑
}
// 方案2: 使用 IndexBar 内部状态(不推荐,组件未暴露)
// IndexBar 内部通过 state.activeIndex 管理当前索引
</script>
<style lang="scss" scoped>
.current-index {
position: fixed;
top: 100rpx;
left: 50%;
transform: translateX(-50%);
padding: 16rpx 32rpx;
background: rgba(0, 0, 0, 0.7);
color: #fff;
border-radius: 8rpx;
font-size: 24rpx;
z-index: 1000;
}
</style>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
说明:
- IndexBar 组件内部管理激活索引,未暴露事件
- 可以通过滚动监听自行实现索引跟踪
- 或在列表项中使用 Intersection Observer API
参考: src/wd/components/wd-index-bar/wd-index-bar.vue:77-79, 151-172
注意事项
- 容器高度必须设置:IndexBar 组件依赖固定高度的容器才能正常工作,必须为其设置
height: 100vh或固定的像素值,否则索引栏可能无法显示或功能异常。
参考: src/wd/components/wd-index-bar/wd-index-bar.vue:250-252
- index 值必须唯一:每个 IndexAnchor 的
index属性值必须唯一,重复的 index 会导致索引栏显示异常和跳转错误。
参考: src/wd/components/wd-index-anchor/wd-index-anchor.vue:46
- 吸顶依赖 sticky 定位:吸顶效果使用 CSS
position: sticky实现,部分旧版本浏览器或小程序可能不支持,使用前请确认兼容性。
参考: src/wd/components/wd-index-anchor/wd-index-anchor.vue:150-156
- 初始化时机:组件在
onMounted后会计算各个锚点的位置,如果内容是异步加载的,需要在数据加载完成后手动调用getInfo方法更新位置。
参考: src/wd/components/wd-index-anchor/wd-index-anchor.vue:90-103
- 触摸事件的阻止:索引栏的触摸事件使用了
.stop.prevent修饰符,阻止了事件冒泡和默认行为,避免在自定义逻辑中再次阻止。
参考: src/wd/components/wd-index-bar/wd-index-bar.vue:17-20
滚动性能优化:大数据列表建议按字母分组渲染,避免一次性渲染所有数据导致性能问题。可以考虑使用虚拟列表优化。
index 类型:index 支持
number和string类型,可以使用字母、数字、符号或 Emoji 作为索引值,但建议保持简洁(1-2个字符)。
参考: src/wd/components/wd-index-bar/wd-index-bar.vue:55, src/wd/components/wd-index-anchor/wd-index-anchor.vue:22, 46
- 钉钉小程序特殊处理:组件对钉钉小程序做了特殊兼容处理,使用了额外的包裹层,其他平台无需关注。
参考: src/wd/components/wd-index-bar/wd-index-bar.vue:4-6, 31-33, src/wd/components/wd-index-anchor/wd-index-anchor.vue:3-5, 15-17, 131-141
- 自动高亮:滚动内容时,右侧索引栏会自动高亮当前所在位置的索引,这是通过计算滚动位置和锚点位置实现的。
参考: src/wd/components/wd-index-bar/wd-index-bar.vue:151-172, 265-274
- 暗色主题:组件内置暗色主题支持,在根元素添加
wot-theme-dark类名即可启用,索引和锚点颜色会自动适配。
参考: src/wd/components/wd-index-bar/wd-index-bar.vue:238-245, src/wd/components/wd-index-anchor/wd-index-anchor.vue:122-128
- 位置计算延迟:组件初始化时会延迟 100ms 计算位置信息,如果页面渲染较慢,可能需要手动调整延迟时间。
参考: src/wd/components/wd-index-bar/wd-index-bar.vue:126-138
- 滚动容器:IndexBar 内部使用
scroll-view组件实现滚动,滚动容器的高度自动继承 IndexBar 的高度。
参考: src/wd/components/wd-index-bar/wd-index-bar.vue:7-14, 253-256
总结
IndexBar 索引栏组件是一个功能完善的列表索引导航组件。通过右侧索引栏和锚点标记,可以实现快速定位和分类展示,提供流畅的触摸交互体验。
使用建议:
- 容器必须设置明确的高度
- index 值保持简洁且唯一
- 大数据列表按字母分组渲染
- 合理使用吸顶效果
- 结合搜索功能提升体验
适用场景:
- 通讯录列表
- 城市选择器
- 商品分类列表
- 地区选择
- 任何需要字母索引的列表
性能优化:
- 使用 computed 缓存分组数据
- 避免一次性渲染大量数据
- 合理使用虚拟列表
- 优化列表项的渲染逻辑
最佳体验:
- 提供清晰的视觉层次
- 添加点击态反馈
- 索引栏支持滑动连续切换
- 配合搜索功能使用
- 空状态友好提示
