自定义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>
posted on 2026-06-23 11:48  yunkuang  阅读(3)  评论(0)    收藏  举报