topfans/frontend/pages/components/Header.vue

644 lines
15 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>
<image class="task-red-dot" src="/static/square/tishi.png" mode="aspectFit"></image>
</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>
<image class="task-red-dot" src="/static/square/tishi.png" mode="aspectFit"></image>
</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>
<image class="task-red-dot" src="/static/square/tishi.png" mode="aspectFit"></image>
</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">
<image class="crystal-bg-img" src="/static/square/shuijingzhanshikuang.png" mode="aspectFill"></image>
<!-- 上层:文字内容 -->
<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" />
<!-- 新手引导弹窗 -->
<GuideModal :visible="showGuideModal" @close="showGuideModal = false" @updated="handleGuideUpdated" />
</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 GuideModal from '@/pages/tasks/GuideModal.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 showGuideModal = 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;
// 切换身份后重新检查每日登录状态
checkAndReportDailyLogin();
};
// 监听余额更新事件
const handleBalanceUpdate = (data) => {
if (userInfo.value) {
userInfo.value = { ...userInfo.value, crystal_balance: data.crystal_balance };
} else {
loadUserInfo();
}
};
// 检查并上报每日首次登录事件
function checkAndReportDailyLogin() {
// 5点之前属于昨日周期不触发登录事件
const now = new Date()
const currentHour = now.getHours()
if (currentHour < 5) return // 5点之前不触发
const today = now.toISOString().split('T')[0]
const starId = uni.getStorageSync('star_id')
if (!starId) return
// 获取用户ID
const userStr = uni.getStorageSync('user')
let userId = null
if (userStr) {
try {
const user = JSON.parse(userStr)
userId = user?.uid || null
} catch (e) {}
}
if (!userId) return
// 每个用户每个明星身份有独立的每日登录状态
const dailyLoginKey = `daily_login_completed_${today}_${userId}_${starId}`
if (!uni.getStorageSync(dailyLoginKey)) {
reportEvent('daily_login', starId).then(() => {
// 报告成功后标记今日已完成,并清除昨日的记录
uni.setStorageSync(dailyLoginKey, true)
const yesterday = new Date(now.getTime() - 86400000).toISOString().split('T')[0]
uni.removeStorageSync(`daily_login_completed_${yesterday}_${userId}_${starId}`)
}).catch(err => {
console.error('上报登录事件失败:', err)
})
}
}
// 组件挂载时加载用户信息并监听事件
onMounted(() => {
loadUserInfo();
uni.$on('avatarUpdated', handleAvatarUpdate);
uni.$on('userInfoUpdated', handleUserInfoUpdate);
uni.$on('balanceUpdated', handleBalanceUpdate);
// 上报每日首次登录事件每日5点重置后首次挂载时
checkAndReportDailyLogin();
});
// 组件卸载时移除事件监听
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 = () => {
showGuideModal.value = true;
};
// 引导更新回调
const handleGuideUpdated = () => {
// 可以在这里刷新相关数据
console.log('[Header] Guide updated');
};
// 执行引导
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/myWorks') {
// 已经在个人信息页面,不执行跳转
return;
}
}
uni.navigateTo({
url: '/pages/profile/myWorks'
});
};
// 将方法暴露给模板
defineExpose({
showTaskModal,
closeTaskModal
});
</script>
<style scoped>
.header-container {
position: fixed;
top: 80rpx;
left: 0;
right: 0;
width: 100%;
height: 144rpx;
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: 8rpx;
/* 固定在顶部 */
left: 50%;
transform: translateX(-50%);
/* 水平居中 */
width: 88rpx;
/* 图标宽度 */
height: 88rpx;
/* 图标高度 */
z-index: 10;
/* 确保图标在文字块上面 */
}
.task-icon-img {
width: 100%;
height: 100%;
}
/* 红点样式 */
.task-red-dot {
position: absolute;
top: 8rpx;
right: 12rpx;
width: 16rpx;
height: 16rpx;
}
/* --- 下层文字背景块 --- */
.task-text-box {
position: absolute;
top: 48rpx;
/* 关键:调整这个值,让文字块往上顶,与图标产生重叠 */
left: 50%;
transform: translateX(-50%);
width: 88rpx;
height: 80rpx;
background-image: url('/static/square/gerenzhongxincangpinkuang.png');
background-size: 100% 100%;
background-repeat: no-repeat;
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
z-index: 5;
/* 必须比图标小,才能被覆盖 */
}
.task-text-label {
font-size: 16rpx;
color: #fff;
text-shadow: 1rpx 1rpx 2rpx rgba(0, 0, 0, 0.9);
margin-top: 32rpx;
}
/* 水晶余额组件 */
.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;
bottom: 4rpx;
padding-right: 28rpx;
padding-left: 56rpx;
}
.crystal-bg-img {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
}
/* --- 上层:文字内容 --- */
.crystal-text-layer {
position: relative;
z-index: 2;
display: flex;
flex-direction: column;
align-items: center;
margin-top: 8rpx;
}
.balance-number {
font-size: 24rpx;
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.9);
}
/* --- 下层:渐变背景 --- */
.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>