diff --git a/src/pages/point-book-detail.vue b/src/pages/point-book-detail.vue index a0f1b1e..2916075 100644 --- a/src/pages/point-book-detail.vue +++ b/src/pages/point-book-detail.vue @@ -85,14 +85,14 @@ const loading = ref(false); const shareImage = async () => { if (loading.value) return; loading.value = true; - await generateShareImage("shareImageCanvas"); + await generateShareImage("shareImageCanvas", record.value); await wxShare("shareImageCanvas"); loading.value = false; }; onLoad(async (options) => { if (options.id) { - const result = await getPointBookDetailAPI(options.id || 209); + const result = await getPointBookDetailAPI(options.id || 222); record.value = result; notes.value = result.remark || ""; const config = uni.getStorageSync("point-book-config"); diff --git a/src/util.js b/src/util.js index f1743b0..4c4c8cb 100644 --- a/src/util.js +++ b/src/util.js @@ -81,11 +81,152 @@ export function renderLine(ctx, from) { ctx.stroke(); } -export function renderText(ctx, text, size, color, x, y, textAlign = "left") { +export function renderText( + ctx, + text, + size, + color, + x, + y, + textAlign = "left", + rotateDeg = 0 +) { ctx.setFontSize(size); ctx.setFillStyle(color); ctx.setTextAlign(textAlign); - ctx.fillText(text, x, y); + if (rotateDeg) { + ctx.save(); + ctx.translate(x, y); + ctx.rotate((Math.PI / 180) * rotateDeg); + ctx.fillText(text, 0, 0); + ctx.restore(); + } else { + ctx.fillText(text, x, y); + } +} + +// 绘制圆角矩形(可选填充和描边) +export function drawRoundedRect( + ctx, + x, + y, + width, + height, + radius, + fillColor = "#fff", + strokeColor = null, + lineWidth = 1 +) { + const r = Math.max(Math.min(radius, Math.min(width, height) / 2), 0); + // 当圆角为 0 时,避免使用 arcTo 导致平台差异,直接绘制普通矩形 + if (r === 0) { + ctx.beginPath(); + if (typeof ctx.rect === "function") { + ctx.rect(x, y, width, height); + } else { + ctx.moveTo(x, y); + ctx.lineTo(x + width, y); + ctx.lineTo(x + width, y + height); + ctx.lineTo(x, y + height); + ctx.lineTo(x, y); + } + if (fillColor) { + if (typeof ctx.setFillStyle === "function") { + ctx.setFillStyle(fillColor); + } else { + ctx.fillStyle = fillColor; + } + ctx.fill(); + } + if (strokeColor) { + if (typeof ctx.setLineWidth === "function") { + ctx.setLineWidth(lineWidth); + } else { + ctx.lineWidth = lineWidth; + } + if (typeof ctx.setStrokeStyle === "function") { + ctx.setStrokeStyle(strokeColor); + } else { + ctx.strokeStyle = strokeColor; + } + ctx.stroke(); + } + return; + } + ctx.beginPath(); + ctx.moveTo(x + r, y); + // 顶边到右上角 + ctx.lineTo(x + width - r, y); + ctx.quadraticCurveTo(x + width, y, x + width, y + r); + // 右边到右下角 + ctx.lineTo(x + width, y + height - r); + ctx.quadraticCurveTo(x + width, y + height, x + width - r, y + height); + // 底边到左下角 + ctx.lineTo(x + r, y + height); + ctx.quadraticCurveTo(x, y + height, x, y + height - r); + // 左边到左上角 + ctx.lineTo(x, y + r); + ctx.quadraticCurveTo(x, y, x + r, y); + // 显式闭合路径,避免某些 Android WebView/Canvas 在左上角出现缺口 + ctx.closePath(); + + if (fillColor) { + if (typeof ctx.setFillStyle === "function") { + ctx.setFillStyle(fillColor); + } else { + ctx.fillStyle = fillColor; + } + ctx.fill(); + } + + if (strokeColor) { + if (typeof ctx.setLineWidth === "function") { + ctx.setLineWidth(lineWidth); + } else { + ctx.lineWidth = lineWidth; + } + if (typeof ctx.setStrokeStyle === "function") { + ctx.setStrokeStyle(strokeColor); + } else { + ctx.strokeStyle = strokeColor; + } + ctx.stroke(); + } +} + +// 将文本按字符拆分并依次向右绘制,每个字符有白色背景,背景宽度根据字符宽度+内边距动态计算 +export function drawTextBoxesLine( + ctx, + text, + startX, + baselineY, + fontSize = 9, + padding = 10, + bgColor = "#fff", + textColor = "#000", + radius = 4, + gap = 0 +) { + const str = String(text || ""); + // 确保测量宽度使用一致的字号 + ctx.setFontSize(fontSize); + const w = ctx.measureText(str).width; // 整段文本宽度 + const hPadding = padding + 4; // 左右额外加宽,保持高度不变 + const rectW = Math.ceil(w) + hPadding * 2; + const rectH = fontSize + padding * 2; + const rectY = baselineY - fontSize - padding / 1.3; // 使文字基线位于背景内部 + drawRoundedRect(ctx, startX, rectY, rectW, rectH, radius, bgColor, null, 0); + // 文本在背景内居中显示 + renderText( + ctx, + str, + fontSize, + textColor, + startX + rectW / 2, + baselineY, + "center" + ); + return startX + rectW + gap; } export function renderRankTitle(ctx, text) { @@ -133,7 +274,9 @@ export const drawRoundImage = async ( y, width, height, - radius + radius, + borderColor = null, + borderWidth = 0 ) => { ctx.save(); // 创建圆角路径 @@ -160,18 +303,56 @@ export const drawRoundImage = async ( // 绘制图片 ctx.drawImage(imgPath, x, y, width, height); ctx.restore(); + + // 绘制边框(可选) + if (borderWidth > 0 && borderColor) { + // 内收路径,使描边完全落在给定宽高内 + const inset = borderWidth / 2; + const bx = x + inset; + const by = y + inset; + const bw = width - inset * 2; + const bh = height - inset * 2; + const br = Math.max(radius - inset, 0); + + ctx.beginPath(); + ctx.moveTo(bx + br, by); + // 右上角 + ctx.lineTo(bx + bw - br, by); + ctx.arcTo(bx + bw, by, bx + bw, by + br, br); + // 右下角 + ctx.lineTo(bx + bw, by + bh - br); + ctx.arcTo(bx + bw, by + bh, bx + bw - br, by + bh, br); + // 左下角 + ctx.lineTo(bx + br, by + bh); + ctx.arcTo(bx, by + bh, bx, by + bh - br, br); + // 左上角 + ctx.lineTo(bx, by + br); + ctx.arcTo(bx, by, bx + br, by, br); + + if (typeof ctx.setLineWidth === "function") { + ctx.setLineWidth(borderWidth); + } else { + ctx.lineWidth = borderWidth; + } + if (typeof ctx.setStrokeStyle === "function") { + ctx.setStrokeStyle(borderColor); + } else { + ctx.strokeStyle = borderColor; + } + ctx.stroke(); + } }; const loadImage = (src) => - new Promise((resolve) => { + new Promise((resolve, reject) => { try { uni.getImageInfo({ src, success: (res) => resolve(res.path || res.tempFilePath || src), - fail: () => resolve(src), + fail: (e) => reject(e), }); } catch (e) { - resolve(src); + reject(src); } }); @@ -180,6 +361,15 @@ export async function generateCanvasImage(canvasId, type, user, data) { var ctx = uni.createCanvasContext(canvasId); const width = 300; const height = 534; + // 先填充整体背景色 + if (typeof ctx.setFillStyle === "function") { + ctx.setFillStyle("#F5F5F5"); + } else { + ctx.fillStyle = "#F5F5F5"; + } + if (typeof ctx.fillRect === "function") { + ctx.fillRect(0, 0, width, height); + } ctx.drawImage("../static/share-bg.png", 0, 0, width, height); const avatar = await loadImage(user.avatar); drawRoundImage(ctx, avatar, 17, 20, 32, 32, 20); @@ -461,6 +651,15 @@ export const generateShareCard = (canvasId, date, actual, total) => { "https://static.shelingxingqiu.com/attachment/2025-11-04/ddzpm4tyh5vunyacsr.png"; const drawWidth = 375; const drawHeight = 300; + // 先填充整体背景色 + if (typeof ctx.setFillStyle === "function") { + ctx.setFillStyle("#F5F5F5"); + } else { + ctx.fillStyle = "#F5F5F5"; + } + if (typeof ctx.fillRect === "function") { + ctx.fillRect(0, 0, drawWidth, drawHeight); + } // 兼容第三个参数传入 "actual/total" 的情况 let a = actual; let t = total; @@ -675,7 +874,74 @@ export const generateShareCard = (canvasId, date, actual, total) => { } }; -export const generateShareImage = (canvasId) => {}; +export const generateShareImage = async (canvasId, data) => { + try { + const pointBook = uni.getStorageSync("point-book"); + console.log("generate data:", data, pointBook); + const ctx = uni.createCanvasContext(canvasId); + + const width = 375; + const height = 800; + + ctx.setFillStyle("#F5F5F5"); + ctx.fillRect(0, 0, width, height); + + const bgUrl = await loadImage( + "https://static.shelingxingqiu.com/attachment/2025-11-14/de82iol96zxgwxbjhb.png" + ); + ctx.drawImage(bgUrl, 0, 0, width, height); + const avatarUrl = await loadImage(data.user.avatar); + drawRoundImage(ctx, avatarUrl, 13, 13, 54, 54, 27, "#000", 1); + renderText(ctx, data.user.name, 18, "#000", 84, 36); + renderText(ctx, data.recordDate, 9, "#fff", 350, 24, "center", 38); + renderText(ctx, "今日打卡", 13, "#fff", 338, 36, "center", 38); + // 以第一个字的 x 为起点,右侧字符依次向右排列,每个字符有白色背景并留 5 像素内边距 + let cursorX = 84; + cursorX = drawTextBoxesLine( + ctx, + pointBook.bowType.name, + cursorX, + 58, + 9, + 5, + "#fff", + "#000", + 6, + 2 + ); + cursorX = drawTextBoxesLine( + ctx, + String(pointBook.distance) + " 米", + cursorX + 6, + 58, + 9, + 5, + "#fff", + "#000", + 6, + 2 + ); + cursorX = drawTextBoxesLine( + ctx, + pointBook.bowtargetType.name, + cursorX + 6, + 58, + 9, + 5, + "#fff", + "#000", + 6, + 2 + ); + // const pointBookQrcodeUrl = await loadImage( + // "https://static.shelingxingqiu.com/attachment/2025-11-13/de7fzgghsfgqu0ytu6.png" + // ); + // ctx.drawImage(pointBookQrcodeUrl, 45, 777, 68, 68); + ctx.draw(); + } catch (e) { + console.error("generateShareImage 绘制失败:", e); + } +}; export const getDirectionText = (angle = 0) => { if (angle < 0) return "";