Files
spring-festival-greetings/pages/fortune/index.vue
2026-01-16 15:09:19 +08:00

550 lines
12 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" :style="{ paddingTop: getBavBarHeight() + 'px' }">
<!-- 顶部导航 -->
<view class="nav-bar">
<view class="back-btn" @tap="goBack"></view>
<text class="nav-title">2026 新年运势</text>
</view>
<!-- 初始状态签筒 -->
<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 class="footer-info">
<text class="info-icon"></text>
<text>今日还有 {{ remainingCount }} 次抽取机会分享可增加次数</text>
</view>
</view>
<!-- 结果状态运势卡片 -->
<view class="state-result" v-else>
<view class="result-card" id="result-card">
<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>
</view>
<view class="result-actions">
<view class="limit-tip"> 每日仅限一次抽取</view>
<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="reset">
<text class="icon"></text> 我的记录
</button>
</view>
<view class="footer-status">
已分享 0/3 · 今日剩余机会: {{ remainingCount }}
</view>
</view>
</view>
<!-- Canvas 用于生成图片 (隐藏) -->
<canvas
canvas-id="shareCanvas"
class="share-canvas"
style="width: 300px; height: 500px; position: fixed; left: 9999px"
></canvas>
</view>
</template>
<script setup>
import { ref, onUnmounted } from "vue";
import { getBavBarHeight } from "@/utils/system";
const status = ref("initial"); // initial, shaking, result
const remainingCount = ref(1);
// 音效控制
const audioContext = uni.createInnerAudioContext();
audioContext.src = "/static/music/shake.mp3";
let playCount = 0;
audioContext.onEnded(() => {
playCount++;
if (playCount < 3) {
audioContext.play();
}
});
onUnmounted(() => {
audioContext.destroy();
});
const fortunes = [
{ title: "好运连连", desc: "2026年你将万事如意惊喜不断。", icon: "☀" },
{ title: "财源滚滚", desc: "正财偏财滚滚来,荷包满满乐开怀。", icon: "💰" },
{ title: "事业有成", desc: "职场顺风又顺水,升职加薪在眼前。", icon: "🚀" },
{ title: "身体健康", desc: "无病无灾身体棒,吃嘛嘛香精神爽。", icon: "💪" },
{ title: "桃花朵朵", desc: "单身贵族遇良缘,花前月下共缠绵。", icon: "🌸" },
];
const currentFortune = ref(fortunes[0]);
const goBack = () => {
if (status.value === "result") {
status.value = "initial";
} else {
uni.navigateBack();
}
};
const startShake = () => {
if (remainingCount.value <= 0) {
uni.showToast({ title: "今日次数已用完", icon: "none" });
return;
}
status.value = "shaking";
// 播放音效
playCount = 0;
audioContext.play();
// 模拟摇晃动画和数据请求
setTimeout(() => {
const idx = Math.floor(Math.random() * fortunes.length);
currentFortune.value = fortunes[idx];
status.value = "result";
remainingCount.value--;
}, 2000);
};
const reset = () => {
status.value = "initial";
};
const saveCard = () => {
uni.showLoading({ title: "生成中..." });
const ctx = uni.createCanvasContext("shareCanvas");
// 绘制背景
ctx.setFillStyle("#FFF8F0");
ctx.fillRect(0, 0, 300, 500);
// 绘制边框
ctx.setStrokeStyle("#D4AF37");
ctx.setLineWidth(2);
ctx.strokeRect(10, 10, 280, 480);
// 绘制内容
ctx.setFillStyle("#CC0000");
ctx.setFontSize(24);
ctx.setTextAlign("center");
ctx.fillText(currentFortune.value.title, 150, 100);
ctx.setFillStyle("#333333");
ctx.setFontSize(14);
ctx.fillText(currentFortune.value.desc, 150, 150);
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-color: #590000; /* 深红背景 */
background-image: linear-gradient(180deg, #590000 0%, #3d0000 100%);
color: #ffe4c4;
display: flex;
flex-direction: column;
align-items: center;
position: relative;
overflow: hidden;
}
/* 导航栏 */
.nav-bar {
width: 100%;
height: 44px;
display: flex;
align-items: center;
padding: 0 16px;
z-index: 100;
}
.back-btn {
font-size: 32px;
color: #ffe4c4;
margin-right: 12px;
line-height: 1;
}
.nav-title {
font-size: 18px;
font-weight: 600;
color: #ffe4c4;
}
/* 初始状态 */
.state-initial {
width: 100%;
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding-bottom: 80px;
}
.header-text {
text-align: center;
margin-bottom: 40px;
}
.title {
font-size: 20px;
color: #ffd700;
letter-spacing: 1px;
}
.year {
font-size: 24px;
font-weight: bold;
color: #ffd700;
margin: 0 4px;
text-decoration: underline;
text-decoration-color: #d4af37;
text-underline-offset: 4px;
}
.underline {
width: 40px;
height: 2px;
background: #d4af37;
margin: 8px auto 0;
}
/* 签筒动画 */
.shaker-container {
margin-bottom: 60px;
position: relative;
}
.shaker-body {
width: 160px;
height: 240px;
background: linear-gradient(135deg, #8b0000 0%, #500000 100%);
border-radius: 20px;
border: 2px solid #d4af37;
position: relative;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
}
.label-box {
width: 60px;
height: 100px;
border: 1px solid #d4af37;
display: flex;
justify-content: center;
align-items: center;
border-radius: 4px;
}
.label-text {
font-size: 24px;
color: #ffd700;
writing-mode: vertical-rl;
font-weight: bold;
letter-spacing: 4px;
}
.sticks {
position: absolute;
top: -40px;
left: 50%;
transform: translateX(-50%);
width: 100px;
height: 60px;
display: flex;
justify-content: center;
align-items: flex-end;
}
.stick {
width: 12px;
height: 80px;
background: #daa520;
margin: 0 2px;
border-radius: 4px 4px 0 0;
border: 1px solid #b8860b;
}
.s2 {
height: 90px;
}
.s3 {
height: 70px;
background: #ffd700;
}
.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: 240px;
height: 50px;
line-height: 50px;
background: linear-gradient(90deg, #d4af37 0%, #ffd700 100%);
border-radius: 25px;
color: #590000;
font-size: 18px;
font-weight: bold;
box-shadow: 0 4px 12px rgba(212, 175, 55, 0.4);
}
.footer-info {
margin-top: 20px;
font-size: 12px;
color: rgba(255, 228, 196, 0.6);
display: flex;
align-items: center;
}
.info-icon {
margin-right: 4px;
font-size: 14px;
}
/* 结果状态 */
.state-result {
width: 100%;
padding: 20px 30px;
animation: fadeIn 0.8s ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.result-card {
background: #fffbf0;
border-radius: 16px;
padding: 20px;
position: relative;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
margin-bottom: 30px;
}
.card-header {
text-align: center;
margin-bottom: 30px;
}
.year-tag {
background: #e63946;
color: #fff;
padding: 4px 16px;
border-radius: 20px;
font-size: 14px;
}
.card-body {
text-align: center;
margin-bottom: 40px;
}
.icon-circle {
width: 80px;
height: 80px;
background: rgba(230, 57, 70, 0.1);
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
margin: 0 auto 20px;
}
.result-icon {
font-size: 40px;
color: #e63946;
}
.result-title {
font-size: 36px;
color: #e63946;
font-weight: bold;
display: block;
margin-bottom: 16px;
}
.divider {
width: 40px;
height: 2px;
background: #d4af37;
margin: 0 auto 20px;
}
.result-desc {
font-size: 16px;
color: #333;
display: block;
margin-bottom: 12px;
}
.result-sub {
font-size: 14px;
color: #666;
}
.card-footer {
border-top: 1px dashed #d4af37;
padding-top: 16px;
display: flex;
justify-content: space-between;
align-items: center;
}
.footer-left {
display: flex;
flex-direction: column;
}
.sub-en {
font-size: 10px;
color: #999;
letter-spacing: 1px;
}
.sub-cn {
font-size: 14px;
font-weight: bold;
color: #333;
}
.footer-right {
display: flex;
align-items: center;
}
.scan-tip {
font-size: 10px;
color: #999;
text-align: right;
margin-right: 8px;
}
.qr-code {
width: 40px;
height: 40px;
background: #ddd; /* 占位 */
background-image: url("https://file.lihailezzc.com/resource/qr-placeholder.png"); /* 替换为真实二维码 */
background-size: cover;
}
.limit-tip {
text-align: center;
color: rgba(255, 255, 255, 0.6);
font-size: 12px;
margin-bottom: 12px;
}
.share-btn {
background: #e63946;
color: #fff;
border-radius: 25px;
font-size: 16px;
margin-bottom: 16px;
display: flex;
justify-content: center;
align-items: center;
}
.secondary-btns {
display: flex;
justify-content: space-between;
margin-bottom: 16px;
}
.sec-btn {
width: 48%;
background: #fff;
color: #333;
font-size: 14px;
border-radius: 12px;
display: flex;
justify-content: center;
align-items: center;
}
.footer-status {
text-align: center;
font-size: 12px;
color: #d4af37;
}
.icon {
margin-right: 6px;
}
</style>