fix: lucky page
This commit is contained in:
@@ -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,34 +232,61 @@ 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;
|
||||||
|
|
||||||
|
// Set canvas size (physical pixels)
|
||||||
|
canvas.width = res[0].width * dpr;
|
||||||
|
canvas.height = res[0].height * dpr;
|
||||||
|
|
||||||
|
// Reset transform
|
||||||
|
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
// Scale context
|
||||||
|
ctx.scale(dpr, dpr);
|
||||||
|
|
||||||
|
const W = res[0].width;
|
||||||
|
const H = res[0].height;
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Load images
|
||||||
|
let avatarUrl = "/static/images/default-avatar.png";
|
||||||
|
if (userInfo.value && userInfo.value.avatarUrl) {
|
||||||
|
avatarUrl = userInfo.value.avatarUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [avatarImg, qrCodeImg] = await Promise.all([
|
||||||
|
loadCanvasImage(avatarUrl).catch(() =>
|
||||||
|
loadCanvasImage("/static/images/default-avatar.png"),
|
||||||
|
),
|
||||||
|
loadCanvasImage("/static/images/qrcode.jpg").catch(() => null),
|
||||||
|
]);
|
||||||
|
|
||||||
// 1. 绘制背景
|
// 1. 绘制背景
|
||||||
ctx.setFillStyle("#ffffff");
|
ctx.fillStyle = "#ffffff";
|
||||||
ctx.fillRect(0, 0, W, H);
|
ctx.fillRect(0, 0, W, H);
|
||||||
|
|
||||||
// 2. 绘制卡片头部渐变
|
// 2. 绘制卡片头部渐变
|
||||||
@@ -267,7 +294,7 @@ const onSaveImage = async () => {
|
|||||||
const grd = ctx.createLinearGradient(0, 0, 0, headerH);
|
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.fillStyle = grd;
|
||||||
ctx.fillRect(0, 0, W, headerH);
|
ctx.fillRect(0, 0, W, headerH);
|
||||||
|
|
||||||
// --- Top Bar ---
|
// --- Top Bar ---
|
||||||
@@ -285,13 +312,14 @@ const onSaveImage = async () => {
|
|||||||
2 * Math.PI,
|
2 * Math.PI,
|
||||||
);
|
);
|
||||||
ctx.clip();
|
ctx.clip();
|
||||||
ctx.drawImage(avatarPath, 40, topY, avatarSize, avatarSize);
|
if (avatarImg) {
|
||||||
|
ctx.drawImage(avatarImg, 40, topY, avatarSize, avatarSize);
|
||||||
|
}
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
|
|
||||||
// 绘制用户昵称
|
// 绘制用户昵称
|
||||||
ctx.setTextAlign("left");
|
ctx.textAlign = "left";
|
||||||
ctx.setFillStyle("#ffffff");
|
ctx.fillStyle = "#ffffff";
|
||||||
ctx.setFontSize(26);
|
|
||||||
ctx.font = "bold 26px sans-serif";
|
ctx.font = "bold 26px sans-serif";
|
||||||
ctx.fillText(
|
ctx.fillText(
|
||||||
userInfo.value?.nickName || "好运用户",
|
userInfo.value?.nickName || "好运用户",
|
||||||
@@ -301,47 +329,42 @@ const onSaveImage = async () => {
|
|||||||
|
|
||||||
// 绘制日期 (Top Right)
|
// 绘制日期 (Top Right)
|
||||||
const dateStr = currentDateStr.value || "2026 CNY SPECIAL";
|
const dateStr = currentDateStr.value || "2026 CNY SPECIAL";
|
||||||
ctx.setFontSize(22);
|
|
||||||
ctx.font = "normal 22px sans-serif";
|
ctx.font = "normal 22px sans-serif";
|
||||||
const dateWidth = ctx.measureText(dateStr).width + 30;
|
const dateWidth = ctx.measureText(dateStr).width + 30;
|
||||||
const dateX = W - 40 - dateWidth;
|
const dateX = W - 40 - dateWidth;
|
||||||
const dateY = topY + 12; // box top
|
const dateY = topY + 12; // box top
|
||||||
// date bg
|
// date bg
|
||||||
ctx.setFillStyle("rgba(255, 255, 255, 0.2)");
|
ctx.fillStyle = "rgba(255, 255, 255, 0.2)";
|
||||||
roundRect(ctx, dateX, dateY, dateWidth, 40, 20);
|
roundRect(ctx, dateX, dateY, dateWidth, 40, 20);
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
// date text
|
// date text
|
||||||
ctx.setFillStyle("#ffffff");
|
ctx.fillStyle = "#ffffff";
|
||||||
ctx.fillText(dateStr, dateX + 15, dateY + 28);
|
ctx.fillText(dateStr, dateX + 15, dateY + 28);
|
||||||
|
|
||||||
// --- Main Content (Centered) ---
|
// --- Main Content (Centered) ---
|
||||||
const centerX = W / 2;
|
const centerX = W / 2;
|
||||||
|
|
||||||
// Label
|
// Label
|
||||||
ctx.setTextAlign("center");
|
ctx.textAlign = "center";
|
||||||
ctx.setFillStyle("rgba(255, 255, 255, 0.9)");
|
ctx.fillStyle = "rgba(255, 255, 255, 0.9)";
|
||||||
ctx.setFontSize(24);
|
|
||||||
ctx.font = "normal 24px sans-serif";
|
ctx.font = "normal 24px sans-serif";
|
||||||
ctx.fillText("今日好运指数", centerX, 180);
|
ctx.fillText("今日好运指数", centerX, 180);
|
||||||
|
|
||||||
// Score
|
// Score
|
||||||
ctx.setFillStyle("#ffffff");
|
ctx.fillStyle = "#ffffff";
|
||||||
ctx.setFontSize(140);
|
|
||||||
ctx.font = "bold 140px sans-serif";
|
ctx.font = "bold 140px sans-serif";
|
||||||
ctx.fillText(resultData.value.score + "%", centerX, 310);
|
ctx.fillText(resultData.value.score + "%", centerX, 310);
|
||||||
|
|
||||||
// Lucky Word
|
// Lucky Word
|
||||||
ctx.setFontSize(52);
|
|
||||||
ctx.font = "bold 52px sans-serif";
|
ctx.font = "bold 52px sans-serif";
|
||||||
ctx.fillText(resultData.value.luckyWord, centerX, 390);
|
ctx.fillText(resultData.value.luckyWord, centerX, 390);
|
||||||
|
|
||||||
// Decorators (Bottom Corners)
|
// Decorators (Bottom Corners)
|
||||||
ctx.setFillStyle("rgba(255, 255, 255, 0.4)");
|
ctx.fillStyle = "rgba(255, 255, 255, 0.4)";
|
||||||
ctx.setFontSize(24);
|
|
||||||
ctx.font = "normal 24px sans-serif";
|
ctx.font = "normal 24px sans-serif";
|
||||||
ctx.setTextAlign("left");
|
ctx.textAlign = "left";
|
||||||
ctx.fillText("福", 40, headerH - 20);
|
ctx.fillText("福", 40, headerH - 20);
|
||||||
ctx.setTextAlign("right");
|
ctx.textAlign = "right";
|
||||||
ctx.fillText("禧", W - 40, headerH - 20);
|
ctx.fillText("禧", W - 40, headerH - 20);
|
||||||
|
|
||||||
// 5. 绘制内容区 (宜/忌)
|
// 5. 绘制内容区 (宜/忌)
|
||||||
@@ -351,25 +374,21 @@ const onSaveImage = async () => {
|
|||||||
|
|
||||||
// 宜
|
// 宜
|
||||||
drawBox(ctx, 32, gridY, boxW, gridH, "#fbfbfb", "#f5f5f5");
|
drawBox(ctx, 32, gridY, boxW, gridH, "#fbfbfb", "#f5f5f5");
|
||||||
ctx.setTextAlign("left");
|
ctx.textAlign = "left";
|
||||||
ctx.setFontSize(24);
|
|
||||||
ctx.setFillStyle("#d81e06");
|
|
||||||
ctx.font = "bold 24px sans-serif";
|
ctx.font = "bold 24px sans-serif";
|
||||||
|
ctx.fillStyle = "#d81e06";
|
||||||
ctx.fillText("✔ 今日宜", 56, gridY + 44);
|
ctx.fillText("✔ 今日宜", 56, gridY + 44);
|
||||||
|
|
||||||
ctx.setFontSize(22);
|
|
||||||
ctx.setFillStyle("#666666");
|
|
||||||
ctx.font = "normal 22px sans-serif";
|
ctx.font = "normal 22px sans-serif";
|
||||||
|
ctx.fillStyle = "#666666";
|
||||||
wrapText(ctx, resultData.value.yi, 56, gridY + 80, boxW - 48, 30);
|
wrapText(ctx, resultData.value.yi, 56, gridY + 80, boxW - 48, 30);
|
||||||
|
|
||||||
// 忌
|
// 忌
|
||||||
drawBox(ctx, 32 + boxW + 24, gridY, boxW, gridH, "#fbfbfb", "#f5f5f5");
|
drawBox(ctx, 32 + boxW + 24, gridY, boxW, gridH, "#fbfbfb", "#f5f5f5");
|
||||||
ctx.setFontSize(24);
|
|
||||||
ctx.setFillStyle("#666666");
|
|
||||||
ctx.font = "bold 24px sans-serif";
|
ctx.font = "bold 24px sans-serif";
|
||||||
|
ctx.fillStyle = "#666666";
|
||||||
ctx.fillText("✖ 今日忌", 32 + boxW + 24 + 24, gridY + 44);
|
ctx.fillText("✖ 今日忌", 32 + boxW + 24 + 24, gridY + 44);
|
||||||
|
|
||||||
ctx.setFontSize(22);
|
|
||||||
ctx.font = "normal 22px sans-serif";
|
ctx.font = "normal 22px sans-serif";
|
||||||
wrapText(
|
wrapText(
|
||||||
ctx,
|
ctx,
|
||||||
@@ -386,9 +405,8 @@ const onSaveImage = async () => {
|
|||||||
drawBox(ctx, 32, elY, W - 64, elH, "#fbfbfb", "#f5f5f5");
|
drawBox(ctx, 32, elY, W - 64, elH, "#fbfbfb", "#f5f5f5");
|
||||||
|
|
||||||
// 标题
|
// 标题
|
||||||
ctx.setFontSize(26);
|
|
||||||
ctx.setFillStyle("#333333");
|
|
||||||
ctx.font = "bold 26px sans-serif";
|
ctx.font = "bold 26px sans-serif";
|
||||||
|
ctx.fillStyle = "#333333";
|
||||||
ctx.fillText("★ 幸运元素", 56, elY + 46);
|
ctx.fillText("★ 幸运元素", 56, elY + 46);
|
||||||
|
|
||||||
// 元素内容
|
// 元素内容
|
||||||
@@ -400,42 +418,40 @@ const onSaveImage = async () => {
|
|||||||
const valY = elY + 126;
|
const valY = elY + 126;
|
||||||
|
|
||||||
// 颜色
|
// 颜色
|
||||||
ctx.setTextAlign("center");
|
ctx.textAlign = "center";
|
||||||
ctx.setFontSize(20);
|
|
||||||
ctx.setFillStyle("#999999");
|
|
||||||
ctx.font = "normal 20px sans-serif";
|
ctx.font = "normal 20px sans-serif";
|
||||||
|
ctx.fillStyle = "#999999";
|
||||||
ctx.fillText("颜色", startX + colW * 0.5, labelY);
|
ctx.fillText("颜色", startX + colW * 0.5, labelY);
|
||||||
|
|
||||||
ctx.setFontSize(26);
|
|
||||||
ctx.setFillStyle("#d84315");
|
|
||||||
ctx.font = "bold 26px sans-serif";
|
ctx.font = "bold 26px sans-serif";
|
||||||
|
ctx.fillStyle = "#d84315";
|
||||||
ctx.fillText(resultData.value.luckyColor, startX + colW * 0.5, valY);
|
ctx.fillText(resultData.value.luckyColor, startX + colW * 0.5, valY);
|
||||||
|
|
||||||
// 数字
|
// 数字
|
||||||
ctx.setFontSize(20);
|
|
||||||
ctx.setFillStyle("#999999");
|
|
||||||
ctx.font = "normal 20px sans-serif";
|
ctx.font = "normal 20px sans-serif";
|
||||||
|
ctx.fillStyle = "#999999";
|
||||||
ctx.fillText("数字", startX + colW * 1.5, labelY);
|
ctx.fillText("数字", startX + colW * 1.5, labelY);
|
||||||
|
|
||||||
ctx.setFontSize(26);
|
|
||||||
ctx.setFillStyle("#333333");
|
|
||||||
ctx.font = "bold 26px sans-serif";
|
ctx.font = "bold 26px sans-serif";
|
||||||
|
ctx.fillStyle = "#333333";
|
||||||
ctx.fillText(resultData.value.luckyNumber, startX + colW * 1.5, valY);
|
ctx.fillText(resultData.value.luckyNumber, startX + colW * 1.5, valY);
|
||||||
|
|
||||||
// 方向
|
// 方向
|
||||||
ctx.setFontSize(20);
|
|
||||||
ctx.setFillStyle("#999999");
|
|
||||||
ctx.font = "normal 20px sans-serif";
|
ctx.font = "normal 20px sans-serif";
|
||||||
|
ctx.fillStyle = "#999999";
|
||||||
ctx.fillText("方向", startX + colW * 2.5, labelY);
|
ctx.fillText("方向", startX + colW * 2.5, labelY);
|
||||||
|
|
||||||
ctx.setFontSize(26);
|
|
||||||
ctx.setFillStyle("#333333");
|
|
||||||
ctx.font = "bold 26px sans-serif";
|
ctx.font = "bold 26px sans-serif";
|
||||||
ctx.fillText(resultData.value.luckyDirection, startX + colW * 2.5, valY);
|
ctx.fillStyle = "#333333";
|
||||||
|
ctx.fillText(
|
||||||
|
resultData.value.luckyDirection,
|
||||||
|
startX + colW * 2.5,
|
||||||
|
valY,
|
||||||
|
);
|
||||||
|
|
||||||
// 分隔线
|
// 分隔线
|
||||||
ctx.setStrokeStyle("#eeeeee");
|
ctx.strokeStyle = "#eeeeee";
|
||||||
ctx.setLineWidth(2);
|
ctx.lineWidth = 2;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
const lineTop = elY + 70;
|
const lineTop = elY + 70;
|
||||||
const lineBottom = elY + 130;
|
const lineBottom = elY + 130;
|
||||||
@@ -449,44 +465,52 @@ const onSaveImage = async () => {
|
|||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
|
||||||
// 7. 语录
|
// 7. 语录
|
||||||
ctx.setTextAlign("center");
|
ctx.textAlign = "center";
|
||||||
ctx.setFontSize(22);
|
|
||||||
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);
|
ctx.fillStyle = "#999999";
|
||||||
|
wrapTextCentered(
|
||||||
|
ctx,
|
||||||
|
`“${resultData.value.quote}”`,
|
||||||
|
W / 2,
|
||||||
|
880,
|
||||||
|
W - 80,
|
||||||
|
30,
|
||||||
|
);
|
||||||
|
|
||||||
// 8. 底部区域 (Footer)
|
// 8. 底部区域 (Footer)
|
||||||
const footerY = 960;
|
const footerY = 960;
|
||||||
|
|
||||||
// 分隔线
|
// 分隔线
|
||||||
ctx.setStrokeStyle("#f0f0f0");
|
ctx.strokeStyle = "#f0f0f0";
|
||||||
ctx.setLineWidth(1);
|
ctx.lineWidth = 1;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(40, footerY);
|
ctx.moveTo(40, footerY);
|
||||||
ctx.lineTo(W - 40, footerY);
|
ctx.lineTo(W - 40, footerY);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
|
||||||
// 底部左侧文字
|
// 底部左侧文字
|
||||||
ctx.setTextAlign("left");
|
ctx.textAlign = "left";
|
||||||
ctx.setFontSize(32);
|
|
||||||
ctx.setFillStyle("#333333");
|
|
||||||
ctx.font = "bold 32px sans-serif";
|
ctx.font = "bold 32px sans-serif";
|
||||||
|
ctx.fillStyle = "#333333";
|
||||||
ctx.fillText("扫码开启今日好运", 40, footerY + 60);
|
ctx.fillText("扫码开启今日好运", 40, footerY + 60);
|
||||||
|
|
||||||
ctx.setFontSize(20);
|
|
||||||
ctx.setFillStyle("#999999");
|
|
||||||
ctx.font = "normal 20px sans-serif";
|
ctx.font = "normal 20px sans-serif";
|
||||||
|
ctx.fillStyle = "#999999";
|
||||||
ctx.fillText("2026 CNY SPECIAL · 新春助手", 40, footerY + 100);
|
ctx.fillText("2026 CNY SPECIAL · 新春助手", 40, footerY + 100);
|
||||||
|
|
||||||
// 底部右侧二维码
|
// 底部右侧二维码
|
||||||
ctx.drawImage("/static/logo.png", W - 140, footerY + 25, 100, 100);
|
if (qrCodeImg) {
|
||||||
|
ctx.drawImage(qrCodeImg, W - 140, footerY + 25, 100, 100);
|
||||||
|
}
|
||||||
|
|
||||||
// 绘制
|
// 生成图片
|
||||||
ctx.draw(false, () => {
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
uni.canvasToTempFilePath(
|
uni.canvasToTempFilePath({
|
||||||
{
|
canvas: canvas,
|
||||||
canvasId: "luckyCanvas",
|
width: W,
|
||||||
|
height: H,
|
||||||
|
destWidth: W * dpr,
|
||||||
|
destHeight: H * dpr,
|
||||||
success: (res) => {
|
success: (res) => {
|
||||||
uni.saveImageToPhotosAlbum({
|
uni.saveImageToPhotosAlbum({
|
||||||
filePath: res.tempFilePath,
|
filePath: res.tempFilePath,
|
||||||
@@ -505,10 +529,13 @@ const onSaveImage = async () => {
|
|||||||
uni.showToast({ title: "生成图片失败", icon: "none" });
|
uni.showToast({ title: "生成图片失败", icon: "none" });
|
||||||
console.error(err);
|
console.error(err);
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
proxy,
|
|
||||||
);
|
|
||||||
}, 200);
|
}, 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
BIN
static/images/qrcode.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 51 KiB |
Reference in New Issue
Block a user