Files
hospital-rescue-app-v2/uni_modules/tmx-ui/components/x-indexbar/x-indexbar.uvue
T
2026-09-24 16:25:22 +08:00

623 lines
17 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script lang="ts">
import { SlotsType, PropType } from "vue"
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
import { checkIsCssUnit, rpx2px, getUid, getUnit } from "../../core/util/xCoreUtil.uts"
import { xConfig, xProvitae } from "../../config/xConfig.uts"
type indexbarTYPE = {
index : number,
title : string
}
/**
*
* @name 索引 xIndexbar
* @description 特别提醒:本组件在1.0.9重构不向下兼容,并且删除了子组件:x-indexbar-item,不再需要子组件。请使用对应的动态插槽来渲染
* 数据,demo是526条数据的测试依赖右边索引滑动跟手流畅。
* 虚拟列表有个缺点会在滚动时分页读取数据并复用布局,因此如果你想做带图片的索引,
* 建议进入应用后启用后台缓存已有的头像或者图片数据类似微信那样缓存图片,这样虚拟加载的时候闪烁感就少了
* 重要:插槽内不要使用任何自定组件布局,也不要增加任何额外的节点,能少就少。
* @page /pages/index/indexbar
* @category 导航组件
* @constant 平台兼容
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
*/
export default {
data() {
return {
listIndexBar: [] as indexbarTYPE[],
boxHeight: 0,
boxWidth: 0,
boxTop: 0,
boxLeft: 0,
nowIndex: 0,
currentIndexBYbar: 0,
// 视图内显示的条数。
viewConunt: 0,
windowTop: 0,
scrollTop: 0,
realScrollY:0,
headerDomRectHeight:0,
_y: 0,
tid: 0,
isMoveing: false,
olsShowView: [] as UTSJSONObject[],
isShowOldViewData: false,
dotParentOffsetTop: -1,
dotRight: 0,
tid2: 0,
showDot: false,
sliderDotSize:16,
// #ifdef MP-WEIXIN
mpBgBounds:null as any|null,
sliderBgBounds:null as any|null
// #endif
}
},
slots: Object as SlotsType<{
header : { title : string, index : number },
default : {
current : UTSJSONObject,
currentIndex : number,
index : number
}
}>,
props: {
/**
* 宽
*/
width: {
type: String,
default: "auto"
},
/**
* 高,%,rpx,px单位均可。
*/
height: {
type: String,
default: "100%"
},
/**
* 侧边指示激活时的文字颜色
* 空值是取全局主题值
*/
dotActiveColor: {
type: String,
default: ""
},
/**
* 侧边指示未激活时的文字颜色
*/
dotColor: {
type: String,
default: "#c0c0c0"
},
/**
* 侧边指示的背景颜色
*/
dotBgColor: {
type: String,
default: "white"
},
list: {
type: Array as PropType<UTSJSONObject[]>,
default: () : UTSJSONObject[] => [] as UTSJSONObject[]
},
/**
* 项目的高
* 只能是数字,或者带rpx,px单位
*/
cellHeight: {
type: String,
default: '50'
},
/**
* 项目的标题高
* 只能是数字,或者带rpx,px单位
*/
titleHeight: {
type: String,
default: '32'
},
customSliderBar:{
type:Array as PropType<Array<string>>,
default:():string[] => [] as string[]
}
},
computed: {
_customBarLen():number{
return this.customSliderBar.length;
},
_height() : string {
return checkIsCssUnit(this.height, xConfig.unit)
},
_width() : string {
return checkIsCssUnit(this.width, xConfig.unit)
},
_cellHeight() : number {
let height = checkIsCssUnit(this.cellHeight, xConfig.unit)
let unit = getUnit(height)
let realheight = parseInt(height)
if (unit == 'rpx') {
realheight = rpx2px(realheight)
}
return realheight;
},
_titleHeight() : number {
let height = checkIsCssUnit(this.titleHeight, xConfig.unit)
let unit = getUnit(height)
let realheight = parseInt(height)
if (unit == 'rpx') {
realheight = rpx2px(realheight)
}
return realheight;
},
_totalHeight() : number {
return this.list.length * this._cellHeight;
},
_viewConunt() : number {
if (this.boxHeight == 0 || this.list.length == 0) return 0;
let itemcount = this.boxHeight / this._cellHeight;
return Math.ceil(itemcount + 1)
},
_dotActiveColor() : string {
if (this.dotActiveColor == "") return getDefaultColor(xConfig.color);
return getDefaultColor(this.dotActiveColor)
},
_dotColor() : string {
return getDefaultColor(this.dotColor)
},
_dotBgColor() : string {
if (xConfig.dark == 'dark') return xConfig.inputDarkColor
return getDefaultColor(this.dotBgColor)
},
_viewList() : UTSJSONObject[] {
if (this.isShowOldViewData) return this.olsShowView
let start = this.nowIndex - this._viewConunt;
let end = this.nowIndex + this._viewConunt;
start = Math.max(0, start)
// end = Math.min(this.list.length-1,end)
let oldIndexList = this.list.slice(start, end);
let list = oldIndexList.map((el : UTSJSONObject, index : number) : UTSJSONObject => {
el.set("oldIndex", index + start)
return el;
})
this.olsShowView = list
return list
},
_list() : UTSJSONObject[] {
return this.list;
},
_listTotal() : UTSJSONObject[] {
let customlist = this.customSliderBar.map((el:string,index:number):UTSJSONObject => {
return {
"id": index,
"name": el,
"logo": "",
"index": el
} as UTSJSONObject
})
return [...customlist,...this.list];
}
},
watch: {
list() {
this.scrollTop = 0
this.nowIndex = 0
this.listIndexBar = [] as indexbarTYPE[]
this.olsShowView = [] as UTSJSONObject[]
this.currentIndexBYbar = 0
this.olsShowView = this._viewList
this.setInitData();
}
},
mounted() {
this.windowTop = uni.getWindowInfo().windowTop
this.getNodeInfo();
this.olsShowView = this._viewList
this.setInitData();
// #ifdef MP-WEIXIN
this.mpweixinGetBoundec()
// #endif
},
beforeUnmount() {
clearTimeout(this.tid)
},
methods: {
// #ifdef MP-WEIXIN
async mpweixinGetBoundec(){
let _this = this;
setTimeout(async function() {
let ele = _this.$refs['xIndexBarRightSliderBg'] as UniElement;
_this.mpBgBounds= await ele.getBoundingClientRectAsync()
let parentele = _this.$refs['xIndexBarRightSlider'] as UniElement;
_this.sliderBgBounds = await parentele.getBoundingClientRectAsync()
}, 100);
},
// #endif
getHeaderDom(){
let t = this;
const ele = this.$refs['headerRef']! as UniElement;
ele.getBoundingClientRectAsync()
?.then((rect:DOMRect)=>{
t.headerDomRectHeight = rect.height
})
.catch(()=>{})
},
setInitData() {
let lst = Date.now()
let list = this._list as UTSJSONObject[]
if (list.length == 0) return
let listbar = [] as indexbarTYPE[]
let listbarTitle = [] as string[];
for (let i = 0; i < this._list.length; i++) {
let current = this._list[i]
let keya = current.getString("index")!;
if (!listbarTitle.includes(keya!)) {
listbarTitle.push(keya)
listbar.push({ index: i+this._customBarLen, title: keya } as indexbarTYPE)
}
}
let customlist = this.customSliderBar.map((el:string,index:number):indexbarTYPE => {
return {
index,
title:el
} as indexbarTYPE
})
this.listIndexBar = [...customlist,...listbar];
console.warn("索引数据处理时间(毫秒)", Date.now() - lst, "如果此值过大,表示sdk有问题,正常在10ms内,安卓dev会在60ms内")
},
sliderOnclik(evt : UniPointerEvent, item : indexbarTYPE, index : number) {
// let customIndex = this.customSliderBar.length + index -1;
this.nowIndex = this.listIndexBar[index].index;
// this.nowIndex = index;
if(index <=this.customSliderBar.length-1){
this.scrollTop = 0
}else{
this.scrollTop = this._cellHeight * this.nowIndex + this.headerDomRectHeight
}
this.currentIndexBYbar = index;
this.transxyByDotTop(evt.clientY)
},
onScrollEvent(evt : UniScrollEvent) {
if (this.isMoveing) return;
this.realScrollY = evt.detail.scrollTop ;
this.transxyend(evt.detail.scrollTop - this._titleHeight - this.headerDomRectHeight)
this.getHeaderDom()
},
ONscrollend(evt : UniScrollEvent) {
// #ifdef APP-IOS||APP-ANDROID
this.scrollTop = evt.detail.scrollTop;
// #endif
},
transxyend(top : number) {
let index = Math.ceil((top) / this._cellHeight)
index = Math.max(0, index)
index = Math.min(this._list.length - 1, index)
if (index == this.nowIndex) return;
this.nowIndex = index;
let title = this._list[index].getString("index")!
if (title == this.listIndexBar[this.currentIndexBYbar].title) return;
let barindexItem = this.listIndexBar.findIndex((el : indexbarTYPE) : boolean => el.title == title)
if (barindexItem == this.currentIndexBYbar) return;
if (barindexItem > -1) {
this.currentIndexBYbar = barindexItem;
}
},
getNodeInfo() {
let t = this;
uni.createSelectorQuery().in(this)
.select(".xIndexBarBox")
.boundingClientRect().exec((ret) => {
let nodeinfo = ret[0] as NodeInfo
t.boxTop = nodeinfo.top!;
t.boxWidth = nodeinfo.width!;
t.boxHeight = nodeinfo.height!;
t.boxLeft = nodeinfo.left!;
})
this.getHeaderDom();
},
hideDot() {
let t = this;
clearTimeout(this.tid2)
this.tid2 = setTimeout(function () {
t.showDot = false
}, 700);
},
itemMove(evt : UniTouchEvent) {
evt.stopPropagation();
evt.preventDefault();
let t = this;
let y = evt.changedTouches[0].clientY
let ele = this.$refs['xIndexBarRightSliderBg'] as UniElement;
let boundsTop = 0
// #ifdef APP || WEB
boundsTop = ele.getBoundingClientRect()!.top
// #endif
// #ifdef MP-WEIXIN
boundsTop = this.mpBgBounds!.top
// #endif
let diff = y - boundsTop;
diff = Math.max(0, Math.min(diff, this.listIndexBar.length * t.sliderDotSize))
let barIndex = Math.floor(diff / t.sliderDotSize)
barIndex = Math.max(0, Math.min(barIndex, this.listIndexBar.length - 1))
if (barIndex != this.currentIndexBYbar) {
this.currentIndexBYbar = barIndex;
}
let index = this.listIndexBar[barIndex].index;
if (index == this.nowIndex) return
this.nowIndex = index;
clearTimeout(this.tid)
this.isShowOldViewData = true
t.transxyByDotTop(y)
// 减少处理次数
this.tid = setTimeout(function () {
if(index<=t._customBarLen-1){
t.scrollTop = 0
}else{
t.scrollTop = index * t._cellHeight + t.headerDomRectHeight
}
t.isShowOldViewData = false;
t.realScrollY = t.scrollTop ;
}, 10);
},
itemStart(evt : UniTouchEvent) {
this.isMoveing = true
this._y = evt.changedTouches[0].clientY;
},
transxyByDotTop(top : number) {
let ele = this.$refs['xIndexBarRightSliderBg'] as UniElement;
let boundsTop = 0
// #ifdef APP || WEB
boundsTop = ele.getBoundingClientRect()!.top
// #endif
// #ifdef MP-WEIXIN
boundsTop = this.mpBgBounds!.top
// #endif
let diff = top - boundsTop!
diff = Math.max(0, Math.min(diff, this.listIndexBar.length * this.sliderDotSize))
let barIndex = Math.floor(diff / this.sliderDotSize)
barIndex = Math.max(0, Math.min(barIndex, this.listIndexBar.length - 1))
let parentele = this.$refs['xIndexBarRightSlider'] as UniElement;
let boundsTop2 = 0
// #ifdef APP || WEB
boundsTop2 = parentele.getBoundingClientRect()!.top
// #endif
// #ifdef MP-WEIXIN
boundsTop2 = this.sliderBgBounds!.top
// #endif
let topdiff = barIndex * this.sliderDotSize + boundsTop - boundsTop2 - (50 - this.sliderDotSize) / 2;
// #ifdef APP-ANDROID
topdiff = -25 + (boundsTop - boundsTop2) + barIndex * this.sliderDotSize + this.sliderDotSize / 2
// #endif
let dotele = this.$refs['xIndexBarDot'] as UniElement;
if (topdiff == this.dotParentOffsetTop) return;
this.dotParentOffsetTop = topdiff
dotele.style.setProperty('transform', `translateY(${topdiff}px) rotate(-45deg)`)
this.showDot = true
this.hideDot();
},
itemEnd() {
this.isMoveing = false
this.dotParentOffsetTop = -1
}
}
}
</script>
<template>
<view class="xIndexBar" :style="{height:_height,width:_width}">
<view :style="{height:_titleHeight+'px',top:realScrollY>=headerDomRectHeight?'0px':(headerDomRectHeight-realScrollY)+'px'}">
<!--
@slot 悬浮的菜单头
@props {title} 当前选中的菜单头
@props {index} 当前选中的菜单头索引
-->
<slot name="header" :title="listIndexBar.length>0?listIndexBar[currentIndexBYbar].title:''"
:index="listIndexBar.length>0?listIndexBar[currentIndexBYbar+(Math.max(customSliderBar.length-1,0))].index:0">
<view style="display: flex;flex-direction: row;align-items: center;height: 100%;">
<x-text v-if="listIndexBar.length>0">{{listIndexBar[currentIndexBYbar<= (_customBarLen-1)?_customBarLen:currentIndexBYbar].title}}</x-text>
</view>
</slot>
</view>
<view class="xIndexBarBox" >
<scroll-view @scrollend="ONscrollend" :show-scrollbar="false" @scroll="onScrollEvent" :scroll-top="scrollTop"
:bounces="false" :enable-back-to-top="true" class="xIndexBar" :style="{height:boxHeight+'px'}">
<view ref="headerRef">
<view style="flex:1">
<!--
@slot 顶部布局,可以自由布局
-->
<slot name="top"></slot>
</view>
<view :style="{height:_titleHeight+'px'}"></view>
</view>
<view :style="{height:_totalHeight+'px',position:'relative'}" v-if="_viewList.length>0">
<template v-for="(item,index) in _viewList" :key="index">
<view class="xIndexBarBoxItem" :style="{
height:_cellHeight+'px',
transform:`translateY(${(_cellHeight*(item.getNumber('oldIndex')!))}px)`
}">
<!--
@slot 动态插槽
@prop {UTSJSONObject} current - 当前的项目信息,可通过此布局当前数据
@prop {number} currentIndex - 右边导航当前的索引
@prop {number} index - 当前索引
-->
<slot :index="(item.getNumber('oldIndex')!)" :currentIndex="nowIndex" :current="item"></slot>
</view>
</template>
</view>
</scroll-view>
<view class="xIndexBarRightSlider" ref="xIndexBarRightSlider">
<view @touchmove="itemMove" @touchstart="itemStart" @touchend="itemEnd" ref="xIndexBarRightSliderBg"
class="xIndexBarRightSliderBg" :style="{background:_dotBgColor}">
<view @click.stop="sliderOnclik($event as UniPointerEvent,item,index)"
v-for="(item,index) in listIndexBar" :key="index" class="xIndexBarRightSliderItem"
:class="[currentIndexBYbar==index?'xIndexBarRightSliderItemHover':'']" :hover-start-time="50"
:hover-stay-time="100" hover-class="xIndexBarRightSliderItemHover" :style="{
backgroundColor:currentIndexBYbar==index?_dotActiveColor:_dotBgColor,
}">
<text :style="{color:currentIndexBYbar==index?'white':_dotColor}" class="xIndexBarRightSliderItemText">{{item.title}}</text>
</view>
</view>
</view>
<view android-layer-type="software" ref="xIndexBarDot" class="xIndexBarDot"
:style="{opacity:!showDot?'0':'1',backgroundColor:_dotActiveColor}">
<text v-if="(_list.length>0)" class="xIndexBarDotText">{{_listTotal[nowIndex]['index']}}</text>
</view>
</view>
</view>
</template>
<style scoped>
.xIndexBar{
display: flex;
flex-direction: column;
}
.xIndexBarBox {
position: relative;
flex: 1;
width:100%;
}
.xIndexBarBoxItem {
position: absolute;
width: 100%;
top: 0px;
}
.xIndexBarDot {
pointer-events: none;
position: absolute;
width: 50px;
height: 50px;
border-radius: 50px 50px 6px 50px;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
/* transform: rotate(-45deg); */
/* transition-duration: 100ms;
transition-property: opacity;
transition-timing-function: linear; */
right: 47px;
top: 0;
/* #ifdef WEB */
box-szing: border-box;
/* #endif */
}
.xIndexBarDotText {
transform: rotate(45deg);
font-size: 18px;
color: white;
font-weight: bold;
}
.xIndexBarRightSliderItemHover {
/* background-color: rgba(0, 0, 0, 0.05); */
/* background-color:red; */
border-radius: 30px;
}
.xIndexBarHeaderTitleBox {
pointer-events: none;
}
.xIndexBarHeaderTitle {
height: 32px;
padding-left: 16px;
display: flex;
justify-content: flex-start;
align-items: center;
flex-direction: row;
/* #ifdef H5 */
position: fixed;
z-index: 5;
font-size: bold;
/* #endif */
}
.xIndexBarRightSlider {
width: 32px;
height: 100%;
position: absolute;
z-index: 8;
right: 5px;
top:0px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.xIndexBarRightSliderItem {
width: 16px;
height: 16px;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
border-radius: 21px;
}
.xIndexBarRightSliderItemText {
font-size: 10px;
line-height: 11px;
/* font-weight: bold; */
}
.xIndexBarRightSliderBg {
border-radius: 21px;
/* box-shadow: 0 0px 10px rgba(0, 0, 0, 0.06); */
position: relative;
}
</style>