fix: lucky page

This commit is contained in:
zzc
2026-02-28 18:45:35 +08:00
parent 3f623ee6ee
commit dc2be76648
2 changed files with 272 additions and 245 deletions

View File

@@ -135,7 +135,7 @@
<!-- 画布用于生成图片 --> <!-- 画布用于生成图片 -->
<canvas <canvas
canvas-id="luckyCanvas" type="2d"
id="luckyCanvas" id="luckyCanvas"
class="lucky-canvas" class="lucky-canvas"
:style="{ width: canvasWidth + 'px', height: canvasHeight + 'px' }" :style="{ width: canvasWidth + 'px', height: canvasHeight + 'px' }"
@@ -232,284 +232,311 @@ const startAnimation = () => {
const onSaveImage = async () => { const onSaveImage = async () => {
uni.showLoading({ title: "生成图片中..." }); uni.showLoading({ title: "生成图片中..." });
let avatarPath = "/static/images/default-avatar.png"; // Default or fallback const query = uni.createSelectorQuery().in(proxy);
if (userInfo.value && userInfo.value.avatarUrl) { query
// Basic check for remote URL .select("#luckyCanvas")
if ( .fields({ node: true, size: true })
userInfo.value.avatarUrl.startsWith("http") || .exec(async (res) => {
userInfo.value.avatarUrl.startsWith("//") if (!res[0] || !res[0].node) {
) { uni.hideLoading();
try { uni.showToast({ title: "Canvas not found", icon: "none" });
const [err, res] = await uni.downloadFile({ return;
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 canvas = res[0].node;
const W = canvasWidth.value; const ctx = canvas.getContext("2d");
const H = canvasHeight.value; const dpr = uni.getSystemInfoSync().pixelRatio || 2;
// 1. 绘制背景 // Set canvas size (physical pixels)
ctx.setFillStyle("#ffffff"); canvas.width = res[0].width * dpr;
ctx.fillRect(0, 0, W, H); canvas.height = res[0].height * dpr;
// 2. 绘制卡片头部渐变 // Reset transform
const headerH = 460; ctx.setTransform(1, 0, 0, 1, 0, 0);
const grd = ctx.createLinearGradient(0, 0, 0, headerH); ctx.clearRect(0, 0, canvas.width, canvas.height);
grd.addColorStop(0, "#d84315");
grd.addColorStop(1, "#ffca28");
ctx.setFillStyle(grd);
ctx.fillRect(0, 0, W, headerH);
// --- Top Bar --- // Scale context
const topY = 40; ctx.scale(dpr, dpr);
const avatarSize = 64;
// 绘制用户头像 (Top Left) const W = res[0].width;
ctx.save(); const H = res[0].height;
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();
// 绘制用户昵称 // Helper function to load image
ctx.setTextAlign("left"); const loadCanvasImage = (url) => {
ctx.setFillStyle("#ffffff"); return new Promise((resolve, reject) => {
ctx.setFontSize(26); const img = canvas.createImage();
ctx.font = "bold 26px sans-serif"; img.onload = () => resolve(img);
ctx.fillText( img.onerror = (e) => reject(e);
userInfo.value?.nickName || "好运用户", img.src = url;
40 + avatarSize + 16, });
topY + 42, };
);
// 绘制日期 (Top Right) try {
const dateStr = currentDateStr.value || "2026 CNY SPECIAL"; // Load images
ctx.setFontSize(22); let avatarUrl = "/static/images/default-avatar.png";
ctx.font = "normal 22px sans-serif"; if (userInfo.value && userInfo.value.avatarUrl) {
const dateWidth = ctx.measureText(dateStr).width + 30; avatarUrl = userInfo.value.avatarUrl;
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 [avatarImg, qrCodeImg] = await Promise.all([
const centerX = W / 2; loadCanvasImage(avatarUrl).catch(() =>
loadCanvasImage("/static/images/default-avatar.png"),
),
loadCanvasImage("/static/images/qrcode.jpg").catch(() => null),
]);
// Label // 1. 绘制背景
ctx.setTextAlign("center"); ctx.fillStyle = "#ffffff";
ctx.setFillStyle("rgba(255, 255, 255, 0.9)"); ctx.fillRect(0, 0, W, H);
ctx.setFontSize(24);
ctx.font = "normal 24px sans-serif";
ctx.fillText("今日好运指数", centerX, 180);
// Score // 2. 绘制卡片头部渐变
ctx.setFillStyle("#ffffff"); const headerH = 460;
ctx.setFontSize(140); const grd = ctx.createLinearGradient(0, 0, 0, headerH);
ctx.font = "bold 140px sans-serif"; grd.addColorStop(0, "#d84315");
ctx.fillText(resultData.value.score + "%", centerX, 310); grd.addColorStop(1, "#ffca28");
ctx.fillStyle = grd;
ctx.fillRect(0, 0, W, headerH);
// Lucky Word // --- Top Bar ---
ctx.setFontSize(52); const topY = 40;
ctx.font = "bold 52px sans-serif"; const avatarSize = 64;
ctx.fillText(resultData.value.luckyWord, centerX, 390);
// Decorators (Bottom Corners) // 绘制用户头像 (Top Left)
ctx.setFillStyle("rgba(255, 255, 255, 0.4)"); ctx.save();
ctx.setFontSize(24); ctx.beginPath();
ctx.font = "normal 24px sans-serif"; ctx.arc(
ctx.setTextAlign("left"); 40 + avatarSize / 2,
ctx.fillText("福", 40, headerH - 20); topY + avatarSize / 2,
ctx.setTextAlign("right"); avatarSize / 2,
ctx.fillText("禧", W - 40, headerH - 20); 0,
2 * Math.PI,
);
ctx.clip();
if (avatarImg) {
ctx.drawImage(avatarImg, 40, topY, avatarSize, avatarSize);
}
ctx.restore();
// 5. 绘制内容区 (宜/忌) // 绘制用户昵称
const gridY = 500; ctx.textAlign = "left";
const boxW = (W - 64 - 24) / 2; ctx.fillStyle = "#ffffff";
const gridH = 140; ctx.font = "bold 26px sans-serif";
ctx.fillText(
userInfo.value?.nickName || "好运用户",
40 + avatarSize + 16,
topY + 42,
);
// 宜 // 绘制日期 (Top Right)
drawBox(ctx, 32, gridY, boxW, gridH, "#fbfbfb", "#f5f5f5"); const dateStr = currentDateStr.value || "2026 CNY SPECIAL";
ctx.setTextAlign("left"); ctx.font = "normal 22px sans-serif";
ctx.setFontSize(24); const dateWidth = ctx.measureText(dateStr).width + 30;
ctx.setFillStyle("#d81e06"); const dateX = W - 40 - dateWidth;
ctx.font = "bold 24px sans-serif"; const dateY = topY + 12; // box top
ctx.fillText("✔ 今日宜", 56, gridY + 44); // date bg
ctx.fillStyle = "rgba(255, 255, 255, 0.2)";
roundRect(ctx, dateX, dateY, dateWidth, 40, 20);
ctx.fill();
// date text
ctx.fillStyle = "#ffffff";
ctx.fillText(dateStr, dateX + 15, dateY + 28);
ctx.setFontSize(22); // --- Main Content (Centered) ---
ctx.setFillStyle("#666666"); const centerX = W / 2;
ctx.font = "normal 22px sans-serif";
wrapText(ctx, resultData.value.yi, 56, gridY + 80, boxW - 48, 30);
// // Label
drawBox(ctx, 32 + boxW + 24, gridY, boxW, gridH, "#fbfbfb", "#f5f5f5"); ctx.textAlign = "center";
ctx.setFontSize(24); ctx.fillStyle = "rgba(255, 255, 255, 0.9)";
ctx.setFillStyle("#666666"); ctx.font = "normal 24px sans-serif";
ctx.font = "bold 24px sans-serif"; ctx.fillText("今日好运指数", centerX, 180);
ctx.fillText("✖ 今日忌", 32 + boxW + 24 + 24, gridY + 44);
ctx.setFontSize(22); // Score
ctx.font = "normal 22px sans-serif"; ctx.fillStyle = "#ffffff";
wrapText( ctx.font = "bold 140px sans-serif";
ctx, ctx.fillText(resultData.value.score + "%", centerX, 310);
resultData.value.ji,
32 + boxW + 24 + 24,
gridY + 80,
boxW - 48,
30,
);
// 6. 幸运元素 // Lucky Word
const elY = 670; ctx.font = "bold 52px sans-serif";
const elH = 160; ctx.fillText(resultData.value.luckyWord, centerX, 390);
drawBox(ctx, 32, elY, W - 64, elH, "#fbfbfb", "#f5f5f5");
// 标题 // Decorators (Bottom Corners)
ctx.setFontSize(26); ctx.fillStyle = "rgba(255, 255, 255, 0.4)";
ctx.setFillStyle("#333333"); ctx.font = "normal 24px sans-serif";
ctx.font = "bold 26px sans-serif"; ctx.textAlign = "left";
ctx.fillText("★ 幸运元素", 56, elY + 46); ctx.fillText("", 40, headerH - 20);
ctx.textAlign = "right";
ctx.fillText("禧", W - 40, headerH - 20);
// 元素内容 // 5. 绘制内容区 (宜/忌)
const contentW = W - 64; const gridY = 500;
const colW = contentW / 3; const boxW = (W - 64 - 24) / 2;
const startX = 32; const gridH = 140;
const labelY = elY + 90; // 宜
const valY = elY + 126; drawBox(ctx, 32, gridY, boxW, gridH, "#fbfbfb", "#f5f5f5");
ctx.textAlign = "left";
ctx.font = "bold 24px sans-serif";
ctx.fillStyle = "#d81e06";
ctx.fillText("✔ 今日宜", 56, gridY + 44);
// 颜色 ctx.font = "normal 22px sans-serif";
ctx.setTextAlign("center"); ctx.fillStyle = "#666666";
ctx.setFontSize(20); wrapText(ctx, resultData.value.yi, 56, gridY + 80, boxW - 48, 30);
ctx.setFillStyle("#999999");
ctx.font = "normal 20px sans-serif";
ctx.fillText("颜色", startX + colW * 0.5, labelY);
ctx.setFontSize(26); // 忌
ctx.setFillStyle("#d84315"); drawBox(ctx, 32 + boxW + 24, gridY, boxW, gridH, "#fbfbfb", "#f5f5f5");
ctx.font = "bold 26px sans-serif"; ctx.font = "bold 24px sans-serif";
ctx.fillText(resultData.value.luckyColor, startX + colW * 0.5, valY); ctx.fillStyle = "#666666";
ctx.fillText("✖ 今日忌", 32 + boxW + 24 + 24, gridY + 44);
// 数字 ctx.font = "normal 22px sans-serif";
ctx.setFontSize(20); wrapText(
ctx.setFillStyle("#999999"); ctx,
ctx.font = "normal 20px sans-serif"; resultData.value.ji,
ctx.fillText("数字", startX + colW * 1.5, labelY); 32 + boxW + 24 + 24,
gridY + 80,
boxW - 48,
30,
);
ctx.setFontSize(26); // 6. 幸运元素
ctx.setFillStyle("#333333"); const elY = 670;
ctx.font = "bold 26px sans-serif"; const elH = 160;
ctx.fillText(resultData.value.luckyNumber, startX + colW * 1.5, valY); drawBox(ctx, 32, elY, W - 64, elH, "#fbfbfb", "#f5f5f5");
// 方向 // 标题
ctx.setFontSize(20); ctx.font = "bold 26px sans-serif";
ctx.setFillStyle("#999999"); ctx.fillStyle = "#333333";
ctx.font = "normal 20px sans-serif"; ctx.fillText("★ 幸运元素", 56, elY + 46);
ctx.fillText("方向", startX + colW * 2.5, labelY);
ctx.setFontSize(26); // 元素内容
ctx.setFillStyle("#333333"); const contentW = W - 64;
ctx.font = "bold 26px sans-serif"; const colW = contentW / 3;
ctx.fillText(resultData.value.luckyDirection, startX + colW * 2.5, valY); const startX = 32;
// 分隔线 const labelY = elY + 90;
ctx.setStrokeStyle("#eeeeee"); const valY = elY + 126;
ctx.setLineWidth(2);
ctx.beginPath();
const lineTop = elY + 70;
const lineBottom = elY + 130;
ctx.moveTo(startX + colW, lineTop);
ctx.lineTo(startX + colW, lineBottom);
ctx.stroke();
ctx.beginPath(); // 颜色
ctx.moveTo(startX + colW * 2, lineTop); ctx.textAlign = "center";
ctx.lineTo(startX + colW * 2, lineBottom); ctx.font = "normal 20px sans-serif";
ctx.stroke(); ctx.fillStyle = "#999999";
ctx.fillText("颜色", startX + colW * 0.5, labelY);
// 7. 语录 ctx.font = "bold 26px sans-serif";
ctx.setTextAlign("center"); ctx.fillStyle = "#d84315";
ctx.setFontSize(22); ctx.fillText(resultData.value.luckyColor, startX + colW * 0.5, valY);
ctx.setFillStyle("#999999");
ctx.font = "italic 22px sans-serif";
wrapTextCentered(ctx, `${resultData.value.quote}`, W / 2, 880, W - 80, 30);
// 8. 底部区域 (Footer) // 数字
const footerY = 960; ctx.font = "normal 20px sans-serif";
ctx.fillStyle = "#999999";
ctx.fillText("数字", startX + colW * 1.5, labelY);
// 分隔线 ctx.font = "bold 26px sans-serif";
ctx.setStrokeStyle("#f0f0f0"); ctx.fillStyle = "#333333";
ctx.setLineWidth(1); ctx.fillText(resultData.value.luckyNumber, startX + colW * 1.5, valY);
ctx.beginPath();
ctx.moveTo(40, footerY);
ctx.lineTo(W - 40, footerY);
ctx.stroke();
// 底部左侧文字 // 方向
ctx.setTextAlign("left"); ctx.font = "normal 20px sans-serif";
ctx.setFontSize(32); ctx.fillStyle = "#999999";
ctx.setFillStyle("#333333"); ctx.fillText("方向", startX + colW * 2.5, labelY);
ctx.font = "bold 32px sans-serif";
ctx.fillText("扫码开启今日好运", 40, footerY + 60);
ctx.setFontSize(20); ctx.font = "bold 26px sans-serif";
ctx.setFillStyle("#999999"); ctx.fillStyle = "#333333";
ctx.font = "normal 20px sans-serif"; ctx.fillText(
ctx.fillText("2026 CNY SPECIAL · 新春助手", 40, footerY + 100); resultData.value.luckyDirection,
startX + colW * 2.5,
valY,
);
// 底部右侧二维码 // 分隔线
ctx.drawImage("/static/logo.png", W - 140, footerY + 25, 100, 100); ctx.strokeStyle = "#eeeeee";
ctx.lineWidth = 2;
ctx.beginPath();
const lineTop = elY + 70;
const lineBottom = elY + 130;
ctx.moveTo(startX + colW, lineTop);
ctx.lineTo(startX + colW, lineBottom);
ctx.stroke();
// 绘制 ctx.beginPath();
ctx.draw(false, () => { ctx.moveTo(startX + colW * 2, lineTop);
setTimeout(() => { ctx.lineTo(startX + colW * 2, lineBottom);
uni.canvasToTempFilePath( ctx.stroke();
{
canvasId: "luckyCanvas", // 7. 语录
success: (res) => { ctx.textAlign = "center";
uni.saveImageToPhotosAlbum({ ctx.font = "italic 22px sans-serif";
filePath: res.tempFilePath, ctx.fillStyle = "#999999";
success: () => { wrapTextCentered(
uni.hideLoading(); ctx,
uni.showToast({ title: "已保存到相册", icon: "success" }); `${resultData.value.quote}`,
}, W / 2,
fail: () => { 880,
uni.hideLoading(); W - 80,
uni.showToast({ title: "保存失败,请授权", icon: "none" }); 30,
}, );
});
}, // 8. 底部区域 (Footer)
fail: (err) => { const footerY = 960;
uni.hideLoading();
uni.showToast({ title: "生成图片失败", icon: "none" }); // 分隔线
console.error(err); ctx.strokeStyle = "#f0f0f0";
}, ctx.lineWidth = 1;
}, ctx.beginPath();
proxy, ctx.moveTo(40, footerY);
); ctx.lineTo(W - 40, footerY);
}, 200); ctx.stroke();
});
// 底部左侧文字
ctx.textAlign = "left";
ctx.font = "bold 32px sans-serif";
ctx.fillStyle = "#333333";
ctx.fillText("扫码开启今日好运", 40, footerY + 60);
ctx.font = "normal 20px sans-serif";
ctx.fillStyle = "#999999";
ctx.fillText("2026 CNY SPECIAL · 新春助手", 40, footerY + 100);
// 底部右侧二维码
if (qrCodeImg) {
ctx.drawImage(qrCodeImg, W - 140, footerY + 25, 100, 100);
}
// 生成图片
setTimeout(() => {
uni.canvasToTempFilePath({
canvas: canvas,
width: W,
height: H,
destWidth: W * dpr,
destHeight: H * dpr,
success: (res) => {
uni.saveImageToPhotosAlbum({
filePath: res.tempFilePath,
success: () => {
uni.hideLoading();
uni.showToast({ title: "已保存到相册", icon: "success" });
},
fail: () => {
uni.hideLoading();
uni.showToast({ title: "保存失败,请授权", icon: "none" });
},
});
},
fail: (err) => {
uni.hideLoading();
uni.showToast({ title: "生成图片失败", icon: "none" });
console.error(err);
},
});
}, 200);
} catch (err) {
console.error(err);
uni.hideLoading();
uni.showToast({ title: "生成图片失败", icon: "none" });
}
});
}; };
// 辅助函数:绘制圆角矩形 // 辅助函数:绘制圆角矩形
@@ -529,9 +556,9 @@ function roundRect(ctx, x, y, w, h, r) {
// 辅助函数:绘制带背景边框的盒子 // 辅助函数:绘制带背景边框的盒子
function drawBox(ctx, x, y, w, h, bg, border) { function drawBox(ctx, x, y, w, h, bg, border) {
ctx.setFillStyle(bg); ctx.fillStyle = bg;
ctx.setStrokeStyle(border); ctx.strokeStyle = border;
ctx.setLineWidth(2); ctx.lineWidth = 2;
roundRect(ctx, x, y, w, h, 20); roundRect(ctx, x, y, w, h, 20);
ctx.fill(); ctx.fill();
ctx.stroke(); ctx.stroke();

BIN
static/images/qrcode.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB