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:
@@ -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',
|
||||
}
|
||||
```
|
||||
@@ -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+')
|
||||
})
|
||||
})
|
||||
@@ -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>
|
||||
@@ -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),只需要配置 nativeTabbarList,customTabbarList 不生效
|
||||
// 如果是使用 CUSTOM_TABBAR(2),只需要配置 customTabbarList,nativeTabbarList 不生效
|
||||
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
|
||||
@@ -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),
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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 }
|
||||
@@ -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[]
|
||||
}
|
||||
Reference in New Issue
Block a user