mirror of
https://github.com/ialley-workshop-open/uni-halo.git
synced 2026-07-27 04:20:43 +08:00
refactor: 项目整体重构与功能完善
1. 配置开发环境接口地址 2. 重构工具函数目录结构,迁移防抖节流等工具到base目录 3. 统一页面布局样式为全屏居中布局 4. 新增大量通用工具函数:深克隆、深合并、uuid、时间格式化、随机数、数组打乱、剪贴板操作等 5. 优化自定义tabbar实现,修复交互逻辑 6. 新增列表数据管理hook和请求loading封装 7. 移除无用测试文件和冗余代码 8. 完善首页内容,增加轮播图、作者卡片、金刚区等模块
This commit is contained in:
@@ -1,69 +0,0 @@
|
||||
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+')
|
||||
})
|
||||
})
|
||||
+56
-13
@@ -1,7 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import type { CustomTabBarItem } from './types'
|
||||
import { getI18nText } from './i18n'
|
||||
import { tabbarStore } from './store'
|
||||
import { tabbarList, tabbarStore } from './store'
|
||||
import { tabbarCacheEnable } from './config'
|
||||
|
||||
defineOptions({
|
||||
name: 'TabbarItem',
|
||||
// #ifdef MP-WEIXIN
|
||||
virtualHost: true,
|
||||
// #endif
|
||||
})
|
||||
|
||||
const props = defineProps<{
|
||||
count: number
|
||||
@@ -35,44 +43,69 @@ function activeBgColor() {
|
||||
}, 0)
|
||||
}
|
||||
|
||||
watch(() => tabbarStore.curIdx, (val) => {
|
||||
watch(() => tabbarStore.curIdx, () => {
|
||||
activeBgColor()
|
||||
})
|
||||
|
||||
function handleClick(index: number) {
|
||||
// 点击原来的不做操作
|
||||
if (index === tabbarStore.curIdx) {
|
||||
return
|
||||
}
|
||||
const list = tabbarList.value
|
||||
if (!list[index]) {
|
||||
return
|
||||
}
|
||||
|
||||
const url = list[index].pagePath
|
||||
tabbarStore.setCurIdx(index)
|
||||
if (tabbarCacheEnable) {
|
||||
uni.switchTab({ url })
|
||||
}
|
||||
else {
|
||||
uni.navigateTo({ url })
|
||||
}
|
||||
}
|
||||
</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),
|
||||
class="relative h-full flex items-center justify-center gap-x-1 rounded-full text-center transition-all duration-200"
|
||||
:class="{
|
||||
'!px-6 shrink-0': isActiveByIndex(index),
|
||||
'text-white': isActiveByIndex(index) && isActive,
|
||||
'flex-1': !isActiveByIndex(index),
|
||||
// 'pl-3': index === 0,
|
||||
// 'pr-3': index === count - 1,
|
||||
}"
|
||||
}" @click="handleClick(index)"
|
||||
>
|
||||
<!-- 背景 -->
|
||||
<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="absolute bottom-0 left-0 z-0 h-full w-full origin-center rounded-full bg-[#1A1A1A] transition-all duration-300"
|
||||
:class="[
|
||||
// isActiveByIndex(index) && isActive ? 'left-0 opacity-100' : '-left-12 opacity-0',
|
||||
isActiveByIndex(index) && isActive ? 'scale-100' : 'scale-0',
|
||||
isActiveByIndex(index) && isActive ? 'uh-scale-100' : 'uh-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" />
|
||||
<wd-icon
|
||||
class="relative z-10 transition-all duration-50" :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']" />
|
||||
<view
|
||||
class="relative z-10 font-bold transition-all duration-50"
|
||||
:class="[item.icon, isActiveByIndex(index) ? 'text-sm' : 'text-lg']"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="item.iconType === 'image'">
|
||||
<image
|
||||
:src="getImageByIndex(index, item)" mode="scaleToFill"
|
||||
:src="getImageByIndex(index, item)" mode="scaleToFill" class="relative z-10 transition-all duration-50"
|
||||
:class="isActiveByIndex(index) ? 'h-16px w-16px' : 'h-22px w-22px'"
|
||||
/>
|
||||
</template>
|
||||
<view v-if="isActiveByIndex(index)" class="text-xs font-bold">
|
||||
<view v-if="isActiveByIndex(index)" class="relative z-10 text-xs text-white font-bold">
|
||||
{{ getI18nText(item.text) }}
|
||||
</view>
|
||||
<!-- 角标显示 -->
|
||||
@@ -90,3 +123,13 @@ watch(() => tabbarStore.curIdx, (val) => {
|
||||
</template>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.uh-scale-0 {
|
||||
transform: scale(0);
|
||||
}
|
||||
|
||||
.uh-scale-100 {
|
||||
transform: scale(1);
|
||||
}
|
||||
</style>
|
||||
|
||||
+8
-19
@@ -43,19 +43,10 @@ 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%',
|
||||
@@ -68,16 +59,14 @@ export const customTabbarList: CustomTabBarItem[] = [
|
||||
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-blog/about/about',
|
||||
// text: '%tabbar.about%',
|
||||
// iconType: 'unocss',
|
||||
// icon: 'i-carbon-menu',
|
||||
// badge: 10,
|
||||
// roles: ['admin'],
|
||||
// },
|
||||
{
|
||||
pagePath: 'pages/me/me',
|
||||
text: '%tabbar.me%',
|
||||
|
||||
+40
-59
@@ -1,7 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { getCurrentInstance } from 'vue'
|
||||
// i-carbon-code
|
||||
import { customTabbarEnable, needHideNativeTabbar, tabbarCacheEnable } from './config'
|
||||
import { customTabbarEnable, needHideNativeTabbar } from './config'
|
||||
import { setTabbarItem } from './i18n'
|
||||
import { tabbarList, tabbarStore } from './store'
|
||||
import { sleep } from '@/utils/common'
|
||||
@@ -16,72 +15,43 @@ defineOptions({
|
||||
|
||||
const instance = getCurrentInstance()
|
||||
|
||||
// 临时配置
|
||||
// tabBar配置
|
||||
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)
|
||||
},
|
||||
})
|
||||
if (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)
|
||||
},
|
||||
})
|
||||
// 另外,支付宝里面,只要是 customTabbar 都需要隐藏
|
||||
if (customTabbarEnable) {
|
||||
uni.hideTabBar({
|
||||
fail(err) {
|
||||
console.log('hideTabBar fail: ', err)
|
||||
},
|
||||
success(res) {
|
||||
// console.log('hideTabBar success: ', res)
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
|
||||
@@ -92,6 +62,10 @@ const activeDomStyle = reactive({
|
||||
width: '70px',
|
||||
})
|
||||
|
||||
function isActiveByIndex(index: number) {
|
||||
return tabbarStore.curIdx === index
|
||||
}
|
||||
|
||||
function setActiveStyle(index: number) {
|
||||
activeDomStyle.left = `${tabbarRect.value[index].left - 21}px`
|
||||
activeDomStyle.width = `${tabbarRect.value[index].width + 9}px`
|
||||
@@ -108,7 +82,6 @@ function getTabBarRect() {
|
||||
query
|
||||
.selectAll('.tabbar-item')
|
||||
.boundingClientRect(async (data) => {
|
||||
console.log(data)
|
||||
tabbarRect.value = (data as UniApp.NodeInfo[]).map(item => ({
|
||||
left: item.left,
|
||||
width: item.width,
|
||||
@@ -135,14 +108,16 @@ onMounted(() => {
|
||||
</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 v-if="customTabbarEnable" :class="{ 'h-54px pb-safe': !tabBarConfig.useFloat }">
|
||||
<view class="fixed bottom-6 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]"
|
||||
class="uh-shadow-sm relative box-border h-54px flex flex-1 items-center overflow-hidden border border-white rounded-full border-solid bg-white/95 p-1 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)"
|
||||
:index="index" :global-active-slider-bar="tabBarConfig.useGlobalActiveSliderBar"
|
||||
class="tabbar-item h-full"
|
||||
:class="[isActiveByIndex(index) ? 'shrink-0' : 'flex-1']"
|
||||
/>
|
||||
<!-- 激活滑块 -->
|
||||
<view
|
||||
@@ -155,7 +130,7 @@ onMounted(() => {
|
||||
</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]"
|
||||
class="uh-shadow-sm box-border h-54px w-54px flex items-center justify-center border border-white rounded-full border-solid bg-white/95 text-2xl backdrop-blur-[3px]"
|
||||
>
|
||||
+
|
||||
</view>
|
||||
@@ -164,3 +139,9 @@ onMounted(() => {
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.uh-shadow-sm {
|
||||
box-shadow: 0 0 24rpx rgba(0, 0, 0, 0.075);
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user