init
This commit is contained in:
117
uni_modules/nutui-uni/components/tabs/hooks.ts
Normal file
117
uni_modules/nutui-uni/components/tabs/hooks.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import type { ComponentInternalInstance } from 'vue'
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { useTouch } from '../_hooks'
|
||||
import { getRandomId } from '../_utils'
|
||||
|
||||
type TouchPosition = 'left' | 'right' | 'top' | 'bottom' | ''
|
||||
export function useTabContentTouch(props: any, tabMethods: any, uni?: ComponentInternalInstance, useRect?: any) {
|
||||
const tabsContentRef = ref<HTMLElement>()
|
||||
const tabsContentID = `tabsContentRef-${getRandomId()}`
|
||||
const tabsContentRefRect = ref({ width: 0, height: 0 })
|
||||
const initUniWidth = async () => {
|
||||
if (uni) {
|
||||
const rect = await useRect(tabsContentID, uni)
|
||||
|
||||
tabsContentRefRect.value.width = rect.width || 0
|
||||
tabsContentRefRect.value.height = rect.height || 0
|
||||
}
|
||||
else {
|
||||
tabsContentRefRect.value.width = tabsContentRef.value?.clientWidth || 0
|
||||
tabsContentRefRect.value.height = tabsContentRef.value?.clientHeight || 0
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
initUniWidth()
|
||||
}, 100)
|
||||
})
|
||||
const touchState = reactive({
|
||||
offset: 0,
|
||||
moving: false,
|
||||
})
|
||||
const touch = useTouch()
|
||||
let position: TouchPosition = ''
|
||||
const setoffset = (deltaX: number, deltaY: number) => {
|
||||
let offset = deltaX
|
||||
if (props.direction === 'horizontal') {
|
||||
position = deltaX > 0 ? 'right' : 'left'
|
||||
// 计算拖拽 百分比
|
||||
offset = (Math.abs(offset) / tabsContentRefRect.value.width) * 100
|
||||
}
|
||||
else {
|
||||
position = deltaY > 0 ? 'bottom' : 'top'
|
||||
offset = deltaY
|
||||
// 计算拖拽 百分比
|
||||
offset = (Math.abs(offset) / tabsContentRefRect.value?.height) * 100
|
||||
}
|
||||
// 拖拽阈值 85%
|
||||
if (offset > 85)
|
||||
offset = 85
|
||||
|
||||
switch (position) {
|
||||
case 'left':
|
||||
case 'top':
|
||||
// 起始tab拖拽拦截
|
||||
if (tabMethods.isEnd()) {
|
||||
offset = 0
|
||||
touchState.moving = false
|
||||
}
|
||||
break
|
||||
case 'right':
|
||||
case 'bottom':
|
||||
offset = -offset
|
||||
// 末位tab拖拽拦截
|
||||
if (tabMethods.isBegin()) {
|
||||
offset = 0
|
||||
touchState.moving = false
|
||||
}
|
||||
break
|
||||
}
|
||||
touchState.offset = offset
|
||||
}
|
||||
const touchMethods = {
|
||||
onTouchStart(event: Event) {
|
||||
if (!props.swipeable)
|
||||
return
|
||||
touch.start(event)
|
||||
},
|
||||
onTouchMove(event: Event) {
|
||||
if (!props.swipeable)
|
||||
return
|
||||
touch.move(event)
|
||||
touchState.moving = true
|
||||
setoffset(touch.deltaX.value, touch.deltaY.value)
|
||||
|
||||
if (props.direction === 'horizontal' && touch.isHorizontal()) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}
|
||||
if (props.direction === 'vertical' && touch.isVertical()) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}
|
||||
},
|
||||
onTouchEnd() {
|
||||
if (touchState.moving) {
|
||||
touchState.moving = false
|
||||
switch (position) {
|
||||
case 'left':
|
||||
case 'top':
|
||||
// 大于 35%阈值 切换至下一 Tab
|
||||
if (touchState.offset > 35)
|
||||
tabMethods.next()
|
||||
|
||||
break
|
||||
case 'right':
|
||||
case 'bottom':
|
||||
if (touchState.offset < -35)
|
||||
tabMethods.prev()
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
return { touchMethods, touchState, tabsContentRef, tabsContentID }
|
||||
}
|
||||
282
uni_modules/nutui-uni/components/tabs/index.scss
Normal file
282
uni_modules/nutui-uni/components/tabs/index.scss
Normal file
@@ -0,0 +1,282 @@
|
||||
@import "../sticky/index";
|
||||
@import "../cascader/index";
|
||||
|
||||
.nut-theme-dark {
|
||||
.nut-tabs {
|
||||
.nut-tabs__titles {
|
||||
background: $dark-background3;
|
||||
|
||||
.nut-tabs__titles-item {
|
||||
color: $dark-color-gray;
|
||||
|
||||
&.nut-tabs-active {
|
||||
color: $dark-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.vertical {
|
||||
.nut-tabs__titles {
|
||||
.nut-tabs__titles-item {
|
||||
&.nut-tabs-active {
|
||||
background-color: $dark-background2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nut-tabs {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
|
||||
.nut-tabs__titles {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
background: $tabs-titles-background-color;
|
||||
border-radius: $tabs-titles-border-radius;
|
||||
|
||||
.nut-tabs__list {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.nut-tabs__titles-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: $tabs-titles-item-font-size;
|
||||
color: $tabs-titles-item-color;
|
||||
|
||||
&__text {
|
||||
text-align: center;
|
||||
|
||||
&.ellipsis {
|
||||
@include oneline-ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
&__smile {
|
||||
.nut-icon {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-size: 12px;
|
||||
color: $tabs-tab-smile-color;
|
||||
}
|
||||
}
|
||||
|
||||
&__smile,
|
||||
&__line {
|
||||
position: absolute;
|
||||
bottom: 15%;
|
||||
left: 50%;
|
||||
width: 0;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
content: "";
|
||||
border-radius: $tabs-titles-item-line-border-radius;
|
||||
opacity: $tabs-titles-item-line-opacity;
|
||||
transition: width 0.3s ease;
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
|
||||
&.nut-tabs-active {
|
||||
font-weight: bold;
|
||||
color: $tabs-titles-item-active-color;
|
||||
|
||||
.nut-tabs__titles-item__line {
|
||||
width: $tabs-horizontal-titles-item-active-line-width;
|
||||
height: 3px;
|
||||
content: "";
|
||||
background: $tabs-horizontal-tab-line-color;
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
color: $disable-color;
|
||||
}
|
||||
}
|
||||
|
||||
&-left {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
&.horizontal {
|
||||
flex-direction: column;
|
||||
|
||||
.nut-sticky__box > .nut-tabs__titles,
|
||||
> .nut-tabs__titles {
|
||||
flex-direction: row;
|
||||
height: $tabs-horizontal-titles-height;
|
||||
|
||||
.nut-tabs__list {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&.scrollable {
|
||||
overflow: auto hidden;
|
||||
}
|
||||
|
||||
.nut-tabs__titles-item {
|
||||
flex: 1 0 auto;
|
||||
width: 0;
|
||||
min-width: $tabs-horizontal-titles-item-min-width;
|
||||
|
||||
&__smile {
|
||||
.nut-icon {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-size: 12px;
|
||||
color: $tabs-tab-smile-color;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
&-left {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.vertical {
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
|
||||
> .nut-tabs__titles {
|
||||
flex-direction: column;
|
||||
width: $tabs-vertical-titles-width;
|
||||
height: auto;
|
||||
padding: 10px 0;
|
||||
|
||||
.nut-tabs__list {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&.scrollable {
|
||||
height: auto;
|
||||
overflow: hidden auto;
|
||||
|
||||
.nut-tabs__titles-placeholder {
|
||||
height: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
.nut-tabs__titles-item {
|
||||
flex: none;
|
||||
width: 100%;
|
||||
min-width: $tabs-horizontal-titles-item-min-width;
|
||||
height: $tabs-vertical-titles-item-height;
|
||||
|
||||
&__line {
|
||||
bottom: none;
|
||||
width: 0;
|
||||
height: 0;
|
||||
transition: height 0.3s ease;
|
||||
transform: translate(0, -50%);
|
||||
}
|
||||
|
||||
&.nut-tabs-active {
|
||||
background-color: #fff;
|
||||
|
||||
.nut-tabs__titles-item__line {
|
||||
left: 10px;
|
||||
width: 3px;
|
||||
height: $tabs-vertical-titles-item-active-line-height;
|
||||
background: $tabs-vertical-tab-line-color;
|
||||
}
|
||||
|
||||
.nut-tabs__titles-item__smile {
|
||||
position: absolute;
|
||||
right: -8px;
|
||||
bottom: 2px;
|
||||
left: auto;
|
||||
width: 36px;
|
||||
height: $tabs-vertical-titles-item-active-line-height;
|
||||
transform: rotate(320deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nut-tabs__content {
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
|
||||
.nut-tab-pane {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__titles {
|
||||
&.large .nut-tabs__titles-item {
|
||||
font-size: $tabs-titles-item-large-font-size;
|
||||
}
|
||||
|
||||
&.normal .nut-tabs__titles-item {
|
||||
font-size: $tabs-titles-item-font-size;
|
||||
}
|
||||
|
||||
&.small .nut-tabs__titles-item {
|
||||
font-size: $tabs-titles-item-small-font-size;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
width: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&.smile {
|
||||
.nut-tabs__titles-item {
|
||||
.nut-tabs__titles-item__smile {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.nut-tabs-active {
|
||||
.nut-tabs__titles-item__smile {
|
||||
display: block;
|
||||
width: 36px;
|
||||
height: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.nut-tabs__titles-item {
|
||||
.uni {
|
||||
height: 46px;
|
||||
line-height: 46px;
|
||||
}
|
||||
}
|
||||
|
||||
.nut-tabs__titles-placeholder {
|
||||
width: auto;
|
||||
min-width: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
scroll-view ::-webkit-scrollbar {
|
||||
display: none;
|
||||
width: 0 !important;
|
||||
height: 0 !important;
|
||||
background: transparent;
|
||||
}
|
||||
1
uni_modules/nutui-uni/components/tabs/index.ts
Normal file
1
uni_modules/nutui-uni/components/tabs/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './tabs'
|
||||
79
uni_modules/nutui-uni/components/tabs/tabs.ts
Normal file
79
uni_modules/nutui-uni/components/tabs/tabs.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import type { ExtractPropTypes, RendererElement, RendererNode, VNode } from 'vue'
|
||||
import { CHANGE_EVENT, CLICK_EVENT, UPDATE_MODEL_EVENT } from '../_constants'
|
||||
import { commonProps, isString, makeNumericProp, makeStringProp, truthProp } from '../_utils'
|
||||
|
||||
export const TAB_KEY = Symbol('tabs')
|
||||
|
||||
export const tabsProps = {
|
||||
...commonProps,
|
||||
/**
|
||||
* @description 绑定当前选中标签的标识符
|
||||
*/
|
||||
modelValue: makeNumericProp(0),
|
||||
/**
|
||||
* @description 标签选中色
|
||||
*/
|
||||
customColor: String,
|
||||
/**
|
||||
* @description 使用横纵方向,可选值`horizontal`、`vertical`
|
||||
*/
|
||||
direction: makeStringProp<'horizontal' | 'vertical'>('horizontal'),
|
||||
/**
|
||||
* @description 标签栏字体尺寸大小,可选值`large` `normal` `small`
|
||||
*/
|
||||
size: makeStringProp<'large' | 'normal' | 'small'>('normal'),
|
||||
/**
|
||||
* @description 选中底部展示样式,可选值 `line`、`smile`
|
||||
*/
|
||||
type: makeStringProp<'line' | 'card' | 'smile'>('line'),
|
||||
/**
|
||||
* @description 标签栏是否可以滚动
|
||||
*/
|
||||
titleScroll: Boolean,
|
||||
/**
|
||||
* @description 是否省略过长的标题文字
|
||||
*/
|
||||
ellipsis: truthProp,
|
||||
/**
|
||||
* @description 是否开启手势左右滑动切换
|
||||
*/
|
||||
swipeable: Boolean,
|
||||
/**
|
||||
* @description 自动高度。设置为 true 时,nut-tabs 和 nut-tabs__content 会随着当前 nut-tab-pane 的高度而发生变化,使用此属性时必须设置nut-tabs的`pane-key`
|
||||
*/
|
||||
autoHeight: Boolean,
|
||||
/**
|
||||
* @description 标签栏背景颜色
|
||||
*/
|
||||
background: String,
|
||||
/**
|
||||
* @description 切换动画时长,单位 ms。0 代表无动画,此时必须设置 pane-key
|
||||
*/
|
||||
animatedTime: makeNumericProp(300),
|
||||
/**
|
||||
* @description 标签间隙
|
||||
*/
|
||||
titleGutter: makeNumericProp(0),
|
||||
/**
|
||||
* @description 横轴方向的标题对齐方式,可选值 left、center
|
||||
*/
|
||||
align: makeStringProp<'left' | 'center'>('center'),
|
||||
}
|
||||
|
||||
export type TabsProps = ExtractPropTypes<typeof tabsProps>
|
||||
|
||||
export const tabsEmits = {
|
||||
[CLICK_EVENT]: (val: Title) => val instanceof Object,
|
||||
[CHANGE_EVENT]: (val: Title) => val instanceof Object,
|
||||
[UPDATE_MODEL_EVENT]: (val: string) => isString(val),
|
||||
}
|
||||
|
||||
export type TabsEmits = typeof tabsEmits
|
||||
|
||||
export class Title {
|
||||
title = ''
|
||||
titleSlot?: VNode<RendererNode, RendererElement, { [key: string]: any }>
|
||||
paneKey = ''
|
||||
disabled = false
|
||||
constructor() { }
|
||||
}
|
||||
348
uni_modules/nutui-uni/components/tabs/tabs.vue
Normal file
348
uni_modules/nutui-uni/components/tabs/tabs.vue
Normal file
@@ -0,0 +1,348 @@
|
||||
<script setup lang="ts">
|
||||
import type { ComponentInternalInstance, CSSProperties, Ref, VNode } from 'vue'
|
||||
import { computed, defineComponent, getCurrentInstance, nextTick, onActivated, onMounted, ref, watch } from 'vue'
|
||||
import { CHANGE_EVENT, CLICK_EVENT, PREFIX, UPDATE_MODEL_EVENT } from '../_constants'
|
||||
import { useProvide, useRect, useSelectorQuery } from '../_hooks'
|
||||
import { getMainClass, getRandomId, pxCheck, TypeOfFun } from '../_utils'
|
||||
import raf from '../_utils/raf'
|
||||
import NutIcon from '../icon/icon.vue'
|
||||
import { useTabContentTouch } from './hooks'
|
||||
import { TAB_KEY, tabsEmits, tabsProps, Title } from './tabs'
|
||||
|
||||
const props = defineProps(tabsProps)
|
||||
const emit = defineEmits(tabsEmits)
|
||||
const instance = getCurrentInstance() as ComponentInternalInstance
|
||||
const { getSelectorNodeInfo, getSelectorNodeInfos } = useSelectorQuery(instance)
|
||||
const refRandomId = getRandomId()
|
||||
const container = ref(null)
|
||||
const { internalChildren } = useProvide(TAB_KEY, `${PREFIX}-tabs`)({
|
||||
activeKey: computed(() => props.modelValue || 0),
|
||||
autoHeight: computed(() => props.autoHeight),
|
||||
animatedTime: computed(() => props.animatedTime),
|
||||
})
|
||||
const titles: Ref<Title[]> = ref([])
|
||||
function renderTitles(vnodes: VNode[]) {
|
||||
vnodes.forEach((vnode: VNode, index: number) => {
|
||||
let type = vnode.type
|
||||
type = (type as any).name || type
|
||||
if (type === 'nut-tab-pane') {
|
||||
const title = new Title()
|
||||
if (vnode.props?.title || vnode.props?.['pane-key'] || vnode.props?.paneKey) {
|
||||
const paneKeyType = TypeOfFun(vnode.props?.['pane-key'])
|
||||
const paneIndex
|
||||
= paneKeyType === 'number' || paneKeyType === 'string' ? String(vnode.props?.['pane-key']) : null
|
||||
const camelPaneKeyType = TypeOfFun(vnode.props?.paneKey)
|
||||
const camelPaneIndex
|
||||
= camelPaneKeyType === 'number' || camelPaneKeyType === 'string' ? String(vnode.props?.paneKey) : null
|
||||
title.title = vnode.props?.title
|
||||
title.paneKey = paneIndex || camelPaneIndex || String(index)
|
||||
title.disabled = vnode.props?.disabled
|
||||
}
|
||||
|
||||
titles.value.push(title)
|
||||
}
|
||||
else {
|
||||
if (vnode.children === ' ')
|
||||
return
|
||||
|
||||
renderTitles(vnode.children as VNode[])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const currentIndex = ref((props.modelValue as number) || 0)
|
||||
function findTabsIndex(value: string | number) {
|
||||
const index = titles.value.findIndex(item => item.paneKey === String(value))
|
||||
// if (titles.value.length === 0)
|
||||
// console.warn('[NutUI] <Tabs> 当前未找到 TabPane 组件元素 , 请检查 .')
|
||||
|
||||
// else if (index === -1)
|
||||
// console.warn('[NutUI] <Tabs> 请检查 v-model 值是否为 paneKey ,如 paneKey 未设置,请采用下标控制 .')
|
||||
|
||||
// else
|
||||
currentIndex.value = index
|
||||
}
|
||||
const getScrollX = computed(() => {
|
||||
return props.titleScroll && props.direction === 'horizontal'
|
||||
})
|
||||
const getScrollY = computed(() => {
|
||||
return props.titleScroll && props.direction === 'vertical'
|
||||
})
|
||||
const titleRef = ref([]) as Ref<HTMLElement[]>
|
||||
const scrollLeft = ref(0)
|
||||
const scrollTop = ref(0)
|
||||
const scrollWithAnimation = ref(false)
|
||||
const navRectRef = ref()
|
||||
const titleRectRef = ref<UniApp.NodeInfo[]>([])
|
||||
const canShowLabel = ref(false)
|
||||
function scrollIntoView() {
|
||||
if (!props.titleScroll)
|
||||
return
|
||||
raf(() => {
|
||||
Promise.all([
|
||||
getSelectorNodeInfo(`#nut-tabs__titles_${refRandomId}`),
|
||||
getSelectorNodeInfos(`#nut-tabs__titles_${refRandomId} .nut-tabs__titles-item`),
|
||||
]).then(([navRect, titleRects]) => {
|
||||
navRectRef.value = navRect
|
||||
titleRectRef.value = titleRects
|
||||
|
||||
if (navRectRef.value) {
|
||||
if (props.direction === 'vertical') {
|
||||
const titlesTotalHeight = titleRects.reduce((prev: number, curr: UniApp.NodeInfo) => prev + curr.height!, 0)
|
||||
if (titlesTotalHeight > navRectRef.value?.height)
|
||||
canShowLabel.value = true
|
||||
|
||||
else
|
||||
canShowLabel.value = false
|
||||
}
|
||||
else {
|
||||
const titlesTotalWidth = titleRects.reduce((prev: number, curr: UniApp.NodeInfo) => prev + curr.width!, 0)
|
||||
if (titlesTotalWidth > navRectRef.value?.width)
|
||||
canShowLabel.value = true
|
||||
|
||||
else
|
||||
canShowLabel.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const titleRect: UniApp.NodeInfo = titleRectRef.value[currentIndex.value]
|
||||
|
||||
let to = 0
|
||||
if (props.direction === 'vertical') {
|
||||
const top = titleRects
|
||||
.slice(0, currentIndex.value)
|
||||
.reduce((prev: number, curr) => prev + curr.height!, 0)
|
||||
to = top - (navRectRef.value?.height - titleRect.height!) / 2
|
||||
}
|
||||
else {
|
||||
const left = titleRects
|
||||
.slice(0, currentIndex.value)
|
||||
.reduce((prev: number, curr) => prev + curr.width!, 0)
|
||||
// eslint-disable-next-line ts/no-non-null-asserted-optional-chain
|
||||
to = left - (navRectRef.value?.width - titleRect?.width!) / 2
|
||||
}
|
||||
|
||||
nextTick(() => {
|
||||
scrollWithAnimation.value = true
|
||||
})
|
||||
|
||||
scrollDirection(to, props.direction)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function scrollDirection(to: number, direction: 'horizontal' | 'vertical') {
|
||||
let count = 0
|
||||
const from = direction === 'horizontal' ? scrollLeft.value : scrollTop.value
|
||||
const frames = 1
|
||||
|
||||
function animate() {
|
||||
if (direction === 'horizontal')
|
||||
scrollLeft.value += (to - from) / frames
|
||||
else
|
||||
scrollTop.value += (to - from) / frames
|
||||
|
||||
if (++count < frames)
|
||||
raf(animate)
|
||||
}
|
||||
|
||||
animate()
|
||||
}
|
||||
function init(vnodes: VNode[] = internalChildren.map(item => item.vnode)) {
|
||||
titles.value = []
|
||||
vnodes = vnodes?.filter(item => typeof item.children !== 'string')
|
||||
|
||||
if (vnodes && vnodes.length)
|
||||
renderTitles(vnodes)
|
||||
|
||||
findTabsIndex(props.modelValue)
|
||||
setTimeout(() => {
|
||||
scrollIntoView()
|
||||
}, 500)
|
||||
}
|
||||
|
||||
watch(
|
||||
() => internalChildren.map(item => item.props),
|
||||
(vnodes: any[]) => {
|
||||
init(internalChildren as any)
|
||||
},
|
||||
{ deep: true, immediate: true },
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(value: string | number) => {
|
||||
findTabsIndex(value)
|
||||
scrollIntoView()
|
||||
},
|
||||
)
|
||||
onMounted(init)
|
||||
onActivated(init)
|
||||
const tabMethods = {
|
||||
isBegin: () => {
|
||||
return currentIndex.value === 0
|
||||
},
|
||||
isEnd: () => {
|
||||
return currentIndex.value === titles.value.length - 1
|
||||
},
|
||||
next: () => {
|
||||
currentIndex.value += 1
|
||||
const nextDisabled = titles.value[currentIndex.value].disabled
|
||||
if (tabMethods.isEnd() && nextDisabled) {
|
||||
tabMethods.prev()
|
||||
return
|
||||
}
|
||||
if (nextDisabled && currentIndex.value < titles.value.length - 1) {
|
||||
tabMethods.next()
|
||||
return
|
||||
}
|
||||
|
||||
tabMethods.updateValue(titles.value[currentIndex.value])
|
||||
},
|
||||
prev: () => {
|
||||
currentIndex.value -= 1
|
||||
const prevDisabled = titles.value[currentIndex.value].disabled
|
||||
if (tabMethods.isBegin() && prevDisabled) {
|
||||
tabMethods.next()
|
||||
return
|
||||
}
|
||||
if (prevDisabled && currentIndex.value > 0) {
|
||||
tabMethods.prev()
|
||||
return
|
||||
}
|
||||
tabMethods.updateValue(titles.value[currentIndex.value])
|
||||
},
|
||||
updateValue: (item: Title) => {
|
||||
emit(UPDATE_MODEL_EVENT, item.paneKey)
|
||||
emit(CHANGE_EVENT, item)
|
||||
},
|
||||
tabChange: (item: Title, index: number) => {
|
||||
emit(CLICK_EVENT, item)
|
||||
if (item.disabled || currentIndex.value === index)
|
||||
return
|
||||
|
||||
currentIndex.value = index
|
||||
tabMethods.updateValue(item)
|
||||
},
|
||||
setTabItemRef: (el: HTMLElement, index: number) => {
|
||||
titleRef.value[index] = el
|
||||
},
|
||||
}
|
||||
const { tabChange } = tabMethods
|
||||
const { touchState, touchMethods, tabsContentID, tabsContentRef } = useTabContentTouch(props, tabMethods, instance, useRect)
|
||||
const contentStyle = computed(() => {
|
||||
let offsetPercent = currentIndex.value * 100
|
||||
if (touchState.moving)
|
||||
offsetPercent += touchState.offset
|
||||
|
||||
let style: CSSProperties = {
|
||||
transform:
|
||||
props.direction === 'horizontal'
|
||||
? `translate3d(-${offsetPercent}%, 0, 0)`
|
||||
: `translate3d( 0,-${offsetPercent}%, 0)`,
|
||||
transitionDuration: touchState.moving ? undefined : `${props.animatedTime}ms`,
|
||||
}
|
||||
if (props.animatedTime === 0)
|
||||
style = {}
|
||||
|
||||
return style
|
||||
})
|
||||
const tabsNavStyle = computed(() => {
|
||||
return {
|
||||
background: props.background,
|
||||
}
|
||||
})
|
||||
const tabsActiveStyle = computed(() => {
|
||||
return {
|
||||
color: props.type === 'smile' ? props.customColor : '',
|
||||
background: props.type === 'line' ? props.customColor : '',
|
||||
}
|
||||
})
|
||||
const titleStyle = computed(() => {
|
||||
if (!props.titleGutter)
|
||||
return {}
|
||||
const px = pxCheck(props.titleGutter)
|
||||
if (props.direction === 'vertical')
|
||||
return { paddingTop: px, paddingBottom: px }
|
||||
|
||||
return { paddingLeft: px, paddingRight: px }
|
||||
})
|
||||
const classes = computed(() => {
|
||||
return getMainClass(props, componentName, {
|
||||
[props.direction]: true,
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
const componentName = `${PREFIX}-tabs`
|
||||
|
||||
export default defineComponent({
|
||||
name: componentName,
|
||||
options: {
|
||||
virtualHost: true,
|
||||
addGlobalClass: true,
|
||||
styleIsolation: 'shared',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view ref="container" :style="customStyle" :class="classes">
|
||||
<scroll-view
|
||||
:id="`nut-tabs__titles_${refRandomId}`"
|
||||
:scroll-x="getScrollX"
|
||||
:scroll-y="getScrollY"
|
||||
:scroll-with-animation="scrollWithAnimation"
|
||||
:scroll-left="scrollLeft"
|
||||
:scroll-top="scrollTop"
|
||||
:enable-flex="true"
|
||||
class="nut-tabs__titles"
|
||||
:class="{ [type]: type, scrollable: titleScroll, [size]: size }"
|
||||
:style="tabsNavStyle"
|
||||
>
|
||||
<view class="nut-tabs__list" :class="{ 'nut-tabs__titles-left': align === 'left' }">
|
||||
<slot v-if="$slots.titles" name="titles" />
|
||||
<template v-else>
|
||||
<view
|
||||
v-for="(item, index) in titles"
|
||||
:key="item.paneKey"
|
||||
class="nut-tabs__titles-item uni"
|
||||
:style="titleStyle"
|
||||
:class="{
|
||||
'nut-tabs-active': item.paneKey === String(modelValue),
|
||||
'disabled': item.disabled,
|
||||
'nut-tabs__titles-item-left': align === 'left',
|
||||
}"
|
||||
@click="tabChange(item, index)"
|
||||
>
|
||||
<view v-if="type === 'line'" class="nut-tabs__titles-item__line" :style="tabsActiveStyle" />
|
||||
<view v-if="type === 'smile'" class="nut-tabs__titles-item__smile" :style="tabsActiveStyle">
|
||||
<NutIcon name="joy-smile" :custom-color="customColor" />
|
||||
</view>
|
||||
<view class="nut-tabs__titles-item__text" :class="{ ellipsis }">
|
||||
{{ item.title }}
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="canShowLabel && titleScroll" class="nut-tabs__titles-placeholder" />
|
||||
</template>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view
|
||||
:id="tabsContentID"
|
||||
ref="tabsContentRef"
|
||||
class="nut-tabs__content"
|
||||
:style="contentStyle"
|
||||
@touchstart="touchMethods.onTouchStart"
|
||||
@touchmove="touchMethods.onTouchMove"
|
||||
@touchend="touchMethods.onTouchEnd"
|
||||
@touchcancel="touchMethods.onTouchEnd"
|
||||
>
|
||||
<slot name="default" />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import './index';
|
||||
</style>
|
||||
9
uni_modules/nutui-uni/components/tabs/type.ts
Normal file
9
uni_modules/nutui-uni/components/tabs/type.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export interface provideData {
|
||||
children: any[]
|
||||
size: string
|
||||
modelValue: string | number
|
||||
unactiveColor: string
|
||||
activeColor: string
|
||||
dot: boolean
|
||||
changeIndex: (index: number, active: number | string) => void
|
||||
}
|
||||
Reference in New Issue
Block a user