完成生成靶子数据图片的头部
This commit is contained in:
@@ -85,14 +85,14 @@ const loading = ref(false);
|
|||||||
const shareImage = async () => {
|
const shareImage = async () => {
|
||||||
if (loading.value) return;
|
if (loading.value) return;
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
await generateShareImage("shareImageCanvas");
|
await generateShareImage("shareImageCanvas", record.value);
|
||||||
await wxShare("shareImageCanvas");
|
await wxShare("shareImageCanvas");
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
onLoad(async (options) => {
|
onLoad(async (options) => {
|
||||||
if (options.id) {
|
if (options.id) {
|
||||||
const result = await getPointBookDetailAPI(options.id || 209);
|
const result = await getPointBookDetailAPI(options.id || 222);
|
||||||
record.value = result;
|
record.value = result;
|
||||||
notes.value = result.remark || "";
|
notes.value = result.remark || "";
|
||||||
const config = uni.getStorageSync("point-book-config");
|
const config = uni.getStorageSync("point-book-config");
|
||||||
|
|||||||
278
src/util.js
278
src/util.js
@@ -81,11 +81,152 @@ export function renderLine(ctx, from) {
|
|||||||
ctx.stroke();
|
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.setFontSize(size);
|
||||||
ctx.setFillStyle(color);
|
ctx.setFillStyle(color);
|
||||||
ctx.setTextAlign(textAlign);
|
ctx.setTextAlign(textAlign);
|
||||||
|
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);
|
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) {
|
export function renderRankTitle(ctx, text) {
|
||||||
@@ -133,7 +274,9 @@ export const drawRoundImage = async (
|
|||||||
y,
|
y,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
radius
|
radius,
|
||||||
|
borderColor = null,
|
||||||
|
borderWidth = 0
|
||||||
) => {
|
) => {
|
||||||
ctx.save();
|
ctx.save();
|
||||||
// 创建圆角路径
|
// 创建圆角路径
|
||||||
@@ -160,18 +303,56 @@ export const drawRoundImage = async (
|
|||||||
// 绘制图片
|
// 绘制图片
|
||||||
ctx.drawImage(imgPath, x, y, width, height);
|
ctx.drawImage(imgPath, x, y, width, height);
|
||||||
ctx.restore();
|
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) =>
|
const loadImage = (src) =>
|
||||||
new Promise((resolve) => {
|
new Promise((resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
uni.getImageInfo({
|
uni.getImageInfo({
|
||||||
src,
|
src,
|
||||||
success: (res) => resolve(res.path || res.tempFilePath || src),
|
success: (res) => resolve(res.path || res.tempFilePath || src),
|
||||||
fail: () => resolve(src),
|
fail: (e) => reject(e),
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
resolve(src);
|
reject(src);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -180,6 +361,15 @@ export async function generateCanvasImage(canvasId, type, user, data) {
|
|||||||
var ctx = uni.createCanvasContext(canvasId);
|
var ctx = uni.createCanvasContext(canvasId);
|
||||||
const width = 300;
|
const width = 300;
|
||||||
const height = 534;
|
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);
|
ctx.drawImage("../static/share-bg.png", 0, 0, width, height);
|
||||||
const avatar = await loadImage(user.avatar);
|
const avatar = await loadImage(user.avatar);
|
||||||
drawRoundImage(ctx, avatar, 17, 20, 32, 32, 20);
|
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";
|
"https://static.shelingxingqiu.com/attachment/2025-11-04/ddzpm4tyh5vunyacsr.png";
|
||||||
const drawWidth = 375;
|
const drawWidth = 375;
|
||||||
const drawHeight = 300;
|
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" 的情况
|
// 兼容第三个参数传入 "actual/total" 的情况
|
||||||
let a = actual;
|
let a = actual;
|
||||||
let t = total;
|
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) => {
|
export const getDirectionText = (angle = 0) => {
|
||||||
if (angle < 0) return "";
|
if (angle < 0) return "";
|
||||||
|
|||||||
Reference in New Issue
Block a user