123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438 |
- <template>
- <view
- class="uv-vtabs"
- :style="[vtabsStyle]"
- >
- <scroll-view
- class="uv-vtabs__bar"
- ref="uv-vtabs__bar"
- :style="[getBarStyle]"
- :scroll-y="barScrollable"
- :scroll-x="scrollX"
- :show-scrollbar="false"
- :scroll-with-animation="true"
- :scroll-top="barScrollTop"
- :scroll-into-view="barScrollToView"
- >
- <view
- :class="[
- 'uv-vtabs__bar-item',
- `uv-vtabs__bar-item--${index}`,
- index == activeIndex && 'uv-vtabs__bar-item-active'
- ]"
- :ref="`uv-vtabs__bar-item--${index}`"
- v-for="(item,index) in list"
- :key="index"
- :id="`bar_${index}`"
- :style="[itemStyle(index)]"
- @tap.stop="clickHandler(index)"
- >
- <view
- class="uv-vtabs__bar-item--line"
- v-if="index == activeIndex"
- :style="[$uv.addStyle(barItemActiveLineStyle)]"
- ></view>
- <text
- :class="[
- 'uv-vtabs__bar-item--value',
- index == activeIndex && 'uv-vtabs__bar-item-active--value'
- ]"
- :style="[itemStyle(index),textStyle(index)]"
- >{{item[keyName]}}</text>
- <view
- class="uv-vtabs__bar-item--badge"
- :style="[$uv.addStyle(barItemBadgeStyle)]"
- v-if="!!(item.badge && (item.badge.show || item.badge.isDot || item.badge.value))"
- >
- <uv-badge
- :show="!!(item.badge && (item.badge.show || item.badge.isDot || item.badge.value))"
- :isDot="item.badge && item.badge.isDot || propsBadge.isDot"
- :value="item.badge && item.badge.value || propsBadge.value"
- :max="item.badge && item.badge.max || propsBadge.max"
- :type="item.badge && item.badge.type || propsBadge.type"
- :showZero="item.badge && item.badge.showZero || propsBadge.showZero"
- :bgColor="item.badge && item.badge.bgColor || propsBadge.bgColor"
- :color="item.badge && item.badge.color || propsBadge.color"
- :shape="item.badge && item.badge.shape || propsBadge.shape"
- :numberType="item.badge && item.badge.numberType || propsBadge.numberType"
- :inverted="item.badge && item.badge.inverted || propsBadge.inverted"
- ></uv-badge>
- </view>
- </view>
- </scroll-view>
- <scroll-view
- class="uv-vtabs__content"
- :style="[getContentStyle,$uv.addStyle(contentStyle)]"
- :scroll-y="true"
- :scroll-x="scrollX"
- :show-scrollbar="false"
- :scroll-top="contentScrollTop"
- :scroll-into-view="contentScrollTo"
- :scroll-with-animation="true"
- @scroll="scrollHandler"
- @scrolltolower="scrolltolower"
- v-if="chain"
- >
- <slot />
- </scroll-view>
- <scroll-view
- v-else
- class="uv-vtabs__content"
- :style="[getContentStyle,$uv.addStyle(contentStyle)]"
- :scroll-y="true"
- :scroll-x="scrollX"
- :show-scrollbar="false"
- :scroll-top="contentScrollTop2"
- @scrolltolower="scrolltolower"
- >
- <slot />
- </scroll-view>
- </view>
- </template>
- <script>
- import mpMixin from '@/uni_modules/uv-ui-tools/libs/mixin/mpMixin.js'
- import mixin from '@/uni_modules/uv-ui-tools/libs/mixin/mixin.js'
- import debounce from '@/uni_modules/uv-ui-tools/libs/function/debounce.js'
- import throttle from '@/uni_modules/uv-ui-tools/libs/function/throttle.js'
- import uvBadgeProps from '@/uni_modules/uv-badge/components/uv-badge/props.js'
- import props from './props.js';
- // #ifdef APP-NVUE
- const dom = uni.requireNativePlugin('dom')
- // #endif
- /**
- * 垂直选项卡
- * @description 该组件兼容所有端,提供了分类展示和联动等功能
- * @tutorial https://www.uvui.cn/components/vtabs.html
- * @property {Array} list 选项数组,元素为对象,如[{name:'uv-ui'}](默认 [] )
- * @property {String} keyName 从list元素对象中读取的键名(默认 name )
- * @property {Number} current 当前选中项,从0开始(默认 0 )
- * @property {Number | String} hdHeight 头部内容的高度,头部有内容必传,否则会有联动误差(默认 0 )
- * @property {Boolean} chain 是否开启联动,开启后右边区域可以滑动查看内容(默认 true )
- * @property {Number|String} height 整个列表的高度,默认auto或空则为屏幕高度(默认 auto屏幕高度 )
- * @property {Number|String} barWidth 左边选项区域的宽度(默认 180rpx )
- * @property {Boolean} barScrollable 左边选项区域是否允许滚动 (默认 true )
- * @property {String} barBgColor 左边选项区域的背景颜色(默认$uv-bg-color)
- * @property {Object} barStyle 左边选项区域的自定义样式 (默认{})
- * @property {Object} barItemStyle 左边选项区域每个选项的自定义样式 (默认{})
- * @property {Object} barItemActiveStyle 左边选项区域选中选项的自定义样式 (默认{})
- * @property {Object} barItemActiveLineStyle 左边选项区域选中选项竖线条的自定义样式 (默认{})
- * @property {Object} barItemBadgeStyle 左边选项区域选中选项徽标的自定义样式,主要用于设置位置 (默认{})
- * @property {Object} contentStyle 右边区域自定义样式 (默认{})
- * @example <uv-vtabs :list="list"><uv-vtabs-item>...</uv-vtabs-item></uv-vtabs>
- */
- export default {
- name: 'uv-vtabs',
- mixins: [mpMixin, mixin, props],
- created() {
- this.children = []
- },
- mounted() {
- this.$nextTick(()=>{
- this.init(this.current);
- })
- },
- data() {
- return {
- activeIndex: 0,
- // 微信小程序下,scroll-view的scroll-into-view属性无法对slot中的内容的id生效,只能通过设置scrollTop的形式去移动滚动条
- contentScrollTop: 0,
- contentScrollTop2: 0,//针对非联动
- contentScrollTo: '',
- scrolling: false,
- barScrolling: false,
- touching: false,
- hasHeight: 0,
- scrollViewHeight: 0,
- barScrollTop: 0,
- barScrollToView: '',
- timer2: 0
- }
- },
- computed: {
- scrollX(){
- // #ifdef APP-NVUE
- return true;
- // #endif
- return false;
- },
- vtabsStyle() {
- const style = {};
- style.height = this.getHeight();
- return this.$uv.deepMerge(style, this.$uv.addStyle(this.customStyle));
- },
- getBarStyle() {
- const style = {};
- style.width = this.$uv.getPx(this.barWidth, true);
- style.background = this.barBgColor;
- style.height = this.getHeight();
- return this.$uv.deepMerge(style, this.$uv.addStyle(this.barStyle));
- },
- itemStyle(){
- return index =>{
- const style = {};
- let barItemInitStyle = this.barItemStyle;
- // 避免在nvue模式下,切换时候上一个选中颜色不变
- if(this.barItemStyle && !this.barItemStyle?.background) {
- barItemInitStyle.background = 'transparent';
- }
- // 是否激活的样式
- const customeStyle = index === this.activeIndex ? this.$uv.addStyle(this.barItemActiveStyle) : this.$uv.addStyle(barItemInitStyle);
- if (this.list[index].disabled) {
- style.color = '#c8c9cc'
- }
- return this.$uv.deepMerge(style, customeStyle);
- }
- },
- // nvue设置字体样式必须要text标签上进行
- textStyle(){
- return index=>{
- const style = {};
- style.width = this.$uv.getPx(this.barWidth, true);
- return style;
- }
- },
- getContentStyle() {
- const style = {};
- style.height = this.getHeight();
- return style;
- },
- propsBadge() {
- return uvBadgeProps
- }
- },
- watch: {
- current(newVal){
- if(!this.touching)
- this.$nextTick(()=>{
- this.init(newVal?newVal:0);
- })
- },
- list(newVal) {
- if (newVal.length) {
- this.$uv.sleep(30).then(res => {
- this.resize();
- })
- }
- },
- activeIndex(newVal){
- if(!this.chain) {// 解决:非联动,内容过多的情况,滚动一段距离,再切换未滚动到顶部的BUG
- this.contentScrollTop2 = 0 - Math.random() * 4 - 4;
- }
- this.$emit('change',newVal);
- }
- },
- methods: {
- init(index){
- let num = 0;
- clearInterval(this.timer2);
- this.timer2 = setInterval(async ()=>{
- num++;
- if(num>50) clearInterval(this.timer2);
- if(this.children.length) {
- clearInterval(this.timer2);
- await this.$uv.sleep(300);
- this.clickHandler(index);
- }
- },100)
- },
- // 内容滚动到底部触发
- scrolltolower(){
- this.$emit('scrolltolower',this.activeIndex);
- },
- async resize() {
- // 如果list数组长度为0就不处理 || 选中目标未变则不处理
- if (this.list.length == 0 || !this.barScrollable) return;
- // 避免滑太快,修复位置
- Promise.all([this.getTabsRect(), this.getAllItemRect()]).then(([tabsRect, itemRect = []]) => {
- this.tabsRect = tabsRect;
- this.scrollViewHeight = 0
- itemRect.map((item, index) => {
- this.scrollViewHeight += item.height;
- this.list[index].rect = item;
- })
- this.setBarScrollTop();
- })
- },
- // 设置左边菜单滚动条的位置,目标:将当前的选项移到中间位置
- setBarScrollTop() {
- const tabRect = this.list[this.activeIndex];
- const offsetTop = this.list
- .slice(0, this.activeIndex)
- .reduce((total, item) => {
- return total + item.rect.height;
- }, 0);
- const scrollViewHeight = this.$uv.getPx(this.getHeight());
- let barScrollTop = tabRect.rect.height / 2 + offsetTop - scrollViewHeight / 2;
- // 先给一点随机值,避免出现不能滚动的BUG
- barScrollTop = Math.min(barScrollTop, this.scrollViewHeight - this.tabsRect.height);
- this.barScrollTop = Math.max(0, barScrollTop);
- // 已经不能滚动的时候,就使用scroll-into-view的方式进行定位,避免失效
- if(barScrollTop>=(this.scrollViewHeight - this.tabsRect.height)) {
- this.timer && clearTimeout(this.timer);
- this.timer = setTimeout(()=>{
- this.barScrollToView = `bar_${this.activeIndex}`;
- },400)
- }
- },
- // 左边菜单点击
- async clickHandler(currentIndex) {
- if (currentIndex == this.activeIndex) return;
- this.touching = true;
- this.activeIndex = currentIndex;
- if(this.chain) {
- // 给一点随机值,避免出现不能滚动的BUG。微信端必须用此方法
- this.contentScrollTop = this.children[currentIndex].top - this.$uv.getPx(this.hdHeight) - Math.random() * 4 - 4;
- // #ifndef MP-WEIXIN
- this.contentScrollTo = `content_${currentIndex}`;
- // #endif
- }
- this.timer && clearTimeout(this.timer);
- throttle(()=>{
- this.resize();
- },300,false)
- debounce(() => {
- this.touching = false;
- }, 900)
- },
- // 内容滚动
- scrollHandler(e) {
- if (this.touching || this.scrolling) return;
- // 每过一定时间取样一次,减少资源损耗以及可能带来的卡顿
- this.scrolling = true;
- this.$uv.sleep(80).then(() => {
- this.scrolling = false;
- })
- const scrollTop = e.detail.scrollTop;
- let children = this.children;
- const len = children.length;
- let top = 0;
- let activeIndex = 0;
- children = this.children.map((item, index) => {
- if (item.height > 0) this.hasHeight = item.height;
- item.height = item.height > 0 ? item.height : this.hasHeight;
- const child = {
- height: item.height,
- top
- }
- // 进行累加,给下一个item提供计算依据
- top += item.height;
- return child;
- })
- for (let i = 0; i < len; i++) {
- const item = children[i];
- const nextItem = children[i + 1];
- // 如果滚动条高度小于第一个item的top值,此时无需设置任意字母为高亮
- if (scrollTop <= children[0].top) {
- activeIndex = 0;
- break
- } else if (!nextItem) {
- // 当不存在下一个item时,意味着历遍到了最后一个
- activeIndex = len - 1;
- break
- } else if (scrollTop > item.top && scrollTop < nextItem.top) {
- activeIndex = i;
- break
- }
- }
- this.activeIndex = activeIndex;
- // 当前选中项索引必然来源于前后两个索引,满足才执行,避免闪烁的bug
- this.timer4 && clearTimeout(this.timer4);
- this.timer4 = setTimeout(()=>{
- this.resize();
- },100)
- },
- // 设置高度
- getHeight() {
- let height = 0;
- const isEmpty = this.$uv.test.empty(this.height);
- if (isEmpty || this.height=='auto') height = this.$uv.addUnit(this.$uv.sys().windowHeight);
- else height = this.$uv.getPx(this.height, true);
- return height;
- },
- // 获取导航菜单的尺寸
- getTabsRect() {
- return new Promise(resolve => {
- this.queryRect('uv-vtabs__bar').then(size => resolve(size))
- })
- },
- // 获取所有标签的尺寸
- getAllItemRect() {
- return new Promise(resolve => {
- const promiseAllArr = this.list.map((item, index) => this.queryRect(
- `uv-vtabs__bar-item--${index}`, true))
- Promise.all(promiseAllArr).then(sizes => resolve(sizes))
- })
- },
- // 获取各个标签的尺寸
- queryRect(el, item) {
- // #ifndef APP-NVUE
- // $uvGetRect为uv-ui自带的节点查询简化方法,详见文档介绍:https://www.uvui.cn/js/getRect.html
- // 组件内部一般用this.$uvGetRect,对外的为getRect,二者功能一致,名称不同
- return new Promise(resolve => {
- this.$uvGetRect(`.${el}`).then(size => {
- resolve(size)
- })
- })
- // #endif
- // #ifdef APP-NVUE
- // nvue下,使用dom模块查询元素高度
- // 返回一个promise,让调用此方法的主体能使用then回调
- return new Promise(resolve => {
- dom.getComponentRect(item ? this.$refs[el][0] : this.$refs[el], res => {
- resolve(res.size)
- })
- })
- // #endif
- }
- }
- }
- </script>
- <style scoped lang="scss">
- @import '@/uni_modules/uv-ui-tools/libs/css/components.scss';
- @import '@/uni_modules/uv-ui-tools/libs/css/color.scss';
- .uv-vtabs {
- @include flex;
- &__bar {
- background: $uv-bg-color;
- &-item {
- position: relative;
- @include flex;
- align-items: center;
- justify-content: center;
- padding: 35rpx 12rpx 35rpx 20rpx;
- &--value {
- /* #ifdef APP-NVUE */
- padding: 0 12rpx;
- /* #endif */
- font-size: 14px;
- color: $uv-content-color;
- }
- &-active {
- background: #fff;
- &--value {
- color: $uv-primary;
- }
- }
- &--line {
- position: absolute;
- width: 2px;
- left: 0;
- top: 0;
- bottom: 0;
- z-index: 1;
- background-color: $uv-primary;
- }
- &--badge {
- position: absolute;
- top: 4px;
- right: 10px;
- z-index: 1;
- }
- }
- }
- &__content {
- flex: 1;
- background: #fff;
- }
- }
- </style>
|