206 lines
4.7 KiB
Vue
206 lines
4.7 KiB
Vue
<template>
|
||
<!--
|
||
外层容器:默认为空壳,仅用于包裹
|
||
如果开启 placeholder,则通过 style 撑开高度
|
||
-->
|
||
<view
|
||
class="nav-wrapper"
|
||
:style="{
|
||
background: props.bgColor
|
||
}"
|
||
>
|
||
<!-- 【占位层】:只在开启 placeholder 时渲染,纯粹为了撑高度 -->
|
||
<view
|
||
v-if="placeholder"
|
||
class="nav-placeholder"
|
||
:style="{ height: totalNavHeight + 'px' }"
|
||
></view>
|
||
|
||
<!-- 【功能层】:Fixed 定位,脱离文档流,永远显示在最上层 -->
|
||
<view class="nav-fixed-layer" :style="fixedLayerStyle">
|
||
<!-- 状态栏区域(用于沉浸式背景填充) -->
|
||
<view class="status-bar" :style="{ height: statusBarHeight + 'px' }"></view>
|
||
|
||
<!-- 导航栏主体 -->
|
||
<view class="nav-main" :style="{ height: navBarHeight + 'px' }">
|
||
<!-- 左侧 -->
|
||
<view class="nav-left" :style="{ width: leftWidth + 'px' }">
|
||
<slot name="left">
|
||
<view class="back-btn" @click="goBack" v-if="showBack">
|
||
<uni-icons type="left" size="22" color="inherit"></uni-icons>
|
||
<text v-if="backText" class="back-text">{{ backText }}</text>
|
||
</view>
|
||
</slot>
|
||
</view>
|
||
|
||
<!-- 中间 -->
|
||
<view class="nav-center">
|
||
<slot name="center">
|
||
<text class="nav-title" :style="{ color: titleColor }">{{ title }}</text>
|
||
</slot>
|
||
</view>
|
||
|
||
<!-- 右侧 -->
|
||
<view class="nav-right" :style="{ width: rightWidth + 'px' }">
|
||
<slot name="right"></slot>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, computed, onMounted } from 'vue'
|
||
|
||
// --- Props ---
|
||
interface Props {
|
||
title?: string
|
||
bgColor?: string
|
||
titleColor?: string
|
||
showBack?: boolean
|
||
backText?: string
|
||
placeholder?: boolean // 是否开启“占位层”
|
||
}
|
||
|
||
const props = withDefaults(defineProps<Props>(), {
|
||
title: '标题',
|
||
bgColor: 'transparent',
|
||
titleColor: '#333333',
|
||
showBack: true,
|
||
backText: '',
|
||
placeholder: true // 默认开启占位,防止遮挡
|
||
})
|
||
const emits = defineEmits(['clickLeft'])
|
||
// --- 状态 ---
|
||
const statusBarHeight = ref(0)
|
||
const navBarHeight = ref(44)
|
||
const capsuleHeight = ref(0)
|
||
const gap = ref(0)
|
||
|
||
// --- 计算属性 ---
|
||
const totalNavHeight = computed(() => statusBarHeight.value + navBarHeight.value)
|
||
|
||
// 2. 功能层样式:Fixed 定位,负责视觉展示
|
||
const fixedLayerStyle = computed(() => ({
|
||
backgroundColor: props.bgColor,
|
||
zIndex: 9999
|
||
}))
|
||
|
||
const leftWidth = computed(() => {
|
||
// #ifdef MP-WEIXIN
|
||
return capsuleHeight.value || 40
|
||
// #endif
|
||
// #ifndef MP-WEIXIN
|
||
return 40
|
||
// #endif
|
||
})
|
||
const rightWidth = computed(() => leftWidth.value)
|
||
|
||
// --- 生命周期 ---
|
||
onMounted(() => {
|
||
initNavBar()
|
||
})
|
||
|
||
// --- 方法 ---
|
||
const initNavBar = () => {
|
||
const windowInfo = uni.getWindowInfo()
|
||
statusBarHeight.value = windowInfo.statusBarHeight || 0
|
||
|
||
// #ifdef MP-WEIXIN
|
||
const menuInfo = uni.getMenuButtonBoundingClientRect()
|
||
if (menuInfo.top && menuInfo.height) {
|
||
gap.value = menuInfo.top - statusBarHeight.value
|
||
navBarHeight.value = (gap.value > 0 ? gap.value : 4) * 2 + menuInfo.height
|
||
capsuleHeight.value = menuInfo.height
|
||
} else {
|
||
navBarHeight.value = 44
|
||
capsuleHeight.value = 0
|
||
}
|
||
// #endif
|
||
|
||
// #ifndef MP-WEIXIN
|
||
navBarHeight.value = 44
|
||
// #endif
|
||
}
|
||
|
||
const goBack = () => {
|
||
emits('clickLeft')
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
/* 【外层容器】:文档流中的普通盒子 */
|
||
.nav-wrapper {
|
||
width: 100%;
|
||
/* 这里不设置 position,让它老老实实在文档流里呆着 */
|
||
}
|
||
|
||
/* 【占位层】:看不见摸不着,只有高度 */
|
||
.nav-placeholder {
|
||
width: 100%;
|
||
/* 仅通过 height 撑开父容器 */
|
||
}
|
||
|
||
/* 【功能层】:脱离文档流,悬浮在页面上方 */
|
||
.nav-fixed-layer {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100vw;
|
||
box-sizing: border-box;
|
||
/* 视觉上的背景色在这里 */
|
||
}
|
||
|
||
.status-bar {
|
||
width: 100%;
|
||
pointer-events: none; /* 防止拦截点击 */
|
||
}
|
||
|
||
.nav-main {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
box-sizing: border-box;
|
||
padding: 0 20rpx;
|
||
padding-top: 10rpx;
|
||
}
|
||
|
||
.nav-left,
|
||
.nav-right {
|
||
display: flex;
|
||
height: 100%;
|
||
}
|
||
|
||
.nav-center {
|
||
flex: 1;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
height: 100%;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.nav-title {
|
||
font-size: 32rpx;
|
||
font-weight: bold;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
color: #000000;
|
||
line-height: 40rpx;
|
||
text-align: center;
|
||
}
|
||
|
||
.back-btn {
|
||
display: flex;
|
||
align-items: center;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.back-text {
|
||
font-size: 28rpx;
|
||
margin-left: 4rpx;
|
||
color: inherit;
|
||
}
|
||
</style>
|