Files
spring-festival-greetings/pages/index/index.vue
2026-02-25 16:34:35 +08:00

1013 lines
24 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="home-container"
:style="{
paddingTop: statusBarHeight + 12 + 'px',
}"
>
<!-- 顶部用户信息栏 -->
<view class="header-section">
<view class="user-info">
<image
class="user-avatar"
:src="userInfo.avatarUrl || '/static/default-avatar.png'"
mode="aspectFill"
/>
<view class="user-texts">
<text class="greeting-info">{{ greetingText }}</text>
<text class="user-name" @tap="handleLogin">
{{ userInfo.nickName || "点击登录" }}
</text>
</view>
</view>
</view>
<!-- 主卡片 (Lucky Status) -->
<view class="main-card">
<view class="card-bg-decor"></view>
<view class="card-top">
<view class="lucky-status">
<text class="status-label">LUCKY STATUS</text>
<text class="status-value"
>{{ luckyScore }}<text class="percent">%</text></text
>
</view>
<view class="date-box">
<view class="date-row">
<uni-icons type="calendar" size="14" color="#8b5a5a" />
<text class="date-text"
>{{ lunarDate.year }}-{{ lunarDate.month
}}{{ lunarDate.day }}</text
>
</view>
<view class="yi-ji-row">
<text class="label"></text
><text class="content">{{ lunarDate.yi }}</text>
</view>
<view class="yi-ji-row">
<text class="label"></text
><text class="content">{{ lunarDate.ji }}</text>
</view>
</view>
</view>
<view class="card-center">
<view class="lucky-color-tag"> 幸运色{{ luckyColor }} </view>
</view>
<view class="card-bottom">
<button class="open-lucky-btn" @tap="onOpenLucky">
<uni-icons
type="star-filled"
size="18"
color="#d81e06"
style="margin-right: 12rpx"
/>
<text>{{
signInfo.isSignedToday ? "今日已开启" : "开启今日好运"
}}</text>
</button>
<view class="week-sign-section">
<view class="sign-header">
<text class="sign-title">已连续签到 {{ continuousDays }} </text>
<text class="sign-tip">连续7天得大奖</text>
</view>
<view class="week-days">
<view
class="day-item"
v-for="(day, index) in weekDays"
:key="index"
:class="{ 'is-today': day.isToday, 'is-signed': day.isSigned }"
>
<text class="day-label">{{ day.label }}</text>
<view class="status-icon">
<uni-icons
v-if="day.isSigned"
type="checkmarkempty"
size="14"
color="#fff"
/>
<uni-icons
v-else-if="day.isToday && !day.isSigned"
type="plus"
size="14"
color="#d81e06"
/>
<text v-else class="dot"></text>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 大家都在用 -->
<view class="section-container compact-section">
<view class="section-header">
<view class="title-left-decor"></view>
<uni-icons
type="heart-filled"
size="18"
color="#d81e06"
style="margin-right: 8rpx"
/>
<text class="section-title">大家都在用</text>
</view>
<view class="feature-row">
<view class="feature-item" @tap="navTo('/pages/avatar/download')">
<view class="left-content">
<view class="icon-wrap yellow-bg">
<image
src="/static/icon/guashi.png"
mode="aspectFit"
class="feature-icon"
/>
</view>
<view class="text-content">
<text class="feature-name">制作今日头像</text>
<!-- <text class="feature-desc">限定 · 如意边框</text> -->
</view>
</view>
</view>
<view class="feature-item" @tap="navTo('/pages/wallpaper/index')">
<view class="left-content">
<view class="icon-wrap blue-bg">
<image
src="/static/icon/bizhi.png"
mode="aspectFit"
class="feature-icon"
/>
</view>
<view class="text-content">
<text class="feature-name">好运祝福壁纸</text>
<!-- <text class="feature-desc">高清 · 福气盈门</text> -->
</view>
</view>
</view>
</view>
</view>
<!-- 排行榜单 -->
<view class="section-container">
<view class="section-header">
<view class="title-left-decor"></view>
<text class="section-title">排行榜单</text>
<view class="rank-tabs-mini">
<view
class="rank-tab-mini"
:class="{ active: currentTab === 'all' }"
@tap="switchTab('all')"
>全部</view
>
<view
class="rank-tab-mini"
:class="{ active: currentTab === 'wallpaper' }"
@tap="switchTab('wallpaper')"
>壁纸</view
>
<view
class="rank-tab-mini"
:class="{ active: currentTab === 'frame' }"
@tap="switchTab('frame')"
>头像</view
>
<view
class="rank-tab-mini"
:class="{ active: currentTab === 'card' }"
@tap="switchTab('card')"
>贺卡</view
>
</view>
</view>
<!-- Ranking List -->
<view class="rank-list">
<view
class="rank-item"
v-for="(item, index) in rankingList"
:key="item.id"
@tap="onRankItemTap(item)"
>
<view class="rank-num" :class="'rank-' + (index + 1)">{{
index + 1
}}</view>
<image :src="item.thumb" mode="aspectFill" class="rank-thumb" />
<view class="rank-info">
<view class="rank-title-row">
<text class="rank-title">{{ item.title }}</text>
</view>
<view class="rank-meta">
<view class="tag-hot" v-if="item.isHot">HOT</view>
<text class="usage-count">{{ item.usageCount }} 人在用</text>
</view>
</view>
<view class="rank-likes">
<uni-icons type="heart-filled" size="16" color="#a85a5a" />
<text class="likes-num">{{ item.likes }}</text>
</view>
</view>
</view>
<view class="rank-footer"> 查看前 10 </view>
</view>
<!-- Floating Share Button -->
<!-- <button class="float-share-btn" open-type="share">
<uni-icons type="upload" size="18" color="#d81e06" />
<text>分享今日运势</text>
</button> -->
<view class="bottom-spacer"></view>
<!-- 登录弹窗 -->
<LoginPopup />
<!-- 运势抽奖弹窗 -->
<LuckyPopup ref="luckyPopupRef" />
</view>
</template>
<script setup>
import { ref, onMounted, computed } from "vue";
import { getStatusBarHeight } from "@/utils/system";
import { onShareAppMessage, onShareTimeline, onShow } from "@dcloudio/uni-app";
import { useUserStore } from "@/stores/user";
import { getRecommendList } from "@/api/system";
import { getUserSignInfo, userSignIn } from "@/api/user";
import LoginPopup from "@/components/LoginPopup/LoginPopup.vue";
import LuckyPopup from "@/components/LuckyPopup/LuckyPopup.vue";
const userStore = useUserStore();
const statusBarHeight = ref(getStatusBarHeight());
const loginPopupRef = ref(null);
const luckyPopupRef = ref(null);
const userInfo = computed(() => userStore?.userInfo || {});
const isLoggedIn = computed(() => !!userStore.userInfo.nickName);
const signInfo = ref({}); // 用户签到信息
const weekDays = computed(() => {
const now = new Date();
const todayStr =
signInfo.value.today ||
`${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
const current = new Date(todayStr);
const day = current.getDay() || 7; // 1 (Mon) - 7 (Sun)
const monday = new Date(current);
monday.setDate(current.getDate() - day + 1);
const days = [];
const labels = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"];
const signedDays = signInfo.value.signedDays || [];
for (let i = 0; i < 7; i++) {
const d = new Date(monday);
d.setDate(monday.getDate() + i);
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, "0");
const da = String(d.getDate()).padStart(2, "0");
const dateStr = `${y}-${m}-${da}`;
let isSigned = false;
const isToday = dateStr === todayStr;
const isFuture = dateStr > todayStr;
if (signedDays.includes(i + 1)) {
isSigned = true;
}
days.push({
label: labels[i],
date: dateStr,
isToday,
isSigned,
isFuture,
});
}
return days;
});
const greetingText = computed(() => {
const hour = new Date().getHours();
if (hour < 6) return "凌晨好";
if (hour < 9) return "早上好";
if (hour < 12) return "上午好";
if (hour < 14) return "中午好";
if (hour < 17) return "下午好";
if (hour < 19) return "傍晚好";
return "晚上好";
});
const handleLogin = () => {
if (!userInfo.value.nickName) {
uni.$emit("show-login-popup");
}
};
const luckyScore = ref(98);
const luckyColor = ref("赤金朱红");
const continuousDays = computed(() => signInfo.value.continuousDays || 0);
const currentMonthDays = computed(() => signInfo.value.totalDays || 0);
const totalMonthDays = computed(() => {
const date = new Date();
return new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate();
}); // 当前月总天数
const currentTab = ref("all");
// 模拟农历数据
const lunarDate = ref({
year: "2026",
month: "正月",
day: "初一",
yi: "开光、祈福、求嗣",
ji: "安葬、动土",
});
const rankingList = ref([]);
onMounted(() => {
fetchUserSingInfo(); // 获取用户签到信息
fetchRankingData();
});
onShow(() => {
if (userStore.userInfo) {
// userInfo.value = userStore.userInfo; // 已改为 computed
}
});
const fetchUserSingInfo = async () => {
if (!isLoggedIn.value) return;
const res = await getUserSignInfo();
signInfo.value = res || {};
};
const fetchRankingData = async () => {
// 模拟数据或调用 API
// 实际项目中可以调用 getRecommendList 并根据 currentTab 筛选
// 这里先使用 Mock 数据展示 UI
rankingList.value = [
{
id: 1,
title: "龙马精神限定框",
thumb: "https://file.lihailezzc.com/resource/avatar_frame_horse_gold.png",
usageCount: "2.4w",
likes: "12.8k",
isHot: true,
type: "frame",
},
{
id: 2,
title: "锦绣江山动态壁纸",
thumb: "https://file.lihailezzc.com/resource/wallpaper_snow_red.png",
usageCount: "1.8w",
likes: "9.2k",
isHot: false,
type: "wallpaper",
},
{
id: 3,
title: "万事大吉贺卡模板",
thumb: "https://file.lihailezzc.com/resource/card_template_1.png",
usageCount: "9.5k",
likes: "4.1k",
isHot: false,
type: "card",
},
];
};
const switchTab = (tab) => {
currentTab.value = tab;
// TODO: 根据 tab 筛选或重新请求数据
// 此处仅做 UI 演示,实际需对接筛选逻辑
};
const onNoticeTap = () => {
uni.showToast({ title: "暂无新消息", icon: "none" });
};
const onWalletTap = () => {
uni.navigateTo({ url: "/pages/mine/vip" });
};
const onOpenLucky = async () => {
if (!isLoggedIn.value) {
uni.$emit("show-login-popup");
return;
}
if (signInfo.value.isSignedToday) {
luckyPopupRef.value?.open();
return;
}
uni.showLoading({ title: "开启好运...", mask: true });
try {
const res = await userSignIn();
if (res && res.success) {
signInfo.value.continuousDays = res.continuousDays;
signInfo.value.isSignedToday = true;
if (typeof res.totalDays === "number") {
signInfo.value.totalDays = res.totalDays;
} else {
signInfo.value.totalDays = (signInfo.value.totalDays || 0) + 1;
}
// Update signedDays locally
if (!signInfo.value.signedDays) {
signInfo.value.signedDays = [];
}
const today = new Date(signInfo.value.today || new Date());
const dayIndex = today.getDay() || 7;
if (!signInfo.value.signedDays.includes(dayIndex)) {
signInfo.value.signedDays.push(dayIndex);
}
// Update user assets (points)
userStore.fetchUserAssets();
luckyPopupRef.value?.open();
} else {
uni.showToast({ title: "签到失败", icon: "none" });
}
} catch (e) {
console.error(e);
uni.showToast({ title: "网络错误,请稍后重试", icon: "none" });
} finally {
uni.hideLoading();
}
};
const navTo = (url) => {
uni.navigateTo({ url });
};
const onRankItemTap = (item) => {
if (item.type === "frame") {
uni.navigateTo({ url: `/pages/avatar/index?id=${item.id}` });
} else if (item.type === "card") {
uni.switchTab({ url: "/pages/make/index" });
} else if (item.type === "wallpaper") {
uni.navigateTo({ url: `/pages/wallpaper/index` });
}
};
onShareAppMessage(() => {
return {
title: "开启你的2026新春好运",
path: "/pages/index/index",
};
});
onShareTimeline(() => {
return {
title: "开启你的2026新春好运",
};
});
</script>
<style lang="scss" scoped>
.home-container {
min-height: 100vh;
background-color: #fbfbf9; /* 柔和的米色背景 */
padding-left: 32rpx;
padding-right: 32rpx;
padding-bottom: 120rpx;
box-sizing: border-box;
}
/* 顶部用户信息 */
.header-section {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24rpx;
.user-info {
display: flex;
align-items: center;
.user-avatar {
width: 64rpx;
height: 64rpx;
border-radius: 50%;
border: 2rpx solid #fff;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
margin-right: 16rpx;
}
.user-texts {
display: flex;
flex-direction: column;
.greeting-info {
font-size: 20rpx;
color: #999;
margin-bottom: 2rpx;
}
.user-name {
font-size: 26rpx;
font-weight: bold;
color: #333;
}
}
}
}
/* 主卡片 (Lucky Status) */
.main-card {
position: relative;
width: 100%;
height: 640rpx;
background: linear-gradient(180deg, #fcece8 0%, #f7dcd6 100%);
border-radius: 40rpx;
padding: 40rpx;
box-sizing: border-box;
box-shadow: 0 20rpx 40rpx rgba(189, 87, 87, 0.15);
display: flex;
flex-direction: column;
justify-content: space-between;
overflow: hidden;
margin-bottom: 48rpx;
.card-bg-decor {
position: absolute;
top: -50rpx;
right: -50rpx;
width: 300rpx;
height: 300rpx;
background: radial-gradient(
circle,
rgba(255, 255, 255, 0.4) 0%,
rgba(255, 255, 255, 0) 70%
);
border-radius: 50%;
}
.card-top {
display: flex;
justify-content: space-between;
align-items: flex-start;
z-index: 2;
.lucky-status {
display: flex;
flex-direction: column;
.status-label {
font-size: 22rpx;
color: #a85a5a;
letter-spacing: 2rpx;
font-weight: 600;
margin-bottom: 8rpx;
}
.status-value {
font-size: 88rpx;
font-weight: bold;
color: #a85a5a;
line-height: 1;
.percent {
font-size: 40rpx;
margin-left: 4rpx;
}
}
}
.date-box {
background: rgba(255, 255, 255, 0.6);
border-radius: 20rpx;
padding: 20rpx;
backdrop-filter: blur(10rpx);
.date-row {
display: flex;
align-items: center;
margin-bottom: 12rpx;
.date-text {
font-size: 24rpx;
color: #8b5a5a;
font-weight: bold;
margin-left: 8rpx;
}
}
.yi-ji-row {
font-size: 20rpx;
margin-bottom: 4rpx;
color: #666;
.label {
color: #999;
}
.content {
color: #555;
}
}
}
}
.card-center {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex: 1;
z-index: 2;
.lucky-color-tag {
background: #fff;
padding: 12rpx 36rpx;
border-radius: 40rpx;
font-size: 26rpx;
color: #d81e06;
font-weight: 600;
box-shadow: 0 4rpx 12rpx rgba(216, 30, 6, 0.15);
letter-spacing: 2rpx;
}
}
.card-bottom {
z-index: 2;
.open-lucky-btn {
width: 100%;
height: 108rpx;
background: #fff;
border-radius: 54rpx;
display: flex;
align-items: center;
justify-content: center;
color: #d81e06;
font-size: 34rpx;
font-weight: 800;
margin-bottom: 32rpx;
box-shadow: 0 8rpx 20rpx rgba(216, 30, 6, 0.2);
letter-spacing: 2rpx;
&::after {
border: none;
}
}
.week-sign-section {
width: 100%;
.sign-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24rpx;
.sign-title {
font-size: 24rpx;
color: #a85a5a;
font-weight: bold;
}
.sign-tip {
font-size: 20rpx;
color: #a85a5a;
opacity: 0.7;
}
}
.week-days {
display: flex;
justify-content: space-between;
.day-item {
display: flex;
flex-direction: column;
align-items: center;
.day-label {
font-size: 20rpx;
color: #a85a5a;
margin-bottom: 12rpx;
opacity: 0.8;
}
.status-icon {
width: 44rpx;
height: 44rpx;
border-radius: 50%;
background: rgba(255, 255, 255, 0.4);
display: flex;
align-items: center;
justify-content: center;
.dot {
width: 8rpx;
height: 8rpx;
border-radius: 50%;
background: #a85a5a;
opacity: 0.3;
}
}
&.is-today {
.day-label {
font-weight: bold;
opacity: 1;
}
.status-icon {
background: #fff;
box-shadow: 0 4rpx 12rpx rgba(216, 30, 6, 0.1);
}
}
&.is-signed {
.status-icon {
background: #d81e06;
box-shadow: 0 4rpx 12rpx rgba(216, 30, 6, 0.2);
}
.day-label {
color: #d81e06;
}
}
}
}
}
}
}
/* 通用 Section 样式 */
.section-container {
margin-bottom: 48rpx;
.section-header {
display: flex;
align-items: center;
margin-bottom: 24rpx;
.title-left-decor {
width: 6rpx;
height: 28rpx;
background: #d81e06;
border-radius: 4rpx;
margin-right: 12rpx;
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
flex: 1;
}
.header-right {
display: flex;
align-items: center;
.refresh-text {
font-size: 22rpx;
color: #999;
margin-left: 6rpx;
}
}
}
}
/* 大家都在用 */
.compact-section {
.section-header {
margin-bottom: 20rpx;
}
}
.feature-row {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
.feature-item {
width: 48%;
background: #fff;
border-radius: 24rpx;
padding: 20rpx;
display: flex;
align-items: center;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.03);
box-sizing: border-box;
margin-bottom: 20rpx;
.left-content {
display: flex;
align-items: center;
width: 100%;
.icon-wrap {
width: 64rpx;
height: 64rpx;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
margin-right: 16rpx;
flex-shrink: 0;
&.yellow-bg {
background: #fff8e1;
}
&.blue-bg {
background: #e3f2fd;
}
.feature-icon {
width: 36rpx;
height: 36rpx;
}
}
.text-content {
display: flex;
flex-direction: column;
overflow: hidden;
.feature-name {
font-size: 26rpx;
font-weight: bold;
color: #333;
margin-bottom: 2rpx;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.feature-desc {
font-size: 20rpx;
color: #999;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
}
}
/* 排行榜单 */
.rank-tabs-mini {
display: flex;
margin-left: auto; /* Push to right */
background: #eee;
border-radius: 30rpx;
padding: 4rpx;
.rank-tab-mini {
padding: 8rpx 20rpx;
border-radius: 26rpx;
font-size: 22rpx;
color: #666;
transition: all 0.3s;
&.active {
background: #fff;
color: #333;
font-weight: bold;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
}
}
}
.rank-list {
background: #fff;
border-radius: 32rpx;
padding: 16rpx 24rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.03);
.rank-item {
display: flex;
align-items: center;
padding: 24rpx 0;
border-bottom: 2rpx solid #f5f5f5;
&:last-child {
border-bottom: none;
}
.rank-num {
width: 40rpx;
height: 40rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
font-weight: bold;
color: #999;
margin-right: 24rpx;
border-radius: 50%;
&.rank-1 {
background: #ffd700;
color: #fff;
}
&.rank-2 {
background: #c0c0c0;
color: #fff;
}
&.rank-3 {
background: #cd7f32;
color: #fff;
}
}
.rank-thumb {
width: 88rpx;
height: 88rpx;
border-radius: 16rpx;
background: #eee;
margin-right: 24rpx;
}
.rank-info {
flex: 1;
.rank-title-row {
display: flex;
align-items: center;
margin-bottom: 8rpx;
.rank-title {
font-size: 28rpx;
font-weight: bold;
color: #333;
}
}
.rank-meta {
display: flex;
align-items: center;
.tag-hot {
font-size: 18rpx;
color: #d81e06;
background: #ffebee;
padding: 2rpx 8rpx;
border-radius: 8rpx;
margin-right: 12rpx;
font-weight: 600;
}
.usage-count {
font-size: 22rpx;
color: #999;
}
}
}
.rank-likes {
display: flex;
align-items: center;
.likes-num {
font-size: 24rpx;
color: #a85a5a;
margin-left: 8rpx;
font-weight: 500;
}
}
}
}
.rank-footer {
text-align: center;
font-size: 22rpx;
color: #ccc;
padding: 24rpx 0;
}
/* 悬浮分享按钮 */
.float-share-btn {
position: fixed;
right: 32rpx;
bottom: 60rpx; /* 避开 tabBar */
background: #fff;
border-radius: 40rpx;
padding: 16rpx 32rpx;
display: flex;
align-items: center;
box-shadow: 0 8rpx 24rpx rgba(216, 30, 6, 0.2);
border: 2rpx solid #ffebee;
z-index: 100;
text {
font-size: 26rpx;
color: #d81e06;
font-weight: 600;
margin-left: 12rpx;
}
}
.bottom-spacer {
height: 120rpx;
}
</style>