文件
shanghaipeiwan/pwuniapp/components/CustomTabBar.vue
xcbyy 8196b1925b feat(ui): 首页/雷达/TabBar 首版高级感视觉升级
- 扩展设计 Token:表面层级、发丝线、圆角、间距、动效曲线、数字字体

- 首页:克制霓虹背景、玻璃搜索栏、大图 Banner、金色标题、横向筛选胶囊、封面渐变、认证角标、卡片错峰入场、匹配弹窗改底部 Sheet

- 雷达:感知舱玻璃层级、细线同心圆、低干扰扫描、Marker 弹性落点、订单卡片左侧状态线、暖色主题适配

- TabBar:玻璃悬浮、选中指示条、中间发单按钮分层高光、暖色主题同步

- 已通过 HBuilderX 编译并生成预览二维码;777 用例全绿
2026-10-23 11:21:55 +08:00

517 行
15 KiB
Vue

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

<template>
<view class="custom-tabbar-root" :class="{ 'theme-warm': currentThemeKey === 'warm' }">
<!-- 底部占位块,确保内容不被导航栏遮挡 -->
<view class="tabbar-placeholder" style="width: 100%; pointer-events: none;"></view>
<!-- 固定在屏幕底部的导航栏实体 -->
<view class="tabbar-container" style="position: fixed; bottom: 0; left: 0; right: 0; margin: 0 auto; max-width: 450px; display: flex; align-items: flex-start; justify-content: space-around; z-index: 999;">
<!-- 发单人端 (5 项:首页 / 附近活动 / 我要发单+ / 友圈 / 个人中心) -->
<template v-if="currentRole === 'PLAYER'">
<!-- 1. 首页 -->
<view
class="tab-item btn-active"
:class="{ active: current === 'home' }"
style="flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 118rpx; position: relative;"
@click="switchTab('home', '/pages/index/index')"
>
<image class="tab-icon-svg" :src="getTabIcon('home')" mode="aspectFit" style="width: 50rpx; height: 50rpx; flex-shrink: 0;" />
<text class="tab-label">首页</text>
</view>
<!-- 2. 附近活动 (原本的活动项目) -->
<view
class="tab-item btn-active"
:class="{ active: current === 'activity' }"
style="flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 118rpx; position: relative;"
@click="switchTab('activity', '/pages/activity/list')"
>
<image class="tab-icon-svg" :src="getTabIcon('activity')" mode="aspectFit" style="width: 50rpx; height: 50rpx; flex-shrink: 0;" />
<text class="tab-label">附近活动</text>
</view>
<!-- 3. 我要发单 (核心大圆圈 + 突出悬浮按键,居中对称) -->
<view class="tab-item special-item-wrap btn-active" style="flex: 1; height: 118rpx; display: flex; flex-direction: column; align-items: center; justify-content: flex-start; position: relative;" @click="handlePublish">
<view class="plus-circle-btn" style="width: 96rpx; height: 96rpx; min-width: 96rpx; min-height: 96rpx; max-width: 96rpx; max-height: 96rpx; border-radius: 50%; display: flex; align-items: center; justify-content: center; flex-shrink: 0;">
<image class="plus-svg" :src="ICONS.tabPlus" mode="aspectFit" style="width: 48rpx; height: 48rpx; flex-shrink: 0;" />
</view>
<text class="tab-label special-label">我要发单</text>
</view>
<!-- 4. 友圈 -->
<view
class="tab-item btn-active"
:class="{ active: current === 'community' }"
style="flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 118rpx; position: relative;"
@click="switchTab('community', '/pages/community/index')"
>
<image class="tab-icon-svg" :src="getTabIcon('community')" mode="aspectFit" style="width: 50rpx; height: 50rpx; flex-shrink: 0;" />
<text class="tab-label">友圈</text>
</view>
<!-- 5. 个人中心 -->
<view
class="tab-item btn-active"
:class="{ active: current === 'user' }"
style="flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 118rpx; position: relative;"
@click="switchTab('user', '/pages/user/index')"
>
<image class="tab-icon-svg" :src="getTabIcon('user')" mode="aspectFit" style="width: 50rpx; height: 50rpx; flex-shrink: 0;" />
<text class="tab-label">个人中心</text>
</view>
</template>
<!-- 接单人端 (3 项:接单大厅 / 我要接单[5km雷达] / 个人中心) -->
<template v-else>
<!-- 1. 接单大厅 -->
<view
class="tab-item btn-active"
:class="{ active: current === 'hall' }"
style="flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 118rpx; position: relative;"
@click="switchTab('hall', '/pages/expert/hall')"
>
<image class="tab-icon-svg" :src="getTabIcon('hall')" mode="aspectFit" style="width: 50rpx; height: 50rpx; flex-shrink: 0;" />
<text class="tab-label">接单大厅</text>
</view>
<!-- 2. 我要接单 (核心特色:地图智能匹配附近5km定位) -->
<view
class="tab-item radar-item-wrap btn-active"
:class="{ active: current === 'radar' }"
style="flex: 1; height: 118rpx; display: flex; flex-direction: column; align-items: center; justify-content: center; position: relative;"
@click="switchTab('radar', '/pages/expert/radar')"
>
<view class="radar-badge-box" style="position: relative; display: flex; align-items: center; justify-content: center;">
<view class="radar-icon-box" :class="{ 'radar-anim': current === 'radar' }" style="width: 52rpx; height: 52rpx; display: flex; align-items: center; justify-content: center;">
<image class="radar-svg" :src="getTabIcon('radar')" mode="aspectFit" style="width: 52rpx; height: 52rpx; flex-shrink: 0;" />
</view>
<text class="radar-pill">附近5km</text>
</view>
<text class="tab-label radar-label">我要接单</text>
</view>
<!-- 3. 个人中心 -->
<view
class="tab-item btn-active"
:class="{ active: current === 'user' }"
style="flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 118rpx; position: relative;"
@click="switchTab('user', '/pages/user/index')"
>
<image class="tab-icon-svg" :src="getTabIcon('user')" mode="aspectFit" style="width: 50rpx; height: 50rpx; flex-shrink: 0;" />
<text class="tab-label">个人中心</text>
</view>
</template>
</view>
</view>
</template>
<script>
export default {
options: {
styleIsolation: 'apply-shared',
virtualHost: true
}
};
</script>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import { ICONS, getTabIconUri } from '@/common/icons.js';
import { getCurrentThemeKey } from '@/common/theme.js';
import { getCurrentRole, setCurrentRole, ROLE_TYPES, isExpertCertified, ensureLoginOrRedirect } from '@/common/role.js';
const props = defineProps({
current: {
type: String,
default: 'activity'
}
});
const currentRole = ref(getCurrentRole());
const currentThemeKey = ref(getCurrentThemeKey());
const getTabIcon = (key) => {
const isActive = props.current === key;
const isWarm = currentThemeKey.value === 'warm';
let color;
if (isActive) {
if (key === 'radar' || (currentRole.value === 'EXPERT' && key === 'hall')) {
color = '#10B981';
} else {
color = isWarm ? '#EA580C' : '#6366F1';
}
} else {
color = isWarm ? '#A8A29E' : '#64748B';
}
return getTabIconUri(key, color);
};
const onRoleChanged = (newRole) => {
if (newRole === 'EXPERT' && !isExpertCertified()) {
currentRole.value = 'PLAYER';
return;
}
currentRole.value = newRole;
};
const onThemeChanged = (newTheme) => {
currentThemeKey.value = newTheme;
};
onMounted(() => {
currentRole.value = getCurrentRole();
if (currentRole.value === 'EXPERT' && !isExpertCertified()) {
currentRole.value = 'PLAYER';
}
currentThemeKey.value = getCurrentThemeKey();
try {
uni.hideTabBar({ animation: false, fail: () => {} });
} catch (e) {}
uni.$on('role-change', onRoleChanged);
uni.$on('theme-change', onThemeChanged);
});
onUnmounted(() => {
uni.$off('role-change', onRoleChanged);
uni.$off('theme-change', onThemeChanged);
});
const switchTab = (tabKey, targetPath) => {
if (props.current === tabKey) return;
if ((tabKey === 'hall' || tabKey === 'radar') && !isExpertCertified()) {
uni.showToast({
title: '尚未通过大神认证,无法访问接单端',
icon: 'none'
});
return;
}
uni.reLaunch({
url: targetPath
});
};
const handlePublish = () => {
if (!ensureLoginOrRedirect('/pages/activity/create')) {
return;
}
uni.navigateTo({
url: '/pages/activity/create'
});
};
</script>
<style lang="scss">
.custom-tabbar-root {
width: 100%;
/* 底部占位高度,确保页面内容滚动到底部不被遮挡 */
.tabbar-placeholder {
width: 100%;
height: calc(126rpx + env(safe-area-inset-bottom));
pointer-events: none;
}
.tabbar-container {
position: fixed;
bottom: 0;
left: 0;
right: 0;
margin: 0 auto;
max-width: 450px;
box-sizing: border-box;
height: calc(118rpx + env(safe-area-inset-bottom));
padding-bottom: env(safe-area-inset-bottom);
display: flex;
align-items: flex-start;
justify-content: space-around;
z-index: 999;
/* 冷光暗夜默认主题 */
background: rgba(21, 29, 48, 0.95);
backdrop-filter: blur(16px);
border-top: 1rpx solid rgba(255, 255, 255, 0.08);
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.35);
}
.tab-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 118rpx;
position: relative;
.tab-icon-svg {
width: 50rpx;
height: 50rpx;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}
.tab-label {
font-size: 22rpx;
color: #94A3B8;
margin-top: 4rpx;
font-weight: 500;
transition: all 0.25s ease;
}
&.active {
.tab-icon-svg {
transform: scale(1.06);
}
.tab-label {
color: #6366F1;
font-weight: 600;
}
}
}
/* 核心:发单人端中间“我要发单”突出大圆圈 + */
.special-item-wrap {
position: relative;
top: -24rpx;
height: 118rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
padding-top: 2rpx;
flex-shrink: 0;
.plus-circle-btn {
width: 96rpx;
height: 96rpx;
min-width: 96rpx;
min-height: 96rpx;
max-width: 96rpx;
max-height: 96rpx;
aspect-ratio: 1 / 1;
border-radius: 50%;
box-sizing: border-box;
flex-shrink: 0;
background: linear-gradient(135deg, #6366F1 0%, #8B5CF6 100%);
display: flex;
align-items: center;
justify-content: center;
border: 4rpx solid #FFFFFF;
box-shadow: 0 8rpx 20rpx rgba(99, 102, 241, 0.45);
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
.plus-svg {
width: 48rpx;
height: 48rpx;
flex-shrink: 0;
}
}
.special-label {
margin-top: 4rpx;
color: #A5B4FC;
font-weight: 600;
font-size: 22rpx;
line-height: 1.2;
white-space: nowrap;
}
&:active .plus-circle-btn {
transform: scale(0.92);
box-shadow: 0 4rpx 12rpx rgba(99, 102, 241, 0.3);
}
}
/* 接单人端:“我要接单”雷达微标与动画 */
.radar-item-wrap {
height: 118rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.radar-badge-box {
position: relative;
display: flex;
align-items: center;
justify-content: center;
.radar-icon-box {
width: 52rpx;
height: 52rpx;
display: flex;
align-items: center;
justify-content: center;
.radar-svg {
width: 52rpx;
height: 52rpx;
}
}
.radar-pill {
position: absolute;
top: -12rpx;
right: -40rpx;
background: linear-gradient(135deg, #10B981, #059669);
color: #FFFFFF;
font-size: 18rpx;
line-height: 1.2;
padding: 2rpx 10rpx;
border-radius: 12rpx;
font-weight: 700;
box-shadow: 0 2rpx 8rpx rgba(16, 185, 129, 0.4);
white-space: nowrap;
}
}
.radar-label {
font-size: 23rpx;
color: #94A3B8;
margin-top: 4rpx;
font-weight: 500;
}
&.active {
.radar-label {
color: #10B981;
font-weight: 600;
}
.radar-anim {
animation: pulseRadar 1.8s infinite;
}
}
}
@keyframes pulseRadar {
0% {
transform: scale(1);
}
50% {
transform: scale(1.15);
}
100% {
transform: scale(1);
}
}
/* 浅色暖阳 (Warm Theme) 适配 */
&.theme-warm {
.tabbar-container {
background: rgba(255, 255, 255, 0.95);
border-top: 1rpx solid #F1EFE9;
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.05);
}
.tab-item {
.tab-label {
color: #78716C;
}
&.active {
.tab-label {
color: #EA580C;
}
}
}
.special-item-wrap {
.plus-circle-btn {
background: linear-gradient(135deg, #EA580C 0%, #F59E0B 100%);
border: 4rpx solid #FFFFFF;
box-shadow: 0 8rpx 20rpx rgba(234, 88, 12, 0.4);
}
.special-label {
color: #EA580C;
}
}
.radar-item-wrap {
&.active {
.radar-label {
color: #10B981;
}
}
}
}
}
/* ===== Premium visual layer: 玻璃悬浮导航 ===== */
.custom-tabbar-root {
.tabbar-container {
background: rgba(13, 20, 36, 0.88);
backdrop-filter: blur(20px) saturate(140%);
border-top: 1rpx solid rgba(255, 255, 255, 0.08);
box-shadow: 0 -12rpx 36rpx rgba(0, 0, 0, 0.28);
}
.tab-item {
transition: transform var(--motion-fast, 120ms) var(--ease-out, ease-out);
&:active {
transform: scale(0.96);
}
.tab-icon-svg {
transition: transform 0.2s var(--ease-out, ease-out), opacity 0.2s ease;
}
.tab-label {
letter-spacing: 0.2rpx;
}
&.active .tab-icon-svg {
transform: translateY(-2rpx) scale(1.04);
}
&.active::after {
content: '';
position: absolute;
bottom: 8rpx;
left: 50%;
width: 24rpx;
height: 4rpx;
border-radius: 4rpx;
background: linear-gradient(90deg, #818CF8, #6366F1);
transform: translateX(-50%);
box-shadow: 0 0 10rpx rgba(99, 102, 241, 0.5);
}
}
.special-item-wrap {
.plus-circle-btn {
background: linear-gradient(145deg, #818CF8 0%, #6366F1 46%, #7C3AED 100%);
border-width: 5rpx;
box-shadow:
0 12rpx 28rpx rgba(99, 102, 241, 0.4),
0 0 0 8rpx rgba(99, 102, 241, 0.08);
}
&:active .plus-circle-btn {
transform: scale(0.94);
}
}
.radar-item-wrap.active::after {
background: linear-gradient(90deg, #6EE7B7, #10B981);
box-shadow: 0 0 10rpx rgba(16, 185, 129, 0.5);
}
&.theme-warm {
.tabbar-container {
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(20px) saturate(130%);
border-top-color: #F0ECE4;
box-shadow: 0 -12rpx 30rpx rgba(28, 25, 23, 0.07);
}
.tab-item.active::after {
background: linear-gradient(90deg, #FB923C, #EA580C);
box-shadow: 0 0 10rpx rgba(234, 88, 12, 0.35);
}
.special-item-wrap .plus-circle-btn {
background: linear-gradient(145deg, #FB923C 0%, #EA580C 55%, #C2410C 100%);
box-shadow:
0 12rpx 26rpx rgba(234, 88, 12, 0.32),
0 0 0 8rpx rgba(234, 88, 12, 0.07);
}
}
}
</style>