topfans/frontend/pages/components/BottomNav.vue
2026-04-07 23:08:49 +08:00

296 lines
6.2 KiB
Vue

<template>
<view class="bottom-nav-container">
<!-- 导航底座 -->
<view class="base-wrapper" :class="{ 'expanded': isExpanded }">
<image
class="base-icon"
src="/static/icon/nav-base.png"
mode="aspectFit"
></image>
</view>
<!-- 小怪兽图标 -->
<view class="monster-wrapper" :class="{ 'expanded': isExpanded }">
<image
class="monster-icon"
src="/static/icon/character.png"
mode="aspectFit"
@click="toggleExpand"
></image>
</view>
<!-- 导航图标 -->
<view
v-for="(item, index) in navItems"
:key="index"
class="nav-icon-wrapper"
:class="{ 'expanded': isExpanded, 'active': activeTab === index }"
:style="getNavIconStyle(index)"
@click="handleNavClick(index)"
>
<image
class="nav-icon"
:src="item.icon"
mode="aspectFit"
></image>
<text class="nav-label">{{ item.name }}</text>
</view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue';
// 定义 props
const props = defineProps({
activeTab: {
type: Number,
default: 0
},
isExpanded: {
type: Boolean,
default: false
}
});
// 定义 emits
const emit = defineEmits(['update:activeTab', 'update:isExpanded']);
// 导航项配置
const navItems = [
{
name: '广场',
icon: '/static/icon/square.png',
angle: 122 // 左上方
},
{
name: '星册',
icon: '/static/icon/starbook.png',
angle: 107 // 右上方
},
{
name: '铸爱',
icon: '/static/icon/castlove.png',
angle: 90.01 // 正上方
},
{
name: '星城',
icon: '/static/icon/dressup.png',
angle: 73 // 右方
},
{
name: '好友',
icon: '/static/icon/friends.png',
angle: 57 // 右下方
}
];
// 扇形半径
const radius = 580; // rpx
// 计算每个导航图标的位置
const getNavIconStyle = (index) => {
if (!props.isExpanded) {
// 未展开时,所有图标都在中心位置,透明且缩小
return {
'--x': '0rpx',
'--y': '0rpx',
'--delay': '0s'
};
}
const item = navItems[index];
const angle = item.angle;
const radian = angle * Math.PI / 180;
const x = radius * Math.cos(radian);
const y = -radius * Math.sin(radian);
// 添加延迟,形成序列动画效果
const delay = index * 0.03;
return {
'--x': `${x}rpx`,
'--y': `${y}rpx`,
'--delay': `${delay}s`
};
};
// 切换展开/收起状态
const toggleExpand = () => {
emit('update:isExpanded', !props.isExpanded);
};
// 处理导航点击
const handleNavClick = (index) => {
// 触发事件,通知父组件切换 tab
emit('update:activeTab', index);
// 收起导航栏
emit('update:isExpanded', false);
};
</script>
<style scoped>
.bottom-nav-container {
position: fixed;
bottom: 0;
left: 0;
right: 0;
width: 100%;
height: 300rpx;
pointer-events: none;
z-index: 1000;
}
/* 导航底座 */
.base-wrapper {
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%) translateY(1500rpx);
width: 1200rpx;
height: 1200rpx;
pointer-events: none;
z-index: 0;
transition: transform 0.3s cubic-bezier(0.34, 1.26, 0.64, 1);
opacity: 0;
}
.base-wrapper.expanded {
transform: translateX(-50%) translateY(900rpx);
opacity: 1;
}
.base-icon {
width: 100%;
height: 100%;
}
/* 小怪兽图标容器 */
.monster-wrapper {
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%) translateY(60%);
width: 300rpx;
height: 300rpx;
pointer-events: auto;
z-index: 3;
transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.monster-wrapper.expanded {
transform: translateX(-50%) translateY(90rpx) scale(1.15);
}
.monster-icon {
width: 100%;
height: 100%;
filter: drop-shadow(0 8rpx 24rpx rgba(255, 107, 157, 0.5))
drop-shadow(0 0 40rpx rgba(255, 140, 180, 0.3));
transition: filter 0.3s ease, transform 0.3s ease;
animation: breathe 2s ease-in-out infinite;
}
.monster-wrapper.expanded .monster-icon {
filter: drop-shadow(0 16rpx 40rpx rgba(255, 107, 157, 0.7))
drop-shadow(0 0 60rpx rgba(255, 140, 180, 0.5));
animation: none;
}
/* 小怪兽呼吸动画 */
@keyframes breathe {
0%, 100% {
transform: scale(1.00);
}
50% {
transform: scale(1.02);
}
}
/* 导航图标容器 */
.nav-icon-wrapper {
position: absolute;
bottom: 0;
left: 50%;
width: 140rpx;
height: 180rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
transform: translate(-50%, 0) scale(0);
opacity: 0;
pointer-events: none;
z-index: 5;
transition: transform 0.35s cubic-bezier(0.34, 1.36, 0.64, 1),
opacity 0.3s ease-out;
transition-delay: var(--delay);
}
.nav-icon-wrapper.expanded {
transform: translate(calc(-50% + var(--x)), calc(200px + var(--y))) scale(1);
opacity: 1;
pointer-events: auto;
}
.nav-icon {
width: 100rpx;
height: 100rpx;
margin-bottom: 8rpx;
transition: transform 0.3s ease, filter 0.3s ease;
filter: drop-shadow(0 6rpx 16rpx rgba(0, 0, 0, 0.4))
drop-shadow(0 2rpx 8rpx rgba(0, 0, 0, 0.2));
position: relative;
}
/* 底部倒影效果 */
.nav-icon::after {
content: '';
position: absolute;
bottom: -100%;
left: 0;
width: 100%;
height: 100%;
background: inherit;
opacity: 0.2;
transform: scaleY(-1);
filter: blur(4rpx);
mask-image: linear-gradient(to bottom, rgba(0,0,0,0.3) 0%, transparent 60%);
-webkit-mask-image: linear-gradient(to bottom, rgba(0,0,0,0.3) 0%, transparent 60%);
pointer-events: none;
}
.nav-icon-wrapper.active .nav-icon {
transform: scale(1.25);
filter: drop-shadow(0 8rpx 24rpx rgba(255, 107, 157, 0.7))
drop-shadow(0 4rpx 16rpx rgba(255, 140, 66, 0.5))
drop-shadow(0 0 32rpx rgba(255, 107, 157, 0.4));
}
.nav-icon-wrapper:active .nav-icon {
transform: scale(0.95);
}
.nav-icon-wrapper.active:active .nav-icon {
transform: scale(1.15);
}
.nav-label {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.98);
font-weight: 500;
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.4),
0 1rpx 4rpx rgba(0, 0, 0, 0.3);
white-space: nowrap;
transition: all 0.3s ease;
}
.nav-icon-wrapper.active .nav-label {
color: #e6e6e6;
font-weight: bold;
text-shadow: 0 2rpx 12rpx rgba(255, 107, 157, 0.6),
0 1rpx 6rpx rgba(0, 0, 0, 0.4);
transform: scale(1.05);
}
</style>