diff --git a/components/LuckyPopup/LuckyPopup.vue b/components/LuckyPopup/LuckyPopup.vue index 0bf6224..453c5a3 100644 --- a/components/LuckyPopup/LuckyPopup.vue +++ b/components/LuckyPopup/LuckyPopup.vue @@ -135,7 +135,7 @@ { 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(); diff --git a/static/images/qrcode.jpg b/static/images/qrcode.jpg new file mode 100644 index 0000000..32e1688 Binary files /dev/null and b/static/images/qrcode.jpg differ