495 lines
14 KiB
Vue
495 lines
14 KiB
Vue
|
|
<template>
|
||
|
|
<view
|
||
|
|
:style="tools._style([customStyle])"
|
||
|
|
:class="tools.cls(classPrefix, [placement]) + ' ' + tClass"
|
||
|
|
>
|
||
|
|
<t-sticky
|
||
|
|
:t-class="tools.cls(classPrefix + '__sticky', [placement])"
|
||
|
|
:disabled="!sticky"
|
||
|
|
:z-index="(stickyProps && stickyProps.zIndex) || 1"
|
||
|
|
:offset-top="(stickyProps && stickyProps.offsetTop) || 0"
|
||
|
|
:container="stickyProps && stickyProps.container"
|
||
|
|
@scroll="onTouchScroll"
|
||
|
|
>
|
||
|
|
<view :class="tools.cls(classPrefix + '__wrapper', [theme])">
|
||
|
|
<scroll-view
|
||
|
|
:class="tools.cls(classPrefix + '__scroll', [placement, ['split', split]])"
|
||
|
|
enhanced
|
||
|
|
enable-flex
|
||
|
|
:scroll-left="offset"
|
||
|
|
:scroll-x="true"
|
||
|
|
scroll-anchoring
|
||
|
|
scroll-with-animation
|
||
|
|
enable-passive
|
||
|
|
:show-scrollbar="false"
|
||
|
|
type="list"
|
||
|
|
@scroll="onScroll"
|
||
|
|
>
|
||
|
|
<view
|
||
|
|
:class="tools.cls(classPrefix + '__nav', [placement, ['evenly', spaceEvenly]])"
|
||
|
|
aria-role="tablist"
|
||
|
|
>
|
||
|
|
<view
|
||
|
|
v-for="(item, index) in tabs"
|
||
|
|
:key="index"
|
||
|
|
:data-index="index"
|
||
|
|
:class="
|
||
|
|
tools.cls(classPrefix + '__item', [theme, ['evenly', spaceEvenly], placement, ['disabled', item.disabled], ['active', currentIndex === index]]) +
|
||
|
|
' ' +
|
||
|
|
(currentIndex === index ? tClassActive : '') +
|
||
|
|
' ' +
|
||
|
|
tClassItem
|
||
|
|
"
|
||
|
|
aria-role="tab"
|
||
|
|
:aria-controls="tabID + '_panel_' + index"
|
||
|
|
:aria-selected="currentIndex === index"
|
||
|
|
:aria-disabled="item.disabled"
|
||
|
|
:aria-label="ariaLabel || (item.badgeProps.dot || item.badgeProps.count ? item.label + tools.getBadgeAriaLabel({ ...item.badgeProps }) : '')"
|
||
|
|
@click="onTabTap"
|
||
|
|
>
|
||
|
|
<view
|
||
|
|
:class="tools.cls(classPrefix + '__item-inner', [theme, ['active', currentIndex === index]])"
|
||
|
|
:aria-hidden="item.badgeProps.dot || item.badgeProps.count"
|
||
|
|
>
|
||
|
|
<block
|
||
|
|
v-if="item.icon"
|
||
|
|
name="icon"
|
||
|
|
>
|
||
|
|
<t-icon
|
||
|
|
:t-class="classPrefix + '__icon'"
|
||
|
|
:custom-style="getIconCustomStyle(item)"
|
||
|
|
:prefix="item.icon.prefix"
|
||
|
|
:name="item.icon.name"
|
||
|
|
:size="item.icon.size"
|
||
|
|
:color="item.icon.color"
|
||
|
|
:aria-hidden="!!item.icon.ariaHidden"
|
||
|
|
:aria-label="item.icon.ariaLabel"
|
||
|
|
:aria-role="item.icon.ariaRole"
|
||
|
|
@click="item.icon.click || ''"
|
||
|
|
/>
|
||
|
|
</block>
|
||
|
|
<block v-if="item.badgeProps">
|
||
|
|
<t-badge
|
||
|
|
:color="item.badgeProps.color || ''"
|
||
|
|
:content="item.label"
|
||
|
|
:count="item.badgeProps.count || 0"
|
||
|
|
:dot="item.badgeProps.dot || false"
|
||
|
|
:max-count="item.badgeProps.maxCount || 99"
|
||
|
|
:offset="item.badgeProps.offset || []"
|
||
|
|
:shape="item.badgeProps.shape || 'circle'"
|
||
|
|
:show-zero="item.badgeProps.showZero || false"
|
||
|
|
:size="item.badgeProps.size || 'medium'"
|
||
|
|
:t-class="
|
||
|
|
tools.cls(classPrefix + '__badge', [
|
||
|
|
['disabled', item.disabled],
|
||
|
|
['active', currentIndex === index]
|
||
|
|
])
|
||
|
|
"
|
||
|
|
:custom-style="getBadgeCustomStyle(item, index)"
|
||
|
|
:t-class-content="item.badgeProps.tClassContent"
|
||
|
|
:t-class-count="item.badgeProps.tClassCount"
|
||
|
|
/>
|
||
|
|
</block>
|
||
|
|
<block v-else>
|
||
|
|
{{ item.label }}
|
||
|
|
</block>
|
||
|
|
</view>
|
||
|
|
|
||
|
|
<view
|
||
|
|
v-if="theme == 'card' && currentIndex - 1 == index"
|
||
|
|
:class="classPrefix + '__item-prefix'"
|
||
|
|
/>
|
||
|
|
|
||
|
|
<view
|
||
|
|
v-if="theme == 'card' && currentIndex + 1 == index"
|
||
|
|
:class="classPrefix + '__item-suffix'"
|
||
|
|
/>
|
||
|
|
</view>
|
||
|
|
<view
|
||
|
|
v-if="theme == 'line' && showBottomLine"
|
||
|
|
:class="tools.cls(classPrefix + '__track', [placement]) + ' ' + tClassTrack"
|
||
|
|
:style="trackStyle(trackOption)"
|
||
|
|
/>
|
||
|
|
</view>
|
||
|
|
</scroll-view>
|
||
|
|
</view>
|
||
|
|
</t-sticky>
|
||
|
|
<slot name="middle" />
|
||
|
|
<view
|
||
|
|
:class="tools.cls(classPrefix + '__content', [['animated', animation]])"
|
||
|
|
@touchstart="onTouchStart"
|
||
|
|
@touchmove="onTouchMove"
|
||
|
|
@touchend="onTouchEnd"
|
||
|
|
@touchcancel="onTouchEnd"
|
||
|
|
>
|
||
|
|
<view
|
||
|
|
:class="classPrefix + '__content-inner ' + tClassContent"
|
||
|
|
:style="animate({ duration: animation && animation.duration, currentIndex: currentIndex })"
|
||
|
|
>
|
||
|
|
<slot />
|
||
|
|
</view>
|
||
|
|
</view>
|
||
|
|
</view>
|
||
|
|
</template>
|
||
|
|
<script>
|
||
|
|
import TSticky from '../sticky/sticky';
|
||
|
|
import TBadge from '../badge/badge';
|
||
|
|
import TIcon from '../icon/icon';
|
||
|
|
import { uniComponent } from '../common/src/index';
|
||
|
|
import props from './props';
|
||
|
|
import { prefix } from '../common/config';
|
||
|
|
import touch from '../mixins/touch';
|
||
|
|
import { getRect, uniqueFactory, coalesce, nextTick } from '../common/utils';
|
||
|
|
import { getObserver } from '../common/wechat';
|
||
|
|
import tools from '../common/utils.wxs';
|
||
|
|
import { ParentMixin, RELATION_MAP } from '../common/relation';
|
||
|
|
import { animate, trackStyle } from './computed';
|
||
|
|
|
||
|
|
const name = `${prefix}-tabs`;
|
||
|
|
const getUniqueID = uniqueFactory('tabs');
|
||
|
|
|
||
|
|
|
||
|
|
export default uniComponent({
|
||
|
|
name,
|
||
|
|
options: {
|
||
|
|
styleIsolation: 'shared',
|
||
|
|
},
|
||
|
|
controlledProps: [{
|
||
|
|
key: 'value',
|
||
|
|
event: 'change',
|
||
|
|
}],
|
||
|
|
externalClasses: [
|
||
|
|
`${prefix}-class`,
|
||
|
|
`${prefix}-class-item`,
|
||
|
|
`${prefix}-class-active`,
|
||
|
|
`${prefix}-class-track`,
|
||
|
|
`${prefix}-class-content`,
|
||
|
|
],
|
||
|
|
mixins: [touch, ParentMixin(RELATION_MAP.TabPanel)],
|
||
|
|
components: {
|
||
|
|
TSticky,
|
||
|
|
TBadge,
|
||
|
|
TIcon,
|
||
|
|
},
|
||
|
|
props: {
|
||
|
|
...props,
|
||
|
|
},
|
||
|
|
emits: [
|
||
|
|
'change',
|
||
|
|
'scroll',
|
||
|
|
'error',
|
||
|
|
'click',
|
||
|
|
],
|
||
|
|
watch: {
|
||
|
|
value: {
|
||
|
|
handler(e) {
|
||
|
|
this.dataValue = e;
|
||
|
|
},
|
||
|
|
immediate: true,
|
||
|
|
},
|
||
|
|
dataValue(name) {
|
||
|
|
if (name !== this.getCurrentName()) {
|
||
|
|
this.setCurrentIndexByName(name);
|
||
|
|
}
|
||
|
|
},
|
||
|
|
},
|
||
|
|
created() {
|
||
|
|
this.children = this.children || [];
|
||
|
|
},
|
||
|
|
|
||
|
|
mounted() {
|
||
|
|
nextTick().then(() => {
|
||
|
|
this.setTrack();
|
||
|
|
});
|
||
|
|
getRect(this, `.${name}`).then((rect) => {
|
||
|
|
this.containerWidth = rect.width;
|
||
|
|
});
|
||
|
|
this.tabID = getUniqueID();
|
||
|
|
},
|
||
|
|
data() {
|
||
|
|
return {
|
||
|
|
prefix,
|
||
|
|
classPrefix: name,
|
||
|
|
tabs: [],
|
||
|
|
currentLabels: [],
|
||
|
|
currentIndex: -1,
|
||
|
|
trackOption: {
|
||
|
|
lineWidth: 0,
|
||
|
|
distance: 0,
|
||
|
|
isInit: true,
|
||
|
|
},
|
||
|
|
offset: 0,
|
||
|
|
scrollLeft: 0,
|
||
|
|
tabID: '',
|
||
|
|
placement: 'top',
|
||
|
|
tools,
|
||
|
|
|
||
|
|
dataValue: coalesce(this.value, this.defaultValue),
|
||
|
|
};
|
||
|
|
},
|
||
|
|
methods: {
|
||
|
|
trackStyle,
|
||
|
|
animate,
|
||
|
|
innerAfterLinked(target) {
|
||
|
|
// mixin 中已注入
|
||
|
|
// this.children.push(target);
|
||
|
|
this.initChildId();
|
||
|
|
target.dataIndex = this.children.length - 1;
|
||
|
|
this.updateTabs();
|
||
|
|
},
|
||
|
|
innerAfterUnlinked(target) {
|
||
|
|
this.children = this.children.filter(item => item.index !== target.dataIndex);
|
||
|
|
this.updateTabs(() => this.setTrack());
|
||
|
|
this.initChildId();
|
||
|
|
},
|
||
|
|
initChildId() {
|
||
|
|
this.children.forEach((item, index) => {
|
||
|
|
item.setId(`${this.tabID}_panel_${index}`);
|
||
|
|
});
|
||
|
|
},
|
||
|
|
onScroll(e = {}) {
|
||
|
|
const { scrollLeft } = e.detail || {};
|
||
|
|
this.scrollLeft = scrollLeft;
|
||
|
|
},
|
||
|
|
updateTabs(cb) {
|
||
|
|
const { children } = this;
|
||
|
|
const tabs = children.map((child) => {
|
||
|
|
const { label, badgeProps, disabled, icon, panel, value, lazy } = child;
|
||
|
|
return {
|
||
|
|
label, badgeProps, disabled, icon, panel, value, lazy,
|
||
|
|
};
|
||
|
|
});
|
||
|
|
|
||
|
|
tabs.forEach((item) => {
|
||
|
|
if (typeof item.icon === 'string') {
|
||
|
|
item.icon = { name: item.icon };
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
this.tabs = tabs;
|
||
|
|
if (typeof cb === 'function') {
|
||
|
|
setTimeout(cb, 33);
|
||
|
|
}
|
||
|
|
|
||
|
|
this.setCurrentIndexByName(this.dataValue);
|
||
|
|
},
|
||
|
|
|
||
|
|
setCurrentIndexByName(name) {
|
||
|
|
const { children } = this;
|
||
|
|
const index = children.findIndex(child => child.getComputedName() === `${name}`);
|
||
|
|
if (index > -1) {
|
||
|
|
this.setCurrentIndex(index);
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
setCurrentIndex(index) {
|
||
|
|
if (index <= -1 || index >= this.children.length) return;
|
||
|
|
const Labels = [];
|
||
|
|
this.children.forEach((child, idx) => {
|
||
|
|
const isActive = index === idx;
|
||
|
|
if (isActive !== child.active || !child.initialized) {
|
||
|
|
child.render(isActive, this);
|
||
|
|
}
|
||
|
|
Labels.push(child.label);
|
||
|
|
});
|
||
|
|
|
||
|
|
const { currentIndex, currentLabels } = this;
|
||
|
|
if (currentIndex === index && currentLabels.join('') === Labels.join('')) return;
|
||
|
|
|
||
|
|
this.currentIndex = index;
|
||
|
|
this.currentLabels = Labels;
|
||
|
|
|
||
|
|
|
||
|
|
setTimeout(() => {
|
||
|
|
this.setTrack();
|
||
|
|
}, 33);
|
||
|
|
},
|
||
|
|
|
||
|
|
getCurrentName() {
|
||
|
|
if (this.children) {
|
||
|
|
const activeTab = this.children[this.currentIndex];
|
||
|
|
if (activeTab) {
|
||
|
|
return activeTab.getComputedName();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
calcScrollOffset(containerWidth, targetLeft, targetWidth, offset) {
|
||
|
|
return offset + targetLeft - (1 / 2) * containerWidth + targetWidth / 2;
|
||
|
|
},
|
||
|
|
|
||
|
|
// 外部无法获取虚拟组件节点位置信息
|
||
|
|
getTabHeight() {
|
||
|
|
return getRect(this, `.${name}`);
|
||
|
|
},
|
||
|
|
|
||
|
|
getTrackSize() {
|
||
|
|
const { bottomLineMode } = this;
|
||
|
|
const targetMap = {
|
||
|
|
fixed: `.${prefix}-tabs__track`,
|
||
|
|
auto: `.${prefix}-tabs__item--active .${prefix}-tabs__item-inner`,
|
||
|
|
full: `.${prefix}-tabs__item--active`,
|
||
|
|
};
|
||
|
|
return new Promise((resolve, reject) => {
|
||
|
|
if (this.trackWidth) {
|
||
|
|
resolve(this.trackWidth);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
getRect(this, targetMap[bottomLineMode] || targetMap.fixed)
|
||
|
|
.then((res) => {
|
||
|
|
if (res) {
|
||
|
|
resolve(res.width);
|
||
|
|
}
|
||
|
|
})
|
||
|
|
.catch(reject);
|
||
|
|
});
|
||
|
|
},
|
||
|
|
|
||
|
|
async setTrack() {
|
||
|
|
const { children } = this;
|
||
|
|
if (!children) return;
|
||
|
|
const { currentIndex } = this;
|
||
|
|
if (currentIndex <= -1) return;
|
||
|
|
|
||
|
|
try {
|
||
|
|
const res = await getRect(this, `.${prefix}-tabs__item`, true);
|
||
|
|
const rect = res[currentIndex];
|
||
|
|
if (!rect) return;
|
||
|
|
let count = 0;
|
||
|
|
let distance = 0;
|
||
|
|
let totalSize = 0;
|
||
|
|
|
||
|
|
res.forEach((item) => {
|
||
|
|
if (count < currentIndex) {
|
||
|
|
distance += item.width;
|
||
|
|
count += 1;
|
||
|
|
}
|
||
|
|
totalSize += item.width;
|
||
|
|
});
|
||
|
|
|
||
|
|
if (this.containerWidth) {
|
||
|
|
const offset = this.calcScrollOffset(this.containerWidth, rect.left, rect.width, this.scrollLeft);
|
||
|
|
const maxOffset = totalSize - this.containerWidth;
|
||
|
|
this.offset = Math.min(Math.max(offset, 0), maxOffset);
|
||
|
|
} else if (!this._hasObserved) {
|
||
|
|
this._hasObserved = true;
|
||
|
|
getObserver(this, `.${name}`).then(() => this.setTrack());
|
||
|
|
}
|
||
|
|
|
||
|
|
const lineWidth = await this.getTrackSize();
|
||
|
|
if (this.theme === 'line') {
|
||
|
|
distance += (rect.width - lineWidth) / 2;
|
||
|
|
}
|
||
|
|
|
||
|
|
const isInit = this.previousIndex === undefined;
|
||
|
|
if (isInit
|
||
|
|
|| this.previousIndex !== currentIndex
|
||
|
|
|| this.lastDistance !== distance
|
||
|
|
) {
|
||
|
|
this.previousIndex = currentIndex;
|
||
|
|
this.trackOption = { lineWidth, distance, isInit };
|
||
|
|
this.lastDistance = distance;
|
||
|
|
}
|
||
|
|
} catch (err) {
|
||
|
|
this.$emit('error', err);
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
onTabTap(event) {
|
||
|
|
const { index } = event.currentTarget.dataset;
|
||
|
|
|
||
|
|
this.changeIndex(index);
|
||
|
|
},
|
||
|
|
|
||
|
|
onTouchStart(event) {
|
||
|
|
if (!this.swipeable) return;
|
||
|
|
|
||
|
|
this.touchStart(event);
|
||
|
|
},
|
||
|
|
|
||
|
|
onTouchMove(event) {
|
||
|
|
if (!this.swipeable) return;
|
||
|
|
|
||
|
|
this.touchMove(event);
|
||
|
|
},
|
||
|
|
|
||
|
|
onTouchEnd() {
|
||
|
|
if (!this.swipeable) return;
|
||
|
|
|
||
|
|
const { direction, deltaX, offsetX } = this;
|
||
|
|
const minSwipeDistance = 50;
|
||
|
|
if (direction === 'horizontal' && offsetX >= minSwipeDistance) {
|
||
|
|
const index = this.getAvailableTabIndex(deltaX);
|
||
|
|
if (index !== -1) {
|
||
|
|
this.changeIndex(index);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
onTouchScroll(event) {
|
||
|
|
this._trigger('scroll', event);
|
||
|
|
},
|
||
|
|
|
||
|
|
changeIndex(index) {
|
||
|
|
const currentTab = this.tabs[index];
|
||
|
|
const { value, label } = currentTab;
|
||
|
|
if (!currentTab?.disabled && index !== this.currentIndex) {
|
||
|
|
this._trigger('change', { value, label });
|
||
|
|
}
|
||
|
|
this._trigger('click', { value, label });
|
||
|
|
},
|
||
|
|
|
||
|
|
getAvailableTabIndex(deltaX) {
|
||
|
|
const step = deltaX > 0 ? -1 : 1;
|
||
|
|
const { currentIndex, tabs } = this;
|
||
|
|
const len = tabs.length;
|
||
|
|
for (let i = step; currentIndex + step >= 0 && currentIndex + step < len; i += step) {
|
||
|
|
const newIndex = currentIndex + i;
|
||
|
|
if (newIndex >= 0 && newIndex < len && tabs[newIndex]) {
|
||
|
|
if (!tabs[newIndex].disabled) {
|
||
|
|
return newIndex;
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
return currentIndex;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return -1;
|
||
|
|
},
|
||
|
|
|
||
|
|
getBadgeCustomStyle(item, index) {
|
||
|
|
if (item.disabled) {
|
||
|
|
return '--td-badge-content-text-color: var(--td-tab-item-disabled-color, var(--td-text-color-disabled, var(--td-font-gray-4, rgba(0, 0, 0, .26))))';
|
||
|
|
}
|
||
|
|
if (this.currentIndex === index) {
|
||
|
|
return '--td-badge-content-text-color: var(--td-tab-item-active-color, var(--td-brand-color, var(--td-primary-color-7, #0052d9)));';
|
||
|
|
}
|
||
|
|
return '';
|
||
|
|
},
|
||
|
|
|
||
|
|
getIconCustomStyle(item) {
|
||
|
|
return tools._style([
|
||
|
|
{
|
||
|
|
fontSize: 'var(--td-tab-icon-size, 18px)',
|
||
|
|
marginRight: 'calc(var(--td-spacer, 8px) / 4)',
|
||
|
|
},
|
||
|
|
item.icon.style || '',
|
||
|
|
]);
|
||
|
|
},
|
||
|
|
},
|
||
|
|
});
|
||
|
|
</script>
|
||
|
|
<style scoped>
|
||
|
|
@import './tabs.css';
|
||
|
|
|
||
|
|
/* #ifndef MP-WEIXIN */
|
||
|
|
:deep(t-tab-panel) {
|
||
|
|
flex-shrink: 0;
|
||
|
|
width: 100%;
|
||
|
|
height: 100%;
|
||
|
|
box-sizing: border-box;
|
||
|
|
overflow-y: auto;
|
||
|
|
-webkit-overflow-scrolling: touch;
|
||
|
|
}
|
||
|
|
/* #endif */
|
||
|
|
</style>
|