This commit is contained in:
Ling53666
2025-08-18 09:11:51 +08:00
commit 02554225da
2516 changed files with 133155 additions and 0 deletions

View File

@ -0,0 +1,157 @@
<view
a:if="{{direction !== 'vertical'}}"
class="ant-tabs {{className ? className : ''}}"
style="{{style}}"
>
<view class="ant-tabs-bar {{type === 'basic' ? 'ant-tabs-bar-underline' : ''}} {{tabsBarClassName ? tabsBarClassName : ''}}">
<view class="ant-tabs-bar-plus">
<slot name="plus"></slot>
</view>
<view
class="ant-tabs-bar-fade ant-tabs-bar-fade-left"
style="opacity: {{leftFade ? '1' : '0'}}"
></view>
<view
class="ant-tabs-bar-fade ant-tabs-bar-fade-right"
style="opacity: {{rightFade ? '1' : '0'}}"
></view>
<scroll-view
class="ant-tabs-bar-scroll-view"
id="ant-tabs-bar-scroll-view{{$id ? '-' + $id : ''}}"
onScroll="onScroll"
scrollLeft="{{scrollLeft}}"
scrollX="{{true}}"
scrollWithAnimation="{{true}}"
scrollAnimationDuration="{{300}}"
>
<view></view>
<block
a:for="{{items}}"
a:for-index="index"
a:for-item="item"
>
<view
id="ant-tabs-bar-item{{$id ? '-' + $id : ''}}-{{index}}"
class="ant-tabs-bar-wrap ant-tabs-bar-wrap-{{type}} {{tabsBarClassName ? tabsBarClassName : ''}}"
>
<view
a:if="{{type === 'basic'}}"
class="ant-tabs-bar-item ant-tabs-bar-basic {{tabClassName ? tabClassName : ''}} {{mixin.value === index && !item.disabled ? 'ant-tabs-bar-active' : ''}} {{item.disabled ? 'ant-tabs-bar-disabled' : ''}} {{mixin.value === index && !item.disabled && tabActiveClassName ? tabActiveClassName : ''}}"
onTap="onChange"
data-index="{{index}}"
>
<view class="ant-tabs-bar-basic-title">
<slot
name="title"
value="{{item}}"
index="{{index}}"
>
{{item.title}}
</slot>
</view>
</view>
<view
a:elif="{{type === 'capsule'}}"
class="ant-tabs-bar-item ant-tabs-bar-capsule {{tabClassName ? tabClassName : ''}} {{mixin.value === index && !item.disabled ? 'ant-tabs-bar-active' : ''}} {{item.disabled ? 'ant-tabs-bar-disabled' : ''}} {{mixin.value === index && !item.disabled && tabActiveClassName ? tabActiveClassName : ''}}"
onTap="onChange"
data-index="{{index}}"
>
<view class="ant-tabs-bar-capsule-title">
<slot
name="title"
value="{{item}}"
index="{{index}}"
>
{{item.title}}
</slot>
</view>
</view>
<view
a:else
class="ant-tabs-bar-item ant-tabs-bar-mixin {{tabClassName ? tabClassName : ''}} {{mixin.value === index && !item.disabled ? 'ant-tabs-bar-active' : ''}} {{item.disabled ? 'ant-tabs-bar-disabled' : ''}} {{mixin.value === index && !item.disabled && tabActiveClassName ? tabActiveClassName : ''}}"
onTap="onChange"
data-index="{{index}}"
>
<view class="ant-tabs-bar-mixin-title">
<slot
name="title"
value="{{item}}"
index="{{index}}"
>
{{item.title}}
</slot>
</view>
<view class="ant-tabs-bar-mixin-subtitle">
<slot
name="subTitle"
value="{{item}}"
index="{{index}}"
>
{{item.subTitle}}
</slot>
</view>
</view>
</view>
</block>
<view></view>
</scroll-view>
</view>
<view class="ant-tabs-content">
<slot
value="{{items[mixin.value]}}"
index="{{mixin.value}}"
></slot>
</view>
</view>
<view
a:else
class="ant-vtabs {{className ? className : ''}}"
style="{{style}}"
>
<view class="ant-vtabs-bar {{tabsBarClassName ? tabsBarClassName : ''}}">
<scroll-view
class="ant-vtabs-bar-scroll-view"
id="ant-tabs-bar-scroll-view{{$id ? '-' + $id : ''}}"
onScroll="onScroll"
scrollTop="{{scrollTop}}"
scrollY="{{true}}"
scrollWithAnimation="{{true}}"
scrollAnimationDuration="{{300}}"
>
<view class="ant-vtabs-bar-item-wrap">
<block
a:for="{{items}}"
a:for-index="index"
a:for-item="item"
>
<view
id="ant-tabs-bar-item{{$id ? '-' + $id : ''}}-{{index}}"
class="ant-vtabs-bar-item {{tabClassName ? tabClassName : ''}} {{mixin.value === index && !item.disabled ? 'ant-vtabs-bar-item-active' : ''}} {{item.disabled ? 'ant-vtabs-bar-item-disabled' : ''}} {{mixin.value === index && !item.disabled && tabActiveClassName ? tabActiveClassName : ''}}"
style="{{mixin.value + 1 === index ? 'border-radius: 0 16rpx 0 0' : ''}};{{mixin.value - 1 === index ? 'border-radius: 0 0 16rpx 0' : ''}}"
onTap="onChange"
data-index="{{index}}"
>
<!--display: inline-->
<text class="ant-vtabs-bar-item-title">
<slot
name="title"
value="{{item}}"
index="{{index}}"
>
{{item.title}}
</slot>
</text>
</view>
</block>
</view>
</scroll-view>
</view>
<view class="ant-vtabs-content">
<slot
value="{{items[mixin.value]}}"
index="{{mixin.value}}"
>
{{items[mixin.value].content}}
</slot>
</view>
</view>

View File

@ -0,0 +1 @@
export {};

View File

@ -0,0 +1,185 @@
import { __awaiter, __generator } from "tslib";
import { Component, triggerEvent, getValueFromProps } from '../_util/simply';
import { TabsDefaultProps } from './props';
import createValue from '../mixins/value';
import { getInstanceBoundingClientRect } from '../_util/jsapi/get-instance-bounding-client-rect';
Component(TabsDefaultProps, {
getInstance: function () {
if (this.$id) {
return my;
}
return this;
},
get$Id: function () {
return this.$id ? "-".concat(this.$id) : '';
},
getBoundingClientRect: function (query) {
return __awaiter(this, void 0, void 0, function () {
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, getInstanceBoundingClientRect(this.getInstance(), query)];
case 1: return [2 /*return*/, _a.sent()];
}
});
});
},
updateFade: function () {
return __awaiter(this, void 0, void 0, function () {
var items, _a, view, item;
return __generator(this, function (_b) {
switch (_b.label) {
case 0:
this.setData({
leftFade: !!this.scrollLeft,
});
items = getValueFromProps(this, 'items');
return [4 /*yield*/, Promise.all([
this.getBoundingClientRect("#ant-tabs-bar-scroll-view".concat(this.get$Id())),
this.getBoundingClientRect("#ant-tabs-bar-item".concat(this.get$Id(), "-").concat(items.length - 1)),
])];
case 1:
_a = _b.sent(), view = _a[0], item = _a[1];
if (!item || !view) {
return [2 /*return*/];
}
this.setData({
rightFade: item.left + item.width / 2 > view.width,
});
return [2 /*return*/];
}
});
});
},
updateScroll: function () {
return __awaiter(this, void 0, void 0, function () {
var current, _a, view, item, _b, direction, scrollMode, scrollTop, needScroll_1, distance, scrollLeft, needScroll, distance;
return __generator(this, function (_c) {
switch (_c.label) {
case 0:
current = this.getValue();
return [4 /*yield*/, Promise.all([
this.getBoundingClientRect("#ant-tabs-bar-scroll-view".concat(this.get$Id())),
this.getBoundingClientRect("#ant-tabs-bar-item".concat(this.get$Id(), "-").concat(current)),
])];
case 1:
_a = _c.sent(), view = _a[0], item = _a[1];
if (!view || !item) {
return [2 /*return*/];
}
_b = getValueFromProps(this, [
'direction',
'scrollMode',
]), direction = _b[0], scrollMode = _b[1];
if (direction === 'vertical') {
scrollTop = this.scrollTop || 0;
needScroll_1 = false;
if (scrollMode === 'center') {
needScroll_1 = true;
scrollTop +=
item.top - view.top - Math.max((view.height - item.height) / 2, 0);
}
else {
distance = item.top - view.top;
if (distance < 0) {
scrollTop += distance;
needScroll_1 = true;
}
else if (distance + item.height > view.height) {
scrollTop += Math.min(distance + item.height - view.height, distance);
needScroll_1 = true;
}
}
if (needScroll_1) {
if (scrollTop === this.data.scrollTop) {
scrollTop += Math.random();
}
this.setData({
scrollTop: scrollTop,
});
}
return [2 /*return*/];
}
scrollLeft = this.scrollLeft || 0;
needScroll = false;
if (scrollMode === 'center') {
needScroll = true;
scrollLeft +=
item.left - view.left - Math.max((view.width - item.width) / 2, 0);
}
else {
distance = item.left - view.left;
if (distance < 0) {
scrollLeft += distance;
needScroll = true;
}
else if (distance + item.width > view.width) {
scrollLeft += Math.min(distance + item.width - view.width, distance);
needScroll = true;
}
}
if (needScroll) {
if (scrollLeft === this.data.scrollLeft) {
scrollLeft += Math.random();
}
this.setData({
scrollLeft: scrollLeft,
});
this.updateFade();
}
return [2 /*return*/];
}
});
});
},
onScroll: function (e) {
return __awaiter(this, void 0, void 0, function () {
var direction;
return __generator(this, function (_a) {
direction = getValueFromProps(this, 'direction');
if (direction === 'vertical') {
this.scrollTop = e.detail.scrollTop;
return [2 /*return*/];
}
this.scrollLeft = e.detail.scrollLeft;
this.updateFade();
return [2 /*return*/];
});
});
},
onChange: function (e) {
var index = parseInt(e.currentTarget.dataset.index, 10);
var items = getValueFromProps(this, 'items');
if (items[index].disabled) {
return;
}
if (this.getValue() === index) {
return;
}
if (!this.isControlled()) {
this.update(index);
}
triggerEvent(this, 'change', index, e);
},
}, {
scrollLeft: 0,
scrollTop: 0,
leftFade: false,
rightFade: false,
}, [
createValue({
valueKey: 'current',
defaultValueKey: 'defaultCurrent',
}),
], {
scrollLeft: 0,
scrollTop: 0,
didMount: function () {
this.updateScroll();
},
didUpdate: function (prevProps, prevData) {
var items = getValueFromProps(this, 'items');
if (prevProps.items !== items || !this.isEqualValue(prevData)) {
this.updateScroll();
}
},
});

View File

@ -0,0 +1,3 @@
{
"component": true
}

View File

@ -0,0 +1,281 @@
@import (reference) './variable.less';
@tabsPrefix: ant-tabs;
.@{tabsPrefix} {
width: 100%;
&-bar {
position: relative;
width: 100%;
display: flex;
align-content: stretch;
background-color: @tabs-inverse-color;
box-sizing: border-box;
&-underline {
border-bottom: @border-width-standard solid @COLOR_BORDER;
}
&-fade {
position: absolute;
top: 0;
bottom: 0;
z-index: 9;
width: 36 * @rpx;
background-color: @tabs-inverse-color;
pointer-events: none;
&-left {
left: 0;
background: linear-gradient(
90deg,
@COLOR_WHITE_CHANGE,
hsla(0, 0%, 100%, 0)
);
}
&-right {
right: 84 * @rpx;
background: linear-gradient(
270deg,
@COLOR_WHITE_CHANGE,
hsla(0, 0%, 100%, 0)
);
}
transition: all 0.2s;
}
&-plus {
display: flex;
width: 72 * @rpx;
height: 72 * @rpx;
color: @COLOR_BLACK_CHANGE;
align-self: center;
align-items: center;
justify-content: center;
overflow: hidden;
margin-right: @h-spacing-large;
order: 1;
&:empty {
display: none;
~ .@{tabsPrefix}-bar-fade-right {
right: 0;
}
}
}
&-scroll-view {
width: 100%;
display: flex;
flex-direction: row;
justify-content: space-between;
flex-wrap: nowrap;
overflow: hidden;
will-change: auto;
&::-webkit-scrollbar {
display: none;
}
}
// 选项卡最后两项间距控制
&-wrap {
display: flex;
flex-shrink: 0;
&-capsule {
flex: 1;
}
&:nth-of-type(2) .@{tabsPrefix}-bar-capsule,
&:nth-of-type(2) .@{tabsPrefix}-bar-mixin {
margin-left: @h-spacing-large;
}
&:nth-last-of-type(2) .@{tabsPrefix}-bar-capsule,
&:nth-last-of-type(2) .@{tabsPrefix}-bar-mixin {
margin-right: @h-spacing-large;
}
}
// 选项卡的通用样式
&-item {
display: flex;
align-items: center;
justify-content: center;
flex: 1 0 auto;
font-size: @font-size-list;
white-space: nowrap;
color: @tabs-basic-color;
.ant-badge {
width: 100%;
}
}
// 基础类型 tab 选项卡样式
&-basic {
position: relative;
display: flex;
align-items: center;
padding: @v-spacing-standard @h-spacing-large;
box-sizing: border-box;
}
&-basic&-active {
color: @tabs-active-color;
position: relative;
&:after {
content: '';
position: absolute;
bottom: 0;
width: calc(100% - @h-spacing-large * 2);
height: 4 * @rpx;
overflow: hidden;
font-size: 0;
border-radius: @corner-radius-circle;
box-sizing: border-box;
background-color: @tabs-active-decorate-color;
}
}
// 胶囊类型选项卡样式
&-capsule {
padding: @v-spacing-large 0;
margin: 0 @h-spacing-large / 2;
box-sizing: border-box;
position: relative;
&-title {
flex: 1;
justify-content: center;
margin-right: 0;
padding: @v-spacing-standard 30 * @rpx;
font-size: 30 * @rpx;
text-align: center;
line-height: @line-height-base;
border-radius: 10vh;
background-color: @COLOR_GREY_CARD;
border-bottom: 0 none;
}
}
&-capsule&-active &-capsule-title {
color: @COLOR_WHITE;
background-color: @tabs-active-decorate-color;
}
// 混合类型的选项卡样式
&-mixin {
flex-direction: column;
margin: 0 @h-spacing-standard;
padding: @v-spacing-large 0;
text-align: center;
&-title {
margin: 2 * @rpx 0;
position: relative;
}
&-subtitle {
padding: 0 @h-spacing-standard;
font-size: @font-size-content;
line-height: @line-height-paragraph;
color: @tabs-weaken-color;
border-radius: 50vh;
background-color: @COLOR_GREY_CARD;
margin: 2 * @rpx 0;
}
}
&-mixin&-active &-mixin-title {
color: @tabs-active-color;
}
&-mixin&-active &-mixin-subtitle {
color: @COLOR_WHITE;
background-color: @tabs-active-decorate-color;
}
// 禁用态选项卡样式
&-disabled {
opacity: @opacity-disabled;
pointer-events: none;
cursor: not-allowed;
}
}
&-content {
overflow: hidden;
background-color: @COLOR_WHITE_CHANGE;
color: @COLOR_TEXT_PRIMARY;
}
}
@vtabsPrefix: ant-vtabs;
.@{vtabsPrefix} {
width: 100%;
display: flex;
flex-direction: row;
&-bar {
width: 210 * @rpx;
overflow: hidden;
&-scroll-view {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
background-color: @COLOR_GREY_CARD;
&::-webkit-scrollbar {
display: none;
}
}
&-item {
position: relative;
width: 100%;
padding: @size-4 @h-spacing-large;
overflow: hidden;
display: flex;
align-items: center;
font-size: @font-size-content;
line-height: 37 * @rpx;
color: @COLOR_TEXT_PRIMARY;
background-color: @COLOR_GREY_CARD;
box-sizing: border-box;
transition: background-color 100ms linear, color 300ms linear;
&-wrap {
background-color: @COLOR_CARD;
}
&-active {
color: @COLOR_BRAND1;
background-color: @COLOR_CARD;
&::after {
position: absolute;
content: '';
top: @size-4;
bottom: @size-4;
left: 0;
width: @border-width-thick;
border-radius: @corner-radius-sm;
background-color: @COLOR_BRAND1;
}
}
&-disabled {
pointer-events: none;
cursor: not-allowed;
.@{vtabsPrefix}-bar-item-title,
.@{vtabsPrefix}-bar-item-count {
opacity: @opacity-disabled;
}
}
&-count {
color: @COLOR_TEXT_WEAK;
margin-left: @size-1;
}
&-icon {
padding-left: 8 * @rpx;
width: 28 * @rpx;
height: 28 * @rpx;
font-size: 26 * @rpx;
overflow: hidden;
position: relative;
top: -4 * @rpx;
display: flex;
justify-content: center;
align-items: center;
&:empty {
display: none;
}
}
}
}
&-content {
overflow: hidden;
flex: 1;
background-color: @COLOR_CARD;
color: @COLOR_TEXT_PRIMARY;
}
}

View File

@ -0,0 +1,50 @@
import { IBaseProps } from '../_util/base';
export interface ITabsProps extends IBaseProps {
/**
* @description 类型basic(基础)capsule(胶囊)mixin(混合)
* @default "basic"
*/
type: 'basic' | 'capsule' | 'mixin';
/**
* @description tabs方向
*/
direction: 'horizontal' | 'vertical';
/**
* @description tab 切换时的回调
*/
onChange: (index: number, e: Record<string, any>) => void;
/**
* @description 选项
*/
current: number;
/**
* @description 列表
*/
items: {
title: string;
content: string;
disabled?: boolean;
subTitle?: string;
}[];
/**
* 选项初始值
*/
defaultCurrent: number;
/**
* 滚动方式direction为horizontal生效
*/
scrollMode: 'edge' | 'center';
/**
* @description tabs bar类名
*/
tabsBarClassName?: string;
/**
* @description tab类名
*/
tabClassName?: string;
/**
*@description tab active类名
*/
tabActiveClassName?: string;
}
export declare const TabsDefaultProps: Partial<ITabsProps>;

View File

@ -0,0 +1,11 @@
export var TabsDefaultProps = {
type: 'basic',
direction: 'horizontal',
current: null,
defaultCurrent: 0,
items: [],
scrollMode: 'edge',
tabsBarClassName: '',
tabActiveClassName: '',
tabClassName: '',
};

View File

@ -0,0 +1,24 @@
@import (reference) '../style/themes/index.less';
// 基础类型的颜色 文本和下划线同一个颜色、未选中的文本颜色,无背景色
// 胶囊类型的颜色,文本和背景色,选中状态的文本和背景色
// 混合型的颜色,主标题的文本在选中与未选中的颜色,描述的未选中颜色以及选中后的文本和背景色
// 选中后的颜色:
// 基础:文本和下划线为蓝色
// 胶囊:文本白色,背景蓝色
// 混合:主标题文本蓝色,描述背景色蓝色,文本白色
// 未选中的颜色
// 基础:文本黑色
// 胶囊:文本黑色,背景色灰色
// 混合:主标题文本黑色,文本灰色无背景
// 黑色、灰色、白色、蓝色
@tabs-basic-color: @COLOR_TEXT_PRIMARY;
@tabs-weaken-color: @COLOR_TEXT_ASSIST;
@tabs-inverse-color: @COLOR_CARD;
@tabs-active-color: @COLOR_BRAND1;
@tabs-active-decorate-color: @COLOR_BRAND1;
@tabs-badge-size:28 * @rpx;