fix: lucky page

This commit is contained in:
zzc
2026-02-28 18:27:21 +08:00
parent fd5bc9d12c
commit 3f623ee6ee

View File

@@ -23,17 +23,39 @@
<view class="lucky-card" id="lucky-card"> <view class="lucky-card" id="lucky-card">
<!-- 头部渐变区 --> <!-- 头部渐变区 -->
<view class="card-header"> <view class="card-header">
<view class="header-decor left"></view> <!-- Top Bar: User Info & Date -->
<view class="header-decor right"></view> <view class="header-top-bar">
<view
<text class="header-label">今日好运指数</text> class="user-info-row"
<view class="score-wrap"> v-if="userInfo && (userInfo.avatarUrl || userInfo.nickName)"
<text class="score">{{ resultData.score }}</text> >
<text class="percent">%</text> <image
class="user-avatar"
:src="
userInfo.avatarUrl || '/static/images/default-avatar.png'
"
mode="aspectFill"
/>
<text class="user-name">{{
userInfo.nickName || "好运用户"
}}</text>
</view>
<view class="date-tag">{{ currentDateStr }}</view>
</view> </view>
<text class="lucky-word">{{ resultData.luckyWord }}</text>
<view class="tag-year">{{ currentDateStr }}</view> <!-- Main Content: Score & Word -->
<view class="header-main">
<text class="header-label">今日好运指数</text>
<view class="score-wrap">
<text class="score">{{ resultData.score }}</text>
<text class="percent">%</text>
</view>
<text class="lucky-word">{{ resultData.luckyWord }}</text>
</view>
<!-- Decorators -->
<view class="header-decor left-bottom"></view>
<view class="header-decor right-bottom"></view>
</view> </view>
<!-- 内容区 --> <!-- 内容区 -->
@@ -122,10 +144,13 @@
</template> </template>
<script setup> <script setup>
import { ref, getCurrentInstance } from "vue"; import { ref, getCurrentInstance, computed } from "vue";
import calendar from "@/utils/lunar.js"; import calendar from "@/utils/lunar.js";
import { useUserStore } from "@/stores/user";
const { proxy } = getCurrentInstance(); const { proxy } = getCurrentInstance();
const userStore = useUserStore();
const userInfo = computed(() => userStore.userInfo);
const popup = ref(null); const popup = ref(null);
const isAnimating = ref(true); const isAnimating = ref(true);
const isFlipping = ref(false); const isFlipping = ref(false);
@@ -135,7 +160,7 @@ const currentDateStr = ref("");
// 画布相关 // 画布相关
const canvasWidth = ref(600); const canvasWidth = ref(600);
const canvasHeight = ref(1000); const canvasHeight = ref(1100);
const resultData = ref({ const resultData = ref({
score: 88, score: 88,
@@ -204,63 +229,125 @@ const startAnimation = () => {
}, 1800); }, 1800);
}; };
const onSaveImage = () => { const onSaveImage = async () => {
uni.showLoading({ title: "生成图片中..." }); uni.showLoading({ title: "生成图片中..." });
let avatarPath = "/static/images/default-avatar.png"; // Default or fallback
if (userInfo.value && userInfo.value.avatarUrl) {
// Basic check for remote URL
if (
userInfo.value.avatarUrl.startsWith("http") ||
userInfo.value.avatarUrl.startsWith("//")
) {
try {
const [err, res] = await uni.downloadFile({
url: userInfo.value.avatarUrl,
});
if (!err && res.statusCode === 200) {
avatarPath = res.tempFilePath;
}
} catch (e) {
console.error("Avatar download failed", e);
}
} else {
avatarPath = userInfo.value.avatarUrl;
}
}
const ctx = uni.createCanvasContext("luckyCanvas", proxy); const ctx = uni.createCanvasContext("luckyCanvas", proxy);
const W = canvasWidth.value; const W = canvasWidth.value;
const H = canvasHeight.value; const H = canvasHeight.value;
const cardH = 850; // 卡片主体高度
// 1. 绘制背景 // 1. 绘制背景
ctx.setFillStyle("#ffffff"); ctx.setFillStyle("#ffffff");
ctx.fillRect(0, 0, W, H); ctx.fillRect(0, 0, W, H);
// 2. 绘制卡片头部渐变 // 2. 绘制卡片头部渐变
const grd = ctx.createLinearGradient(0, 0, 0, 360); const headerH = 460;
const grd = ctx.createLinearGradient(0, 0, 0, headerH);
grd.addColorStop(0, "#d84315"); grd.addColorStop(0, "#d84315");
grd.addColorStop(1, "#ffca28"); grd.addColorStop(1, "#ffca28");
ctx.setFillStyle(grd); ctx.setFillStyle(grd);
ctx.fillRect(0, 0, W, 360); ctx.fillRect(0, 0, W, headerH);
// 3. 头部装饰文字 // --- Top Bar ---
ctx.setFillStyle("rgba(255, 255, 255, 0.6)"); const topY = 40;
ctx.setFontSize(24); const avatarSize = 64;
ctx.fillText("福", 30, 40);
ctx.fillText("禧", W - 50, 40);
// 4. 头部内容 // 绘制用户头像 (Top Left)
ctx.save();
ctx.beginPath();
ctx.arc(
40 + avatarSize / 2,
topY + avatarSize / 2,
avatarSize / 2,
0,
2 * Math.PI,
);
ctx.clip();
ctx.drawImage(avatarPath, 40, topY, avatarSize, avatarSize);
ctx.restore();
// 绘制用户昵称
ctx.setTextAlign("left");
ctx.setFillStyle("#ffffff");
ctx.setFontSize(26);
ctx.font = "bold 26px sans-serif";
ctx.fillText(
userInfo.value?.nickName || "好运用户",
40 + avatarSize + 16,
topY + 42,
);
// 绘制日期 (Top Right)
const dateStr = currentDateStr.value || "2026 CNY SPECIAL";
ctx.setFontSize(22);
ctx.font = "normal 22px sans-serif";
const dateWidth = ctx.measureText(dateStr).width + 30;
const dateX = W - 40 - dateWidth;
const dateY = topY + 12; // box top
// date bg
ctx.setFillStyle("rgba(255, 255, 255, 0.2)");
roundRect(ctx, dateX, dateY, dateWidth, 40, 20);
ctx.fill();
// date text
ctx.setFillStyle("#ffffff");
ctx.fillText(dateStr, dateX + 15, dateY + 28);
// --- Main Content (Centered) ---
const centerX = W / 2;
// Label
ctx.setTextAlign("center"); ctx.setTextAlign("center");
ctx.setFillStyle("rgba(255, 255, 255, 0.9)"); ctx.setFillStyle("rgba(255, 255, 255, 0.9)");
ctx.setFontSize(24); ctx.setFontSize(24);
ctx.fillText("今日好运指数", W / 2, 80); ctx.font = "normal 24px sans-serif";
ctx.fillText("今日好运指数", centerX, 180);
// 分数 // Score
ctx.setFillStyle("#ffffff"); ctx.setFillStyle("#ffffff");
ctx.setFontSize(120); ctx.setFontSize(140);
ctx.font = "bold 120px sans-serif"; ctx.font = "bold 140px sans-serif";
ctx.fillText(resultData.value.score + "%", W / 2, 200); ctx.fillText(resultData.value.score + "%", centerX, 310);
// 幸运词 // Lucky Word
ctx.setFontSize(48); ctx.setFontSize(52);
ctx.font = "bold 48px sans-serif"; ctx.font = "bold 52px sans-serif";
ctx.fillText(resultData.value.luckyWord, W / 2, 280); ctx.fillText(resultData.value.luckyWord, centerX, 390);
// 日期标签背景 // Decorators (Bottom Corners)
const dateStr = currentDateStr.value || "2026 CNY SPECIAL"; ctx.setFillStyle("rgba(255, 255, 255, 0.4)");
ctx.setFillStyle("rgba(0, 0, 0, 0.15)"); ctx.setFontSize(24);
const dateWidth = ctx.measureText(dateStr).width + 40; ctx.font = "normal 24px sans-serif";
roundRect(ctx, W / 2 - dateWidth / 2, 310, dateWidth, 34, 17); ctx.setTextAlign("left");
ctx.fill(); ctx.fillText("福", 40, headerH - 20);
ctx.setTextAlign("right");
// 日期文字 ctx.fillText("禧", W - 40, headerH - 20);
ctx.setFillStyle("#ffffff");
ctx.setFontSize(20);
ctx.fillText(dateStr, W / 2, 334);
// 5. 绘制内容区 (宜/忌) // 5. 绘制内容区 (宜/忌)
const gridY = 400; const gridY = 500;
const boxW = (W - 64 - 24) / 2; // (600 - padding*2 - gap)/2 const boxW = (W - 64 - 24) / 2;
const gridH = 140; // 增加高度防止内容溢出 const gridH = 140;
// 宜 // 宜
drawBox(ctx, 32, gridY, boxW, gridH, "#fbfbfb", "#f5f5f5"); drawBox(ctx, 32, gridY, boxW, gridH, "#fbfbfb", "#f5f5f5");
@@ -268,7 +355,6 @@ const onSaveImage = () => {
ctx.setFontSize(24); ctx.setFontSize(24);
ctx.setFillStyle("#d81e06"); ctx.setFillStyle("#d81e06");
ctx.font = "bold 24px sans-serif"; ctx.font = "bold 24px sans-serif";
// 图标模拟
ctx.fillText("✔ 今日宜", 56, gridY + 44); ctx.fillText("✔ 今日宜", 56, gridY + 44);
ctx.setFontSize(22); ctx.setFontSize(22);
@@ -295,8 +381,8 @@ const onSaveImage = () => {
); );
// 6. 幸运元素 // 6. 幸运元素
const elY = 570; // 下移,避免与上面重叠 const elY = 670;
const elH = 160; // 增加高度 const elH = 160;
drawBox(ctx, 32, elY, W - 64, elH, "#fbfbfb", "#f5f5f5"); drawBox(ctx, 32, elY, W - 64, elH, "#fbfbfb", "#f5f5f5");
// 标题 // 标题
@@ -306,15 +392,14 @@ const onSaveImage = () => {
ctx.fillText("★ 幸运元素", 56, elY + 46); ctx.fillText("★ 幸运元素", 56, elY + 46);
// 元素内容 // 元素内容
const contentW = W - 64; // 内容区域总宽度 const contentW = W - 64;
const colW = contentW / 3; // 三等分 const colW = contentW / 3;
const startX = 32; // 起始X坐标 const startX = 32;
// 调整Y坐标确保不重叠
const labelY = elY + 90; const labelY = elY + 90;
const valY = elY + 126; const valY = elY + 126;
// 颜色 (第一列) // 颜色
ctx.setTextAlign("center"); ctx.setTextAlign("center");
ctx.setFontSize(20); ctx.setFontSize(20);
ctx.setFillStyle("#999999"); ctx.setFillStyle("#999999");
@@ -326,7 +411,7 @@ const onSaveImage = () => {
ctx.font = "bold 26px sans-serif"; ctx.font = "bold 26px sans-serif";
ctx.fillText(resultData.value.luckyColor, startX + colW * 0.5, valY); ctx.fillText(resultData.value.luckyColor, startX + colW * 0.5, valY);
// 数字 (第二列) // 数字
ctx.setFontSize(20); ctx.setFontSize(20);
ctx.setFillStyle("#999999"); ctx.setFillStyle("#999999");
ctx.font = "normal 20px sans-serif"; ctx.font = "normal 20px sans-serif";
@@ -337,7 +422,7 @@ const onSaveImage = () => {
ctx.font = "bold 26px sans-serif"; ctx.font = "bold 26px sans-serif";
ctx.fillText(resultData.value.luckyNumber, startX + colW * 1.5, valY); ctx.fillText(resultData.value.luckyNumber, startX + colW * 1.5, valY);
// 方向 (第三列) // 方向
ctx.setFontSize(20); ctx.setFontSize(20);
ctx.setFillStyle("#999999"); ctx.setFillStyle("#999999");
ctx.font = "normal 20px sans-serif"; ctx.font = "normal 20px sans-serif";
@@ -348,7 +433,7 @@ const onSaveImage = () => {
ctx.font = "bold 26px sans-serif"; ctx.font = "bold 26px sans-serif";
ctx.fillText(resultData.value.luckyDirection, startX + colW * 2.5, valY); ctx.fillText(resultData.value.luckyDirection, startX + colW * 2.5, valY);
// 分隔线 1 // 分隔线
ctx.setStrokeStyle("#eeeeee"); ctx.setStrokeStyle("#eeeeee");
ctx.setLineWidth(2); ctx.setLineWidth(2);
ctx.beginPath(); ctx.beginPath();
@@ -358,7 +443,6 @@ const onSaveImage = () => {
ctx.lineTo(startX + colW, lineBottom); ctx.lineTo(startX + colW, lineBottom);
ctx.stroke(); ctx.stroke();
// 分隔线 2
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(startX + colW * 2, lineTop); ctx.moveTo(startX + colW * 2, lineTop);
ctx.lineTo(startX + colW * 2, lineBottom); ctx.lineTo(startX + colW * 2, lineBottom);
@@ -369,11 +453,10 @@ const onSaveImage = () => {
ctx.setFontSize(22); ctx.setFontSize(22);
ctx.setFillStyle("#999999"); ctx.setFillStyle("#999999");
ctx.font = "italic 22px sans-serif"; ctx.font = "italic 22px sans-serif";
// 语录换行处理,下移坐标 wrapTextCentered(ctx, `${resultData.value.quote}`, W / 2, 880, W - 80, 30);
wrapTextCentered(ctx, `${resultData.value.quote}`, W / 2, 780, W - 80, 30);
// 8. 底部区域 (Footer) // 8. 底部区域 (Footer)
const footerY = 850; const footerY = 960;
// 分隔线 // 分隔线
ctx.setStrokeStyle("#f0f0f0"); ctx.setStrokeStyle("#f0f0f0");
@@ -395,9 +478,7 @@ const onSaveImage = () => {
ctx.font = "normal 20px sans-serif"; ctx.font = "normal 20px sans-serif";
ctx.fillText("2026 CNY SPECIAL · 新春助手", 40, footerY + 100); ctx.fillText("2026 CNY SPECIAL · 新春助手", 40, footerY + 100);
// 底部右侧二维码 (占位图) // 底部右侧二维码
// 假设二维码在 static/icon/yunshi.png 或 logo.png
// 实际开发中应替换为小程序码
ctx.drawImage("/static/logo.png", W - 140, footerY + 25, 100, 100); ctx.drawImage("/static/logo.png", W - 140, footerY + 25, 100, 100);
// 绘制 // 绘制
@@ -506,7 +587,7 @@ defineExpose({ open, close });
/* 动画容器 */ /* 动画容器 */
.animation-container { .animation-container {
width: 600rpx; width: 600rpx;
height: 850rpx; height: 920rpx; /* Updated height */
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
@@ -616,77 +697,115 @@ defineExpose({ open, close });
.lucky-card { .lucky-card {
width: 100%; width: 100%;
height: 850rpx; height: 920rpx; /* Increased height */
background: #fff; background: #fff;
border-radius: 40rpx; border-radius: 40rpx;
overflow: hidden; overflow: hidden;
margin-bottom: 40rpx; margin-bottom: 40rpx;
.card-header { .card-header {
height: 360rpx; height: 460rpx; /* Increased height */
background: linear-gradient(180deg, #d84315 0%, #ffca28 100%); background: linear-gradient(180deg, #d84315 0%, #ffca28 100%);
position: relative; position: relative;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; padding: 30rpx 40rpx;
justify-content: center; box-sizing: border-box;
color: #fff; color: #fff;
.header-top-bar {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
margin-bottom: 20rpx;
.user-info-row {
display: flex;
align-items: center;
.user-avatar {
width: 64rpx;
height: 64rpx;
border-radius: 50%;
border: 2rpx solid rgba(255, 255, 255, 0.8);
margin-right: 16rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
}
.user-name {
font-size: 26rpx;
font-weight: bold;
color: #fff;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1);
}
}
.date-tag {
background: rgba(255, 255, 255, 0.2);
padding: 8rpx 20rpx;
border-radius: 30rpx;
font-size: 22rpx;
color: #fff;
letter-spacing: 1rpx;
}
}
.header-main {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.header-label {
font-size: 24rpx;
margin-bottom: 8rpx;
opacity: 0.9;
letter-spacing: 2rpx;
}
.score-wrap {
display: flex;
align-items: baseline;
line-height: 1;
margin-bottom: 16rpx;
.score {
font-size: 140rpx; /* Bigger */
font-weight: bold;
text-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.15);
}
.percent {
font-size: 40rpx;
margin-left: 8rpx;
font-weight: 500;
opacity: 0.9;
}
}
.lucky-word {
font-size: 52rpx;
font-weight: bold;
letter-spacing: 6rpx;
text-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
}
}
.header-decor { .header-decor {
position: absolute; position: absolute;
top: 20rpx; bottom: 20rpx;
font-size: 24rpx; font-size: 24rpx;
opacity: 0.6; opacity: 0.4;
&.left { &.left-bottom {
left: 30rpx; left: 40rpx;
} }
&.right { &.right-bottom {
right: 30rpx; right: 40rpx;
} }
} }
.header-label {
font-size: 24rpx;
margin-bottom: 16rpx;
opacity: 0.9;
letter-spacing: 2rpx;
}
.score-wrap {
display: flex;
align-items: baseline;
line-height: 1;
margin-bottom: 16rpx;
.score {
font-size: 120rpx;
font-weight: bold;
text-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
}
.percent {
font-size: 40rpx;
margin-left: 4rpx;
font-weight: 500;
}
}
.lucky-word {
font-size: 48rpx;
font-weight: bold;
letter-spacing: 4rpx;
text-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
margin-bottom: 32rpx;
}
.tag-year {
background: rgba(0, 0, 0, 0.15);
padding: 8rpx 24rpx;
border-radius: 30rpx;
font-size: 20rpx;
letter-spacing: 2rpx;
}
} }
.card-body { .card-body {