1
0
mirror of https://github.com/ialley-workshop-open/uni-halo.git synced 2026-07-27 04:20:43 +08:00

chore: 清理旧文件并重构项目基础结构

- 删除大量废弃的旧代码、依赖和配置文件
- 新增基础项目配置、工具函数、页面模板和请求库
- 重构目录结构,统一项目规范
- 添加commitlint、husky等代码规范工具
This commit is contained in:
小莫唐尼
2026-06-11 02:13:47 +08:00
parent de142c4729
commit 3945ee611d
587 changed files with 34429 additions and 120475 deletions
+78
View File
@@ -0,0 +1,78 @@
# tabbar 说明
## tabbar 3种策略
`tabbar` 分为 `4 种` 情况:
- 0 `无 tabbar`,只有一个页面入口,底部无 `tabbar` 显示;常用语临时活动页。
- 1 `原生 tabbar`,使用 `switchTab` 切换 `tabbar``tabbar` 页面有缓存。
- 优势:原生自带的 `tabbar`,最先渲染,有缓存。
- 劣势:只能使用 2 组图片来切换选中和非选中状态,修改颜色只能重新换图片(或者用 iconfont)。
- 2 `有缓存自定义 tabbar`,使用 `switchTab` 切换 `tabbar``tabbar` 页面有缓存。使用了第三方 UI 库的 `tabbar` 组件,并隐藏了原生 `tabbar` 的显示。
- 优势:可以随意配置自己想要的 `svg icon`,切换字体颜色方便。有缓存。可以实现各种花里胡哨的动效等。
- 劣势:首次点击 `tabbar` 会闪烁。
## tabbar 配置说明
- 如果使用的是 `原生tabbar`,需要配置 `nativeTabbarList`,每个 `item` 需要配置 `path``text``iconPath``selectedIconPath` 等属性。
- 如果使用的是 `自定义tabbar`,需要配置 `customTabbarList`,每个 `item` 需要配置 `path``text``icon``iconType` 等属性(如果是 `image` 图片还需要配置2种图片)。
## 文件说明
`config.ts` 专门配置 `nativeTabbarList``customTabbarList` 的相关信息,请按照文件里面的注释配置相关项。
使用 `原生tabbar` 时,不需要关心下面2个文件:
- `store.ts` ,专门给 `自定义 tabbar` 提供状态管理,代码几乎不需要修改。
- `index.vue` ,专门给 `自定义 tabbar` 提供渲染逻辑,代码可以稍微修改,以符合自己的需求。
## 自定义tabbar的不同类型的配置
- uiLib 图标
```js
{
// ... 其他配置
"iconType": "uniUi",
"icon": "home",
}
```
- unocss 图标
```js
{
// ... 其他配置
// 注意 unocss 图标需要如下处理:(二选一)
// 1)在fg-tabbar.vue页面上引入一下并注释掉(见tabbar/index.vue代码第2行)
// 2)配置到 unocss.config.ts 的 safelist 中
iconType: 'unocss',
icon: 'i-carbon-code',
}
```
- iconfont 图标
```js
{
// ... 其他配置
// 注意 iconfont 图标需要额外加上 'iconfont',如下
iconType: 'iconfont',
icon: 'iconfont icon-my',
}
```
- image 本地图片
```js
{
// ... 其他配置
// 使用 image’时,需要配置 icon + iconActive 2张图片(不推荐)
// 既然已经用了自定义tabbar了,就不建议用图片了,所以不推荐
iconType: 'image',
icon: '/static/tabbar/home.png',
iconActive: '/static/tabbar/homeHL.png',
}
```
+69
View File
@@ -0,0 +1,69 @@
import type { CustomTabBarItem } from './types'
import { mount } from '@vue/test-utils'
import { afterEach, describe, expect, it, vi } from 'vitest'
import TabbarItem from './TabbarItem.vue'
// mock tabbar store,避免 uni.getStorageSync 在模块加载时执行
vi.mock('./store', () => ({
tabbarStore: { curIdx: 0 },
}))
const baseItem: CustomTabBarItem = {
text: '首页',
pagePath: 'pages/index/index',
iconType: 'unocss',
icon: 'i-carbon-home',
}
describe('TabbarItem', () => {
let wrapper: ReturnType<typeof mount>
afterEach(() => {
wrapper?.unmount()
})
it('渲染 text 文本', () => {
wrapper = mount(TabbarItem, {
props: { item: baseItem, index: 0 },
})
expect(wrapper.text()).toContain('首页')
})
it('isBulge=true 时不渲染文本', () => {
wrapper = mount(TabbarItem, {
props: { item: baseItem, index: 0, isBulge: true },
})
expect(wrapper.text()).not.toContain('首页')
})
it('iconType=unocss 时渲染图标 class', () => {
wrapper = mount(TabbarItem, {
props: { item: baseItem, index: 0 },
})
expect(wrapper.html()).toContain('i-carbon-home')
})
it('badge=dot 时渲染小红点(包含 rounded-full 样式)', () => {
const item: CustomTabBarItem = { ...baseItem, badge: 'dot' }
wrapper = mount(TabbarItem, {
props: { item, index: 0 },
})
expect(wrapper.html()).toContain('rounded-full')
})
it('badge 为数字时渲染数字角标', () => {
const item: CustomTabBarItem = { ...baseItem, badge: 5 }
wrapper = mount(TabbarItem, {
props: { item, index: 0 },
})
expect(wrapper.text()).toContain('5')
})
it('badge > 99 时显示 99+', () => {
const item: CustomTabBarItem = { ...baseItem, badge: 100 }
wrapper = mount(TabbarItem, {
props: { item, index: 0 },
})
expect(wrapper.text()).toContain('99+')
})
})
+92
View File
@@ -0,0 +1,92 @@
<script setup lang="ts">
import type { CustomTabBarItem } from './types'
import { getI18nText } from './i18n'
import { tabbarStore } from './store'
const props = defineProps<{
count: number
item: CustomTabBarItem
index: number
isBulge?: boolean
globalActiveSliderBar?: boolean
}>()
function getImageByIndex(index: number, item: CustomTabBarItem) {
if (!item.iconActive) {
console.warn('image 模式下,需要配置 iconActive (高亮时的图片),否则无法切换高亮图片')
return item.icon
}
return tabbarStore.curIdx === index ? item.iconActive : item.icon
}
function isActiveByIndex(index: number) {
return tabbarStore.curIdx === index
}
let timer: ReturnType<typeof setTimeout> | null = null
const isActive = ref(true)
function activeBgColor() {
clearTimeout(timer)
isActive.value = false
timer = setTimeout(() => {
timer = null
isActive.value = true
}, 0)
}
watch(() => tabbarStore.curIdx, (val) => {
activeBgColor()
})
</script>
<template>
<view
class="relative h-full flex items-center justify-center gap-x-1 rounded-full transition-all duration-200" :class="{
'!px-3 shrink-0': isActiveByIndex(index),
'text-white': isActiveByIndex(index) && isActive,
'flex-1': !isActiveByIndex(index),
// 'pl-3': index === 0,
// 'pr-3': index === count - 1,
}"
>
<!-- 背景 -->
<view
v-if="!props.globalActiveSliderBar"
class="absolute bottom-0 left-0 z-[-1] h-full w-full origin-center rounded-full bg-[#1A1A1A] transition-all duration-200"
:class="[
// isActiveByIndex(index) && isActive ? 'left-0 opacity-100' : '-left-12 opacity-0',
isActiveByIndex(index) && isActive ? 'scale-100' : 'scale-0',
]"
/>
<template v-if="item.iconType === 'uiLib'">
<!-- <wd-icon name="home" /> (https://wot-design-uni.cn/component/icon.html) -->
<wd-icon :name="item.icon" :size="isActiveByIndex(index) ? 14 : 22" />
</template>
<template v-if="item.iconType === 'unocss' || item.iconType === 'iconfont'">
<view class="font-bold transition-all duration-200" :class="[item.icon, isActiveByIndex(index) ? 'text-sm' : 'text-lg']" />
</template>
<template v-if="item.iconType === 'image'">
<image
:src="getImageByIndex(index, item)" mode="scaleToFill"
:class="isActiveByIndex(index) ? 'h-16px w-16px' : 'h-22px w-22px'"
/>
</template>
<view v-if="isActiveByIndex(index)" class="text-xs font-bold">
{{ getI18nText(item.text) }}
</view>
<!-- 角标显示 -->
<template v-if="item.badge">
<template v-if="item.badge === 'dot'">
<view class="absolute right-0 top-0 h-2 w-2 rounded-full bg-#f56c6c" />
</template>
<template v-else>
<view
class="absolute top-0 box-border h-5 min-w-5 center scale-80 rounded-full bg-#f56c6c px-1 text-center text-xs text-white -right-2"
>
{{ item.badge > 99 ? '99+' : item.badge }}
</view>
</template>
</template>
</view>
</template>
+154
View File
@@ -0,0 +1,154 @@
import type { TabBar } from '@uni-helper/vite-plugin-uni-pages'
import type { CustomTabBarItem, NativeTabBarItem } from './types'
/**
* tabbar 选择的策略,更详细的介绍见 tabbar.md 文件
* 0: 'NO_TABBAR' `无 tabbar`
* 1: 'NATIVE_TABBAR' `原生 tabbar`
* 2: 'CUSTOM_TABBAR' `自定义 tabbar`
*
* 温馨提示:本文件的任何代码更改了之后,都需要重新运行,否则 pages.json 不会更新导致配置不生效
*/
export const TABBAR_STRATEGY_MAP = {
NO_TABBAR: 0,
NATIVE_TABBAR: 1,
CUSTOM_TABBAR: 2,
}
// TODO: 1/3. 通过这里切换使用tabbar的策略
// 如果是使用 NO_TABBAR(0)nativeTabbarList 和 customTabbarList 都不生效
// 如果是使用 NATIVE_TABBAR(1),只需要配置 nativeTabbarListcustomTabbarList 不生效
// 如果是使用 CUSTOM_TABBAR(2),只需要配置 customTabbarListnativeTabbarList 不生效
export const selectedTabbarStrategy = TABBAR_STRATEGY_MAP.CUSTOM_TABBAR
// TODO: 2/3. 使用 NATIVE_TABBAR 时,更新下面的 tabbar 配置
export const nativeTabbarList: NativeTabBarItem[] = [
{
iconPath: 'static/tabbar/home.png',
selectedIconPath: 'static/tabbar/homeHL.png',
pagePath: 'pages/index/index',
text: '%tabbar.home%',
},
{
iconPath: 'static/tabbar/personal.png',
selectedIconPath: 'static/tabbar/personalHL.png',
pagePath: 'pages/me/me',
text: '%tabbar.me%',
},
]
// TODO: 3/3. 使用 CUSTOM_TABBAR 时,更新下面的 tabbar 配置
// 如果需要配置鼓包,需要在 'tabbar/store.ts' 里面设置,最后在 `tabbar/index.vue` 里面更改鼓包的图片
export const customTabbarList: CustomTabBarItem[] = [
{
text: '%tabbar.home%',
pagePath: 'pages/index/index',
// 注意 unocss 图标需要如下处理:(二选一)
// 2)配置到 unocss.config.ts 的 safelist 中
iconType: 'unocss',
icon: 'i-carbon-home',
// badge: 'dot',
},
{
pagePath: 'pages-demo/i18n/index',
text: '%tabbar.i18n%',
iconType: 'unocss',
icon: 'i-carbon-ibm-watson-language-translator',
// badge: 10,
},
{
pagePath: 'pages/gallery/gallery',
text: '%tabbar.gallery%',
iconType: 'uiLib',
icon: 'image',
},
{
pagePath: 'pages/moments/moments',
text: '%tabbar.moments%',
iconType: 'uiLib',
icon: 'gift',
},
{
pagePath: 'pages-blog/about/about',
text: '%tabbar.about%',
// 1)在fg-tabbar.vue页面上引入一下并注释掉(见tabbar/index.vue代码第2行)
// 2)配置到 unocss.config.ts 的 safelist 中
iconType: 'unocss',
icon: 'i-carbon-menu',
// badge: 10,
roles: ['admin'],
},
{
pagePath: 'pages/me/me',
text: '%tabbar.me%',
iconType: 'uiLib',
icon: 'user',
},
// 其他类型演示
// 1、uiLib
// {
// pagePath: 'pages/index/index',
// text: '首页',
// iconType: 'uiLib',
// icon: 'home',
// },
// 2、iconfont
// {
// pagePath: 'pages/index/index',
// text: '首页',
// // 注意 iconfont 图标需要额外加上 'iconfont',如下
// iconType: 'iconfont',
// icon: 'iconfont icon-my',
// },
// 3、image
// {
// pagePath: 'pages/index/index',
// text: '首页',
// // 使用 image’时,需要配置 icon + iconActive 2张图片
// iconType: 'image',
// icon: '/static/tabbar/home.png',
// iconActive: '/static/tabbar/homeHL.png',
// },
]
/**
* 是否启用 tabbar 缓存
* NATIVE_TABBAR(1) 和 CUSTOM_TABBAR(2) 时,需要tabbar缓存
*/
export const tabbarCacheEnable
= [TABBAR_STRATEGY_MAP.NATIVE_TABBAR, TABBAR_STRATEGY_MAP.CUSTOM_TABBAR].includes(selectedTabbarStrategy)
/**
* 是否启用自定义 tabbar
* CUSTOM_TABBAR(2) 时,启用自定义tabbar
*/
export const customTabbarEnable = [TABBAR_STRATEGY_MAP.CUSTOM_TABBAR].includes(selectedTabbarStrategy)
/**
* 是否需要隐藏原生 tabbar
* CUSTOM_TABBAR(2) 时,需要隐藏原生tabbar
*/
export const needHideNativeTabbar = selectedTabbarStrategy === TABBAR_STRATEGY_MAP.CUSTOM_TABBAR
const _tabbarList = customTabbarEnable ? customTabbarList.map(item => ({ text: item.text, pagePath: item.pagePath })) : nativeTabbarList
export const tabbarList = customTabbarEnable ? customTabbarList : nativeTabbarList
// NATIVE_TABBAR(1) 时,显示原生Tabbar,在i18n的情况下需要 setTabbarItem (框架已经处理)
export const isNativeTabbar = selectedTabbarStrategy === TABBAR_STRATEGY_MAP.NATIVE_TABBAR
const _tabbar: TabBar = {
// 只有微信小程序支持 custom。App 和 H5 不生效
custom: selectedTabbarStrategy === TABBAR_STRATEGY_MAP.CUSTOM_TABBAR,
color: '#999999',
selectedColor: '#018d71',
backgroundColor: '#F8F8F8',
borderStyle: 'black',
height: '50px',
fontSize: '10px',
iconWidth: '24px',
spacing: '3px',
list: _tabbarList as unknown as TabBar['list'],
}
export const tabBar = tabbarCacheEnable ? _tabbar : undefined
+29
View File
@@ -0,0 +1,29 @@
import { t } from '@/locale'
import { isCurrentPageTabbar } from '@/utils'
import { isNativeTabbar, tabbarList } from './config'
// h5 中一直可以生效,小程序里面默认是无法动态切换的,这里借助vue模板自带响应式的方式
// 直接替换 %xxx% 为 t('xxx')即可
export function getI18nText(key: string) {
// 获取 %xxx% 中的 xxx
const match = key.match(/%(.+?)%/)
if (match) {
key = match[1]
}
console.log('设置多语言:', key)
return t(key)
}
export function setTabbarItem() {
// 只有使用原生Tabbar才需要 setTabbarItem
// 而且只有当前页是tabbar页才能设置
console.log('设置多语言:setTabBarItem', isNativeTabbar, isCurrentPageTabbar())
if (isNativeTabbar && isCurrentPageTabbar()) {
tabbarList.forEach((item, index) => {
uni.setTabBarItem({
index,
text: getI18nText(item.text),
})
})
}
}
+166
View File
@@ -0,0 +1,166 @@
<script setup lang="ts">
import { getCurrentInstance } from 'vue'
// i-carbon-code
import { customTabbarEnable, needHideNativeTabbar, tabbarCacheEnable } from './config'
import { setTabbarItem } from './i18n'
import { tabbarList, tabbarStore } from './store'
import { sleep } from '@/utils/common'
import TabbarItem from './TabbarItem.vue'
// #ifdef MP-WEIXIN
// 将自定义节点设置成虚拟的(去掉自定义组件包裹层),更加接近Vue组件的表现,能更好的使用flex属性
defineOptions({
virtualHost: true,
})
// #endif
const instance = getCurrentInstance()
// 临时配置
const tabBarConfig = reactive({
useFloat: true,
useGlobalActiveSliderBar: false,
})
/**
* 中间的鼓包tabbarItem的点击事件
*/
function handleClickBulge() {
uni.showToast({
title: '点击了中间的鼓包tabbarItem',
icon: 'none',
})
}
function handleClick(index: number) {
// 点击原来的不做操作
if (index === tabbarStore.curIdx) {
return
}
const list = tabbarList.value
if (!list[index]) {
return
}
if (list[index].isBulge) {
handleClickBulge()
return
}
const url = list[index].pagePath
tabbarStore.setCurIdx(index)
if (tabbarCacheEnable) {
uni.switchTab({ url })
}
else {
uni.navigateTo({ url })
}
}
// #ifndef MP-WEIXIN || MP-ALIPAY
// 因为有了 custom:true 微信里面不需要多余的hide操作
onLoad(() => {
// 解决原生 tabBar 未隐藏导致有2个 tabBar 的问题
needHideNativeTabbar
&& uni.hideTabBar({
fail(err) {
console.log('hideTabBar fail: ', err)
},
success(res) {
// console.log('hideTabBar success: ', res)
},
})
})
// #endif
// #ifdef MP-ALIPAY
onMounted(() => {
// 解决支付宝自定义tabbar 未隐藏导致有2个 tabBar 的问题; 注意支付宝很特别,需要在 onMounted 钩子调用
customTabbarEnable // 另外,支付宝里面,只要是 customTabbar 都需要隐藏
&& uni.hideTabBar({
fail(err) {
console.log('hideTabBar fail: ', err)
},
success(res) {
// console.log('hideTabBar success: ', res)
},
})
})
// #endif
const tabbarRect = ref<Array<{ left: number, width: number }>>([])
const activeDomStyle = reactive({
left: 'initial',
width: '70px',
})
function setActiveStyle(index: number) {
activeDomStyle.left = `${tabbarRect.value[index].left - 21}px`
activeDomStyle.width = `${tabbarRect.value[index].width + 9}px`
}
watch(() => tabbarStore.curIdx, async (newVal) => {
await sleep(150)
setActiveStyle(newVal)
})
// 获取tabbar的元素位置
function getTabBarRect() {
const query = uni.createSelectorQuery().in(instance.proxy)
query
.selectAll('.tabbar-item')
.boundingClientRect(async (data) => {
console.log(data)
tabbarRect.value = (data as UniApp.NodeInfo[]).map(item => ({
left: item.left,
width: item.width,
}))
setActiveStyle(tabbarStore.prevIdx)
if (tabbarStore.curIdx !== tabbarStore.prevIdx) {
await sleep(100)
setActiveStyle(tabbarStore.curIdx)
}
})
.exec()
}
// 注意,上面处理的是自定义tabbar,下面处理的是原生tabbar,参考:https://unibest.tech/base/10-i18n
onShow(() => {
console.log('onShow')
setTabbarItem()
})
onMounted(() => {
console.log('onMounted')
getTabBarRect()
})
</script>
<template>
<view v-if="customTabbarEnable" :class="{ 'h-50px pb-safe': !tabBarConfig.useFloat }">
<view class="fixed bottom-4 left-4 right-4 z-100 flex items-center justify-between gap-x-4" @touchmove.stop.prevent>
<view
class="relative box-border h-50px flex flex-1 items-center overflow-hidden border border-white rounded-full border-solid bg-white/95 p-1 shadow-[0_0_10px_rgba(0,0,0,0.075)] backdrop-blur-[3px]"
>
<TabbarItem
v-for="(item, index) in tabbarList" :key="index" :item="item" :count="tabbarList.length"
:index="index" :global-active-slider-bar="tabBarConfig.useGlobalActiveSliderBar" class="tabbar-item relative text-center" @click="handleClick(index)"
/>
<!-- 激活滑块 -->
<view
v-if="tabBarConfig.useGlobalActiveSliderBar"
class="absolute left-0 top-0 z-[-1] box-border h-full origin-center rounded-full p-1 transition-all duration-300"
:style="[activeDomStyle]"
>
<view class="h-full w-full rounded-full bg-[#1A1A1A]" />
</view>
</view>
<!-- 右侧搜索框或者菜单 -->
<view
class="box-border h-50px w-50px flex items-center justify-center border border-white rounded-full border-solid bg-white/95 text-2xl shadow-[0_0_10px_rgba(0,0,0,0.075)] backdrop-blur-[3px]"
>
+
</view>
<view v-if="!tabBarConfig.useFloat" class="pb-safe" />
</view>
</view>
</template>
+127
View File
@@ -0,0 +1,127 @@
import type { CustomTabBarItem, CustomTabBarItemBadge } from './types'
import { computed, reactive } from 'vue'
import { useUserStore } from '@/store/user'
import { sleep } from '@/utils/common'
import { tabbarList as _tabbarList, selectedTabbarStrategy, TABBAR_STRATEGY_MAP } from './config'
/** tabbarList 里面的 path 从 pages.config.ts 得到 */
const baseTabbarList = reactive<CustomTabBarItem[]>(_tabbarList.map(item => ({
...item,
pagePath: item.pagePath.startsWith('/') ? item.pagePath : `/${item.pagePath}`, // 统一成 '/' 开头的路径
})))
const userRoles = computed(() => {
const userStore = useUserStore()
const userInfo = userStore.userInfo.value
if (Array.isArray(userInfo?.roles) && userInfo.roles.length > 0) {
return userInfo.roles
}
if (userInfo?.role) {
return [userInfo.role]
}
return []
})
const tabbarList = computed(() => {
const roles = userRoles.value
if (roles.length === 0) {
return baseTabbarList.filter(item => !item.roles || item.roles.length === 0)
}
return baseTabbarList.filter(item => !item.roles || item.roles.length === 0 || item.roles.some(role => roles.includes(role)))
})
export function isPageTabbar(path: string) {
if (selectedTabbarStrategy === TABBAR_STRATEGY_MAP.NO_TABBAR) {
return false
}
const _path = normalizeRoutePath(path)
return _path === '/' || tabbarList.value.some(item => item.pagePath === _path)
}
export function normalizeRoutePath(path?: string) {
if (!path) {
return ''
}
const _path = path.split('?')[0]
return _path.startsWith('/') ? _path : `/${_path}`
}
function getCurrentPagePath() {
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
return normalizeRoutePath(currentPage?.route)
}
function findTabbarIndexByPath(path?: string) {
const normalizedPath = normalizeRoutePath(path)
if (normalizedPath === '/') {
return 0
}
return tabbarList.value.findIndex(item => item.pagePath === normalizedPath)
}
/**
* 自定义 tabbar 的状态管理,原生 tabbar 无需关注本文件
* tabbar 状态,增加 storageSync 保证刷新浏览器时在正确的 tabbar 页面
* 使用reactive简单状态,而不是 pinia 全局状态
*/
const tabbarStore = reactive({
curIdx: uni.getStorageSync('app-tabbar-index') || 0,
prevIdx: uni.getStorageSync('app-tabbar-index') || 0,
async setCurIdx(idx: number) {
this.curIdx = idx
this.prevIdx = uni.getStorageSync('app-tabbar-index') || 0
await sleep(30)
uni.setStorageSync('app-tabbar-index', idx)
},
setTabbarItemBadge(idx: number, badge: CustomTabBarItemBadge) {
const list = tabbarList.value
if (list[idx]) {
list[idx].badge = badge
}
},
setAutoCurIdx(path: string) {
const list = tabbarList.value
if (list.length === 0) {
this.setCurIdx(0)
return
}
const index = findTabbarIndexByPath(path)
if (index >= 0) {
this.setCurIdx(index)
return
}
if (this.curIdx < 0 || this.curIdx >= list.length) {
this.setCurIdx(0)
}
},
syncCurIdxByCurrentPage() {
const currentPath = getCurrentPagePath()
if (currentPath) {
this.setAutoCurIdx(currentPath)
}
},
syncCurIdxByCurrentPageAsync() {
setTimeout(() => {
this.syncCurIdxByCurrentPage()
}, 0)
},
isCurrentRouteTabbarItem(index: number) {
const item = tabbarList.value[index]
if (!item) {
return false
}
return findTabbarIndexByPath(getCurrentPagePath()) === index
},
restorePrevIdx() {
if (this.prevIdx === this.curIdx)
return
this.setCurIdx(this.prevIdx)
this.prevIdx = uni.getStorageSync('app-tabbar-index') || 0
},
})
export { tabbarList, tabbarStore }
+37
View File
@@ -0,0 +1,37 @@
import type { TabBar } from '@uni-helper/vite-plugin-uni-pages'
import type { UserRole } from '@/api/types/login'
import type { RemoveLeadingSlashFromUnion } from '@/typings'
/**
* 原生 tabbar 的单个选项配置
*/
export type NativeTabBarItem = TabBar['list'][number] & {
pagePath: RemoveLeadingSlashFromUnion<_LocationUrl>
}
/** badge 显示一个数字或 小红点(样式可以直接在 tabbar/index.vue 里面修改) */
export type CustomTabBarItemBadge = number | 'dot'
/** 自定义 tabbar 的单个选项配置 */
export interface CustomTabBarItem {
text: string
pagePath: RemoveLeadingSlashFromUnion<_LocationUrl>
/** 图标类型,不建议用 image 模式,因为需要配置 2 张图,更麻烦 */
iconType: 'uiLib' | 'unocss' | 'iconfont' | 'image'
/**
* icon 的路径
* - uiLib: wot-design-uni 图标的 icon prop
* - unocss: unocss 图标的类名
* - iconfont: iconfont 图标的类名
* - image: 图片的路径
*/
icon: string
/** 只有在 image 模式下才需要,传递的是高亮的图片 */
iconActive?: string
/** badge 显示一个数字或 小红点 */
badge?: CustomTabBarItemBadge
/** 是否是中间的鼓包tabbarItem */
isBulge?: boolean
// roles 不写 → 所有用户都能看到;roles 写了 → 只有匹配角色可见
roles?: UserRole[]
}