分享图兼容性修复

This commit is contained in:
kron
2025-11-17 12:02:07 +08:00
parent 046d1a7c9e
commit d9563a25c6
4 changed files with 544 additions and 410 deletions

View File

@@ -105,222 +105,6 @@ export function renderText(
}
}
// 绘制圆角矩形(可选填充和描边)
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 drawRingCircle(ctx, x, y, text, diameter = 12) {
const fillColor = "#ff4444";
const borderColor = "#ffffff";
const borderWidth = 1;
const r = diameter / 2;
// 传入的 x/y 需要减去半径和边框长度
const cx = x - r - borderWidth;
const cy = y - r - borderWidth;
// 画圆填充
ctx.beginPath();
if (typeof ctx.arc === "function") {
ctx.arc(cx, cy, r, 0, Math.PI * 2);
} else {
// 简易兼容:没有 arc 方法时,用圆角矩形近似
drawRoundedRect(ctx, cx - r, cy - r, r * 2, r * 2, r, fillColor, null, 0);
}
if (typeof ctx.setFillStyle === "function") {
ctx.setFillStyle(fillColor);
} else {
ctx.fillStyle = fillColor;
}
// 添加微弱阴影提升立体感
if (typeof ctx.setShadow === "function") {
ctx.setShadow(0, 1, 2, "rgba(0,0,0,0.25)");
} else {
ctx.shadowColor = "rgba(0,0,0,0.25)";
ctx.shadowBlur = 2;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 1;
}
ctx.fill();
// 重置阴影,避免影响后续描边和文字
if (typeof ctx.setShadow === "function") {
ctx.setShadow(0, 0, 0, "rgba(0,0,0,0)");
} else {
ctx.shadowColor = "transparent";
ctx.shadowBlur = 0;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;
}
// 画白色 1px 边框
if (typeof ctx.setLineWidth === "function") {
ctx.setLineWidth(borderWidth);
} else {
ctx.lineWidth = borderWidth;
}
if (typeof ctx.setStrokeStyle === "function") {
ctx.setStrokeStyle(borderColor);
} else {
ctx.strokeStyle = borderColor;
}
// 当 arc 不可用时drawRoundedRect 已经完成边框;否则对圆描边
if (typeof ctx.arc === "function") {
ctx.stroke();
}
// 在圆心绘制数字,使用水平缩放实现窄体,并保持圆心居中
const fontSize = 9;
if (typeof ctx.setFontSize === "function") {
ctx.setFontSize(fontSize);
} else {
ctx.font = `${fontSize}px sans-serif`;
}
if (typeof ctx.setFillStyle === "function") {
ctx.setFillStyle("#ffffff");
} else {
ctx.fillStyle = "#ffffff";
}
ctx.save();
ctx.translate(cx, cy);
// 居中对齐
if (typeof ctx.setTextAlign === "function") {
ctx.setTextAlign("center");
} else {
ctx.textAlign = "center";
}
if (typeof ctx.setTextBaseline === "function") {
ctx.setTextBaseline("middle");
} else {
ctx.textBaseline = "middle";
}
ctx.scale(0.7, 1);
ctx.fillText(String(text ?? ""), 0, 0);
ctx.restore();
}
// 将文本按字符拆分并依次向右绘制,每个字符有白色背景,背景宽度根据字符宽度+内边距动态计算
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) {
const fontSize = 8;
const textWidth = ctx.measureText(text).width;
@@ -435,7 +219,7 @@ export const drawRoundImage = async (
}
};
const loadImage = (src) =>
export const loadImage = (src) =>
new Promise((resolve, reject) => {
try {
uni.getImageInfo({
@@ -526,21 +310,64 @@ export async function generateCanvasImage(canvasId, type, user, data) {
export const wxShare = async (canvasId = "shareCanvas") => {
try {
// 先尝试按 id 查找 <canvas type="2d"> 节点
const { pixelRatio: dpr = 1 } = uni.getSystemInfoSync() || {};
const info = await new Promise((resolve) => {
const query = uni.createSelectorQuery();
query
.select(`#${canvasId}`)
.fields({ node: true, size: true })
.exec((res) => resolve(res && res[0]));
});
const canvas = info?.node;
const cssWidth = info?.width;
const cssHeight = info?.height;
if (canvas && typeof canvas.getContext === "function") {
// 2D 画布导出:传入 canvas 节点
const tempPath = await new Promise((resolve, reject) => {
uni.canvasToTempFilePath({
canvas,
width: cssWidth,
height: cssHeight,
// 按 DPR 导出更清晰的图片
destWidth: Math.round(cssWidth * dpr),
destHeight: Math.round(cssHeight * dpr),
fileType: "png",
quality: 1,
success: (r) => {
const p = r.tempFilePath || r.apFilePath || r.filePath;
resolve(p);
},
fail: reject,
});
});
wx.showShareImageMenu({
entrancePath: "pages/index",
path: tempPath,
});
return tempPath;
}
// 回退:旧版非 2D 画布(通过 canvasId 导出)
const res = await uni.canvasToTempFilePath({
canvasId,
fileType: "png", // 图片格式,可选 jpg 或 png
quality: 1, // 图片质量,取值范围为 0-1
fileType: "png",
quality: 1,
});
wx.showShareImageMenu({
entrancePath: "pages/index",
path: res.tempFilePath,
});
return res.tempFilePath;
} catch (error) {
console.log("生成图片失败:", error);
uni.showToast({
title: "生成图片失败",
icon: "error",
});
throw error;
}
};
@@ -966,190 +793,6 @@ export const generateShareCard = (canvasId, date, actual, total) => {
}
};
export function drawLine(ctx, x1, y1, x2, y2, color = "#000", width = 1) {
ctx.beginPath();
ctx.lineWidth = width;
ctx.strokeStyle = color;
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
}
export const generateShareImage = async (canvasId, data) => {
try {
const pointBook = uni.getStorageSync("point-book");
const arrowData = data.groups && data.groups[0] ? data.groups[0] : {};
const hasPoint = arrowData.list.some((arrow) => arrow.x && arrow.y);
const ctx = uni.createCanvasContext(canvasId);
const width = 375;
const height = hasPoint ? 800 : 440;
ctx.setFillStyle("#F5F5F5");
ctx.fillRect(0, 0, width, height);
const bgUrl = await loadImage(
"https://static.shelingxingqiu.com/attachment/2025-11-14/de88ugdgqwecnmsqfv.png"
);
ctx.drawImage(bgUrl, 0, 0, width, 300);
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", 39);
renderText(ctx, "今日打卡", 13, "#fff", 336, 38, "center", 39);
// 以第一个字的 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
);
renderText(ctx, "落点稳定性", 13, "#999", 25, 110);
renderText(ctx, "黄心率", 13, "#999", 145, 110);
renderText(ctx, "10环数", 13, "#999", 262, 110);
renderText(ctx, arrowData.stability, 15, "#000", 25, 133);
renderText(
ctx,
Number((arrowData.yellowRate * 100).toFixed(2)) + "%",
15,
"#000",
145,
133
);
renderText(ctx, arrowData.tenRings, 15, "#000", 262, 133);
renderText(ctx, "平均环数", 13, "#999", 25, 175);
renderText(ctx, "总换数", 13, "#999", 145, 175);
renderText(ctx, arrowData.averageRing, 15, "#000", 25, 200);
renderText(
ctx,
`${arrowData.userTotalRing}/${arrowData.totalRing}`,
15,
"#000",
145,
200
);
if (hasPoint) {
drawRoundedRect(ctx, 20, 230, 5, 15, 5, "#FED847");
renderText(ctx, "落点分布", 13, "#999", 32, 242);
const bowUrl = await loadImage(pointBook.bowtargetType.iconPng);
ctx.drawImage(bowUrl, 375 * 0.08, 250, 375 * 0.84, 375 * 0.84);
arrowData.list.forEach((arrow, index) => {
const px = 375 * 0.08 + 375 * 0.84 * arrow.x;
const py = 250 + 375 * 0.84 * arrow.y;
drawRingCircle(ctx, px, py, index + 1);
});
}
const ringNames = ["X", 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, "M"];
const ringBarHeight = hasPoint ? 700 : 340;
// 统计每个环数的次数
let rings = new Array(12).fill(0);
arrowData.list.forEach((arrow) => {
if (arrow.ring === 0) rings[0]++;
else if (arrow.ring === -1) rings[11]++;
else rings[arrow.ring]++;
});
[rings[0], rings[11]] = [rings[11], rings[0]];
rings = rings.reverse();
rings = rings.map((count) => ({
count,
rate: count / arrowData.list.length,
}));
// 渲染柱子
const barColor = (rate) => {
if (rate >= 0.4) return "#FDC540";
if (rate >= 0.2) return "#FED847";
return "#ffe88f";
};
const barWidth = 20;
const barSpacing = 7.1;
const startX = 38;
const startY = ringBarHeight - 15;
rings.forEach((ring, index) => {
const barHeight = ring.rate * 90;
if (ring.rate > 0) {
renderText(
ctx,
Number((ring.rate * 100).toFixed(1)) + "%",
9,
"#333",
startX + index * (barWidth + barSpacing),
startY - barHeight - (hasPoint ? 6 : 3)
);
}
ctx.fillStyle = barColor(ring.rate);
ctx.fillRect(
startX + index * (barWidth + barSpacing),
startY - barHeight,
barWidth,
barHeight
);
});
renderText(ctx, "环值", 9, "#999", 15, ringBarHeight - 1);
ringNames.forEach((ring, index) => {
renderText(
ctx,
ring,
11,
"#333",
48 + index * 27,
ringBarHeight,
"center"
);
});
drawLine(ctx, 15, ringBarHeight - 15, 362, ringBarHeight - 15, "#333");
const pointBookQrcodeUrl = await loadImage(
"https://static.shelingxingqiu.com/attachment/2025-11-13/de7fzgghsfgqu0ytu6.png"
);
ctx.drawImage(pointBookQrcodeUrl, 40, hasPoint ? 715 : 358, 68, 68);
renderText(ctx, "射灵星球", 18, "#333", 120, hasPoint ? 734 : 380);
renderText(
ctx,
"高效记录每一箭,快来一起打卡吧!",
13,
"#999",
120,
hasPoint ? 756 : 402
);
renderText(ctx, "扫码打卡", 13, "#FFA118", 120, hasPoint ? 777 : 422);
ctx.draw();
} catch (e) {
console.error("generateShareImage 绘制失败:", e);
}
};
export const getDirectionText = (angle = 0) => {
if (angle < 0) return "";
if (angle >= 337.5 || angle < 22.5) {