first commit

This commit is contained in:
lingxiao865
2026-02-10 08:05:03 +08:00
commit c5af079d8c
1094 changed files with 97530 additions and 0 deletions

View File

@@ -0,0 +1,494 @@
<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>