自定义uniapp顶部导航栏
代码内容
点击查看代码
<template>
<view class="top-nav" :style="navBarStyle">
<view class="nav-content" :style="contentStyle">
<view class="nav-title-group">
<view class="nav-icon-btn" @click="$emit('profile')">
👤
<view class="badge" v-if="showNotifyBadge"></view>
</view>
<!-- 天气区域:实时信息 + 雪花动画 -->
<view class="nav-weather">
<view class="weather-info">
<text class="weather-icon">{{ weatherIcon }}</text>
<text class="weather-temp">{{ weatherTemp }}°</text>
</view>
<!-- 雪花飘落模拟(仅冬季效果) -->
<view class="snowflakes" v-if="isWinter">
<view v-for="i in 10" :key="i" class="snowflake" :style="getSnowStyle(i)"></view>
</view>
</view>
<!-- <view class="nav-icon-btn" @click="$emit('notify')">
🔔
<view class="badge" v-if="showNotifyBadge"></view>
</view> -->
</view>
<view class="nav-actions">
<image class="title-image"
src="https://minxianbigdata.oss-cn-hangzhou.aliyuncs.com/%E6%A0%87%E9%A2%982.png" mode="aspectFit">
</image>
</view>
</view>
<view class="notice" >
</view>
</view>
</template>
<script setup>
import {
ref,
computed,
onMounted
} from 'vue';
defineProps({
showNotifyBadge: {
type: Boolean,
default: true
}
});
defineEmits(['notify', 'profile']);
// 胶囊/状态栏信息
const menuTop = ref(0);
const menuHeight = ref(32);
const menuBottom = ref(32);
onMounted(() => {
// 微信小程序环境获取胶囊信息
// #ifdef MP-WEIXIN
try {
const rect = uni.getMenuButtonBoundingClientRect();
menuTop.value = rect.top;
menuHeight.value = rect.height;
menuBottom.value = rect.bottom;
} catch (e) {
console.warn('获取胶囊失败', e);
setDefaultNavInfo();
}
// #endif
// #ifndef MP-WEIXIN
setDefaultNavInfo();
// #endif
// 获取实时天气
fetchWeather();
});
function setDefaultNavInfo() {
const systemInfo = uni.getSystemInfoSync();
const statusBarHeight = systemInfo.statusBarHeight || 20;
// 默认胶囊位置:通常在状态栏下方 6~8px,高度约32px
menuTop.value = statusBarHeight + 6;
menuHeight.value = 32;
menuBottom.value = menuTop.value + menuHeight.value;
}
// 整体导航栏样式(背景覆盖到屏幕顶部)
const navBarStyle = computed(() => ({
height: `${menuBottom.value + 8}px`
}));
// 内容区域样式(对齐胶囊高度,垂直居中)
const contentStyle = computed(() => ({
top: `${menuTop.value}px`,
height: `${menuHeight.value}px`
}));
// ---------- 天气数据 ----------
const weatherTemp = ref('--');
const weatherIcon = ref('🌤️');
const weatherCode = ref(0);
const isWinter = ref(false); // 是否显示冬季雪花效果
// 获取实时天气(使用免费 Open-Meteo API,无需 key)
async function fetchWeather() {
try {
// 尝试获取用户位置,失败则默认北京坐标
let lat = 39.9042;
let lon = 116.4074;
// #ifdef MP-WEIXIN || H5
try {
const location = await getLocation();
if (location) {
lat = location.latitude;
lon = location.longitude;
}
} catch (e) {
console.warn('定位失败,使用默认位置');
}
// #endif
const res = await uni.request({
// todo
url: `https://api.open-meteo.com/v1/forecast`,
data: {
latitude: lat,
longitude: lon,
current_weather: true
}
});
if (res.statusCode === 200 && res.data.current_weather) {
const w = res.data.current_weather;
weatherTemp.value = Math.round(w.temperature);
weatherCode.value = w.weathercode;
weatherIcon.value = getWeatherEmoji(w.weathercode);
// 冬天判定:北半球12-2月 或 温度低于5℃
const month = new Date().getMonth() + 1;
isWinter.value = (month === 12 || month <= 2) || w.temperature < 5;
}
} catch (e) {
console.error('天气获取失败', e);
// 失败保留默认值
}
}
// 获取位置封装(返回 Promise)
function getLocation() {
return new Promise((resolve, reject) => {
uni.getLocation({
type: 'gcj02',
success: (res) => resolve({
latitude: res.latitude,
longitude: res.longitude
}),
fail: reject
});
});
}
// 天气代码映射为 emoji(Open-Meteo 标准)
function getWeatherEmoji(code) {
if (code <= 1) return '☀️'; // 晴
if (code <= 3) return '⛅️'; // 多云
if (code <= 48) return '🌫️'; // 雾/霾
if (code <= 57) return '🌧️'; // 小雨/毛毛雨
if (code <= 67) return '🌧️'; // 雨
if (code <= 77) return '❄️'; // 雪
if (code <= 82) return '🌧️'; // 阵雨
if (code <= 86) return '🌨️'; // 阵雪
return '⛈️'; // 雷暴
}
// ---------- 雪花随机样式 ----------
function getSnowStyle(index) {
const left = Math.random() * 100; // 横向位置 0-100%
const animDelay = Math.random() * 3; // 动画延迟
const size = 4 + Math.random() * 4; // 雪花大小 4-8px
const opacity = 0.4 + Math.random() * 0.4; // 透明度
return {
left: `${left}%`,
width: `${size}px`,
height: `${size}px`,
opacity,
animationDelay: `${animDelay}s`
};
}
</script>
<style lang="scss" scoped>
@import '@/styles/theme.scss';
.top-nav {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 20;
background: rgba(255, 253, 248, 0.85);
backdrop-filter: blur(18px);
border-bottom: 1px solid rgba($border, 0.6);
}
.nav-content {
position: absolute;
left: 0;
right: 0;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px;
}
.nav-title-group {
display: flex;
align-items: center;
gap: 8px;
}
.nav-ornament {
width: 4px;
height: 18px;
background: $red;
border-radius: 2px;
}
.nav-title {
font-size: 17px;
font-weight: 800;
letter-spacing: 1.5px;
color: $ink;
line-height: 1;
}
.title-image {
height: 50px;
width: 90px;
}
.highlight {
color: $red-dark;
}
.nav-actions {
display: flex;
gap: 14px;
align-items: center;
height: 30px;
margin-right: 85px;
}
.nav-icon-btn {
width: 24px;
height: 24px;
border-radius: 50%;
background: rgba(255, 253, 248, 0.7);
border: 1.5px solid $border;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
position: relative;
color: $ink-light;
}
.badge {
position: absolute;
top: 3px;
right: 3px;
width: 4px;
height: 4px;
background: $red;
border-radius: 50%;
border: 1.5px solid #fff;
animation: badgePulse 2s ease-in-out infinite;
}
@keyframes badgePulse {
0%,
100% {
transform: scale(1);
}
50% {
transform: scale(1.5);
}
}
/* ========== 天气模块 ========== */
.nav-weather {
position: relative;
display: flex;
align-items: center;
height: 100%;
/* 继承胶囊高度,不超出 */
padding: 0 6px;
border-radius: 20px;
background: rgba(240, 248, 255, 0.6);
/* 淡蓝底色,冬季感 */
overflow: hidden;
/* 雪花动画裁切 */
}
.weather-info {
display: flex;
align-items: center;
gap: 2px;
z-index: 1;
/* 位于雪花上方 */
}
.weather-icon {
font-size: 16px;
/* 图标大小适配胶囊高度 */
line-height: 1;
}
.weather-temp {
font-size: 12px;
font-weight: 600;
color: #2c5282;
/* 深蓝,冬季氛围 */
}
/* 雪花动画容器 */
.snowflakes {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 0;
}
.snowflake {
position: absolute;
top: -10px;
background: white;
border-radius: 50%;
box-shadow: 0 0 2px rgba(255, 255, 255, 0.8);
animation: snowFall linear infinite;
animation-duration: 2.5s;
/* 快速飘落,配合窄高度 */
}
/* 雪花下落动画:从容器上方落到下方隐藏 */
@keyframes snowFall {
0% {
transform: translateY(-10px) rotate(0deg);
opacity: 0;
}
10% {
opacity: 1;
}
90% {
opacity: 1;
}
100% {
transform: translateY(calc(32px + 10px)) rotate(360deg);
/* 32px为典型胶囊高度,实际由父容器决定 */
opacity: 0;
}
}
</style>
浙公网安备 33010602011771号