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
+14
View File
@@ -0,0 +1,14 @@
import { withInstall, withNoopInstall } from '../../utils'
import Tabbar from './src/tabbar.vue'
import TabbarItem from './src/tabbar-item.vue'
export const TnTabbar = withInstall(Tabbar, {
TabbarItem,
})
export const TnTabbarItem = withNoopInstall(TabbarItem)
export default TnTabbar
export * from './src/tabbar'
export * from './src/tabbar-item'
export * from './src/types'
export type { TnTabbarInstance, TnTabbarItemInstance } from './src/instance'
@@ -0,0 +1,4 @@
export * from './tabbar-custom'
export * from './tabbar-item-custom'
export * from './use-tabbar'
export * from './use-tabbar-item'
@@ -0,0 +1,136 @@
import { computed, toRef } from 'vue'
import { useComponentColor, useNamespace } from '../../../../hooks'
import { formatDomSizeValue } from '../../../../utils'
import type { CSSProperties } from 'vue'
import type { TabbarProps } from '../tabbar'
export const useTabbarCustomStyle = (props: TabbarProps) => {
const ns = useNamespace('tabbar')
// 解析颜色
const [bgColorClass, bgColorStyle] = useComponentColor(
toRef(props, 'bgColor'),
'bg'
)
// tabbar对应的类
const tabbarClass = computed<string>(() => {
const cls: string[] = [ns.b()]
// 是否固定在底部
if (props.fixed) cls.push(ns.m('fixed'))
// 是否预留安全距离
if (props.safeAreaInsetBottom) cls.push('tn-u-safe-area')
// 是否有顶部阴影
if (props.topShadow) cls.push(ns.m('top-shadow'))
return cls.join(' ')
})
// tabbar对应的样式
const tabbarStyle = computed<CSSProperties>(() => {
const style: CSSProperties = {}
// 设置zIndex
if (props.zIndex) style.zIndex = props.zIndex
// 设置高度
if (props.height) style.height = formatDomSizeValue(props.height)
return style
})
// 背景颜色对应的类
const bgClass = computed<string>(() => {
const cls: string[] = [ns.e('bg')]
// 设置背景颜色
if (bgColorClass.value && !props.frosted) cls.push(bgColorClass.value)
// 设置毛玻璃效果
// #ifndef MP-ALIPAY
if (props.frosted) cls.push(ns.em('bg', 'frosted'))
// #endif
return cls.join(' ')
})
// 背景颜色对应的样式
const bgStyle = computed<CSSProperties>(() => {
const style: CSSProperties = {}
// 设置zIndex
if (props.zIndex) style.zIndex = props.zIndex - 1
// 设置背景颜色
if (!bgColorClass.value)
style.backgroundColor = bgColorStyle.value || 'var(--tn-color-white)'
// #ifndef MP-ALIPAY
if (props.frosted)
style.backgroundColor = bgColorStyle.value || 'rgba(255, 255, 255, 0.5)'
// #endif
return style
})
// 占位容器对应的样式
const placeholderStyle = computed<CSSProperties>(() => {
const style: CSSProperties = {}
// 设置zIndex
if (props.zIndex) style.zIndex = props.zIndex - 2
// 设置高度
if (props.height) style.height = formatDomSizeValue(props.height)
return style
})
// 突起按钮对应类和样式
const bulgeClass = computed<string>(() => {
const cls: string[] = [ns.e('bulge')]
if (props.topShadow) {
cls.push(ns.em('bulge', 'top-shadow'))
}
// 设置背景颜色
if (bgColorClass.value && !props.frosted) cls.push(bgColorClass.value)
// 设置毛玻璃效果
// #ifndef MP-ALIPAY
if (props.frosted) cls.push(ns.em('bulge', 'frosted'))
// #endif
return cls.join(' ')
})
const bulgeStyle = computed<CSSProperties>(() => {
const style: CSSProperties = {}
// 设置zIndex
if (props.zIndex) style.zIndex = props.zIndex - 1
else style.zIndex = 'inherit'
// 设置背景颜色
if (!bgColorClass.value)
style.backgroundColor = bgColorStyle.value || 'var(--tn-color-white)'
// #ifndef MP-ALIPAY
if (props.frosted)
style.backgroundColor = bgColorStyle.value || 'rgba(255, 255, 255, 0.5)'
// #endif
return style
})
return {
ns,
tabbarClass,
tabbarStyle,
bgClass,
bgStyle,
placeholderStyle,
bulgeClass,
bulgeStyle,
}
}
@@ -0,0 +1,178 @@
import { computed, inject, toRef } from 'vue'
import { tabbarContextKey } from '../../../../tokens'
import { useComponentColor, useNamespace } from '../../../../hooks'
import { formatDomSizeValue } from '../../../../utils'
import type { CSSProperties, Ref } from 'vue'
import type { TabbarItemProps } from '../tabbar-item'
import type { TabbarItemRect } from '../types'
type TabbarItemElementStyleValue = (type: 'icon' | 'text') => CSSProperties
type TabbarItemBulgeElementStyleValue = (
rectInfo: TabbarItemRect
) => CSSProperties
export const useTabbarItemCustomStyle = (
props: TabbarItemProps,
isActive: Ref<boolean>
) => {
const ns = useNamespace('tabbar-item')
const tabbarContext = inject(tabbarContextKey)
const activeColor = computed<string | undefined>(
() => props.activeColor || tabbarContext?.activeColor
)
const inactiveColor = computed<string | undefined>(
() => props.inactiveColor || tabbarContext?.inactiveColor
)
// 解析颜色
const [inactiveColorClass, inactiveColorStyle] = useComponentColor(
inactiveColor,
'text'
)
const [activeColorClass, activeColorStyle] = useComponentColor(
activeColor,
'text'
)
const [inactiveBgClass, inactiveBgStyle] = useComponentColor(
inactiveColor,
'bg'
)
const [activebgClass, activebgStyle] = useComponentColor(activeColor, 'bg')
const [bulgeBgClass, bulgeBgStyle] = useComponentColor(
toRef(props, 'bulgeBgColor'),
'bg'
)
const [bulgeTextColorClass, bulgeTextColorStyle] = useComponentColor(
toRef(props, 'bulgeTextColor'),
'text'
)
// 文字尺寸
const fontSize = computed<string>(() =>
formatDomSizeValue(props.fontSize || tabbarContext?.fontSize || '')
)
// tabbarItem对应的类
const tabbarItemClass = computed<string>(() => {
const cls: string[] = [ns.b()]
// 设置文字,图标颜色
if (isActive.value) {
if (activeColorClass.value) cls.push(activeColorClass.value)
} else {
if (inactiveColorClass.value) cls.push(inactiveColorClass.value)
}
// 判断是否只有图标或者文字
if (
(props.icon && props.activeIcon && !props.text) ||
(props.text && !props.icon && !props.activeIcon)
)
cls.push(ns.is('only-element'))
// 选中的状态
if (isActive.value) cls.push(ns.is('active'))
// 点击切换是是否有动画
if (tabbarContext?.switchAnimation) cls.push(ns.is('animation'))
return cls.join(' ')
})
// tabbarItem对应的样式
const tabbarItemStyle = computed<CSSProperties>(() => {
const style: CSSProperties = {}
// 设置文字,图标颜色
if (isActive.value) {
if (!activeColorClass.value)
style.color = activeColorStyle.value || 'var(--tn-color-primary)'
} else {
if (!inactiveColorClass.value)
style.color = inactiveColorStyle.value || 'var(--tn-color-gray)'
}
return style
})
// tabbarItem元素对应的样式
const tabbarItemElementStyle = computed<TabbarItemElementStyleValue>(() => {
return (type: 'icon' | 'text') => {
const style: CSSProperties = {}
// 设置文字尺寸
if (type === 'text') {
if (fontSize.value) style.fontSize = fontSize.value
}
return style
}
})
// 凸起按钮对应的类
const bulgeClass = computed<string>(() => {
const cls: string[] = [ns.e('bulge')]
// 设置背景颜色
if (props.bulgeBgColor) {
if (bulgeBgClass.value) cls.push(bulgeBgClass.value)
} else {
if (isActive.value) {
if (activebgClass.value) cls.push(activebgClass.value)
} else {
if (inactiveBgClass.value) cls.push(inactiveBgClass.value)
}
}
// 设置文字颜色
if (bulgeTextColorClass.value) cls.push(bulgeTextColorClass.value)
return cls.join(' ')
})
// 凸起按钮对应的样式
const bulgeStyle = computed<TabbarItemBulgeElementStyleValue>(() => {
return (rectInfo: TabbarItemRect) => {
const style: CSSProperties = {}
// 设置容器信息
let width = rectInfo.width
if (rectInfo?.maxWidth) {
width = rectInfo.maxWidth
}
style.width = `${width * 0.5}px`
style.height = style.width
style.top = `-${width * 0.16}px`
// 设置背景
if (props.bulgeBgColor) {
if (!bulgeBgClass.value) style.backgroundColor = bulgeBgStyle.value
} else {
if (isActive.value) {
if (!activebgClass.value) {
style.backgroundColor =
activebgStyle.value || 'var(--tn-color-primary)'
}
} else {
if (!inactiveBgClass.value) {
style.backgroundColor =
inactiveBgStyle.value || 'var(--tn-color-gray)'
}
}
}
// 设置文字颜色
if (bulgeTextColorStyle.value) style.color = bulgeTextColorStyle.value
return style
}
})
return {
ns,
tabbarItemClass,
tabbarItemStyle,
tabbarItemElementStyle,
bulgeClass,
bulgeStyle,
}
}
@@ -0,0 +1,116 @@
import {
computed,
getCurrentInstance,
inject,
nextTick,
onMounted,
onUnmounted,
ref,
} from 'vue'
import { tabbarContextKey } from '../../../../tokens'
import { useSelectorQuery } from '../../../../hooks'
import { debugWarn, generateId } from '../../../../utils'
import type { SetupContext } from 'vue'
import type { TabbarItemEmits, TabbarItemProps } from '../tabbar-item'
import type { TabbarItemRect } from '../types'
export const useTabbarItem = (
props: TabbarItemProps,
emit: SetupContext<TabbarItemEmits>['emit']
) => {
const tabbarContext = inject(tabbarContextKey)
// instance
const instance = getCurrentInstance()!
if (!tabbarContext) {
debugWarn('TnTabbarItem', 'TnTabbarItem必须在TnTabbar中使用')
}
if (!instance) {
debugWarn('TnTabbarItem', 'TnTabbarItem必须在setup中使用')
}
const uid = instance.uid || generateId()
const itemId = `tti-${uid}`
const { getSelectorNodeInfo } = useSelectorQuery(instance)
// 当前Item是否已激活
const isActive = computed<boolean>(() => tabbarContext?.activeUid === uid)
// 图标尺寸
const iconSize = computed<string>(
() => props.iconSize || tabbarContext?.iconSize || ''
)
// 是否有角标
const hasBadge = computed<boolean>(() => !!props.badge)
// item点击事件
const itemClick = () => {
if (isActive.value || props.disabled) return
tabbarContext?.setActiveItem(uid)
emit('click')
}
const itemRectInfo = ref<TabbarItemRect>({
width: 0,
height: 0,
left: 0,
})
let initRectCount = 0
// 获取容器节点信息
const getItemRectInfo = async () => {
try {
const rectInfo = await getSelectorNodeInfo(`#${itemId}`)
if (rectInfo.width && rectInfo.width < 30) {
throw new Error('获取TabbarItem节点宽度失败')
}
initRectCount = 0
itemRectInfo.value.width = rectInfo.width || 0
itemRectInfo.value.height = rectInfo.height || 0
itemRectInfo.value.left = rectInfo.left || 0
if (itemRectInfo.value.width > 80) {
itemRectInfo.value.maxWidth = 80
}
tabbarContext?.setBulgeCircle(itemRectInfo.value)
} catch (err) {
if (initRectCount > 10) {
initRectCount = 0
debugWarn('TnTabbarItem', `获取TabbarItem节点信息失败: ${err}`)
return
}
initRectCount++
setTimeout(() => {
getItemRectInfo()
}, 150)
}
}
tabbarContext?.addItem({
uid,
name: props.name,
})
onMounted(() => {
nextTick(() => {
if (props.bulge) getItemRectInfo()
})
})
onUnmounted(() => {
tabbarContext?.removeItem(uid)
})
return {
itemId,
isActive,
itemRectInfo,
iconSize,
hasBadge,
itemClick,
}
}
@@ -0,0 +1,184 @@
import {
getCurrentInstance,
nextTick,
provide,
reactive,
ref,
toRefs,
watch,
} from 'vue'
import { tabbarContextKey } from '../../../../tokens'
import { CHANGE_EVENT, UPDATE_MODEL_EVENT } from '../../../../constants'
import { useOrderedChildren, useSelectorQuery } from '../../../../hooks'
import { debugWarn, generateId, isBoolean, isPromise } from '../../../../utils'
import type { TabbarItemContext } from '../../../../tokens'
import type { TabbarProps } from '../tabbar'
import type { TabbarItemRect } from '../types'
export const useTabbar = (props: TabbarProps) => {
const { emit } = getCurrentInstance()!
const {
children: items,
addChild,
removeChild: removeItem,
} = useOrderedChildren<TabbarItemContext>()
const { getSelectorNodeInfo } = useSelectorQuery()
const rectId = `tt-${generateId()}`
const activeUid = ref<number>(-1)
// 添加Item
const addItem = (item: TabbarItemContext) => {
if (props.modelValue !== undefined && activeUid.value === -1) {
if (
props.modelValue === item.name ||
props.modelValue === items.value.length
) {
nextTick(() => {
updateActiveId(item.uid)
setTimeout(() => {
if (activeUid.value !== -1) return
activeUid.value = item.uid
}, 50)
})
}
}
addChild(item)
}
// 更新当前激活的Uid
const updateActiveId = (uid: number, changeEmit = false) => {
activeUid.value = uid
const itemIndex = items.value.findIndex((item) => item.uid === uid)
const value = items.value[itemIndex]?.name || itemIndex
emit(UPDATE_MODEL_EVENT, value)
if (changeEmit) {
nextTick(() => {
emit(CHANGE_EVENT, value)
})
}
}
// 设置当前被点击Item
const setActiveItem = (uid: number) => {
// 是否有切换拦截
if (!props.beforeSwitch) {
updateActiveId(uid, true)
return
}
const index = items.value.findIndex((item) => item.uid === uid)
const shouldSwitch = props.beforeSwitch(index)
const isPromiseOrBoolean = [
isPromise(shouldSwitch),
isBoolean(shouldSwitch),
].includes(true)
if (!isPromiseOrBoolean) {
debugWarn(
'TnTabbar',
'beforeSwitch切换前拦截函数必须返回Promise或者Boolean'
)
return
}
if (isPromise(shouldSwitch)) {
shouldSwitch.then((res) => {
if (res) updateActiveId(uid, true)
})
} else {
if (shouldSwitch) updateActiveId(uid, true)
}
}
// 根据modelValue设置当前激活的Item
const setActiveItemByValue = (value?: string | number) => {
if (value === undefined) {
// 如果没有传递任何值则设置第一个Item为激活状态
updateActiveId(items.value[0].uid)
return
}
let item: TabbarItemContext | undefined
// 如果类型是number,则先通过索引进行查找
if (typeof value === 'number') {
item = items.value?.[value]
}
// 如果没有找到,则通过name查找
if (!item) {
item = items.value.find((item) => item.name === value)
}
if (!item) {
// 设置第一个Item为激活状态
updateActiveId(items.value[0].uid)
} else {
updateActiveId(item.uid)
}
}
watch(
() => props.modelValue,
(val) => {
nextTick(() => {
setActiveItemByValue(val)
})
}
)
// 凸起按钮节点信息
const bulgeRectInfo = ref<TabbarItemRect>({
width: 0,
height: 0,
left: 0,
})
// 是否设置凸起按钮
const hasBulgeButton = ref<boolean>(false)
// 设置凸起按钮的位置
const setBulgeCircle = async (itemRectInfo: TabbarItemRect) => {
const { left } = itemRectInfo
try {
const rectInfo = await getSelectorNodeInfo(`#${rectId}`)
const { left: tabbarRectLeft } = rectInfo
let width = itemRectInfo.width
if (itemRectInfo?.maxWidth) {
width = itemRectInfo.maxWidth
}
bulgeRectInfo.value.width = width * 0.75
bulgeRectInfo.value.height = bulgeRectInfo.value.width
bulgeRectInfo.value.left =
left - (tabbarRectLeft || 0) + itemRectInfo.width / 2
hasBulgeButton.value = true
} catch (err) {
debugWarn('TnTabbar', `获取Tabbar节点信息失败: ${err}`)
}
}
// provide
provide(
tabbarContextKey,
reactive({
...toRefs(props),
items,
activeUid,
addItem,
removeItem,
setActiveItem,
setBulgeCircle,
})
)
return {
rectId,
hasBulgeButton,
bulgeRectInfo,
setActiveItem,
setActiveItemByValue,
}
}
@@ -0,0 +1,5 @@
import type Tabbar from './tabbar.vue'
import type TabbarItem from './tabbar-item.vue'
export type TnTabbarInstance = InstanceType<typeof Tabbar>
export type TnTabbarItemInstance = InstanceType<typeof TabbarItem>
@@ -0,0 +1,68 @@
import { buildProps, definePropType } from '../../../utils'
import { tabbarBaseProps } from '../../base/common-props/tabbar'
import type { ExtractPropTypes } from 'vue'
import type { BadgeProps } from '../../badge'
export type TabbarItemBadgeConfig = Partial<Pick<BadgeProps, 'dot'>>
export const tabbarItemProps = buildProps({
...tabbarBaseProps,
/**
* @description item的唯一标识,与modelValue对应
*/
name: {
type: [String, Number],
},
/**
* @description 图标
*/
icon: String,
/**
* @description 选中时的图标
*/
activeIcon: String,
/**
* @description 文字
*/
text: String,
/**
* @description item是否凸起
*/
bulge: Boolean,
/**
* @description 凸起按钮背景颜色
*/
bulgeBgColor: String,
/**
* @description 凸起按钮字体颜色
*/
bulgeTextColor: String,
/**
* @description 角标的值
*/
badge: {
type: [String, Number],
},
/**
* @description 角标配置
*/
badgeConfig: {
type: definePropType<TabbarItemBadgeConfig>(Object),
default: () => ({}),
},
/**
* @description 是否禁用
*/
disabled: Boolean,
})
export const tabbatItemEmits = {
/**
* @description 点击事件
*/
click: () => true,
}
export type TabbarItemProps = ExtractPropTypes<typeof tabbarItemProps>
export type TabbarItemEmits = typeof tabbatItemEmits
@@ -0,0 +1,78 @@
<script lang="ts" setup>
import TnIcon from '../../icon/src/icon.vue'
import TnBadge from '../../badge/src/badge.vue'
import { tabbarItemProps, tabbatItemEmits } from './tabbar-item'
import { useTabbarItem, useTabbarItemCustomStyle } from './composables'
const props = defineProps(tabbarItemProps)
const emits = defineEmits(tabbatItemEmits)
const { itemId, isActive, itemRectInfo, iconSize, hasBadge, itemClick } =
useTabbarItem(props, emits)
const {
ns,
tabbarItemClass,
tabbarItemStyle,
tabbarItemElementStyle,
bulgeClass,
bulgeStyle,
} = useTabbarItemCustomStyle(props, isActive)
</script>
// #ifdef MP-WEIXIN
<script lang="ts">
export default {
options: {
// 在微信小程序中将组件节点渲染为虚拟节点,更加接近Vue组件的表现(不会出现shadow节点下再去创建元素)
virtualHost: true,
},
}
</script>
// #endif
<template>
<view
:id="itemId"
:class="[tabbarItemClass]"
:style="tabbarItemStyle"
@tap.stop="itemClick"
>
<slot>
<!-- 图标 -->
<template v-if="bulge">
<view :class="[bulgeClass]" :style="bulgeStyle(itemRectInfo)">
<TnIcon :name="isActive ? activeIcon : icon" :size="iconSize" />
</view>
</template>
<template v-else>
<view
v-if="icon && activeIcon"
:class="[ns.e('icon')]"
:style="tabbarItemElementStyle('icon')"
>
<TnIcon :name="isActive ? activeIcon : icon" :size="iconSize" />
<!-- 角标 -->
<TnBadge
v-if="hasBadge"
:value="badge"
:dot="badgeConfig.dot"
:size="badgeConfig.dot ? '16' : ''"
type="danger"
/>
</view>
</template>
<!-- 文字 -->
<view
v-if="text"
:class="[ns.e('text')]"
:style="tabbarItemElementStyle('text')"
>
{{ text }}
</view>
</slot>
</view>
</template>
<style lang="scss" scoped>
@import '../../../theme-chalk/src/tabbar-item.scss';
</style>
+86
View File
@@ -0,0 +1,86 @@
import { CHANGE_EVENT, UPDATE_MODEL_EVENT, ZIndex } from '../../../constants'
import { buildProps, definePropType, isNumber, isString } from '../../../utils'
import { tabbarBaseProps } from '../../base/common-props/tabbar'
import type { ExtractPropTypes } from 'vue'
export type TabbarSwitchBeforeSwitchFunc = (
index: number
) => Promise<boolean> | boolean
export const tabbarProps = buildProps({
...tabbarBaseProps,
/**
* @description tabbar选中绑定的值
*/
modelValue: {
type: [String, Number],
default: 0,
},
/**
* @description tabbar高度
*/
height: {
type: String,
default: '100rpx',
},
/**
* @description tabbar背景颜色,如果需要设置毛玻璃的背景颜色,只能传递rgba的颜色值
*/
bgColor: String,
/**
* @description 开启毛玻璃效果
*/
frosted: Boolean,
/**
* @description 显示顶部阴影
*/
topShadow: {
type: Boolean,
default: true,
},
/**
* @description 切换时是否显示动画
*/
switchAnimation: Boolean,
/**
* @description 是否固定在底部
*/
fixed: Boolean,
/**
* @description 在固定之后是否开启占位空间
*/
placeholder: {
type: Boolean,
default: true,
},
/**
* @description 是否开启底部安全边距
*/
safeAreaInsetBottom: {
type: Boolean,
default: true,
},
/**
* @description 切换前回调
*/
beforeSwitch: {
type: definePropType<TabbarSwitchBeforeSwitchFunc>(Function),
},
/**
* @description zIndex
*/
zIndex: {
type: Number,
default: ZIndex.tabbar,
},
})
export const tabbarEmits = {
[UPDATE_MODEL_EVENT]: (val: string | number) =>
isNumber(val) || isString(val),
[CHANGE_EVENT]: (val: string | number) => isNumber(val) || isString(val),
}
export type TabbarProps = ExtractPropTypes<typeof tabbarProps>
export type TabbarEmits = typeof tabbarEmits
+60
View File
@@ -0,0 +1,60 @@
<script lang="ts" setup>
import { tabbarEmits, tabbarProps } from './tabbar'
import { useTabbar, useTabbarCustomStyle } from './composables'
const props = defineProps(tabbarProps)
defineEmits(tabbarEmits)
const { rectId, bulgeRectInfo, hasBulgeButton, setActiveItemByValue } =
useTabbar(props)
const {
ns,
tabbarClass,
tabbarStyle,
bgClass,
bgStyle,
placeholderStyle,
bulgeClass,
bulgeStyle,
} = useTabbarCustomStyle(props)
defineExpose({
/**
* @description 手动设置当前激活的item
*/
setActiveItem: setActiveItemByValue,
})
</script>
<template>
<view :id="rectId" :class="[tabbarClass]" :style="tabbarStyle">
<!-- 凸起按钮 -->
<view
v-if="hasBulgeButton"
:class="[bulgeClass]"
:style="{
width: `${bulgeRectInfo.width}px`,
height: `${bulgeRectInfo.height}px`,
left: `${bulgeRectInfo.left}px`,
top: `-${bulgeRectInfo.height * 0.35}px`,
...bulgeStyle,
}"
/>
<!-- 背景颜色 -->
<view :class="[bgClass]" :style="bgStyle" />
<!-- 内容 -->
<view :class="[ns.e('content'), { 'tn-u-safe-area': safeAreaInsetBottom }]">
<slot />
</view>
</view>
<!-- 占位容器 -->
<view
v-if="fixed && placeholder"
:class="[ns.e('placeholder'), { 'tn-u-safe-area': safeAreaInsetBottom }]"
:style="placeholderStyle"
/>
</template>
<style lang="scss" scoped>
@import '../../../theme-chalk/src/tabbar.scss';
</style>
+6
View File
@@ -0,0 +1,6 @@
export interface TabbarItemRect {
width: number
height: number
left: number
maxWidth?: number
}