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