first commit

This commit is contained in:
lingxiao865
2026-02-10 08:05:03 +08:00
commit c5af079d8c
1094 changed files with 97530 additions and 0 deletions

View File

@@ -0,0 +1,76 @@
:: BASE_DOC ::
## API
### Indexes Props
name | type | default | description | required
-- | -- | -- | -- | --
custom-style | Object | - | CSS(Cascading Style Sheets) | N
current | String / Number | - | `v-model:current` is supported | N
default-current | String / Number | - | uncontrolled property | N
index-list | Array | - | Typescript`Array<string \| number>` | N
list | Array | [] | `deprecated`。Typescript`ListItem[] ` `interface ListItem { title: string; index: string; children: { title: string; [key: string]: any} [] }`。[see more ts definition](https://github.com/Tencent/tdesign-miniprogram/tree/develop/packages/uniapp-components/indexes/type.ts) | N
sticky | Boolean | true | Typescript`Boolean` | N
sticky-offset | Number | 0 | \- | N
### Indexes Events
name | params | description
-- | -- | --
change | `(context: { index: string \| number })` | \-
select | `(context: { index: string \| number })` | \-
### Indexes Slots
name | Description
-- | --
\- | \-
### IndexesAnchor Props
name | type | default | description | required
-- | -- | -- | -- | --
custom-style | Object | - | CSS(Cascading Style Sheets) | N
index | String / Number | - | \- | N
### IndexesAnchor Slots
name | Description
-- | --
\- | \-
### IndexesAnchor External Classes
className | Description
-- | --
t-class | class name of root node
t-class-sidebar | \-
t-class-sidebar-item | \-
### CSS Variables
The component provides the following CSS variables, which can be used to customize styles.
Name | Default Value | Description
-- | -- | --
--td-indexes-sidebar-active-bg-color | @brand-color | -
--td-indexes-sidebar-active-color | @text-color-anti | -
--td-indexes-sidebar-color | @text-color-primary | -
--td-indexes-sidebar-font | @font-body-small | -
--td-indexes-sidebar-item-size | 40rpx | -
--td-indexes-sidebar-right | 16rpx | -
--td-indexes-sidebar-tips-bg-color | @brand-color-light | -
--td-indexes-sidebar-tips-color | @brand-color | -
--td-indexes-sidebar-tips-font | @font-title-extraLarge | -
--td-indexes-sidebar-tips-right | calc(100% + 32rpx) | -
--td-indexes-sidebar-tips-size | 96rpx | -
--td-indexes-anchor-active-bg-color | @bg-color-container | -
--td-indexes-anchor-active-color | @brand-color | -
--td-indexes-anchor-active-font-weight | 600 | -
--td-indexes-anchor-bg-color | @bg-color-secondarycontainer | -
--td-indexes-anchor-border-color | @component-border | -
--td-indexes-anchor-color | @text-color-primary | -
--td-indexes-anchor-font | @font-body-medium | -
--td-indexes-anchor-padding | 8rpx 32rpx | -
--td-indexes-anchor-top | 0 | -

View File

@@ -0,0 +1,105 @@
---
title: Indexes 索引
description: 用于页面中信息快速检索,可以根据目录中的页码快速找到所需的内容。
spline: navigation
isComponent: true
---
## 引入
可在 `main.ts` 或在需要使用的页面或组件中引入。
```js
import TIndexes from '@tdesign/uniapp/indexes/indexes.vue';
import TIndexesAnchor from '@tdesign/uniapp/indexes-anchor/indexes-anchor.vue';
```
### 基础索引
{{ base }}
### 自定义索引
{{ custom }}
## FAQ
### 在滚动元素中, Indexes 索引组件失效([#3746](https://github.com/Tencent/tdesign-miniprogram/issues/3746)
`Indexes` 组件自 `0.32.0` 版本开始移除了对 `scroll-view` 的依赖,组件内部使用 [wx.pageScrollTo](https://developers.weixin.qq.com/miniprogram/dev/api/ui/scroll/wx.pageScrollTo.html) 滚动到指定位置,因此只支持页面级滚动,不支持在滚动元素中嵌套使用,包括 overflow: scroll、 scroll-view 等。
### API
### Indexes Props
名称 | 类型 | 默认值 | 描述 | 必传
-- | -- | -- | -- | --
custom-style | Object | - | 自定义样式 | N
current | String / Number | - | 索引列表的激活项,默认首项。支持语法糖 `v-model:current` | N
default-current | String / Number | - | 索引列表的激活项,默认首项。非受控属性 | N
index-list | Array | - | 索引字符列表。不传默认 `A-Z`。TS 类型:`Array<string \| number>` | N
list | Array | [] | 已废弃。索引列表的列表数据。每个元素包含三个子元素index(string)索引值例如123...或ABC等title(string): 索引标题可不填将默认设为索引值children(Array<{title: string}>): 子元素列表title为子元素的展示文案。TS 类型:`ListItem[] ` `interface ListItem { title: string; index: string; children: { title: string; [key: string]: any} [] }`。[详细类型定义](https://github.com/Tencent/tdesign-miniprogram/tree/develop/packages/uniapp-components/indexes/type.ts) | N
sticky | Boolean | true | 索引是否吸顶默认为true。TS 类型:`Boolean` | N
sticky-offset | Number | 0 | 锚点吸顶时与顶部的距离 | N
### Indexes Events
名称 | 参数 | 描述
-- | -- | --
change | `(context: { index: string \| number })` | 索引发生变更时触发事件
select | `(context: { index: string \| number })` | 点击侧边栏时触发事件
### Indexes Slots
名称 | 描述
-- | --
\- | 默认插槽,自定义内容区域内容
### IndexesAnchor Props
名称 | 类型 | 默认值 | 描述 | 必传
-- | -- | -- | -- | --
custom-style | Object | - | 自定义样式 | N
index | String / Number | - | 索引字符 | N
### IndexesAnchor Slots
名称 | 描述
-- | --
\- | 默认插槽,自定义内容区域内容
### IndexesAnchor External Classes
类名 | 描述
-- | --
t-class | 根节点样式类
t-class-sidebar | 侧边栏样式类
t-class-sidebar-item | 侧边栏选项样式类
### CSS Variables
组件提供了下列 CSS 变量,可用于自定义样式。
名称 | 默认值 | 描述
-- | -- | --
--td-indexes-sidebar-active-bg-color | @brand-color | -
--td-indexes-sidebar-active-color | @text-color-anti | -
--td-indexes-sidebar-color | @text-color-primary | -
--td-indexes-sidebar-font | @font-body-small | -
--td-indexes-sidebar-item-size | 40rpx | -
--td-indexes-sidebar-right | 16rpx | -
--td-indexes-sidebar-tips-bg-color | @brand-color-light | -
--td-indexes-sidebar-tips-color | @brand-color | -
--td-indexes-sidebar-tips-font | @font-title-extraLarge | -
--td-indexes-sidebar-tips-right | calc(100% + 32rpx) | -
--td-indexes-sidebar-tips-size | 96rpx | -
--td-indexes-anchor-active-bg-color | @bg-color-container | -
--td-indexes-anchor-active-color | @brand-color | -
--td-indexes-anchor-active-font-weight | 600 | -
--td-indexes-anchor-bg-color | @bg-color-secondarycontainer | -
--td-indexes-anchor-border-color | @component-border | -
--td-indexes-anchor-color | @text-color-primary | -
--td-indexes-anchor-font | @font-body-medium | -
--td-indexes-anchor-padding | 8rpx 32rpx | -
--td-indexes-anchor-top | 0 | -

View File

@@ -0,0 +1,4 @@
export function getFirstCharacter(str) {
return str.toString().substring(0, 1);
}

View File

@@ -0,0 +1,12 @@
/* eslint-disable */
/**
* 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC
* */
export default {
/** 索引字符 */
index: {
type: [String, Number],
},
};

View File

@@ -0,0 +1,50 @@
.t-indexes {
position: relative;
height: 100vh;
}
.t-indexes__sidebar {
position: fixed;
right: var(--td-indexes-sidebar-right, 16rpx);
width: var(--td-indexes-sidebar-item-size, 40rpx);
color: var(--td-indexes-sidebar-color, var(--td-text-color-primary, var(--td-font-gray-1, rgba(0, 0, 0, 0.9))));
font: var(--td-indexes-sidebar-font, var(--td-font-body-small, 24rpx / 40rpx var(--td-font-family, PingFang SC, Microsoft YaHei, Arial Regular)));
display: flex;
flex-flow: column nowrap;
top: 50%;
transform: translateY(-50%);
z-index: 1;
}
.t-indexes__sidebar-item {
border-radius: 50%;
position: relative;
text-align: center;
}
.t-indexes__sidebar-item--active {
background-color: var(--td-indexes-sidebar-active-bg-color, var(--td-brand-color, var(--td-primary-color-7, #0052d9)));
color: var(--td-indexes-sidebar-active-color, var(--td-text-color-anti, var(--td-font-white-1, #ffffff)));
}
.t-indexes__sidebar-item + .t-indexes__sidebar-item {
margin-top: 4rpx;
}
.t-indexes__sidebar-tips {
display: flex;
align-items: center;
justify-content: center;
min-width: var(--td-indexes-sidebar-tips-size, 96rpx);
max-width: 198rpx;
padding: 0 32rpx;
box-sizing: border-box;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
height: var(--td-indexes-sidebar-tips-size, 96rpx);
font: var(--td-indexes-sidebar-tips-font, var(--td-font-title-extraLarge, 600 40rpx / 56rpx var(--td-font-family, PingFang SC, Microsoft YaHei, Arial Regular)));
color: var(--td-indexes-sidebar-tips-color, var(--td-brand-color, var(--td-primary-color-7, #0052d9)));
background-color: var(--td-indexes-sidebar-tips-bg-color, var(--td-brand-color-light, var(--td-primary-color-1, #f2f3ff)));
border-radius: var(--td-indexes-sidebar-tips-size, 96rpx);
position: absolute;
top: 50%;
bottom: 0;
transform: translateY(-50%);
right: var(--td-indexes-sidebar-tips-right, calc(100% + 32rpx));
}

View File

@@ -0,0 +1,371 @@
<template>
<view
:style="tools._style([customStyle])"
:class="classPrefix + ' ' + tClass"
disable-scroll
>
<view
:id="'id-' + classPrefix + '__bar'"
:class="classPrefix + '__sidebar ' + tClassSidebar"
@touchmove.stop.prevent="onTouchMove"
@touchcancel.stop.prevent="onTouchCancel"
@touchend.stop.prevent="onTouchEnd"
>
<view
v-for="(item, index) in _indexList"
:key="index"
:class="tools.cls(classPrefix + '__sidebar-item', [['active', dataCurrent === item]]) + ' ' + tClassSidebarItem"
:data-index="index"
@click.stop="onClick(item, index)"
>
<view
aria-role="button"
:aria-label="dataCurrent === item ? '已选中' + item : ''"
>
{{ getFirstCharacter(item) }}
</view>
<view
v-if="showTips && dataCurrent === item"
:class="classPrefix + '__sidebar-tips'"
>
{{ dataCurrent }}
</view>
</view>
</view>
<slot />
</view>
</template>
<script>
import TIcon from '../icon/icon';
import TCell from '../cell/cell';
import TCellGroup from '../cell-group/cell-group';
import { uniComponent } from '../common/src/index';
import { prefix } from '../common/config';
import props from './props';
import { getRect, throttle, systemInfo } from '../common/utils';
import pageScrollMixin from '../mixins/page-scroll';
import tools from '../common/utils.wxs';
import { getFirstCharacter } from './computed.js';
import { ParentMixin, RELATION_MAP } from '../common/relation';
const name = `${prefix}-indexes`;
export default uniComponent({
name,
options: {
styleIsolation: 'shared',
},
controlledProps: [
{
key: 'current',
event: 'change',
},
],
externalClasses: [
`${prefix}-class`,
`${prefix}-class-sidebar`,
`${prefix}-class-sidebar-item`,
],
mixins: [
pageScrollMixin(),
ParentMixin(RELATION_MAP.IndexesAnchor),
],
components: {
TIcon,
TCell,
TCellGroup,
},
props: {
...props,
},
emits: [
'change',
'select',
],
data() {
return {
prefix,
classPrefix: name,
_height: 0,
_indexList: [],
scrollTop: 0,
activeAnchor: this.current,
showTips: false,
tools,
timer: null,
groupTop: [],
sidebar: null,
currentTouchAnchor: null,
dataCurrent: this.current,
};
},
watch: {
indexList: {
handler(v, pre) {
const cb = () => {
this.setIndexList(v);
this.setHeight(this._height);
};
if (!pre?.length) {
// 防止抖音小程序报错
setTimeout(() => {
cb();
}, 33);
} else {
cb();
}
},
immediate: true,
},
height(v) {
this.setHeight(v);
},
stickyOffset() {
this.setAnchorByCurrent(this.dataCurrent, 'update', true);
},
current: {
handler(val) {
this.dataCurrent = val;
},
immediate: true,
},
dataCurrent: {
handler(e) {
if (e && this.activeAnchor && e !== this.activeAnchor) {
this.setAnchorByCurrent(e, 'update');
}
},
immediate: true,
},
},
mounted() {
this.timer = null;
this.groupTop = [];
this.sidebar = null;
if (this._height === 0) {
this.setHeight();
}
if (this.indexList === null) {
this.setIndexList();
}
},
methods: {
getFirstCharacter,
setHeight(height) {
if (!height) {
const { windowHeight } = systemInfo;
height = windowHeight;
}
this._height = height;
setTimeout(() => {
this.getAllRect();
});
},
setIndexList(list) {
if (!list) {
const start = 'A'.charCodeAt(0);
const alphabet = [];
for (let i = start, end = start + 26; i < end; i += 1) {
alphabet.push(String.fromCharCode(i));
}
this._indexList = alphabet;
} else {
this._indexList = list;
}
},
getAllRect() {
this.getAnchorsRect().then(() => {
this.groupTop.forEach((item, index) => {
const next = this.groupTop[index + 1];
item.totalHeight = (next?.top || Infinity) - item.top;
});
const current = this.dataCurrent || this._indexList[0];
this.setAnchorByCurrent(current, 'init');
})
.catch((err) => {
console.log('err', err);
});
this.getSidebarRect();
},
getAnchorsRect() {
return Promise.all((this.children || [])
.map(child => getRect(child, `.${name}-anchor`)
.then((rect) => {
this.groupTop.push({
height: rect.height,
top: rect.top,
anchor: child.index,
});
})
.catch((err) => {
console.log('err', err);
})));
},
getSidebarRect() {
getRect(this, `#id-${name}__bar`).then((rect) => {
const { top, height } = rect;
const { length } = this._indexList;
this.sidebar = {
top,
height,
itemHeight: (height - (length - 1) * 2) / length, // margin = 2px
};
})
.catch((err) => {
console.log('err', err);
});
},
toggleTips(flag) {
if (!flag) {
clearInterval(this.timer);
this.timer = setTimeout(() => {
this.showTips = false;
}, 300);
} else {
this.showTips = true;
}
},
setAnchorByCurrent(current, source, force) {
const { stickyOffset } = this;
if (this.activeAnchor !== null && this.activeAnchor === current && !force) return;
const target = this.groupTop.find(item => item.anchor === current);
if (target) {
const scrollTop = target.top - stickyOffset;
this.setAnchorOnScroll(scrollTop);
uni.pageScrollTo({
scrollTop,
duration: 0,
});
if (['click', 'touch'].includes(source)) {
this.toggleTips(true);
this.$emit('select', { index: current });
}
}
},
onClick(current) {
this.setAnchorByCurrent(current, 'click');
},
onTouchMove(e) {
this.onAnchorTouch(e);
},
onTouchCancel() {
this.toggleTips(false);
},
onTouchEnd(e) {
this.toggleTips(false);
this.onAnchorTouch(e);
},
onAnchorTouch: throttle(function (e) {
const getAnchorIndex = (clientY) => {
const offsetY = clientY - this.sidebar.top;
const max = this._indexList.length - 1;
if (offsetY <= 0) {
return 0;
}
if (offsetY > this.sidebar.height) {
return max;
}
return Math.min(max, Math.floor(offsetY / this.sidebar.itemHeight));
};
const index = getAnchorIndex(e.changedTouches[0].clientY);
this.setAnchorByCurrent(this._indexList[index], 'touch');
}, 1000 / 30), // 30 frame
setAnchorOnScroll(scrollTop) {
if (!this.groupTop) {
return;
}
const { sticky, stickyOffset } = this;
scrollTop += stickyOffset;
const curIndex = this.groupTop.findIndex(group => scrollTop >= group.top - group.height && scrollTop <= group.top + group.totalHeight - group.height);
if (curIndex === -1) return;
const curGroup = this.groupTop[curIndex];
this.activeAnchor = curGroup.anchor;
setTimeout(() => {
this._trigger('change', { index: curGroup.anchor, current: curGroup.anchor });
});
if (sticky) {
const offset = curGroup.top - scrollTop;
const betwixt = offset < curGroup.height && offset > 0 && scrollTop > stickyOffset;
this.children.forEach((child, index) => {
if (index === curIndex) {
const sticky = scrollTop > stickyOffset;
const anchorStyle = `transform: translate3d(0, ${betwixt ? offset : 0}px, 0); top: ${stickyOffset}px`;
if (anchorStyle !== child.anchorStyle || sticky !== child.sticky) {
child.sticky = sticky;
child.active = true;
child.dataStyle = `height: ${curGroup.height}px`;
child.anchorStyle = anchorStyle;
}
} else if (index + 1 === curIndex) {
// 两个 anchor 同时出现时的上一个
const anchorStyle = `transform: translate3d(0, ${
betwixt ? offset - curGroup.height : 0
}px, 0); top: ${stickyOffset}px`;
if (anchorStyle !== child.anchorStyle) {
child.sticky = true;
child.active = true;
child.dataStyle = `height: ${curGroup.height}px`;
child.anchorStyle = anchorStyle;
}
} else {
child.active = false;
child.sticky = false;
child.dataStyle = '';
child.anchorStyle = '';
}
});
}
},
onScroll({ scrollTop }) {
this.setAnchorOnScroll(scrollTop);
},
},
});
</script>
<style scoped>
@import './indexes.css';
</style>

View File

@@ -0,0 +1,40 @@
/* eslint-disable */
/**
* 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC
* */
export default {
/** 索引列表的激活项,默认首项 */
current: {
type: [String, Number],
},
/** 索引列表的激活项,默认首项,非受控属性 */
defaultCurrent: {
type: [String, Number],
},
/** 索引字符列表。不传默认 `A-Z` */
indexList: {
type: Array,
},
/** 索引是否吸顶默认为true */
sticky: {
type: Boolean,
default: true,
},
/** 锚点吸顶时与顶部的距离 */
stickyOffset: {
type: Number,
default: 0,
},
/** 索引发生变更时触发事件 */
onChange: {
type: Function,
default: () => ({}),
},
/** 点击侧边栏时触发事件 */
onSelect: {
type: Function,
default: () => ({}),
},
};

View File

@@ -0,0 +1,38 @@
/* eslint-disable */
/**
* 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC
* */
export interface TdIndexesProps {
/**
* 索引列表的激活项,默认首项
*/
current?: string | number;
/**
* 索引列表的激活项,默认首项,非受控属性
*/
defaultCurrent?: string | number;
/**
* 索引字符列表。不传默认 `A-Z`
*/
indexList?: Array<string | number>;
/**
* 索引是否吸顶默认为true
* @default true
*/
sticky?: Boolean;
/**
* 锚点吸顶时与顶部的距离
* @default 0
*/
stickyOffset?: number;
/**
* 索引发生变更时触发事件
*/
onChange?: (context: { index: string | number }) => void;
/**
* 点击侧边栏时触发事件
*/
onSelect?: (context: { index: string | number }) => void;
}