Files
spring-festival-greetings/pages/index/index.vue
2026-02-03 02:41:19 +08:00

717 lines
17 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="spring-page" :style="{ paddingTop: getBavBarHeight() + 'px' }">
<!-- 顶部 Banner -->
<view class="hero">
<view class="hero-badge">
<text class="badge-dot"></text>
<text class="badge-text">{{ countdownText }}</text>
</view>
<view class="hero-title">
<text class="year">2026</text>
<text class="main">新春祝福</text>
</view>
<text class="hero-sub">新年快乐万事如意!</text>
<!-- <image class="hero-decor" src="https://file.lihailezzc.com/resource/58c8d19e5f2d9c958a7b8b9f44b8c3e3.png" mode="aspectFill" /> -->
</view>
<!-- 功能入口宫格 -->
<view class="feature-grid">
<view
v-for="(item, idx) in features"
:key="idx"
class="feature-item"
@tap="onFeatureTap(item)"
>
<image
class="feature-icon"
:src="item.icon"
mode="aspectFill"
:style="{
backgroundColor: idx < 2 ? '#FEF2F2' : '#FFFBEC',
}"
/>
<view class="feature-texts">
<text class="feature-title">{{ item.title }}</text>
<text class="feature-sub">{{ item.subtitle }}</text>
</view>
</view>
</view>
<!-- 今日灵感 -->
<view class="daily-section">
<view class="daily-header">
<text class="daily-title">今日灵感</text>
<text class="daily-date">{{ todayDate }}</text>
</view>
<view class="daily-card">
<view class="daily-content">
<text class="quote-mark"></text>
<text class="daily-text">{{ dailyGreeting }}</text>
<text class="quote-mark right"></text>
</view>
<view class="daily-actions">
<!-- <view class="action-btn copy" @tap="copyGreeting">
<text class="icon"></text> 复制
</view> -->
<view class="action-btn use" @tap="useGreeting">
<text class="icon"></text> 去制作
</view>
</view>
</view>
</view>
<!-- 大家都在用网格布局 -->
<view class="section">
<view class="section-header">
<view class="section-bar"></view>
<text class="section-title">大家都在用</text>
<!-- <text class="section-more" @tap="onMore('use')">查看更多 ></text> -->
</view>
<view class="use-grid">
<view
v-for="(card, i) in recommendList"
:key="i"
class="use-card"
@tap="onCardClick(card)"
>
<view class="card-cover-wrap">
<image :src="card.imageUrl" class="card-cover" mode="aspectFill" />
<view v-if="card.tag" class="card-tag" :class="`tag--${card.tag}`">
{{ getTagText(card.tag) }}
</view>
</view>
<view class="card-info">
<view class="card-title">{{ card.title }}</view>
<view class="card-desc">{{ card.content }}</view>
<view class="card-footer">
<view class="cta-btn" @tap.stop="onCardClick(card)">
{{ getCtaText(card.type) }}
</view>
</view>
</view>
</view>
</view>
<view v-if="loading" class="loading-text">加载中...</view>
<view v-if="!hasMore && recommendList.length > 0" class="no-more-text"
>没有更多了</view
>
</view>
</view>
</template>
<script setup>
import { ref, onMounted } from "vue";
import {
onPullDownRefresh,
onShareAppMessage,
onReachBottom,
} from "@dcloudio/uni-app";
import { getBavBarHeight } from "@/utils/system";
import { getRecommendList } from "@/api/system";
const countdownText = ref("");
const updateCountdown = () => {
const now = new Date();
const springFestival = new Date("2026-02-17T00:00:00"); // 2026春节
// 只比较日期,忽略时分秒
now.setHours(0, 0, 0, 0);
springFestival.setHours(0, 0, 0, 0);
const diffTime = now.getTime() - springFestival.getTime();
const days = Math.floor(diffTime / (1000 * 60 * 60 * 24));
if (days < 0) {
countdownText.value = `距春节还有 ${Math.abs(days)}`;
} else if (days === 0) {
countdownText.value = "大年初一";
} else {
const cnNums = ["一", "二", "三", "四", "五", "六", "七", "八", "九", "十"];
if (days < 10) {
countdownText.value = `大年初${cnNums[days]}`;
} else if (days < 15) {
const sub = days + 1 - 10;
const subCn = ["一", "二", "三", "四", "五"][sub - 1];
countdownText.value = `正月十${subCn}`;
} else if (days === 14) {
// days=14是第15天
countdownText.value = "元宵节";
} else {
countdownText.value = "蛇年大吉";
}
}
};
const todayDate = ref("");
const dailyGreeting = ref("");
const inspirationList = [
"岁岁常欢愉,年年皆胜意。愿你新的一年,多喜乐,长安宁。",
"新的一年,愿日子如熹光,温柔又安详。你我赤诚且勇敢,欣喜也在望。",
"祝你:百事无忌,平安喜乐。万事胜意,得偿所愿。",
"凡是过往皆为序章。愿2026年烟火向星辰所愿皆成真。",
"愿新的一年,仍有阳光满路,温暖如初。",
"辞暮尔尔,烟火年年。朝朝暮暮,岁岁平安。",
"愿你即使单枪匹马,也能勇敢无畏。新的一年,万事尽可期待。",
"所求皆如愿,所行化坦途。多喜乐,长安宁。",
"愿你手握山河,且行且歌。新年快乐,万事大吉。",
"长路浩浩荡荡,万物尽可期待。祝你新年好运。",
"何其有幸,年岁并进。新的一年,愿你快乐,不止新年。",
"愿新年,胜旧年。无病无灾,岁岁平安。",
"烟火起,照人间,举杯敬此年。烟花落,看人间,家家户户皆团圆。",
"家人闲坐,灯火可亲。新年伊始,喜乐安宁。",
"愿今年所有的遗憾,都是明年惊喜的铺垫。",
];
const copyGreeting = () => {
uni.setClipboardData({
data: dailyGreeting.value,
success: () => {
uni.showToast({ title: "复制成功", icon: "none" });
},
});
};
const useGreeting = () => {
uni.setStorageSync("TEMP_BLESSING_TEXT", dailyGreeting.value);
uni.switchTab({
url: "/pages/make/index",
});
};
onMounted(() => {
updateCountdown();
const date = new Date();
todayDate.value = `${date.getMonth() + 1}${date.getDate()}`;
// Daily Inspiration Logic
const startOfYear = new Date(date.getFullYear(), 0, 0);
const diff = date - startOfYear;
const oneDay = 1000 * 60 * 60 * 24;
const dayOfYear = Math.floor(diff / oneDay);
const index = dayOfYear % inspirationList.length;
dailyGreeting.value = inspirationList[index];
fetchRecommendList();
});
const features = ref([
{
title: "新春祝福卡片",
subtitle: "定制专属贺卡吧",
icon: "/static/icon/celebrate.png",
type: "card",
},
{
title: "新年运势",
subtitle: "抽取新年关键词",
icon: "/static/icon/yunshi.png",
type: "fortune",
},
{
title: "新春头像",
subtitle: "焕上节日新饰",
icon: "/static/icon/guashi.png",
type: "avatar_decor",
},
{
title: "精美壁纸",
subtitle: "获取精美壁纸",
icon: "/static/icon/bizhi.png",
type: "wallpaper",
},
]);
const recommendList = ref([]);
const page = ref(1);
const hasMore = ref(true);
const loading = ref(false);
const fetchRecommendList = async (isRefresh = false) => {
if (loading.value || (!hasMore.value && !isRefresh)) return;
loading.value = true;
if (isRefresh) {
page.value = 1;
hasMore.value = true;
}
try {
const res = await getRecommendList(page.value);
const list = res?.list || [];
if (isRefresh) {
recommendList.value = list;
} else {
recommendList.value = [...recommendList.value, ...list];
}
if (res.hasNext !== undefined) {
hasMore.value = res.hasNext;
} else {
// Fallback if API doesn't return hasNext
if (list.length < 10) hasMore.value = false;
}
if (list.length > 0) {
page.value++;
} else {
hasMore.value = false;
}
} catch (e) {
console.error(e);
} finally {
loading.value = false;
}
};
const getTagText = (tag) => {
const map = {
hot: "热门",
new: "新款",
featured: "精选",
hot2: "爆款",
};
return map[tag] || tag;
};
const getCtaText = (type) => {
const map = {
frame: "去制作",
decor: "去装饰",
avatar: "去查看",
card: "去写祝福",
fortune: "去抽取",
};
return map[type] || "立即查看";
};
const onCardClick = (card) => {
// 构造传递的数据
const query = `recommendId=${card.recommendId || ""}&type=${card.type || ""}&imageUrl=${encodeURIComponent(card.imageUrl || "")}`;
if (
card.scene === "avatar_download" ||
["frame", "decor", "avatar"].includes(card.type)
) {
uni.navigateTo({
url: `/pages/avatar/index?${query}`,
});
return;
}
// Default fallback based on type
if (card.type === "card") {
// 贺卡制作通常是 Tab 页,通过 Storage 传递参数
uni.setStorageSync("RECOMMEND_CARD_DATA", {
recommendId: card.recommendId,
imageUrl: card.imageUrl,
type: card.type,
});
uni.switchTab({ url: "/pages/make/index" });
} else if (card.type === "fortune") {
uni.navigateTo({ url: "/pages/fortune/index" });
} else {
// 默认跳转到头像页
uni.navigateTo({
url: `/pages/avatar/index?${query}`,
});
}
};
const onFeatureTap = (item) => {
if (item.type === "fortune") {
uni.navigateTo({ url: "/pages/fortune/index" });
return;
}
if (item.type === "card") {
uni.switchTab({ url: "/pages/make/index" });
return;
}
if (item.type === "avatar_decor" || item.type === "avatar_frame") {
uni.navigateTo({ url: "/pages/avatar/index" });
return;
}
if (item.type === "wallpaper") {
uni.navigateTo({ url: "/pages/wallpaper/index" });
return;
}
uni.showToast({ title: `进入:${item.title}`, icon: "none" });
};
onReachBottom(() => {
fetchRecommendList();
});
const onMore = () => {
uni.showToast({ title: "更多模板即将上线~", icon: "none" });
};
onPullDownRefresh(async () => {
await fetchRecommendList(true);
setTimeout(() => {
uni.stopPullDownRefresh();
uni.showToast({ title: "已为你更新内容", icon: "success" });
}, 600);
});
onShareAppMessage(() => {
return {
title: "新春祝福",
path: "/pages/detail/index",
imageUrl: "/static/images/bg.jpg",
success: function (res) {
uni.showToast({ title: "分享成功", icon: "success" });
},
fail: function (res) {
uni.showToast({ title: "分享失败", icon: "none" });
},
};
});
</script>
<style lang="scss" scoped>
.spring-page {
min-height: 100vh;
box-sizing: border-box;
background: #f8f6f6;
}
/* 顶部 Banner */
.hero {
position: relative;
margin: 24rpx auto;
padding: 32rpx;
height: 324rpx;
width: 92vw;
border-radius: 24rpx;
color: #fff;
background: url("http://file.lihailezzc.com/77ea2597-569c-4f13-b5af-22606742adcfssss.jpg");
background-size: cover;
overflow: hidden;
.hero-badge {
display: inline-flex;
align-items: center;
background: rgba(255, 255, 255, 0.2);
border-radius: 999rpx;
padding: 8rpx 16rpx;
font-size: 22rpx;
.badge-dot {
margin-right: 8rpx;
}
}
.hero-title {
margin-top: 24rpx;
display: flex;
align-items: baseline;
.year {
font-size: 44rpx;
font-weight: 700;
margin-right: 12rpx;
}
.main {
font-size: 44rpx;
font-weight: 700;
}
}
.hero-sub {
margin-top: 8rpx;
font-size: 24rpx;
opacity: 0.9;
}
.hero-decor {
position: absolute;
right: 12rpx;
bottom: 12rpx;
width: 160rpx;
height: 160rpx;
opacity: 0.3;
border-radius: 16rpx;
}
}
/* 功能入口宫格 - 2x2 */
.feature-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-gap: 20rpx;
padding: 0 24rpx;
margin-top: 8rpx;
.feature-item {
position: relative;
display: flex;
align-items: left;
flex-direction: column;
padding: 20rpx;
border-radius: 18rpx;
background: #fff;
box-shadow: 0 6rpx 16rpx rgba(0, 0, 0, 0.04);
}
.feature-item::after {
content: "";
position: absolute;
top: 0;
right: 0;
width: 60rpx;
height: 60rpx;
border-bottom-left-radius: 80rpx;
z-index: 1;
}
.feature-item:nth-child(1)::after,
.feature-item:nth-child(2)::after {
background: #fef2f2; /* 可换成你的主题粉 */
}
.feature-item:nth-child(3)::after,
.feature-item:nth-child(4)::after {
background: #fffbec; /* 温柔一点的黄 */
}
.feature-icon {
width: 60rpx;
height: 60rpx;
border-radius: 50%;
padding: 5rpx;
}
.feature-texts {
margin-top: 20rpx;
display: flex;
flex-direction: column;
.feature-title {
font-size: 28rpx;
color: #222;
font-weight: 600;
}
.feature-sub {
font-size: 22rpx;
color: #888;
margin-top: 4rpx;
}
}
}
/* 今日灵感 */
.daily-section {
margin: 24rpx 24rpx 0;
.daily-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
.daily-title {
font-size: 32rpx;
font-weight: 700;
color: #333;
}
.daily-date {
font-size: 24rpx;
color: #999;
font-family: monospace;
}
}
.daily-card {
background: #fff;
border-radius: 20rpx;
padding: 30rpx;
box-shadow: 0 8rpx 24rpx rgba(255, 59, 48, 0.08);
position: relative;
overflow: hidden;
}
.daily-card::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 8rpx;
height: 100%;
background: #ff3b30;
}
.daily-content {
position: relative;
padding: 10rpx 20rpx;
.quote-mark {
font-size: 60rpx;
color: #ff3b30;
opacity: 0.2;
position: absolute;
line-height: 1;
font-family: serif;
}
.quote-mark:first-child {
top: -10rpx;
left: -10rpx;
}
.quote-mark.right {
bottom: -20rpx;
right: 0;
}
.daily-text {
font-size: 30rpx;
color: #444;
line-height: 1.8;
font-style: italic;
display: block;
text-align: justify;
}
}
.daily-actions {
display: flex;
justify-content: flex-end;
margin-top: 30rpx;
gap: 20rpx;
.action-btn {
display: flex;
align-items: center;
padding: 12rpx 24rpx;
border-radius: 999rpx;
font-size: 24rpx;
transition: all 0.2s;
.icon {
margin-right: 6rpx;
font-size: 26rpx;
}
}
.action-btn.copy {
background: #f5f5f5;
color: #666;
}
.action-btn.use {
background: #ff3b30;
color: #fff;
box-shadow: 0 4rpx 12rpx rgba(255, 59, 48, 0.3);
}
}
}
/* 通用区块标题 */
.section {
margin-top: 28rpx;
}
.section-header {
display: flex;
align-items: center;
padding: 0 24rpx;
.section-bar {
width: 10rpx;
height: 30rpx;
border-radius: 6rpx;
background: #ff3b30;
margin-right: 12rpx;
}
.section-title {
font-size: 28rpx;
color: #222;
flex: 1;
font-weight: 600;
}
.section-more {
font-size: 24rpx;
color: #ff3b30;
}
}
/* 大家都在用 - 网格列表 */
.use-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20rpx;
padding: 0 24rpx;
margin-top: 16rpx;
padding-bottom: 40rpx;
}
.use-card {
background: #fff;
border-radius: 20rpx;
overflow: hidden;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
display: flex;
flex-direction: column;
}
.card-cover-wrap {
position: relative;
width: 100%;
padding-bottom: 120%; /* 竖向卡片 */
background: #f5f5f5;
}
.card-cover {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.card-tag {
position: absolute;
top: 12rpx;
left: 12rpx;
padding: 4rpx 12rpx;
border-radius: 8rpx;
font-size: 20rpx;
color: #fff;
font-weight: 500;
&.tag--hot {
background: linear-gradient(135deg, #ff3b30, #ff9500);
}
&.tag--featured {
background: linear-gradient(135deg, #007aff, #5ac8fa);
}
&.tag--hot2 {
background: linear-gradient(135deg, #ff2d55, #ff375f);
}
&.tag--new {
background: linear-gradient(135deg, #5856d6, #af52de);
}
&.tag--default {
background: rgba(0, 0, 0, 0.5);
}
}
.card-info {
padding: 16rpx;
flex: 1;
display: flex;
flex-direction: column;
}
.card-title {
font-size: 28rpx;
font-weight: 600;
color: #333;
margin-bottom: 8rpx;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.card-desc {
font-size: 22rpx;
color: #999;
line-height: 1.4;
margin-bottom: 16rpx;
/* 限制2行 */
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.card-footer {
margin-top: auto;
display: flex;
justify-content: flex-end;
}
.cta-btn {
font-size: 22rpx;
color: #ff3b30;
background: rgba(255, 59, 48, 0.08);
padding: 8rpx 20rpx;
border-radius: 999rpx;
font-weight: 500;
}
.loading-text,
.no-more-text {
text-align: center;
font-size: 24rpx;
color: #999;
padding: 20rpx 0;
width: 100%;
}
</style>