新增统一导航栏组件YjyfNavBar,替换各页面原生导航 新增服务选择抽屉组件GoodsPickerDrawer,实现分类选品流程 新增tabbar图标矢量资源,优化底部导航样式 新增后端运力统计接口与多商品下单逻辑 修复页面跳转逻辑,统一使用switchTab/redirectTo 优化订单创建与评价页的UI组件化,提升一致性
392 行
9.2 KiB
Vue
392 行
9.2 KiB
Vue
<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>
|