topfans/frontend/pages/components/Header.vue
2026-04-07 23:08:49 +08:00

567 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="header-container">
<!-- 返回按钮可选 -->
<view v-if="showBack" class="back-button" @click="handleBack">
<text class="back-icon" :style="{ color: backIconColor }">←</text>
</view>
<!-- 右侧图标区域 -->
<view class="right-icons">
<!-- 任务图标 -->
<view v-if="showTaskIcon" class="daily-task-group" @click="handleTaskClick">
<!-- 1. 上层:任务图标(悬浮在上面) -->
<view class="task-icon-box">
<image class="task-icon-img" src="/static/icon/task.png" mode="aspectFit"></image>
<view class="task-red-dot"></view>
</view>
<!-- 2. 下层:文字背景块 -->
<view class="task-text-box">
<text class="task-text-label">每日任务</text>
</view>
</view>
<!-- 新手引导 -->
<view v-if="showGuideIcon" class="daily-task-group" @click="handleGuideClick">
<!-- 1. 上层:新手引导(悬浮在上面) -->
<view class="task-icon-box">
<image class="task-icon-img" src="/static/icon/onboarding-bg.png" mode="aspectFit"></image>
<view class="task-red-dot"></view>
</view>
<!-- 2. 下层:文字背景块 -->
<view class="task-text-box">
<text class="task-text-label">新手引导</text>
</view>
</view>
<!-- 星援活动 -->
<view v-if="showStarActivityIcon" class="daily-task-group" @click="handleTaskClick">
<!-- 1. 上层:星援活动(悬浮在上面) -->
<view class="task-icon-box">
<image class="task-icon-img" src="/static/icon/bus-icon.png" mode="aspectFit"></image>
<view class="task-red-dot"></view>
</view>
<!-- 2. 下层:文字背景块 -->
<view class="task-text-box">
<text class="task-text-label">星援活动</text>
</view>
</view>
<!-- 顶粉水晶余额 -->
<view class="crystal-balance-new">
<!-- 1. 左侧钻石图标:层级最高,盖住右侧背景 -->
<view class="crystal-icon-box">
<image class="crystal-icon" src="/static/icon/crystal.png" mode="aspectFit"></image>
</view>
<!-- 2. 右侧容器:用于包裹背景和文字 -->
<view class="crystal-info-container">
<!-- 上层:文字内容(层级中间,盖住背景) -->
<view class="crystal-text-layer">
<text class="balance-number">{{ crystalBalance }}</text>
</view>
<!-- 下层:渐变背景(层级最低,被文字盖住,但盖住容器底色) -->
<view class="crystal-bg-layer">
<text class="balance-income">收益 27.1/H</text>
</view>
</view>
</view>
<!-- 用户头像 -->
<view class="icon-item avatar-icon" @click="handleAvatarClick">
<Avatar :key="avatarKey" :userId="userUid" :avatarUrl="userAvatarUrl" :size="96" :borderWidth="2" />
</view>
</view>
<!-- 任务弹窗 -->
<TaskModal :visible="showTaskModal" @close="closeTaskModal" />
<!-- 新手引导列表弹窗 -->
<GuideListModal :visible="showGuideListModal" @start-guide="handleStartGuide"
@claim-success="handleClaimSuccess" @close="showGuideListModal = false" />
</view>
</template>
<script setup>
import { computed, ref, onMounted, onUnmounted } from 'vue';
import { useStore } from 'vuex';
import Avatar from './Avatar.vue';
import TaskModal from './TaskModal.vue';
import GuideListModal from '@/components/GuideListModal.vue';
// 定义 props
const props = defineProps({
showBack: {
type: Boolean,
default: false
},
backUrl: {
type: String,
default: ''
},
backIconColor: {
type: String,
default: '#000000'
},
showTaskIcon: {
type: Boolean,
default: true
},
showGuideIcon: {
type: Boolean,
default: true
},
showStarActivityIcon: {
type: Boolean,
default: true
}
});
const store = useStore();
// 使用ref存储用户信息使其具有响应式
const userInfo = ref(null);
const avatarKey = ref(0); // 用于强制刷新Avatar组件
// 任务弹窗显示状态
const showTaskModal = ref(false);
// 新手引导弹窗显示状态
const showGuideListModal = ref(false);
// 从本地存储读取用户信息
const loadUserInfo = () => {
try {
const userStr = uni.getStorageSync('user');
if (userStr) {
userInfo.value = JSON.parse(userStr);
}
} catch (e) {
console.error('解析用户信息失败:', e);
userInfo.value = null;
}
};
// 获取用户UID
const userUid = computed(() => {
return userInfo.value?.uid || '';
});
// 获取用户头像URL
const userAvatarUrl = computed(() => {
return userInfo.value?.avatar_url || '';
});
// 获取水晶余额
const crystalBalance = computed(() => {
return userInfo.value?.crystal_balance ?? 9527;
});
// 监听头像更新事件
const handleAvatarUpdate = (data) => {
// 重新加载用户信息(含新 avatar_url
loadUserInfo();
// 强制刷新Avatar组件
avatarKey.value += 1;
};
// 监听用户信息更新事件(如切换身份)
const handleUserInfoUpdate = () => {
loadUserInfo();
avatarKey.value += 1;
};
// 监听余额更新事件
const handleBalanceUpdate = (data) => {
if (userInfo.value) {
userInfo.value = { ...userInfo.value, crystal_balance: data.crystal_balance };
} else {
loadUserInfo();
}
};
// 组件挂载时加载用户信息并监听事件
onMounted(() => {
loadUserInfo();
uni.$on('avatarUpdated', handleAvatarUpdate);
uni.$on('userInfoUpdated', handleUserInfoUpdate);
uni.$on('balanceUpdated', handleBalanceUpdate);
});
// 组件卸载时移除事件监听
onUnmounted(() => {
uni.$off('avatarUpdated', handleAvatarUpdate);
uni.$off('userInfoUpdated', handleUserInfoUpdate);
uni.$off('balanceUpdated', handleBalanceUpdate);
});
// 处理返回按钮点击
const handleBack = () => {
if (props.backUrl) {
uni.navigateTo({
url: props.backUrl
});
} else {
// 获取页面栈
const pages = getCurrentPages();
if (pages.length > 1) {
// 有上一页,执行返回
uni.navigateBack();
} else {
// 没有上一页跳转到square页面
uni.reLaunch({
url: '/pages/square/square'
});
}
}
};
// 处理任务图标点击
const handleTaskClick = () => {
showTaskModal.value = true;
};
// 点击新手引导
const handleGuideClick = () => {
showGuideListModal.value = true;
};
// 执行引导
const handleStartGuide = (params) => {
const key = typeof params === 'string' ? params : (params?.key || '');
const fromStep = typeof params === 'object' ? (params.fromStep ?? 0) : 0;
const targetPage = typeof params === 'object' ? (params.targetPage || '') : '';
console.log('[Header] handleStartGuide:', { key, fromStep, targetPage });
showGuideListModal.value = false;
// 根据引导类型跳转到对应页面
if (key === 'square_home') {
const navigateUrl = targetPage || '/pages/square/square';
uni.navigateTo({
url: navigateUrl,
success: () => {
setTimeout(() => {
if (fromStep === 0) {
store.dispatch("guide/startGuideFromBeginning", key);
} else {
store.dispatch("guide/resumeGuide", key);
}
}, 500);
}
});
} else {
if (fromStep === 0) {
store.dispatch("guide/startGuideFromBeginning", key);
} else {
store.dispatch("guide/resumeGuide", key);
}
}
};
// 领取奖励成功
const handleClaimSuccess = (reward) => {
console.log("[Header] 领取奖励成功:", reward);
};
// 关闭任务弹窗
const closeTaskModal = () => {
showTaskModal.value = false;
};
// 处理头像点击
const handleAvatarClick = () => {
// 获取当前页面栈
const pages = getCurrentPages();
if (pages.length > 0) {
const currentPage = pages[pages.length - 1];
// 检查当前页面是否是个人信息页面
if (currentPage.route === 'pages/profile/profile') {
// 已经在个人信息页面,不执行跳转
return;
}
}
uni.navigateTo({
url: '/pages/profile/profile'
});
};
// 将方法暴露给模板
defineExpose({
showTaskModal,
closeTaskModal
});
</script>
<style scoped>
.header-container {
position: fixed;
top: 80rpx;
left: 0;
right: 0;
width: 100%;
height: 80rpx;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 32rpx;
box-sizing: border-box;
z-index: 200;
}
.back-button {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.back-icon {
font-size: 48rpx;
font-weight: bold;
}
.right-icons {
display: flex;
align-items: center;
justify-content: flex-end;
/* gap: 30rpx; */
margin-left: auto;
}
.daily-task-group {
position: relative;
/* 必须是相对定位,作为子元素的定位基准 */
width: 100rpx;
/* 控制整体宽度,根据图标大小调整 */
height: 120rpx;
/* 预估高度,包含图标和文字块 */
}
/* --- 上层图标样式 --- */
.task-icon-box {
position: absolute;
top: 0;
/* 固定在顶部 */
left: 50%;
transform: translateX(-50%);
/* 水平居中 */
width: 90rpx;
/* 图标宽度 */
height: 90rpx;
/* 图标高度 */
z-index: 10;
/* 确保图标在文字块上面 */
}
.task-icon-img {
width: 100%;
height: 100%;
}
/* 红点样式 */
.task-red-dot {
position: absolute;
top: 6rpx;
right: 6rpx;
width: 16rpx;
height: 16rpx;
background-color: red;
border-radius: 50%;
border: 2rpx solid #fff;
}
/* --- 下层文字背景块 --- */
.task-text-box {
position: absolute;
top: 48rpx;
/* 关键:调整这个值,让文字块往上顶,与图标产生重叠 */
left: 50%;
transform: translateX(-50%);
width: 88rpx;
height: 80rpx;
background: linear-gradient(to bottom right,
#F0E4B1 0%,
/* 左:浅橙粉 */
#F08399 50%,
#B94E73 100%
/* 右:柔粉红 */
);
/* 立体感核心:多层阴影 + 内阴影模拟凸起 */
box-shadow:
/* 外层投影 - 让按钮浮起 */
0 4rpx 12rpx rgba(255, 143, 158, 0.2),
0 2rpx 6rpx rgba(255, 143, 158, 0.15),
/* 内阴影 - 模拟顶部受光 + 底部凹陷 */
inset 0 2rpx 4rpx rgba(255, 255, 255, 0.4),
/* 顶部高光 */
inset 0 -2rpx 4rpx rgba(0, 0, 0, 0.05);
/* 底部暗部 */
/* 粉色渐变背景,可微调 */
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
z-index: 5;
/* 必须比图标小,才能被覆盖 */
box-shadow: 0 4rpx 10rpx rgba(255, 107, 187, 0.3);
/* 可选:增加一点阴影层次感 */
}
.task-text-label {
font-size: 18rpx;
color: #fff;
text-shadow: 1rpx 1rpx 2rpx rgba(255, 255, 255, 0.5);
margin-top: 24rpx;
}
/* 水晶余额组件 */
.crystal-balance-new {
height: 120rpx;
display: flex;
align-items: center;
/* 底部对齐 */
position: relative;
/* 左侧留出足够空间给钻石图标,防止文字被压在图标正下方 */
padding-left: 50rpx;
top: 8rpx;
}
/* --- 左侧钻石图标 --- */
.crystal-icon-box {
position: absolute;
left: 0;
bottom: 10rpx;
/* 与右侧底部平齐 */
z-index: 10;
/* 最高层级:盖住右侧背景 */
width: 100rpx;
height: 100rpx;
}
.crystal-icon {
width: 100%;
height: 100%;
}
/* --- 右侧容器 --- */
.crystal-info-container {
position: relative;
/* 自动撑开宽度 */
height: 56rpx;
width: 80rpx;
/* 容器高度 */
border-radius: 16rpx;
background: rgba(0, 0, 0, 0.5);
bottom: 4rpx;
padding-right: 28rpx;
padding-left: 56rpx;
}
/* --- 上层:文字内容 --- */
.crystal-text-layer {
position: relative;
z-index: 2;
display: flex;
flex-direction: column;
align-items: center;
margin-top: 8rpx;
}
.balance-number {
font-size: 22rpx;
font-weight: bold;
color: #FFB800;
font-family: 'ZaoZiGongFangJianHei-1', sans-serif;
text-shadow:
0 0 10rpx rgba(255, 184, 0, 0.8),
0 2rpx 4rpx rgba(0, 0, 0, 0.5);
letter-spacing: 1rpx;
display: flex;
align-items: center;
white-space: nowrap;
}
.balance-income {
font-size: 18rpx;
color: #FFFFFF;
margin-bottom: 2rpx;
margin-left: 28rpx;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.5);
}
/* --- 下层:渐变背景 --- */
.crystal-bg-layer {
display: flex;
align-items: center;
justify-content: center;
position: absolute;
bottom: -18rpx;
left: 0;
width: 100%;
height: 65%;
z-index: 1;
/* 关键:层级低于文字,高于透明底色 */
background: linear-gradient(to bottom right,
#F0E4B1 0%,
/* 左:浅橙粉 */
#F08399 50%,
#B94E7399 100%
/* 右:柔粉红 */
);
/* 立体感核心:多层阴影 + 内阴影模拟凸起 */
box-shadow:
/* 外层投影 - 让按钮浮起 */
0 4rpx 12rpx rgba(255, 143, 158, 0.2),
0 2rpx 6rpx rgba(255, 143, 158, 0.15),
/* 内阴影 - 模拟顶部受光 + 底部凹陷 */
inset 0 2rpx 4rpx rgba(255, 255, 255, 0.4),
/* 顶部高光 */
inset 0 -2rpx 4rpx rgba(0, 0, 0, 0.05);
/* 底部暗部 */
border-radius: 10rpx;
}
.icon-item {
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
margin-left: 32rpx;
margin-top: 16rpx;
}
.task-icon {
width: 64rpx;
height: 64rpx;
}
.icon-image {
width: 100%;
height: 100%;
}
.avatar-icon {
width: 60rpx;
height: 60rpx;
}
</style>