| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371 |
- <template>
- <scroll-view
- :scroll-x="true"
- :scroll-with-animation="true"
- :show-scrollbar="false"
- :scroll-left="currentScrollPos"
- :style="{
- width: theme.resolveSize(props.width),
- height: theme.resolveSize(props.height),
- ...innerStyle,
- }"
- >
- <view
- class="nana-tabs"
- :style="{
- minWidth: theme.resolveSize(props.width),
- height: theme.resolveSize(props.height),
- }"
- >
- <Touchable
- v-for="(tab, index) in filteredTabs"
- :ref="(ref) => tabsRefs[index] = ref"
- :key="index"
- direction="row"
- :pressedColor="themedUnderlayColor"
- :innerStyle="itemStyle"
- :width="itemWidthArr[index].width > 0 ?
- theme.resolveSize(itemWidthArr[index].width - tabPaddingHorizontal * 2) :
- undefined"
- :touchable="!tab.disabled"
- :flexShrink="0"
- :padding="[ 0, tabPaddingHorizontal ]"
- center
- innerClass="tab-item"
- @click="onTabClick(index)"
- >
- <slot name="tab"
- :tab="tab"
- :index="index"
- :width="itemWidthArr[index].width"
- :active="currentIndex == index"
- >
- <Badge
- content="0"
- v-bind="tab.badgeProps"
- >
- <text
- class="tab-item-text"
- :style="{
- color: tab.disabled ? themedDisableTextColor : (currentIndex == index ? themedActiveTextColor : themedTextColor),
- whiteSpace: props.noWrap ? 'nowrap' : 'normal',
- ...(currentIndex == index ? activeTextStyle : textStyle),
- }"
- >
- {{ tab.text }}
- </text>
- </Badge>
- </slot>
- </Touchable>
- <view
- v-if="showIndicator"
- :style="{
- backgroundColor: theme.resolveThemeColor(activeTextColor),
- ...indicatorStyle,
- width: `${currentIndicatorWidth}px`,
- transform: `translateX(${currentIndicatorPos}px)`,
- }"
- :class="[
- 'tab-indicator',
- indicatorAnim ? 'anim' : '',
- ]"
- />
- </view>
- </scroll-view>
- </template>
- <script setup lang="ts">
- import { computed, nextTick, onMounted, ref, watch } from 'vue';
- import { propGetThemeVar, useTheme, type TextStyle, type ViewStyle } from '../theme/ThemeDefine';
- import type { BadgeProps } from '../display/Badge.vue';
- import Badge from '../display/Badge.vue';
- import Touchable from '../feedback/Touchable.vue';
- export interface TabsItemData {
- /**
- * 标签显示文字
- */
- text: string,
- /**
- * 自定义数据
- */
- data?: any,
- /**
- * 是否显示。
- * @default true
- */
- visible?: boolean;
- /**
- * 是否禁用选择。
- * @default false
- */
- disabled?: boolean,
- /**
- * 红点标记的自定义属性。同 Badge 组件。
- */
- badgeProps?: BadgeProps,
- /**
- * 标签宽度。此设置优先级最高, 会覆盖 Tabs 组件的设置。
- * 如果设置为-1,则根据文字宽度自动调整。
- * 如果设置为 undefined ,则使用 defaultItemWidth 。
- * @default undefined
- */
- width?: number,
- /**
- * 指示器宽度,默认值从 Tabs 组件继承。
- */
- indicatorWidth?: number,
- /**
- * 懒加载?,仅在TabsPage组件中有效
- */
- lazy?: boolean,
- }
- export interface TabsProps {
- /**
- * 标签数据
- */
- tabs: TabsItemData[];
- /**
- * 当前选中的标签位置
- */
- currentIndex: number;
- /**
- * 整个组件的高度
- * @default 90rpx
- */
- height?: number,
- /**
- * 整个组件的宽度
- * @default deviceWidth
- */
- width?: number,
- /**
- * 是否自动根据容器宽度调整标签宽度
- * @default true
- */
- autoItemWidth?: boolean,
- /**
- * 指示器切换标签时是否有动画效果
- * @default true
- */
- indicatorAnim?: boolean,
- /**
- * 是否在用户切换标签后自动滚动至当前选中的条目
- * @default true
- */
- autoScroll?: boolean,
- /**
- * 是否禁止标签文字换行
- * @default true
- */
- noWrap?: boolean,
- /**
- * 标签宽度,在 autoItemWidth 为 false 时有效。
- * 如果设置为-1,则根据文字宽度自动调整。
- * @default 100 (rpx)
- */
- defaultItemWidth?: number,
- /**
- * 默认的指示器宽度。
- * @default itemWidth / 4
- */
- defaultIndicatorWidth?: number,
- /**
- * 标签自定义样式
- */
- itemStyle?: ViewStyle,
- /**
- * 标签正常状态的文字颜色
- * @default text
- */
- textColor?: string,
- /**
- * 标签禁用状态的文字颜色
- * @default grey
- */
- disableTextColor?: string,
- /**
- * 标签正常状态的文字样式
- */
- textStyle?: TextStyle,
- /**
- * 标签激活状态的文字颜色
- * @default primary
- */
- activeTextColor?: string,
- /**
- * 标签激活状态的文字样式
- */
- activeTextStyle?: TextStyle,
- /**
- * 标签按下颜色
- * @default pressed.white
- */
- underlayColor?: string,
- /**
- * 指示器自定义样式
- */
- indicatorStyle?: ViewStyle,
- /**
- * 显示指示器
- * @default true
- */
- showIndicator?: boolean,
- innerStyle?: ViewStyle,
- }
- const emit = defineEmits([ 'update:currentIndex', 'click' ])
- const props = withDefaults(defineProps<TabsProps>(), {
- height: () => propGetThemeVar('TabsDefaultHeight', 90),
- width: () => propGetThemeVar('TabsDefaultWidth', 750),
- autoItemWidth: true,
- indicatorAnim: true,
- showIndicator: true,
- autoScroll: true,
- noWrap: true,
- defaultItemWidth: () => propGetThemeVar('TabsDefaultItemWidth', 120),
- defaultIndicatorWidth: () => propGetThemeVar('TabsDefaultIndicatorWidth', 100),
- textColor: () => propGetThemeVar('TabsTextColor', 'text.title'),
- disableTextColor: () => propGetThemeVar('TabsDisableTextColor', 'grey'),
- activeTextColor: () => propGetThemeVar('TabsActiveTextColor', 'primary'),
- underlayColor: () => propGetThemeVar('TabsUnderlayColor', 'pressed.white'),
- });
- const theme = useTheme();
- const tabPaddingHorizontal = computed(() => theme.getVar('TabsItemPaddingHorizontal', 10));
- const themedUnderlayColor = computed(() => theme.resolveThemeColor(props.underlayColor));
- const themedActiveTextColor = computed(() => theme.resolveThemeColor(props.activeTextColor));
- const themedTextColor = computed(() => theme.resolveThemeColor(props.textColor));
- const themedDisableTextColor = computed(() => theme.resolveThemeColor(props.disableTextColor));
-
- const mersuredTabs = ref<(number|undefined)[]>([]);
- const tabsRefs = ref<any[]>([]);
- const filteredTabs = computed(() => props.tabs.filter(tab => tab.visible !== false));
- const itemWidthArr = computed(() => {
- const result : {
- width: number,
- indicatorWidth: number,
- }[] = [];
- for (let i = 0; i <= filteredTabs.value.length; i++) {
- if (i === filteredTabs.value.length) {
- result.push({
- width: 0,
- indicatorWidth: 0,
- });
- nextTick(() => measureTab(i))
- } else {
- const itemWidth = filteredTabs.value[i].width ||
- (
- props.autoItemWidth ?
- (props.width / filteredTabs.value.length) :
- props.defaultItemWidth
- );
- const itemIndicatorWidth = filteredTabs.value[i].indicatorWidth || props.defaultIndicatorWidth || itemWidth / 4;
- result.push({
- width: itemWidth,
- indicatorWidth: itemIndicatorWidth,
- });
- if (itemWidth <= 0)
- nextTick(() => measureTab(i))
- }
- }
- return result;
- });
- const currentCanMeasure = ref(true);
- const currentScrollPos = ref(0);
- const currentIndicatorPos = ref(0);
- const currentIndicatorWidth = ref(0);
- function measureTab(index: number) {
- if (!currentCanMeasure.value)
- return;
- tabsRefs.value[index]?.measure().then((res: any) => {
- if (res[0])
- mersuredTabs.value[index] = res[0].width;
- else
- mersuredTabs.value[index] = uni.upx2px(props.defaultItemWidth);
- })
- }
- function loadPos() {
- const currentIndex = props.currentIndex;
- const current = itemWidthArr.value[currentIndex];
- if (!current)
- return;
- const itemWidth = current.width >= 0 ? uni.upx2px(current.width) : mersuredTabs.value[currentIndex] || 0;
- const targetWidth = uni.upx2px(current.indicatorWidth);
- let scrollLeft = 0;
- let targetLeft = 0;
- for (let i = currentIndex - 1; i >= 0; i--) {
- const width = itemWidthArr.value[i].width > 0 ? uni.upx2px(itemWidthArr.value[i].width) : mersuredTabs.value[i] || 0;
- targetLeft += width;
- scrollLeft += width;
- }
- if (targetWidth < itemWidth)
- targetLeft += itemWidth / 2 - targetWidth / 2;
- currentIndicatorPos.value = targetLeft;
- currentIndicatorWidth.value = targetWidth;
- currentScrollPos.value = scrollLeft + (currentIndex == itemWidthArr.value.length - 1 ? itemWidth : 0);
- }
- watch(mersuredTabs, loadPos, { deep: true });
- watch(() => props.currentIndex, loadPos);
- onMounted(() => {
- nextTick(() => {
- currentCanMeasure.value = true;
- measureTab(0);
- setTimeout(() => {
- loadPos();
- }, 200);
- });
- })
- function onTabClick(index: number) {
- emit('click', props.tabs[index]);
- if (index !== props.currentIndex) {
- emit('update:currentIndex', index);
- }
- }
- </script>
- <style lang="scss">
- .nana-tabs {
- display: flex;
- flex-direction: row;
- position: relative;
- flex-shrink: 0;
- flex-grow: 0;
- height: auto;
- .tab-item {
- position: relative;
- padding-top: 20rpx;
- padding-bottom: 30rpx;
- box-sizing: content-box;
- }
- .tab-item-text {
- font-size: 15px;
- }
- .tab-indicator {
- position: absolute;
- border-radius: 3rpx;
- left: 0;
- bottom: 4rpx;
- z-index: 10;
- height: 6rpx;
- &.anim {
- transition: all 0.25s ease-in-out;
- }
- }
- }
- </style>
|