2025/2/8第一次更新

This commit is contained in:
爱吃咸鱼小猫咪
2025-02-08 18:50:38 +08:00
commit d7af560866
26519 changed files with 5046029 additions and 0 deletions
@@ -0,0 +1,4 @@
export * from './tabs-custom'
export * from './tabs-item-custom'
export * from './use-tabs-item'
export * from './use-tabs'
@@ -0,0 +1,83 @@
import { computed, toRef } from 'vue'
import { useComponentColor, useNamespace } from '../../../../hooks'
import { formatDomSizeValue } from '../../../../utils'
import type { CSSProperties } from 'vue'
import type { TabsProps } from '../tabs'
export const useTabsCustomStyle = (props: TabsProps) => {
const ns = useNamespace('tabs')
// 解析颜色
const [bgColorClass, bgColorStyle] = useComponentColor(
toRef(props, 'bgColor'),
'bg'
)
const [barColorClass, barColorStyle] = useComponentColor(
toRef(props, 'barColor'),
'bg'
)
// tabs对应的类
const tabsClass = computed<string>(() => {
const cls: string[] = [ns.b()]
// 设置底部阴影
if (props.bottomShadow) cls.push(ns.m('bottom-shadow'))
// 设置背景颜色
if (bgColorClass.value) cls.push(bgColorClass.value)
return cls.join(' ')
})
// tabs的样式
const tabsStyle = computed<CSSProperties>(() => {
const style: CSSProperties = {}
// 设置背景颜色
if (!bgColorClass.value) {
style.backgroundColor = bgColorStyle.value || 'var(--tn-color-white)'
}
// 设置高度
if (props.height) {
style.height = formatDomSizeValue(props.height)
if (props.offsetTop) {
style.height = `calc(${style.height} + ${props.offsetTop}px)`
}
}
return style
})
// bar对应的类
const barClass = computed<string>(() => {
const cls: string[] = [ns.e('bar')]
// 设置滑块颜色
if (barColorClass.value) cls.push(barColorClass.value)
return cls.join(' ')
})
// bar的样式
const barStyle = computed<CSSProperties>(() => {
const style: CSSProperties = {}
// 设置滑块颜色
if (!barColorClass.value) {
style.backgroundColor = barColorStyle.value || 'var(--tn-color-primary)'
}
// 设置滑块的宽度
if (props.barWidth) style.width = formatDomSizeValue(props.barWidth)
return style
})
return {
ns,
tabsClass,
tabsStyle,
barClass,
barStyle,
}
}
@@ -0,0 +1,99 @@
import { computed, inject } from 'vue'
import { tabsContextKey } from '../../../../tokens'
import { useComponentColor, useNamespace } from '../../../../hooks'
import { formatDomSizeValue, isEmptyVariableInDefault } from '../../../../utils'
import type { CSSProperties, Ref } from 'vue'
import type { TabsItemProps } from '../tabs-item'
export const useTabsItemCustomStyle = (
props: TabsItemProps,
isActive: Ref<boolean>
) => {
const ns = useNamespace('tabs-item')
const tabsContext = inject(tabsContextKey)
const normalColor = computed<string | undefined>(
() => props.color || tabsContext?.color
)
const activeColor = computed<string | undefined>(
() => props.activeColor || tabsContext?.activeColor
)
const activeBold = computed<boolean>(() =>
isEmptyVariableInDefault(tabsContext?.activeBold, true)
)
const activeFontSize = computed<string | undefined>(
() => props.activeFontSize || tabsContext?.activeFontSize
)
// 解析颜色
const [textColorClass, textColorStyle] = useComponentColor(
normalColor,
'text'
)
const [activeTextColorClass, activeTextColorStyle] = useComponentColor(
activeColor,
'text'
)
// tabsItem对应的类
const tabsItemClass = computed<string>(() => {
const cls: string[] = [ns.b()]
// 设置颜色
if (isActive.value) {
if (activeTextColorClass.value) {
cls.push(activeTextColorClass.value)
}
if (activeBold.value) {
cls.push(ns.m('bold'))
}
} else {
if (textColorClass.value) {
cls.push(textColorClass.value)
}
}
// 设置可以滚动
if (tabsContext?.scroll) cls.push(ns.m('scroll'))
// 是否有设置滑块
if (!tabsContext?.showBar) cls.push(ns.is('no-bar'))
return cls.join(' ')
})
// tabsItem样式
const tabsItemStyle = computed<CSSProperties>(() => {
const style: CSSProperties = {}
// 设置字体大小
if (props.fontSize || tabsContext?.fontSize) {
style.fontSize = formatDomSizeValue(
props.fontSize || tabsContext?.fontSize || ''
)
}
// 设置颜色
if (isActive.value) {
if (!activeTextColorClass.value) {
style.color = activeTextColorStyle.value || 'var(--tn-color-primary)'
}
if (activeFontSize.value) {
style.fontSize = formatDomSizeValue(activeFontSize.value)
}
} else {
if (!textColorClass.value) {
style.color = textColorStyle.value || 'var(--tn-text-color-primary)'
}
}
return style
})
return {
ns,
tabsItemClass,
tabsItemStyle,
}
}
@@ -0,0 +1,110 @@
import {
computed,
getCurrentInstance,
inject,
nextTick,
onMounted,
onUnmounted,
} from 'vue'
import { tabsContextKey } from '../../../../tokens'
import { useSelectorQuery } from '../../../../hooks'
import { debugWarn, generateId, isEmpty } from '../../../../utils'
import type { TabsItemProps } from '../tabs-item'
import type { TabsItemRect } from '../types'
export const useTabsItem = (props: TabsItemProps) => {
const instance = getCurrentInstance()
if (!instance) {
debugWarn('TnTabsItem', '请在 setup 中使用 useTabsItem')
}
const { emit, uid } = instance!
const componentId = `tti-${generateId()}`
const tabsContext = inject(tabsContextKey)
const { getSelectorNodeInfo } = useSelectorQuery(instance)
// 当前节点是否被激活
const isActive = computed<boolean>(() => tabsContext?.activeUid === uid)
// 判断是否有角标
const hasBadge = computed<boolean>(() => !isEmpty(props.badgeConfig))
// tabsItem节点信息
const tabsItemRect: TabsItemRect = {
width: 0,
height: 0,
left: 0,
}
let initCount = 0
// 初始化、获取节点信息
const initTabsItemRectInfo = async () => {
try {
const rectInfo = await getSelectorNodeInfo(`#${componentId}`)
tabsItemRect.width = rectInfo.width || 0
tabsItemRect.height = rectInfo.height || 0
tabsItemRect.left = rectInfo.left || 0
// 添加item
// #ifndef APP-PLUS || MP-ALIPAY
tabsContext?.addItem({
uid,
elementRect: tabsItemRect,
name: props.name,
})
// #endif
// #ifdef APP-PLUS || MP-ALIPAY
setTimeout(() => {
tabsContext?.addItem({
uid,
elementRect: tabsItemRect,
name: props.name,
})
}, 250)
// #endif
} catch (err) {
if (initCount > 10) {
initCount = 0
debugWarn('TnTabsItem', `获取tabsItem节点信息失败: ${err}`)
return
}
initCount++
setTimeout(() => {
initTabsItemRectInfo()
}, 150)
}
}
// item点击事件
const itemClickEvent = () => {
if (props.disabled) return
emit('click')
tabsContext?.setActiveItem(uid)
}
onMounted(() => {
nextTick(() => {
// 初始化节点
setTimeout(() => {
initTabsItemRectInfo()
}, 200)
})
})
onUnmounted(() => {
// 移除item
tabsContext?.removeItem(uid)
})
return {
componentId,
isActive,
hasBadge,
itemClickEvent,
}
}
@@ -0,0 +1,266 @@
import {
computed,
getCurrentInstance,
nextTick,
onMounted,
provide,
reactive,
ref,
toRefs,
useSlots,
watch,
} from 'vue'
import { CHANGE_EVENT, UPDATE_MODEL_EVENT } from '../../../../constants'
import { tabsContextKey } from '../../../../tokens'
import { useOrderedChildren, useSelectorQuery } from '../../../../hooks'
import { debugWarn, generateId, isBoolean, isPromise } from '../../../../utils'
import type { TabsItemContext } from '../../../../tokens'
import type { TabsProps } from '../tabs'
import type { TabsBarRect, TabsRect } from '../types'
export const useTabs = (props: TabsProps) => {
const instance = getCurrentInstance()
if (!instance) {
debugWarn('TnTabs', '请在 setup 函数中使用 useTabs ')
}
const { emit } = instance!
const slots = useSlots()
const {
children: items,
addChild,
removeChild: removeItem,
} = useOrderedChildren<TabsItemContext>()
const { getSelectorNodeInfo } = useSelectorQuery(instance)
const componentId = `tt-${generateId()}`
const barComponentId = `${componentId}-b`
// 是否需要显示bar滑块
const showBar = computed<boolean>(() => props.bar || !!slots.bar)
// 当前被激活的ItemUid
const activeUid = ref<number>(-1)
// 添加tabsItem到items容器中
const addItem = (item: TabsItemContext) => {
if (props.modelValue !== undefined && activeUid.value === -1) {
if (
props.modelValue === item.name ||
props.modelValue === items.value.length
) {
nextTick(() => {
updateActiveUid(item.uid)
})
}
}
addChild(item)
}
// tabs容器节点信息
const tabsRect: TabsRect = {
width: 0,
height: 0,
left: 0,
}
// bar滑块的容器节点信息
const barRect: TabsBarRect = {
width: 0,
height: 0,
left: 0,
}
// 滑块滑动的距离
const barOffsetLeft = ref<number>(0)
// scrollView的滚动距离
const scrollLeft = ref<number>(0)
// 更新偏移位置信息
const updateOffsetPosition = (index: number) => {
if (!props.scroll && !props.bar && !slots.bar) return
// 获取当前Item
const item = items.value[index].elementRect
if (props.bar || slots.bar) {
// 更新滑块的偏移位置
barOffsetLeft.value =
item.left - tabsRect.left + (item.width - barRect.width) / 2
}
if (props.scroll) {
// 更新scrollView的偏移位置
const scrollLeftValue =
item.left - tabsRect.left - (tabsRect.width - item.width) / 2
scrollLeft.value = scrollLeftValue < 0 ? 0 : scrollLeftValue
}
}
// 更新当前激活的ItemUid
const updateActiveUid = (uid: number, changeEmit = false) => {
activeUid.value = uid
const itemIndex = items.value.findIndex((item) => item.uid === uid)
const value = items.value[itemIndex].name
? items.value[itemIndex].name
: itemIndex
updateOffsetPosition(itemIndex)
emit(UPDATE_MODEL_EVENT, value)
if (changeEmit) {
emit(CHANGE_EVENT, value)
}
}
// 设置当前被点击Item
const setActiveItem = (uid: number) => {
if (!props.beforeSwitch) {
updateActiveUid(uid, true)
return
}
const itemIndex = items.value.findIndex((item) => item.uid === uid)
const shouldSwitch = props.beforeSwitch(itemIndex)
const isPromiseOrBoolean = [
isPromise(shouldSwitch),
isBoolean(shouldSwitch),
].includes(true)
if (!isPromiseOrBoolean) {
debugWarn('TnTabs', 'beforeSwitch返回值必须是Promise或者Boolean')
return
}
if (isPromise(shouldSwitch)) {
shouldSwitch
.then((res) => {
if (res) {
updateActiveUid(uid, true)
}
})
.catch((err) => {
debugWarn('TnTabs', `执行beforeSwitch出错:${err}`)
})
} else {
if (shouldSwitch) {
updateActiveUid(uid, true)
}
}
}
// 通过索引更新当前激活的ActiveItem
const updateActiveItemByValue = (value?: string | number) => {
if (value === undefined) {
// 如果没有传递任何值则设置第一个Item为激活状态
updateActiveUid(items.value[0].uid)
return
}
let item: TabsItemContext | undefined
// 如果类型是number,则先通过索引进行查找
if (typeof value === 'number') {
item = items.value?.[value]
}
// 如果没有找到,则通过name查找
if (!item) {
item = items.value.find((item) => item.name === value)
}
if (!item) {
// 设置第一个Item为激活状态
updateActiveUid(items.value[0].uid)
} else {
updateActiveUid(item.uid)
}
}
watch(
() => props.modelValue,
(val) => {
updateActiveItemByValue(val)
}
)
let initCount = 0
// 获取Tabs容器节点信息
const getTabsRectInfo = async () => {
try {
const rectInfo = await getSelectorNodeInfo(`#${componentId}`)
initCount = 0
tabsRect.width = rectInfo.width || 0
tabsRect.height = rectInfo.height || 0
tabsRect.left = rectInfo.left || 0
} catch (err) {
if (initCount > 10) {
initCount = 0
debugWarn('TnTabs', `获取Tabs容器节点信息出错: ${err}`)
return
}
initCount++
setTimeout(() => {
getTabsRectInfo()
}, 150)
}
}
// 获取Bar滑块的容器节点信息
const getBarRectInfo = async () => {
if (!props.bar && !slots.bar) return
try {
const rectInfo = await getSelectorNodeInfo(`#${barComponentId}`)
initCount = 0
barRect.width = rectInfo.width || 0
barRect.height = rectInfo.height || 0
barRect.left = rectInfo.left || 0
} catch (err) {
if (initCount > 10) {
initCount = 0
debugWarn('TnTabs', `获取Bar滑块节点信息出错: ${err}`)
return
}
initCount++
setTimeout(() => {
getBarRectInfo()
}, 150)
}
}
onMounted(() => {
nextTick(() => {
// #ifndef MP-ALIPAY
getTabsRectInfo()
// #endif
// #ifdef MP-ALIPAY
setTimeout(() => {
getTabsRectInfo()
}, 50)
// #endif
getBarRectInfo()
})
})
provide(
tabsContextKey,
reactive({
...toRefs(props),
items,
activeUid,
showBar,
addItem,
removeItem,
setActiveItem,
})
)
return {
tabItems: items,
componentId,
barComponentId,
barOffsetLeft,
scrollLeft,
showBar,
}
}
@@ -0,0 +1,5 @@
import type Tabs from './tabs.vue'
import type TabsItem from './tabs-item.vue'
export type TnTabsInstance = InstanceType<typeof Tabs>
export type TnTabsItemInstance = InstanceType<typeof TabsItem>
+47
View File
@@ -0,0 +1,47 @@
import { buildProps, definePropType } from '../../../utils'
import { tabsBaseProps } from '../../base/common-props/tabs'
import type { ExtractPropTypes } from 'vue'
import type { BadgeProps } from '../../badge'
export type TabsItemBadgeConfig = Partial<Pick<BadgeProps, 'dot'>> & {
value?: string | number
}
export const tabsItemProps = buildProps({
...tabsBaseProps,
/**
* @description 唯一标识
*/
name: {
type: [String, Number],
},
/**
* @description 标题
*/
title: {
type: String,
required: true,
},
/**
* @description 角标配置
*/
badgeConfig: {
type: definePropType<TabsItemBadgeConfig>(Object),
default: () => ({}),
},
/**
* @description 是否禁用
*/
disabled: Boolean,
})
export const tabsItemEmits = {
/**
* @description 点击事件
*/
click: () => true,
}
export type TabsItemProps = ExtractPropTypes<typeof tabsItemProps>
export type TabsItemEmits = typeof tabsItemEmits
@@ -0,0 +1,55 @@
<script lang="ts" setup>
import TnBadge from '../../badge/src/badge.vue'
import { tabsItemEmits, tabsItemProps } from './tabs-item'
import { useTabsItem, useTabsItemCustomStyle } from './composables'
const props = defineProps(tabsItemProps)
defineEmits(tabsItemEmits)
const { componentId, isActive, hasBadge, itemClickEvent } = useTabsItem(props)
const { ns, tabsItemClass, tabsItemStyle } = useTabsItemCustomStyle(
props,
isActive
)
</script>
// #ifdef MP-WEIXIN
<script lang="ts">
export default {
options: {
// 在微信小程序中将组件节点渲染为虚拟节点,更加接近Vue组件的表现(不会出现shadow节点下再去创建元素)
virtualHost: true,
},
}
</script>
// #endif
<template>
<view
:id="componentId"
:class="[tabsItemClass]"
:style="tabsItemStyle"
@tap.stop="itemClickEvent"
>
<slot>
<view :class="[ns.e('content')]">
<view :class="[ns.e('content__value')]">
<!-- 角标 -->
<TnBadge
v-if="hasBadge"
:value="badgeConfig.value"
:dot="badgeConfig.dot"
:size="badgeConfig.dot ? '16' : ''"
type="danger"
@click="itemClickEvent"
/>
{{ title }}
</view>
</view>
</slot>
</view>
</template>
<style lang="scss" scoped>
@import '../../../theme-chalk/src/tabs-item.scss';
</style>
+90
View File
@@ -0,0 +1,90 @@
import { CHANGE_EVENT, UPDATE_MODEL_EVENT } from '../../../constants'
import { buildProps, definePropType, isNumber, isString } from '../../../utils'
import { tabsBaseProps } from '../../base/common-props/tabs'
import type { ExtractPropTypes } from 'vue'
export type TabsSwitchBeforeFunc = (index: number) => Promise<boolean> | boolean
export const tabsProps = buildProps({
...tabsBaseProps,
/**
* @description tabs绑定的值,与tabsItem name属性对应值,如果tabsItem没有设置name,则默认为索引值
*/
modelValue: {
type: [String, Number],
default: 0,
},
/**
* @description tabs高度
*/
height: {
type: String,
default: '80rpx',
},
/**
* @description 滑块的宽度
*/
barWidth: {
type: String,
default: '40rpx',
},
/**
* @description 背景颜色,以tn开头时使用图鸟内置的颜色
*/
bgColor: String,
/**
* @description bar滑块颜色,以tn开头时使用图鸟内置的颜色
*/
barColor: String,
/**
* @description 显示底部阴影
*/
bottomShadow: {
type: Boolean,
default: true,
},
/**
* @description 是否可以滚动
*/
scroll: {
type: Boolean,
default: true,
},
/**
* @description 是否显示滑块
*/
bar: {
type: Boolean,
default: true,
},
/**
* @description 选中后的字体是否加粗
*/
activeBold: {
type: Boolean,
default: true,
},
/**
* @description 距离顶部的距离,默认单位 px
*/
offsetTop: {
type: Number,
default: 0,
},
/**
* @description 切换前回调
*/
beforeSwitch: {
type: definePropType<TabsSwitchBeforeFunc>(Function),
},
})
export const tabsEmits = {
[UPDATE_MODEL_EVENT]: (val: string | number) =>
isString(val) || isNumber(val),
[CHANGE_EVENT]: (val: string | number) => isString(val) || isNumber(val),
}
export type TabsProps = ExtractPropTypes<typeof tabsProps>
export type TabsEmits = typeof tabsEmits
+68
View File
@@ -0,0 +1,68 @@
<script lang="ts" setup>
import { formatDomSizeValue } from '../../../utils'
import { tabsEmits, tabsProps } from './tabs'
import { useTabs, useTabsCustomStyle } from './composables'
const props = defineProps(tabsProps)
defineEmits(tabsEmits)
const {
tabItems,
componentId,
barComponentId,
barOffsetLeft,
scrollLeft,
showBar,
} = useTabs(props)
const { ns, tabsClass, tabsStyle, barClass, barStyle } =
useTabsCustomStyle(props)
</script>
<template>
<view :id="componentId" :class="[tabsClass]" :style="tabsStyle">
<!-- 距离顶部的距离占位 -->
<view
v-if="offsetTop"
:class="[ns.e('top-placeholder')]"
:style="{ height: `${offsetTop}px` }"
/>
<!-- 内容区域 -->
<scroll-view
:class="[ns.e('scroll-view')]"
:style="{
height: formatDomSizeValue(height || '100%'),
}"
:scroll-x="scroll"
scroll-with-animation
:scroll-left="scrollLeft"
>
<view
:class="[
ns.e('container'),
ns.is('scroll', scroll),
ns.is('no-bar', !showBar),
]"
>
<slot />
<!-- 滑块 -->
<view
v-if="bar || $slots.bar"
:id="barComponentId"
:class="[ns.e('bar-container')]"
:style="{
left: `${barOffsetLeft}px`,
opacity: `${barOffsetLeft && tabItems.length ? 1 : 0}`,
}"
>
<slot name="bar">
<view :class="[barClass]" :style="barStyle" />
</slot>
</view>
</view>
</scroll-view>
</view>
</template>
<style lang="scss" scoped>
@import '../../../theme-chalk/src/tabs.scss';
</style>
+8
View File
@@ -0,0 +1,8 @@
export interface TabsItemRect {
width: number
height: number
left: number
}
export type TabsRect = TabsItemRect
export type TabsBarRect = TabsItemRect