Files
spring-festival-greetings/pages/wallpaper/detail.vue
2026-02-26 14:58:04 +08:00

697 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="detail-page" :style="{ paddingTop: navHeight + 'px' }">
<!-- Navbar -->
<view
class="nav-bar"
:style="{ height: navHeight + 'px', paddingTop: statusBarHeight + 'px' }"
>
<view class="nav-content">
<view class="back" @tap="goBack">
<text class="back-icon"></text>
</view>
<text class="title">壁纸详情</text>
</view>
</view>
<view class="content-container">
<!-- Sharer Info -->
<view class="sharer-info" v-if="detailData.from">
<image
:src="detailData.from.avatar || '/static/default-avatar.png'"
class="avatar"
mode="aspectFill"
/>
<view class="info-text">
<view class="nickname">{{
detailData.from.nickname || "神秘好友"
}}</view>
<view class="action-text">给你分享了一张2026新春精美壁纸</view>
<view class="sub-text"> 2026 且马贺岁</view>
</view>
</view>
<!-- Wallpaper Preview -->
<view class="preview-card">
<view class="preview-badge">PREVIEW</view>
<image
:src="getThumbUrl(detailData.imageUrl)"
mode="widthFix"
class="main-image"
@tap="previewImage"
/>
</view>
<!-- Action Buttons -->
<view class="action-buttons">
<view class="points-display" v-if="isLoggedIn">
<text class="label">当前积分</text>
<text class="value">{{ userPoints }}</text>
</view>
<button class="btn primary-btn" @tap="downloadWallpaper">
<view class="btn-content">
<view class="btn-main">
<text class="btn-icon"></text>
<text>下载高清壁纸</text>
</view>
<text class="btn-sub">消耗 20 积分</text>
</view>
</button>
</view>
<!-- More Wallpapers -->
<view class="more-section">
<view class="section-header">
<text class="section-title">更多同款壁纸</text>
<view class="more-link" @tap="goToIndex">
<text>查看更多</text>
<text class="arrow"></text>
</view>
</view>
<scroll-view scroll-x class="more-scroll" :show-scrollbar="false">
<view class="scroll-inner">
<view
v-for="(item, index) in recommendList"
:key="index"
class="scroll-item"
@tap="onRecommendClick(item)"
>
<image
:src="getThumbUrl(item.imageUrl)"
mode="aspectFill"
class="scroll-img"
/>
<text class="item-title">{{ item.title || "新春壁纸" }}</text>
</view>
</view>
</scroll-view>
</view>
<!-- Footer -->
<view class="footer">
<view class="footer-divider">
<view class="line"></view>
<text class="footer-text">2026 HAPPY NEW YEAR</text>
<view class="line"></view>
</view>
<view class="footer-sub">新春祝福 · 传递温情</view>
</view>
</view>
<LoginPopup ref="loginPopupRef" @logind="handleLogind" />
<RewardAd ref="rewardAdRef" @onReward="handleAdReward" />
</view>
</template>
<script setup>
import { ref, computed } from "vue";
import { onLoad, onShareAppMessage, onShareTimeline } from "@dcloudio/uni-app";
import { getBavBarHeight } from "@/utils/system";
import { getPageDetail } from "@/api/system";
import { getWallpaperSameList } from "@/api/wallpaper";
import {
getShareToken,
saveViewRequest,
saveRemoteImageToLocal,
saveRecordRequest,
trackRecord,
} from "@/utils/common.js";
import { useUserStore } from "@/stores/user";
import { abilityCheck, watchAdReward } from "@/api/system.js";
import LoginPopup from "@/components/LoginPopup/LoginPopup.vue";
import RewardAd from "@/components/RewardAd/RewardAd.vue";
const userStore = useUserStore();
const loginPopupRef = ref(null);
const rewardAdRef = ref(null);
const isLoggedIn = computed(() => !!userStore.userInfo.nickName);
const userPoints = computed(() => userStore.userInfo.points || 0);
const navHeight = getBavBarHeight();
const statusBarHeight = uni.getSystemInfoSync().statusBarHeight;
const detailData = ref({
imageUrl: "",
from: null,
});
const recommendList = ref([]);
const shareToken = ref("");
const categoryId = ref("");
onLoad(async (options) => {
if (options.shareToken) {
shareToken.value = options.shareToken;
await fetchDetail();
}
if (options.id) {
detailData.value.id = options.id;
}
if (options.imageUrl) {
detailData.value.imageUrl = decodeURIComponent(options.imageUrl);
}
fetchRecommend(options.id);
});
onShareAppMessage(async () => {
const token = await getShareToken("wallpaper_download", detailData.value?.id);
return {
title: "快来看看我刚领到的新年精美壁纸 🖼",
path: `/pages/wallpaper/detail?shareToken=${token || ""}`,
imageUrl:
detailData.value?.imageUrl ||
"https://file.lihailezzc.com/resource/8dd026d76ef7a63d123b7fd698fb989b.png",
};
});
onShareTimeline(async () => {
const token = await getShareToken("wallpaper_download", detailData.value?.id);
return {
title: "快来看看我刚领到的新年精美壁纸 🖼",
query: `shareToken=${token}`,
imageUrl:
detailData.value?.imageUrl ||
"https://file.lihailezzc.com/resource/8dd026d76ef7a63d123b7fd698fb989b.png",
};
});
const getThumbUrl = (url) => {
return `${url}?imageView2/1/w/340/h/600/q/80`;
};
const fetchDetail = async () => {
try {
const res = await getPageDetail(shareToken.value);
saveViewRequest(shareToken.value, "wallpaper_detail", res.id);
if (res) {
detailData.value = res;
fetchRecommend(res.id);
}
} catch (e) {
console.error("Failed to fetch detail", e);
uni.showToast({
title: "获取详情失败",
icon: "none",
});
}
};
const fetchRecommend = async (id) => {
if (!id && !detailData.value.id) return;
try {
const res = await getWallpaperSameList(id || detailData.value.id);
recommendList.value = res;
categoryId.value = res[0]?.categoryId || "";
} catch (e) {
console.error("Failed to fetch recommendations", e);
}
};
const goBack = () => {
const pages = getCurrentPages();
if (pages.length > 1) {
uni.navigateBack();
} else {
uni.reLaunch({
url: "/pages/index/index",
});
}
};
const goToIndex = () => {
uni.navigateTo({
url: `/pages/wallpaper/index?categoryId=${categoryId.value}`,
});
};
const goToFortune = () => {
uni.navigateTo({
url: "/pages/fortune/index",
});
};
const previewImage = () => {
if (detailData.value.imageUrl) {
uni.previewImage({
urls: [detailData.value.imageUrl],
});
}
};
const onRecommendClick = (item) => {
// Update detailData with new item info
detailData.value = {
...detailData.value,
id: item.id,
imageUrl: item.imageUrl,
title: item.title,
};
// Refresh recommendations for the new item
fetchRecommend(item.id);
// Scroll to top or just update view
uni.pageScrollTo({
scrollTop: 0,
duration: 300,
});
};
const handleLogind = async () => {
// Logic after successful login if needed
};
const handleAdReward = async (token) => {
try {
const res = await watchAdReward(token);
if (res) {
uni.showToast({
title: "获得50积分",
icon: "success",
});
await userStore.fetchUserAssets();
}
} catch (e) {
console.error("Reward claim failed", e);
uni.showToast({ title: "奖励发放失败", icon: "none" });
}
};
const downloadWallpaper = async () => {
trackRecord({
eventName: "wallpaper_download_click",
eventType: `click`,
elementId: detailData.value?.id || "",
});
if (!isLoggedIn.value) {
loginPopupRef.value.open();
return;
}
const abilityRes = await abilityCheck("wallpaper_download");
if (!abilityRes.canUse) {
if (
abilityRes?.blockType === "need_share" &&
abilityRes?.message === "分享可继续"
) {
uni.showToast({
title: "分享给好友即可下载",
icon: "none",
});
return;
}
if (
abilityRes?.blockType === "need_ad" &&
abilityRes?.message === "观看广告可继续"
) {
uni.showModal({
title: "积分不足",
content: "观看广告可获得50积分继续下载",
success: (res) => {
if (res.confirm) {
rewardAdRef.value.show();
}
},
});
return;
}
uni.showToast({
title: "您今日壁纸下载次数已用完,明日再试",
icon: "none",
});
return;
}
uni.showLoading({ title: "下载中..." });
try {
// Parallelize save record and download
// Wait for saveRecordRequest to ensure backend deducts points
await Promise.all([
saveRecordRequest(
"",
detailData.value?.id,
"wallpaper_download",
detailData.value?.imageUrl,
),
saveRemoteImageToLocal(detailData.value?.imageUrl),
]);
// Refresh user assets to show updated points
await userStore.fetchUserAssets();
uni.hideLoading();
uni.showToast({ title: "保存成功 消耗 20 积分", icon: "success" });
} catch (e) {
uni.hideLoading();
console.error("Download failed", e);
uni.showToast({ title: "下载失败", icon: "none" });
}
};
</script>
<style lang="scss" scoped>
.detail-page {
min-height: 100vh;
background: #ffffff;
box-sizing: border-box;
}
.nav-bar {
position: fixed;
top: 0;
left: 0;
width: 100%;
z-index: 100;
background-color: #ffffff;
.nav-content {
height: 44px; // Standard nav height
display: flex;
align-items: center;
padding: 0 24rpx;
.back {
display: flex;
align-items: center;
margin-right: 24rpx;
/* 增大点击区域 */
padding: 20rpx;
margin-left: -20rpx;
.back-icon {
font-size: 50rpx;
color: #333;
font-weight: 300;
line-height: 1;
}
}
.title {
font-size: 34rpx;
font-weight: 600;
color: #333;
flex: 1;
text-align: center;
margin-right: 50rpx; /* Balance back button */
}
}
}
.content-container {
padding: 20px;
padding-bottom: 40px;
}
.sharer-info {
display: flex;
align-items: center;
margin-bottom: 24px;
.avatar {
width: 48px;
height: 48px;
border-radius: 50%;
margin-right: 12px;
border: 2px solid #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.info-text {
.nickname {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 4px;
}
.action-text {
font-size: 14px;
color: #666;
margin-bottom: 2px;
}
.sub-text {
font-size: 12px;
color: #ff3b30;
font-weight: 500;
}
}
}
.preview-card {
position: relative;
width: 100%;
background: #fff;
border-radius: 16px;
padding: 12px;
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.04);
border: 1px solid #f5f5f5;
margin-bottom: 24px;
.preview-badge {
position: absolute;
top: 24px;
right: 24px;
background: rgba(0, 0, 0, 0.2);
backdrop-filter: blur(4px);
color: #fff;
font-size: 10px;
padding: 4px 8px;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.2);
z-index: 10;
}
.main-image {
width: 460rpx;
margin: 0 auto;
border-radius: 12px;
display: block;
background-color: #fafafa; // Placeholder color
box-shadow: 0 12rpx 32rpx rgba(0, 0, 0, 0.1);
}
}
.action-buttons {
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 32px;
.btn {
width: 100%;
height: 52px;
border-radius: 26px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
font-weight: 600;
border: none;
margin: 0;
transition: all 0.2s;
.btn-icon {
margin-right: 8px;
font-size: 18px;
}
&.primary-btn {
background: linear-gradient(90deg, #ff3b30 0%, #ff6b6b 100%);
color: #fff;
box-shadow: 0 8px 20px rgba(255, 59, 48, 0.15);
height: 64px; /* Increased height for two lines */
}
&.secondary-btn {
background: #f9f9f9;
color: #333;
border: 1px solid #eee;
}
&:active {
transform: scale(0.98);
opacity: 0.9;
}
}
.points-display {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 8px;
font-size: 14px;
color: #666;
.value {
color: #ff5722;
font-weight: bold;
font-size: 16px;
margin-left: 4px;
}
}
.btn-content {
display: flex;
flex-direction: column;
align-items: center;
line-height: 1.2;
.btn-main {
display: flex;
align-items: center;
font-size: 18px;
font-weight: bold;
margin-bottom: 2px;
}
.btn-sub {
font-size: 12px;
opacity: 0.9;
font-weight: normal;
}
}
}
.more-section {
margin-bottom: 32px;
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
.section-title {
font-size: 18px;
font-weight: 600;
color: #333;
}
.more-link {
display: flex;
align-items: center;
font-size: 14px;
color: #ff3b30;
.arrow {
margin-left: 2px;
font-size: 16px;
}
}
}
.more-scroll {
width: 100%;
white-space: nowrap;
}
.scroll-inner {
display: flex;
padding-right: 20px;
}
.scroll-item {
display: inline-flex;
flex-direction: column;
width: 200rpx;
margin-right: 20rpx;
flex-shrink: 0;
.scroll-img {
width: 100%;
height: 355rpx;
border-radius: 12rpx;
margin-bottom: 12rpx;
background-color: #f5f5f5;
}
.item-title {
font-size: 24rpx;
color: #333;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
.fortune-banner-wrap {
margin-bottom: 40px;
}
.wallpaper-banner {
background: #fafafa;
border-radius: 24rpx;
padding: 30rpx;
display: flex;
align-items: center;
margin-bottom: 60rpx;
border: 1px solid #f5f5f5;
}
.banner-icon {
width: 80rpx;
height: 80rpx;
background: #fff;
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
margin-right: 24rpx;
}
.banner-icon text {
font-size: 40rpx;
color: #ff3b30;
}
.banner-content {
flex: 1;
}
.banner-title {
font-size: 30rpx;
font-weight: bold;
color: #333;
display: block;
margin-bottom: 8rpx;
}
.banner-desc {
font-size: 22rpx;
color: #999;
}
.banner-arrow {
font-size: 36rpx;
color: #ccc;
}
.footer {
text-align: center;
padding-bottom: 20px;
.footer-divider {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 8px;
.line {
width: 40px;
height: 1px;
background: #e0e0e0;
}
.footer-text {
margin: 0 12px;
font-size: 12px;
color: #999;
letter-spacing: 1px;
font-weight: 600;
}
}
.footer-sub {
font-size: 10px;
color: #ccc;
}
}
</style>