644 lines
15 KiB
Vue
644 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>
|
||
<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>
|