文件
yjyf/yjyf-app/components/YjyfNavBar.vue
YuanWen 9ae2b976a8 feat: 重构首页与全局UI,新增服务选择抽屉与统一导航组件
新增统一导航栏组件YjyfNavBar,替换各页面原生导航
新增服务选择抽屉组件GoodsPickerDrawer,实现分类选品流程
新增tabbar图标矢量资源,优化底部导航样式
新增后端运力统计接口与多商品下单逻辑
修复页面跳转逻辑,统一使用switchTab/redirectTo
优化订单创建与评价页的UI组件化,提升一致性
2026-11-02 18:27:37 +08:00

392 行
9.2 KiB
Vue

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

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

<template>
<view class="yjyf-navbar-wrapper">
<!-- 固定在顶部的导航条 -->
<view
class="yjyf-navbar"
:class="[
'theme-' + theme,
{ 'is-fixed': fixed, 'has-border': border && theme === 'white' }
]"
:style="{
paddingTop: statusBarHeight + 'px',
background: customBackground || ''
}"
>
<view class="nav-content" :style="{ height: navBarHeight + 'px' }">
<!-- 1. 绝对居中的标题容器(保障物理视口绝对正中心,不被两端挤压) -->
<view
class="nav-title-box"
:class="['align-' + titleAlign]"
:style="{
paddingLeft: (titleAlign === 'left' ? leftWidth : sideSafeWidth) + 'px',
paddingRight: (titleAlign === 'left' ? rightSpaceWidth : sideSafeWidth) + 'px'
}"
>
<slot>
<text
class="nav-title"
:class="'title-' + theme"
:style="{ color: titleColor || defaultTitleColor }"
>
{{ title }}
</text>
</slot>
</view>
<!-- 2. 左侧操作区域:返回按钮 / 返回首页 / 自定义插槽 -->
<view class="nav-left" :style="{ width: leftWidth + 'px' }">
<slot name="left">
<view class="back-btn clickable-hover" v-if="canBack" @tap="handleBack">
<text class="back-icon" :class="'icon-' + theme">‹</text>
<text class="back-text" v-if="backText" :class="'icon-' + theme">{{ backText }}</text>
</view>
<view class="home-btn clickable-hover" v-else-if="canHome" @tap="handleHome">
<text class="home-icon" :class="'icon-' + theme">⌂</text>
</view>
</slot>
</view>
<!-- 3. 右侧胶囊安全避让占位 / 右侧自定义插槽 -->
<view
class="nav-right"
:style="{ width: rightSpaceWidth + 'px' }"
>
<slot name="right"></slot>
</view>
</view>
</view>
<!-- 占位垫高块,防止页面内容被 fixed 导航栏遮挡 -->
<view
class="yjyf-navbar-placeholder"
v-if="fixed && placeholder"
:style="{ height: totalNavHeight + 'px' }"
></view>
</view>
</template>
<script setup>
import { ref, computed, onMounted, nextTick } from 'vue';
const props = defineProps({
title: { type: String, default: '' },
theme: {
type: String,
default: 'white', // 'white' 白底黑字 | 'orange' 橙金渐变白字 | 'transparent' 透明
validator: (v) => ['white', 'orange', 'transparent'].includes(v)
},
customBackground: { type: String, default: '' },
titleColor: { type: String, default: '' },
titleAlign: { type: String, default: 'center' }, // 'center' | 'left'
showBack: { type: Boolean, default: null }, // null表示智能自动判断:非Tab页默认显示,Tab页默认不显示
showHome: { type: Boolean, default: false },
backText: { type: String, default: '' },
fixed: { type: Boolean, default: true },
placeholder: { type: Boolean, default: true },
border: { type: Boolean, default: false }
});
const emit = defineEmits(['back', 'home']);
// 系统底栏 Tab 页面白名单(这些页面默认不显示返回按钮)
const TAB_ROUTES = [
'pages/index/index',
'pages/order/list',
'pages/message/message',
'pages/mine/mine'
];
// 安全区域与各尺寸响应式变量
const statusBarHeight = ref(20);
const navBarHeight = ref(44);
const rightSpaceWidth = ref(16);
const leftWidth = ref(50);
const currentRoute = ref('');
// 计算总高度
const totalNavHeight = computed(() => {
return statusBarHeight.value + navBarHeight.value;
});
// 对称避让安全距离(确保居中标题绝不偏心,两端完全对称)
const sideSafeWidth = computed(() => {
return Math.max(leftWidth.value, rightSpaceWidth.value);
});
// 标题默认颜色计算
const defaultTitleColor = computed(() => {
if (props.theme === 'orange' || props.theme === 'transparent') {
return '#FFFFFF';
}
return '#1F1F1F';
});
// 判断当前是否是底栏 Tab 页面
const isTabRoute = computed(() => {
if (!currentRoute.value) return false;
return TAB_ROUTES.some((route) => currentRoute.value.includes(route));
});
// 判断是否展示返回按钮
const canBack = computed(() => {
// 1. 外部显式指定优先级最高
if (props.showBack !== null && props.showBack !== undefined) {
return !!props.showBack;
}
// 2. 若是 Tab 页面,默认隐藏返回按钮
if (isTabRoute.value) {
return false;
}
// 3. 非 Tab 页面(二级/三级页面、下单页、详情页等),默认一律展示返回按钮
return true;
});
// 判断是否显示首页按钮
const canHome = computed(() => {
if (props.showHome && !canBack.value) {
return true;
}
return false;
});
// 获取当前路由路径
function updateCurrentRoute() {
try {
const pages = getCurrentPages();
if (pages && pages.length) {
const cur = pages[pages.length - 1];
currentRoute.value = cur.route || (cur.$page && cur.$page.route) || '';
}
} catch (e) {}
}
function initSystemInfo() {
updateCurrentRoute();
try {
const sys = uni.getSystemInfoSync();
if (sys && sys.statusBarHeight) {
statusBarHeight.value = sys.statusBarHeight;
}
// #ifdef MP-WEIXIN
if (typeof uni.getMenuButtonBoundingClientRect === 'function') {
const menu = uni.getMenuButtonBoundingClientRect();
if (menu && menu.top && menu.width) {
// 导航高度严格对齐微信胶囊按钮居中公式
navBarHeight.value = (menu.top - statusBarHeight.value) * 2 + menu.height;
// 右侧胶囊安全避让距离
const rightMargin = (sys.windowWidth - menu.right) > 0 ? (sys.windowWidth - menu.right) : 7;
rightSpaceWidth.value = menu.width + (rightMargin * 2);
leftWidth.value = Math.max(50, rightSpaceWidth.value);
return;
}
}
// #endif
// #ifndef MP-WEIXIN
rightSpaceWidth.value = 16;
leftWidth.value = 50;
navBarHeight.value = 44;
// #endif
} catch (e) {
statusBarHeight.value = 20;
navBarHeight.value = 44;
rightSpaceWidth.value = 16;
leftWidth.value = 50;
}
}
initSystemInfo();
onMounted(() => {
initSystemInfo();
nextTick(() => {
updateCurrentRoute();
});
});
function handleBack() {
emit('back');
let pages = [];
try {
pages = getCurrentPages() || [];
} catch (e) {}
if (pages.length > 1) {
uni.navigateBack({
fail: () => {
uni.reLaunch({ url: '/pages/index/index' });
}
});
} else {
// 单层页面栈(如分享页、直接跳转页),智能兜底返回首页
uni.reLaunch({ url: '/pages/index/index' });
}
}
function handleHome() {
emit('home');
uni.reLaunch({ url: '/pages/index/index' });
}
</script>
<style scoped>
.yjyf-navbar-wrapper {
position: relative;
width: 100%;
z-index: 990;
}
.yjyf-navbar {
width: 100%;
box-sizing: border-box;
transition: background-color 0.2s ease;
}
.yjyf-navbar.is-fixed {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 990;
}
/* 导航内容排版容器 */
.nav-content {
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
box-sizing: border-box;
}
/* 1. 绝对居中的标题容器(脱离两端挤压,保障永远物理居中) */
.nav-title-box {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
display: flex;
align-items: center;
box-sizing: border-box;
z-index: 1;
pointer-events: none;
}
.nav-title-box.align-center {
justify-content: center;
text-align: center;
}
.nav-title-box.align-left {
justify-content: flex-start;
text-align: left;
}
.nav-title {
font-size: 34rpx;
font-weight: 600;
line-height: 1.4;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
pointer-events: auto;
letter-spacing: 0.5rpx;
}
/* 2. 左侧操作区域(返回 / 首页) */
.nav-left {
position: relative;
height: 100%;
display: flex;
align-items: center;
padding-left: 12rpx;
flex-shrink: 0;
box-sizing: border-box;
z-index: 2;
pointer-events: auto;
}
.back-btn, .home-btn {
display: flex;
align-items: center;
justify-content: center;
height: 64rpx;
min-width: 64rpx;
padding: 0 12rpx;
border-radius: 32rpx;
}
.back-icon {
font-size: 56rpx;
line-height: 1;
font-weight: 300;
margin-top: -6rpx;
}
.home-icon {
font-size: 40rpx;
line-height: 1;
}
.back-text {
font-size: 26rpx;
margin-left: 4rpx;
}
/* 3. 右侧胶囊安全避让占位 */
.nav-right {
position: relative;
height: 100%;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: flex-end;
padding-right: 12rpx;
box-sizing: border-box;
z-index: 2;
pointer-events: auto;
}
/* 主题体系 */
/* 1. 白底黑字主题 (white) */
.theme-white {
background: #FFFFFF;
}
.theme-white.has-border {
border-bottom: 1rpx solid #EFEFEF;
}
.icon-white {
color: #1F1F1F;
}
.title-white {
color: #1F1F1F;
}
/* 2. 橙金渐变主题 (orange) */
.theme-orange {
background: linear-gradient(135deg, #FF9436 0%, #F0710D 100%);
}
.icon-orange {
color: #FFFFFF;
}
.title-orange {
color: #FFFFFF;
}
/* 3. 透明主题 (transparent) */
.theme-transparent {
background: transparent;
}
.icon-transparent {
color: #FFFFFF;
}
.title-transparent {
color: #FFFFFF;
}
/* 占位垫高块 */
.yjyf-navbar-placeholder {
width: 100%;
}
</style>