623 lines
17 KiB
Plaintext
623 lines
17 KiB
Plaintext
<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> |