641 lines
15 KiB
Vue
641 lines
15 KiB
Vue
<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="handleStarActivityClick">
|
||
<!-- 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>
|
||
|
||
<!-- 任务弹窗 -->
|
||
<DailyTasks :visible="showTaskModal" @close="showTaskModal = false" />
|
||
|
||
<!-- 新手引导列表弹窗 -->
|
||
<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 DailyTasks from '@/pages/tasks/daily-tasks.vue';
|
||
import GuideListModal from '@/components/GuideListModal.vue';
|
||
import { getActivityListApi } from '@/utils/api.js';
|
||
import { reportEvent } from '@/utils/task-api.js';
|
||
|
||
// 定义 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);
|
||
|
||
// 上报每日首次登录事件(每天首次挂载时)
|
||
const today = new Date().toISOString().split('T')[0]
|
||
const dailyLoginKey = `daily_login_completed_${today}`
|
||
// 先判断是否处于登录状态(是否有 star_id)
|
||
const starId = uni.getStorageSync('star_id')
|
||
if (starId && !uni.getStorageSync(dailyLoginKey)) {
|
||
reportEvent('daily_login', starId).then(() => {
|
||
// 报告成功后标记今日已完成
|
||
uni.setStorageSync(dailyLoginKey, true)
|
||
}).catch(err => {
|
||
console.error('上报登录事件失败:', err)
|
||
})
|
||
}
|
||
});
|
||
|
||
// 组件卸载时移除事件监听
|
||
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 handleStarActivityClick = async () => {
|
||
try {
|
||
// 从本地存储获取star_id
|
||
const starId = uni.getStorageSync('star_id');
|
||
if (!starId) {
|
||
uni.showToast({
|
||
title: '无法获取用户信息',
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
|
||
// 显示加载提示
|
||
uni.showLoading({
|
||
title: '加载中...'
|
||
});
|
||
|
||
// 调用API获取活动列表
|
||
const response = await getActivityListApi(starId, 1, 10);
|
||
|
||
uni.hideLoading();
|
||
|
||
// 检查响应数据
|
||
if (response && response.data && response.data.activities) {
|
||
const activities = response.data.activities;
|
||
|
||
// 查找activity_type为bus的活动
|
||
const busActivity = activities.find(activity => activity.activity_type === 'bus');
|
||
|
||
if (busActivity) {
|
||
// 跳转到应援活动页面
|
||
uni.navigateTo({
|
||
url: `/pages/support-activity/index?id=${busActivity.id}`
|
||
});
|
||
} else {
|
||
uni.showToast({
|
||
title: '暂无巴士应援活动',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
} else {
|
||
uni.showToast({
|
||
title: '获取活动列表失败',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
} catch (error) {
|
||
uni.hideLoading();
|
||
console.error('获取活动列表失败:', error);
|
||
uni.showToast({
|
||
title: error.message || '获取活动列表失败',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
};
|
||
|
||
// 点击新手引导
|
||
const handleGuideClick = () => {
|
||
uni.navigateTo({
|
||
url: '/pages/tasks/guide'
|
||
});
|
||
};
|
||
|
||
// 执行引导
|
||
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>
|