723 lines
17 KiB
Vue
723 lines
17 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="crystal-balance-new" @click="handleAvatarClick">
|
||
<!-- 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">{{ exhibitionRevenue }}</text>
|
||
</view>
|
||
|
||
<!-- 收益文字 -->
|
||
<!-- <view class="crystal-bg-layer">
|
||
<text class="balance-income">收益 {{ hourlyEarnings }}/时</text>
|
||
</view> -->
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 右侧图标区域 -->
|
||
<view class="right-icons">
|
||
<view class="left-items">
|
||
<!-- 任务图标 -->
|
||
<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="star-activity-list">
|
||
<view
|
||
v-for="activity in starActivities"
|
||
:key="activity.id"
|
||
class="daily-task-group"
|
||
@click="handleActivityClick(activity)"
|
||
>
|
||
<!-- 上层:活动图标 -->
|
||
<view class="task-icon-box">
|
||
<image
|
||
class="task-icon-img"
|
||
:src="activity.icon || '/static/icon/bus-icon.png'"
|
||
mode="aspectFit"
|
||
></image>
|
||
<image
|
||
class="task-red-dot"
|
||
src="/static/square/tishi.png"
|
||
mode="aspectFit"
|
||
></image>
|
||
</view>
|
||
|
||
<!-- 下层:文字背景块 -->
|
||
<view class="task-text-box">
|
||
<text class="task-text-label">{{
|
||
activity.theme || "星援活动"
|
||
}}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<!-- 用户头像 -->
|
||
<view class="icon-item avatar-icon" @click="handleAvatarClick">
|
||
<Avatar
|
||
:key="avatarKey"
|
||
:userId="userUid"
|
||
:avatarUrl="userAvatarUrl"
|
||
:size="68"
|
||
: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 { onShow } from "@dcloudio/uni-app";
|
||
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 { useBanner } from "@/pages/square/composables/useBanner.js";
|
||
import { reportEvent } from "@/utils/task-api.js";
|
||
import { getEarningsSummaryApi } from "@/utils/api.js";
|
||
|
||
// 获取星援活动数据(复用 square 的 useBanner)
|
||
const { bannerActivities, loadBannerActivities } = useBanner();
|
||
|
||
// 定义 props
|
||
const props = defineProps({
|
||
showBack: {
|
||
type: Boolean,
|
||
default: false,
|
||
},
|
||
backUrl: {
|
||
type: String,
|
||
default: "",
|
||
},
|
||
backIconColor: {
|
||
type: String,
|
||
default: "#000000",
|
||
},
|
||
showTaskIcon: {
|
||
type: Boolean,
|
||
default: false,
|
||
},
|
||
showGuideIcon: {
|
||
type: Boolean,
|
||
default: false,
|
||
},
|
||
showStarActivityIcon: {
|
||
type: Boolean,
|
||
default: false,
|
||
},
|
||
});
|
||
|
||
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 exhibitionRevenue = ref(0); // 当前展出收益
|
||
const hourlyEarnings = ref(0); // 每小时收益之和
|
||
|
||
// 加载收益汇总
|
||
const loadEarningsSummary = async () => {
|
||
try {
|
||
const response = await getEarningsSummaryApi();
|
||
|
||
if (response.code === 200) {
|
||
const data = response.data;
|
||
exhibitionRevenue.value = data.crystal_balance || 0;
|
||
hourlyEarnings.value = data.total_hourly_earnings || 0;
|
||
}
|
||
} catch (e) {
|
||
console.error("获取收益汇总失败:", e);
|
||
}
|
||
};
|
||
|
||
// 监听头像更新事件
|
||
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,
|
||
};
|
||
exhibitionRevenue.value = data.crystal_balance || 0;
|
||
} 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);
|
||
});
|
||
}
|
||
}
|
||
|
||
// 星援活动列表 - 从 bannerActivities 中获取非 expired 的活动
|
||
const starActivities = computed(() => {
|
||
// return bannerActivities.value
|
||
return bannerActivities.value.filter(
|
||
(activity) => activity.status !== "expired",
|
||
);
|
||
});
|
||
|
||
// 组件挂载时加载用户信息和星援活动数据
|
||
onMounted(async () => {
|
||
await loadUserInfo();
|
||
await loadBannerActivities();
|
||
// await loadEarningsSummary();
|
||
uni.$on("avatarUpdated", handleAvatarUpdate);
|
||
uni.$on("userInfoUpdated", handleUserInfoUpdate);
|
||
uni.$on("balanceUpdated", handleBalanceUpdate);
|
||
|
||
// 上报每日首次登录事件(每日5点重置后首次挂载时)
|
||
checkAndReportDailyLogin();
|
||
});
|
||
|
||
onShow(() => {
|
||
// 每次页面显示时刷新收益数据
|
||
loadEarningsSummary();
|
||
});
|
||
|
||
// 组件卸载时移除事件监听
|
||
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 handleActivityClick = (activity) => {
|
||
if (activity) {
|
||
uni.navigateTo({
|
||
url: `/pages/support-activity/index?id=${activity.id}`,
|
||
});
|
||
}
|
||
};
|
||
|
||
// 点击新手引导
|
||
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;
|
||
}
|
||
|
||
.left-items {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: flex-start;
|
||
}
|
||
|
||
.star-activity-list {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8rpx;
|
||
width: 68rpx;
|
||
/* 控制整体宽度,根据图标大小调整 */
|
||
height: 156rpx;
|
||
/* 预估高度,包含图标和文字块 */
|
||
}
|
||
|
||
.daily-task-group {
|
||
position: relative;
|
||
/* 必须是相对定位,作为子元素的定位基准 */
|
||
width: 68rpx;
|
||
/* 控制整体宽度,根据图标大小调整 */
|
||
height: 156rpx;
|
||
/* 预估高度,包含图标和文字块 */
|
||
margin-right: 24rpx;
|
||
}
|
||
|
||
/* --- 上层图标样式 --- */
|
||
.task-icon-box {
|
||
position: absolute;
|
||
top: 32rpx;
|
||
/* 固定在顶部 */
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
/* 水平居中 */
|
||
width: 34rpx;
|
||
/* 图标宽度 */
|
||
height: 48rpx;
|
||
/* 图标高度 */
|
||
z-index: 10;
|
||
/* 确保图标在文字块上面 */
|
||
}
|
||
|
||
.task-icon-img {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
/* 红点样式 */
|
||
.task-red-dot {
|
||
position: absolute;
|
||
top: 8rpx;
|
||
right: 0;
|
||
width: 16rpx;
|
||
height: 16rpx;
|
||
}
|
||
|
||
/* --- 下层文字背景块 --- */
|
||
.task-text-box {
|
||
position: absolute;
|
||
top: 48rpx;
|
||
/* 关键:调整这个值,让文字块往上顶,与图标产生重叠 */
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
width: 60rpx;
|
||
height: 60rpx;
|
||
background-image: url("/static/square/gerenzhongxincangpinkuang.png");
|
||
background-size: 100% 100%;
|
||
background-repeat: no-repeat;
|
||
border-radius: 20rpx;
|
||
box-shadow: 0px 4px 4px 0px #ee262654;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 5;
|
||
/* 必须比图标小,才能被覆盖 */
|
||
}
|
||
|
||
.task-text-label {
|
||
font-weight: 500;
|
||
font-size: 24rpx;
|
||
/* line-height: 100%;
|
||
letter-spacing: 0%; */
|
||
color: #fff9e7;
|
||
text-shadow: 1rpx 1rpx 2rpx rgba(0, 0, 0, 0.84);
|
||
margin-top: 32rpx;
|
||
white-space: nowrap;
|
||
transform: scale(0.5);
|
||
}
|
||
|
||
/* 水晶余额组件 */
|
||
.crystal-balance-new {
|
||
height: 120rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
/* 底部对齐 */
|
||
position: relative;
|
||
/* 左侧留出足够空间给钻石图标,防止文字被压在图标正下方 */
|
||
padding-left: 50rpx;
|
||
top: 8rpx;
|
||
}
|
||
|
||
/* --- 左侧钻石图标 --- */
|
||
.crystal-icon-box {
|
||
position: absolute;
|
||
left: 8rpx;
|
||
bottom: 24rpx;
|
||
/* 与右侧底部平齐 */
|
||
z-index: 10;
|
||
width: 68rpx;
|
||
height: 60rpx;
|
||
transform: rotate(-15deg);
|
||
opacity: 1;
|
||
}
|
||
|
||
.crystal-icon {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
/* --- 右侧容器 --- */
|
||
.crystal-info-container {
|
||
position: relative;
|
||
height: 34rpx;
|
||
/* width: 120rpx; */
|
||
opacity: 1;
|
||
border-radius: 20rpx;
|
||
border-width: 1px;
|
||
border: 1px solid #f3a68a40;
|
||
box-shadow: 2px 2px 4.1px 0px #f936365c;
|
||
}
|
||
|
||
.crystal-bg-img {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
border-radius: 120rpx;
|
||
z-index: 0;
|
||
}
|
||
|
||
/* --- 上层:文字内容 --- */
|
||
.crystal-text-layer {
|
||
height: 100%;
|
||
position: relative;
|
||
z-index: 2;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
border-radius: 120rpx;
|
||
opacity: 0.75;
|
||
background: linear-gradient(
|
||
270deg,
|
||
rgba(255, 15, 19, 0.83) 0%,
|
||
rgba(30, 180, 217, 0.470865) 57.01%,
|
||
rgba(51, 241, 255, 0) 178.07%
|
||
);
|
||
padding: 0 24rpx;
|
||
}
|
||
|
||
.balance-number {
|
||
font-size: 24rpx;
|
||
font-weight: bold;
|
||
color: #ffb800;
|
||
font-family: "yt", 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;
|
||
}
|
||
|
||
.task-icon {
|
||
width: 64rpx;
|
||
height: 64rpx;
|
||
}
|
||
|
||
.icon-image {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
.avatar-icon {
|
||
width: 60rpx;
|
||
height: 60rpx;
|
||
}
|
||
</style>
|