This commit is contained in:
2026-01-05 12:47:14 +08:00
commit 1fc846fae3
1614 changed files with 162035 additions and 0 deletions

View File

@@ -0,0 +1,35 @@
.nut-theme-dark {
.nut-tabbar {
background: $dark-background;
border-color: $dark-background;
}
}
.nut-tabbar {
box-sizing: content-box;
display: flex;
align-items: center;
width: 100%;
height: $tabbar-height;
background: $white;
border: 0;
border-top: $tabbar-border-top;
border-bottom: $tabbar-border-bottom;
box-shadow: $tabbar-box-shadow;
&:last-child {
border-right: 0;
}
&-bottom {
position: fixed;
bottom: 0;
left: 0;
z-index: 888;
}
&-safebottom {
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
}
}

View File

@@ -0,0 +1,2 @@
export * from './tabbar'
export * from './types'

View File

@@ -0,0 +1,43 @@
import type { ExtractPropTypes, InjectionKey } from 'vue'
import { UPDATE_MODEL_EVENT } from '../_constants'
import { commonProps, isNumber, isString, makeNumericProp } from '../_utils'
import type { TabbarContext } from './types'
export const tabbarProps = {
...commonProps,
/**
* @description 选中标签的索引值或者名称
*/
modelValue: makeNumericProp(0),
/**
* @description 是否固定在页面底部
*/
bottom: Boolean,
/**
* @description icon激活的颜色
*/
activeColor: String,
/**
* @description icon未激活的颜色
*/
unactiveColor: String,
/**
* @description 是否开启iphone系列全面屏底部安全区适配
*/
safeAreaInsetBottom: Boolean,
/**
* @description 固定在底部时,是否在标签位置生成一个等高的占位元素
*/
placeholder: Boolean,
}
export type TabBarProps = ExtractPropTypes<typeof tabbarProps>
export const tabbarEmits = {
tabSwitch: (val: any, index: number | string) => (val instanceof Object) && (isNumber(index) || isString(index)),
[UPDATE_MODEL_EVENT]: (val: string | number) => isString(val) || isNumber(val),
}
export type TabBarEmits = typeof tabbarEmits
export const TABBAR_CONTEXT_KEY: InjectionKey<TabbarContext> = Symbol('TABBAR_CONTEXT')

View File

@@ -0,0 +1,107 @@
<script lang="ts" setup>
import type { CSSProperties } from 'vue'
import { computed, defineComponent, getCurrentInstance, onMounted, provide, reactive, ref, watch } from 'vue'
import { PREFIX, UPDATE_MODEL_EVENT } from '../_constants'
import { useSelectorQuery } from '../_hooks'
import { getMainClass } from '../_utils'
import { TABBAR_CONTEXT_KEY, tabbarEmits, tabbarProps } from './tabbar'
import type { TabbarContext } from './types'
const props = defineProps(tabbarProps)
const emit = defineEmits(tabbarEmits)
const instance = getCurrentInstance()!
const { getSelectorNodeInfo } = useSelectorQuery(instance)
const classes = computed(() => {
return getMainClass(props, componentName, {
'nut-tabbar-bottom': props.bottom,
'nut-tabbar-safebottom': props.safeAreaInsetBottom,
})
})
const wrapperClasses = computed(() => {
return {
'nut-tabbar__placeholder': props.bottom && props.placeholder,
}
})
const trulyHeight = ref<number>()
const wrapperStyles = computed(() => {
const value: CSSProperties = {}
if (trulyHeight.value != null) {
value.height = `${trulyHeight.value}px`
}
return value
})
const parentData: TabbarContext = reactive({
modelValue: props.modelValue,
activeColor: props.activeColor,
unactiveColor: props.unactiveColor,
children: [],
changeIndex,
})
provide(TABBAR_CONTEXT_KEY, parentData)
watch(() => props.modelValue, (value) => {
parentData.modelValue = value
})
watch(() => [props.activeColor, props.unactiveColor], ([activeColor, unactiveColor]) => {
parentData.activeColor = activeColor
parentData.unactiveColor = unactiveColor
})
function changeIndex(index: number, active: number | string) {
parentData.modelValue = active
emit(UPDATE_MODEL_EVENT, active)
emit('tabSwitch', parentData.children[index], active)
}
async function fetchTrulyHeight() {
const node = await getSelectorNodeInfo('.nut-tabbar')
trulyHeight.value = node.height
}
onMounted(() => {
if (props.bottom && props.placeholder) {
setTimeout(() => {
fetchTrulyHeight()
}, 500)
}
})
</script>
<script lang="ts">
const componentName = `${PREFIX}-tabbar`
export default defineComponent({
name: componentName,
options: {
virtualHost: true,
addGlobalClass: true,
styleIsolation: 'shared',
},
})
</script>
<template>
<view :class="wrapperClasses" :style="wrapperStyles">
<view :class="classes" :style="props.customStyle">
<slot />
</view>
</view>
</template>
<style lang="scss">
@import "./index";
</style>

View File

@@ -0,0 +1,7 @@
export interface TabbarContext {
modelValue: number | string
activeColor: string | undefined
unactiveColor: string | undefined
children: any[]
changeIndex: (index: number, active: number | string) => void
}