Files
spring-festival-greetings/pages/fortune/index.vue
2026-02-05 23:43:51 +08:00

799 lines
18 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="fortune-page">
<NavBar title="2026 新年运势" :transparent="true" color="#ffd700" />
<!-- 初始状态签筒 -->
<view class="state-initial" v-if="status !== 'result'">
<view class="header-text">
<text class="title">点击开启你的</text>
<text class="year">2026</text>
<text class="title">年度关键词</text>
<view class="underline"></view>
</view>
<view class="shaker-container" :class="{ shaking: status === 'shaking' }">
<view class="shaker-body">
<view class="sticks">
<view class="stick s1"></view>
<view class="stick s2"></view>
<view class="stick s3"></view>
<view class="stick s4"></view>
<view class="stick s5"></view>
</view>
<view class="label-box">
<text class="label-text">祈福</text>
</view>
</view>
</view>
<button
class="action-btn"
@tap="startShake"
:disabled="status === 'shaking'"
>
{{ status === "shaking" ? "抽取中..." : "立即抽取" }}
</button>
<view v-if="isLoggedIn" class="footer-info">
<text class="info-icon"></text>
<text v-if="allowShareCount - useShareCount > 0">
今日还有 {{ remainingCount }} 次抽取机会分享可增加次数
</text>
<text v-else> 今日还有 {{ remainingCount }} 次抽取机会 </text>
</view>
</view>
<!-- 结果状态运势卡片 -->
<view class="state-result" v-else>
<view
class="result-card"
id="result-card"
:class="{ 'image-mode': !!currentFortune.imageUrl }"
>
<template v-if="currentFortune.imageUrl">
<image
:src="currentFortune.imageUrl"
mode="widthFix"
class="fortune-image"
/>
</template>
<template v-else>
<view class="card-header">
<text class="year-tag">2026 乙巳年</text>
</view>
<view class="card-body">
<view class="icon-circle">
<text class="result-icon"></text>
</view>
<text class="result-title">{{ currentFortune.title }}</text>
<view class="divider"></view>
<text class="result-desc">{{ currentFortune.desc }}</text>
<text class="result-sub">旧岁千般皆如意新年万事定称心</text>
</view>
<view class="card-footer">
<view class="footer-left">
<text class="sub-en">LUCKY CHARM</text>
<text class="sub-cn">每日运势签</text>
</view>
<view class="footer-right">
<text class="scan-tip">长按识别\n扫码祈福</text>
<view class="qr-code"></view>
</view>
</view>
</template>
</view>
<view class="result-actions">
<button class="share-btn" open-type="share">
<text class="icon"></text> 分享获取额外抽取机会
</button>
<view class="secondary-btns">
<button class="sec-btn" @tap="saveCard">
<text class="icon">📥</text> 保存运势卡片
</button>
<button class="sec-btn" @tap="goToRecord">
<text class="icon"></text> 我的记录
</button>
</view>
<view class="footer-status">
已分享 {{ useShareCount }}/{{ allowShareCount }} · 今日剩余机会:
{{ remainingCount }}
</view>
</view>
</view>
<!-- Canvas 用于生成图片 (隐藏) -->
<!-- <canvas
canvas-id="shareCanvas"
class="share-canvas"
style="width: 300px; height: 500px; position: fixed; left: 9999px"
></canvas> -->
<LoginPopup ref="loginPopupRef" @logind="handleLogind" />
</view>
</template>
<script setup>
import { ref, onUnmounted, computed } from "vue";
import { getDeviceInfo } from "@/utils/system";
import { onLoad, onShow, onShareAppMessage } from "@dcloudio/uni-app";
import { abilityCheck } from "@/api/system.js";
import { drawFortune } from "@/api/fortune.js";
import { getShareReward } from "@/api/system.js";
import LoginPopup from "@/components/LoginPopup/LoginPopup.vue";
import { useUserStore } from "@/stores/user";
import {
getShareToken,
saveRemoteImageToLocal,
saveRecordRequest,
} from "@/utils/common.js";
import NavBar from "@/components/NavBar/NavBar.vue";
const userStore = useUserStore();
const loginPopupRef = ref(null);
const isLoggedIn = computed(() => !!userStore.userInfo.nickName);
const status = ref("initial"); // initial, shaking, result
const remainingCount = ref(0);
const allowShareCount = ref(0);
const useShareCount = ref(0);
const canUse = ref(true);
// 音效控制
const audioContext = uni.createInnerAudioContext();
audioContext.src = "/static/music/shake.mp3";
let playCount = 0;
audioContext.onEnded(() => {
playCount++;
if (playCount < 3) {
audioContext.play();
}
});
onLoad(() => {});
onShow(() => {
checkDrawStatus();
});
onUnmounted(() => {
audioContext.destroy();
});
onShareAppMessage(async () => {
const shareToken = await getShareToken("fortune_draw", cardId.value);
getRewardByShare();
return {
title: "马年运势我已经抽过了,你的会是什么?",
path: `${cardId.value ? `/pages/fortune/detail?shareToken=${shareToken}` : `/pages/fortune/index?shareToken=${shareTokenRes.shareToken}`}`,
imageUrl:
"https://file.lihailezzc.com/resource/cfed2edbfa19250b836a87a4bbf0d5ad.png",
};
});
const handleLogind = async () => {
checkDrawStatus();
};
const getRewardByShare = async () => {
const res = await getShareReward({ scene: "fortune_draw" });
if (res.success) {
uni.showToast({ title: "分享成功,额外抽取机会已增加" });
checkDrawStatus();
}
};
const checkDrawStatus = async () => {
if (!isLoggedIn.value) return;
const res = await abilityCheck("fortune_draw");
remainingCount.value = res.remain || 0;
allowShareCount.value = res.allowShareCount || 0;
useShareCount.value = res.useShareCount || 0;
};
const currentFortune = ref({});
const cardId = ref("");
const goToRecord = () => {
uni.navigateTo({
url: "/pages/fortune/record",
});
};
const startShake = async () => {
if (!isLoggedIn.value) {
loginPopupRef.value.open();
return;
}
if (remainingCount.value <= 0) {
uni.showToast({ title: "今日次数已用完", icon: "none" });
return;
}
status.value = "shaking";
// 播放音效
playCount = 0;
audioContext.play();
// 模拟摇晃动画和数据请求
const minTime = 2000;
const startT = Date.now();
try {
const res = await drawFortune();
const endT = Date.now();
const waitTime = Math.max(0, minTime - (endT - startT));
setTimeout(() => {
currentFortune.value = res;
cardId.value = res.dataId;
status.value = "result";
remainingCount.value--;
}, waitTime);
} catch (e) {
setTimeout(() => {
status.value = "initial";
uni.showToast({ title: "网络请求失败", icon: "none" });
}, minTime);
}
};
const reset = () => {
status.value = "initial";
};
const saveCard = () => {
if (currentFortune.value.imageUrl) {
uni.showLoading({ title: "保存中..." });
saveRemoteImageToLocal(currentFortune.value.imageUrl);
saveRecordRequest(
"",
cardId.value,
"fortune_draw",
currentFortune.value.imageUrl,
);
return;
}
// uni.showLoading({ title: "生成中..." });
// const ctx = uni.createCanvasContext("shareCanvas");
// // 绘制背景
// ctx.setFillStyle("#FFFBF0");
// ctx.fillRect(0, 0, 300, 500);
// // 绘制外边框
// ctx.setStrokeStyle("#E6CAA0");
// ctx.setLineWidth(1);
// ctx.strokeRect(0, 0, 300, 500);
// // 绘制内装饰边框
// ctx.setStrokeStyle("#D4AF37");
// ctx.setLineWidth(2);
// ctx.strokeRect(10, 10, 280, 480);
// // 绘制年份标签
// ctx.setFillStyle("#E63946");
// // 圆角矩形模拟(简化)
// ctx.fillRect(100, 40, 100, 24);
// ctx.setFillStyle("#FFFFFF");
// ctx.setFontSize(14);
// ctx.setTextAlign("center");
// ctx.fillText("2026 乙巳年", 150, 57);
// // 绘制标题
// ctx.setFillStyle("#C0392B");
// ctx.setFontSize(32);
// ctx.font = "bold 32px serif";
// ctx.fillText(currentFortune.value.title, 150, 120);
// // 绘制分隔线
// ctx.setStrokeStyle("#D4AF37");
// ctx.beginPath();
// ctx.moveTo(130, 140);
// ctx.lineTo(170, 140);
// ctx.stroke();
// // 绘制描述
// ctx.setFillStyle("#333333");
// ctx.setFontSize(16);
// // 简单换行处理(假设文字不长)
// ctx.fillText(currentFortune.value.desc, 150, 180);
// // 绘制底部文字
// ctx.setFillStyle("#888888");
// ctx.setFontSize(12);
// ctx.fillText("旧岁千般皆如意,新年万事定称心。", 150, 220);
// ctx.draw(false, () => {
// uni.canvasToTempFilePath({
// canvasId: "shareCanvas",
// success: (res) => {
// uni.saveImageToPhotosAlbum({
// filePath: res.tempFilePath,
// success: () => {
// uni.hideLoading();
// uni.showToast({ title: "已保存到相册" });
// },
// fail: () => {
// uni.hideLoading();
// uni.showToast({ title: "保存失败", icon: "none" });
// },
// });
// },
// fail: (err) => {
// uni.hideLoading();
// console.error(err);
// },
// });
// });
};
</script>
<style scoped>
.fortune-page {
min-height: 100vh;
background: radial-gradient(
circle at 50% 30%,
#d93030 0%,
#8b0000 60%,
#4a0000 100%
);
color: #ffe4c4;
display: flex;
flex-direction: column;
align-items: center;
position: relative;
overflow-x: hidden;
}
/* 装饰纹理背景 */
.fortune-page::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image:
repeating-linear-gradient(
45deg,
rgba(255, 215, 0, 0.03) 0,
rgba(255, 215, 0, 0.03) 1px,
transparent 1px,
transparent 10px
),
repeating-linear-gradient(
-45deg,
rgba(255, 215, 0, 0.03) 0,
rgba(255, 215, 0, 0.03) 1px,
transparent 1px,
transparent 10px
);
pointer-events: none;
z-index: 0;
}
/* 初始状态 */
.state-initial {
width: 100%;
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
padding-top: 130px;
padding-bottom: 40px;
position: relative;
z-index: 1;
}
.header-text {
text-align: center;
margin-bottom: 60px;
}
.title {
font-size: 22px;
color: #ffe4c4;
letter-spacing: 2px;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.year {
font-size: 32px;
font-weight: bold;
color: #ffd700;
margin: 0 8px;
font-family: serif;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.4);
position: relative;
}
.year::after {
content: "";
position: absolute;
bottom: -4px;
left: 0;
width: 100%;
height: 3px;
background: rgba(255, 215, 0, 0.6);
border-radius: 2px;
}
.underline {
display: none;
}
/* 签筒动画 */
.shaker-container {
margin-bottom: 50px;
position: relative;
}
.shaker-body {
width: 160px;
height: 250px;
/* 更有质感的木纹/红漆效果 */
background: linear-gradient(
90deg,
#600000 0%,
#a00000 20%,
#d00000 45%,
#d00000 55%,
#a00000 80%,
#600000 100%
);
border-radius: 20px;
border: 4px solid #b8860b;
border-top: none; /* 顶部开口 */
position: relative;
box-shadow:
inset 0 10px 20px rgba(0, 0, 0, 0.4),
0 15px 35px rgba(0, 0, 0, 0.6);
display: flex;
justify-content: center;
align-items: center;
}
/* 签筒口边缘 */
.shaker-body::before {
content: "";
position: absolute;
top: -10px;
left: -4px;
right: -4px;
height: 20px;
border: 4px solid #b8860b;
border-radius: 50%;
background: #400000; /* 内部阴影 */
z-index: -1;
box-shadow: inset 0 5px 10px rgba(0, 0, 0, 0.8);
}
.label-box {
width: 60px;
height: 100px;
background: #800000;
border: 2px solid #ffd700;
display: flex;
justify-content: center;
align-items: center;
border-radius: 6px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
.label-text {
font-size: 28px;
color: #ffd700;
writing-mode: vertical-rl;
font-weight: bold;
letter-spacing: 6px;
font-family: serif;
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.5);
}
.sticks {
position: absolute;
top: -40px;
left: 50%;
transform: translateX(-50%);
width: 120px;
height: 60px;
display: flex;
justify-content: center;
align-items: flex-end;
z-index: -2;
}
.stick {
width: 14px;
height: 90px;
background: linear-gradient(90deg, #daa520 0%, #ffd700 50%, #daa520 100%);
margin: 0 2px;
border-radius: 4px 4px 0 0;
border: 1px solid #b8860b;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.s2 {
height: 110px;
}
.s3 {
height: 90px;
background: linear-gradient(90deg, #b8860b 0%, #ffd700 50%, #b8860b 100%);
}
.s4 {
height: 100px;
}
.shaking {
animation: shake 0.5s cubic-bezier(0.36, 0.07, 0.19, 0.97) both infinite;
}
@keyframes shake {
10%,
90% {
transform: translate3d(-1px, 0, 0) rotate(-1deg);
}
20%,
80% {
transform: translate3d(2px, 0, 0) rotate(2deg);
}
30%,
50%,
70% {
transform: translate3d(-4px, 0, 0) rotate(-4deg);
}
40%,
60% {
transform: translate3d(4px, 0, 0) rotate(4deg);
}
}
.action-btn {
width: 260px;
height: 56px;
line-height: 56px;
background: linear-gradient(180deg, #ffec8b 0%, #ffd700 40%, #daa520 100%);
border-radius: 28px;
color: #590000;
font-size: 20px;
font-weight: bold;
box-shadow:
0 6px 0 #b8860b,
0 15px 20px rgba(0, 0, 0, 0.4);
transition: all 0.1s;
border: none;
}
.action-btn:active {
transform: translateY(4px);
box-shadow:
0 2px 0 #b8860b,
0 5px 10px rgba(0, 0, 0, 0.4);
}
.action-btn[disabled] {
opacity: 0.8;
transform: none;
}
.footer-info {
margin-top: 30px;
font-size: 13px;
color: rgba(255, 228, 196, 0.8);
display: flex;
align-items: center;
background: rgba(0, 0, 0, 0.2);
padding: 8px 16px;
border-radius: 20px;
}
.info-icon {
margin-right: 6px;
font-size: 15px;
}
/* 结果状态 */
.state-result {
width: 100%;
padding: 130px 30px 20px;
animation: fadeIn 0.8s ease-out;
position: relative;
z-index: 1;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.result-card {
background: #fffbf0;
border-radius: 12px;
padding: 24px;
position: relative;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.4);
margin-bottom: 30px;
border: 1px solid #e6caa0;
}
/* 增加卡片内边框纹理 */
.result-card:not(.image-mode)::after {
content: "";
position: absolute;
top: 6px;
left: 6px;
right: 6px;
bottom: 6px;
border: 1px solid #d4af37;
border-radius: 8px;
pointer-events: none;
}
.result-card.image-mode {
padding: 0;
overflow: hidden;
background: transparent;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
border: none;
}
.result-card.image-mode::after {
display: none;
}
.fortune-image {
width: 100%;
display: block;
border-radius: 12px;
}
.card-header {
text-align: center;
margin-bottom: 24px;
}
.year-tag {
background: linear-gradient(90deg, #e63946 0%, #d62828 100%);
color: #fff;
padding: 6px 20px;
border-radius: 20px;
font-size: 14px;
font-weight: bold;
box-shadow: 0 4px 10px rgba(214, 40, 40, 0.3);
letter-spacing: 1px;
}
.card-body {
text-align: center;
margin-bottom: 40px;
}
.icon-circle {
width: 90px;
height: 90px;
background: rgba(230, 57, 70, 0.08);
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
margin: 0 auto 20px;
border: 1px solid rgba(230, 57, 70, 0.2);
}
.result-icon {
font-size: 48px;
}
.result-title {
font-size: 40px;
color: #c0392b;
font-weight: bold;
display: block;
margin-bottom: 16px;
font-family: serif;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.divider {
width: 60px;
height: 3px;
background: linear-gradient(90deg, transparent, #d4af37, transparent);
margin: 0 auto 20px;
}
.result-desc {
font-size: 18px;
color: #333;
display: block;
margin-bottom: 16px;
line-height: 1.6;
font-weight: 500;
}
.result-sub {
font-size: 14px;
color: #888;
font-style: italic;
}
.card-footer {
border-top: 1px dashed #e0e0e0;
padding-top: 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.footer-left {
display: flex;
flex-direction: column;
}
.sub-en {
font-size: 10px;
color: #aaa;
letter-spacing: 2px;
margin-bottom: 2px;
}
.sub-cn {
font-size: 16px;
font-weight: bold;
color: #555;
font-family: serif;
}
.scan-tip {
font-size: 10px;
color: #999;
text-align: right;
margin-right: 10px;
line-height: 1.4;
}
.qr-code {
width: 48px;
height: 48px;
background: #eee;
background-image: url("https://file.lihailezzc.com/resource/qr-placeholder.png");
background-size: cover;
border-radius: 4px;
}
.limit-tip {
text-align: center;
color: rgba(255, 255, 255, 0.8);
font-size: 13px;
margin-bottom: 16px;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
}
.share-btn {
background: linear-gradient(90deg, #e63946 0%, #d62828 100%);
color: #fff;
border-radius: 25px;
font-size: 16px;
font-weight: bold;
margin-bottom: 16px;
display: flex;
justify-content: center;
align-items: center;
height: 48px;
box-shadow: 0 4px 12px rgba(214, 40, 40, 0.4);
border: none;
}
.secondary-btns {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
}
.sec-btn {
width: 48%;
height: 44px;
background: rgba(255, 255, 255, 0.9);
color: #8b0000;
font-size: 14px;
font-weight: 600;
border-radius: 22px;
display: flex;
justify-content: center;
align-items: center;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
border: none;
}
.footer-status {
text-align: center;
font-size: 12px;
color: rgba(255, 215, 0, 0.8);
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
}
.icon {
margin-right: 6px;
}
</style>