|
@@ -185,7 +185,11 @@ async function generateImageWithQRCode() {
|
|
|
drawReviewEffectText(tempCtx, w, h);
|
|
drawReviewEffectText(tempCtx, w, h);
|
|
|
}
|
|
}
|
|
|
} else {
|
|
} else {
|
|
|
- drawBottomMask(tempCtx, w, h);
|
|
|
|
|
|
|
+ // 先计算文字区域,获取第一行文字的Y坐标
|
|
|
|
|
+ const topLineY = calculateTextOverlayTopY(tempCtx, w, h);
|
|
|
|
|
+ // 先绘制遮罩(作为背景)
|
|
|
|
|
+ drawBottomMask(tempCtx, w, h, topLineY);
|
|
|
|
|
+ // 再绘制文字(显示在遮罩上方)
|
|
|
drawBottomTextOverlay(tempCtx, w, h);
|
|
drawBottomTextOverlay(tempCtx, w, h);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -276,7 +280,11 @@ function drawWatermark2(sourceImg, displayImg) {
|
|
|
drawReviewEffectText(ctx, w, h);
|
|
drawReviewEffectText(ctx, w, h);
|
|
|
}
|
|
}
|
|
|
} else {
|
|
} else {
|
|
|
- drawBottomMask(ctx, w, h);
|
|
|
|
|
|
|
+ // 先计算文字区域,获取第一行文字的Y坐标
|
|
|
|
|
+ const topLineY = calculateTextOverlayTopY(ctx, w, h);
|
|
|
|
|
+ // 先绘制遮罩(作为背景)
|
|
|
|
|
+ drawBottomMask(ctx, w, h, topLineY);
|
|
|
|
|
+ // 再绘制文字(显示在遮罩上方)
|
|
|
drawBottomTextOverlay(ctx, w, h);
|
|
drawBottomTextOverlay(ctx, w, h);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -323,6 +331,59 @@ function drawImageCover(ctx, img, w, h) {
|
|
|
|
|
|
|
|
ctx.drawImage(img, sx, sy, sw, sh, 0, 0, w, h);
|
|
ctx.drawImage(img, sx, sy, sw, sh, 0, 0, w, h);
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+// 计算文字区域的顶部Y坐标(不实际绘制)
|
|
|
|
|
+function calculateTextOverlayTopY(ctx, w, h) {
|
|
|
|
|
+ const paddingX = 12;
|
|
|
|
|
+ const paddingBottom = 12;
|
|
|
|
|
+ const lineHeight = 16;
|
|
|
|
|
+
|
|
|
|
|
+ // ⬇️ 从底部开始,一行一行往上
|
|
|
|
|
+ let y = h - paddingBottom;
|
|
|
|
|
+
|
|
|
|
|
+ // 第三行(最底):药物处方(支持自动换行)
|
|
|
|
|
+ ctx.font = "10px sans-serif";
|
|
|
|
|
+ const prescriptionFull =
|
|
|
|
|
+ "药物处方:" + (buildPrescriptionText(props.imgData?.prescriptionList) || "");
|
|
|
|
|
+
|
|
|
|
|
+ // 计算文本最大宽度
|
|
|
|
|
+ const maxWidth = w - paddingX * 2;
|
|
|
|
|
+
|
|
|
|
|
+ // 自动换行处理
|
|
|
|
|
+ const prescriptionLines = [];
|
|
|
|
|
+ let line = "";
|
|
|
|
|
+ for (let i = 0; i < prescriptionFull.length; i++) {
|
|
|
|
|
+ const testLine = line + prescriptionFull[i];
|
|
|
|
|
+ const testWidth = ctx.measureText(testLine).width;
|
|
|
|
|
+ if (testWidth > maxWidth && line !== "") {
|
|
|
|
|
+ prescriptionLines.push(line);
|
|
|
|
|
+ line = prescriptionFull[i];
|
|
|
|
|
+ } else {
|
|
|
|
|
+ line = testLine;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (line) {
|
|
|
|
|
+ prescriptionLines.push(line);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 计算药物处方第一行的实际位置(最上面一行的 baseline)
|
|
|
|
|
+ const prescriptionFirstLineY = prescriptionLines.length > 1
|
|
|
|
|
+ ? y - (prescriptionLines.length - 1) * lineHeight
|
|
|
|
|
+ : y;
|
|
|
|
|
+
|
|
|
|
|
+ // 第二行:农事名称 + 农场名称
|
|
|
|
|
+ const spacing = 16;
|
|
|
|
|
+ const singleLineY = prescriptionFirstLineY - spacing;
|
|
|
|
|
+
|
|
|
|
|
+ const workNameText = props.imgData?.farmWorkName || "";
|
|
|
|
|
+ let firstLineY = singleLineY;
|
|
|
|
|
+
|
|
|
|
|
+ // 第一行(最上)的 baseline
|
|
|
|
|
+ const topLineY = firstLineY - 20;
|
|
|
|
|
+
|
|
|
|
|
+ return topLineY;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
function drawBottomTextOverlay(ctx, w, h) {
|
|
function drawBottomTextOverlay(ctx, w, h) {
|
|
|
const paddingX = 12;
|
|
const paddingX = 12;
|
|
|
const paddingBottom = 12;
|
|
const paddingBottom = 12;
|
|
@@ -336,75 +397,73 @@ function drawBottomTextOverlay(ctx, w, h) {
|
|
|
// ⬇️ 从底部开始,一行一行往上
|
|
// ⬇️ 从底部开始,一行一行往上
|
|
|
let y = h - paddingBottom;
|
|
let y = h - paddingBottom;
|
|
|
|
|
|
|
|
- // 第三行(最底)
|
|
|
|
|
- ctx.font = "10px sans-serif";
|
|
|
|
|
- ctx.drawImage(imageCache.get("address"), paddingX, y - 9, 9, 10);
|
|
|
|
|
- ctx.fillText("荔博园(广东省广州市从化区)", paddingX + 12, y);
|
|
|
|
|
-
|
|
|
|
|
- // 第二行:农事名称 + 药物处方(处方过长时向下换行,整体块仍贴近底部)
|
|
|
|
|
- // 单行基准位置(仅一行时的 baseline)
|
|
|
|
|
- const singleLineY = y - 15;
|
|
|
|
|
-
|
|
|
|
|
- // 计算处方文本(长度大于 30 时才自动换行)
|
|
|
|
|
|
|
+ // 第三行(最底):药物处方(支持自动换行)
|
|
|
ctx.font = "10px sans-serif";
|
|
ctx.font = "10px sans-serif";
|
|
|
- const workNameText = props.imgData?.farmWorkName || "";
|
|
|
|
|
const prescriptionFull =
|
|
const prescriptionFull =
|
|
|
"药物处方:" + (buildPrescriptionText(props.imgData?.prescriptionList) || "");
|
|
"药物处方:" + (buildPrescriptionText(props.imgData?.prescriptionList) || "");
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
|
|
+ // 计算文本最大宽度
|
|
|
const maxWidth = w - paddingX * 2;
|
|
const maxWidth = w - paddingX * 2;
|
|
|
-
|
|
|
|
|
- let firstLineY = singleLineY;
|
|
|
|
|
-
|
|
|
|
|
- if (prescriptionFull.length <= 30) {
|
|
|
|
|
- // 不足 30 个字符,保持一行:名称 + 处方在同一 baseline
|
|
|
|
|
- firstLineY = singleLineY;
|
|
|
|
|
-
|
|
|
|
|
- // 农事名称
|
|
|
|
|
- ctx.font = "16px PangMenZhengDao";
|
|
|
|
|
- ctx.fillText(workNameText, paddingX, firstLineY);
|
|
|
|
|
-
|
|
|
|
|
- // 处方文本紧跟在名称后面
|
|
|
|
|
- ctx.font = "10px sans-serif";
|
|
|
|
|
- const nameWidth = ctx.measureText(workNameText).width;
|
|
|
|
|
- const gap = 8;
|
|
|
|
|
- ctx.fillText(prescriptionFull, paddingX + nameWidth + gap + 28, firstLineY);
|
|
|
|
|
- } else {
|
|
|
|
|
- // 超过 30 个字符,自动换行显示(处方整体从左对齐)
|
|
|
|
|
- const prescriptionLines = [];
|
|
|
|
|
- let line = "";
|
|
|
|
|
- for (let i = 0; i < prescriptionFull.length; i++) {
|
|
|
|
|
- const testLine = line + prescriptionFull[i];
|
|
|
|
|
- const testWidth = ctx.measureText(testLine).width;
|
|
|
|
|
- if (testWidth > maxWidth && line !== "") {
|
|
|
|
|
- prescriptionLines.push(line);
|
|
|
|
|
- line = prescriptionFull[i];
|
|
|
|
|
- } else {
|
|
|
|
|
- line = testLine;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- if (line) {
|
|
|
|
|
|
|
+
|
|
|
|
|
+ // 自动换行处理
|
|
|
|
|
+ const prescriptionLines = [];
|
|
|
|
|
+ let line = "";
|
|
|
|
|
+ for (let i = 0; i < prescriptionFull.length; i++) {
|
|
|
|
|
+ const testLine = line + prescriptionFull[i];
|
|
|
|
|
+ const testWidth = ctx.measureText(testLine).width;
|
|
|
|
|
+ if (testWidth > maxWidth && line !== "") {
|
|
|
prescriptionLines.push(line);
|
|
prescriptionLines.push(line);
|
|
|
|
|
+ line = prescriptionFull[i];
|
|
|
|
|
+ } else {
|
|
|
|
|
+ line = testLine;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (line) {
|
|
|
|
|
+ prescriptionLines.push(line);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 从底部向上绘制多行文本
|
|
|
|
|
+ let textY = y;
|
|
|
|
|
+ for (let i = prescriptionLines.length - 1; i >= 0; i--) {
|
|
|
|
|
+ ctx.fillText(prescriptionLines[i], paddingX, textY);
|
|
|
|
|
+ if (i > 0) {
|
|
|
|
|
+ textY -= lineHeight; // 向上换行
|
|
|
}
|
|
}
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 计算药物处方第一行的实际位置(最上面一行的 baseline)
|
|
|
|
|
+ // 单行时:第一行位置 = y(底部位置)
|
|
|
|
|
+ // 多行时:第一行位置 = y - (行数 - 1) * lineHeight
|
|
|
|
|
+ const prescriptionFirstLineY = prescriptionLines.length > 1
|
|
|
|
|
+ ? y - (prescriptionLines.length - 1) * lineHeight
|
|
|
|
|
+ : y;
|
|
|
|
|
+
|
|
|
|
|
+ // 第二行:农事名称 + 农场名称
|
|
|
|
|
+ // 在药物处方第一行上方留出足够间距,确保不重叠
|
|
|
|
|
+ // 需要考虑字体大小差异:药物处方是 10px,农事名称是 16px
|
|
|
|
|
+ // 单行时使用较小间距,多行时使用较大间距
|
|
|
|
|
+ const spacing = 16;
|
|
|
|
|
+ const singleLineY = prescriptionFirstLineY - spacing;
|
|
|
|
|
+
|
|
|
|
|
+ ctx.font = "10px sans-serif";
|
|
|
|
|
+ const workNameText = props.imgData?.farmWorkName || "";
|
|
|
|
|
+ const farmNameText = props.imgData?.farmName || "";
|
|
|
|
|
|
|
|
- const blockLines = 1 + prescriptionLines.length; // 1 行名称 + N 行处方
|
|
|
|
|
- // 整个“第二行块”的第一行 baseline,使最后一行仍然靠近 singleLineY
|
|
|
|
|
- firstLineY = singleLineY - (blockLines - 1) * lineHeight;
|
|
|
|
|
|
|
+ let firstLineY = singleLineY;
|
|
|
|
|
|
|
|
- // 农事名称(第一行,靠左)
|
|
|
|
|
- ctx.font = "16px PangMenZhengDao";
|
|
|
|
|
- ctx.fillText(workNameText, paddingX, firstLineY);
|
|
|
|
|
|
|
+ // 农事名称
|
|
|
|
|
+ // ctx.font = "16px PangMenZhengDao";
|
|
|
|
|
+ ctx.font = "16px PangMenZhengDao";
|
|
|
|
|
+ ctx.fillText(workNameText, paddingX, firstLineY);
|
|
|
|
|
|
|
|
- // 处方文本,从下一行开始,全部从左侧对齐
|
|
|
|
|
- ctx.font = "10px sans-serif";
|
|
|
|
|
- let textY = firstLineY + lineHeight;
|
|
|
|
|
- prescriptionLines.forEach((text) => {
|
|
|
|
|
- ctx.fillText(text, paddingX, textY);
|
|
|
|
|
- textY += lineHeight;
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ // 农场名称紧跟在名称后面
|
|
|
|
|
+ const nameWidth = ctx.measureText(workNameText).width;
|
|
|
|
|
+ ctx.font = "10px sans-serif";
|
|
|
|
|
+ const gap = 8;
|
|
|
|
|
+ ctx.fillText(farmNameText, paddingX + nameWidth + gap, firstLineY);
|
|
|
|
|
|
|
|
- // 第一行(最上)的 baseline 在整个“第二行块”之上 17px
|
|
|
|
|
- y = firstLineY - 17;
|
|
|
|
|
|
|
+ // 第一行(最上)的 baseline 在整个"第二行块"之上 17px
|
|
|
|
|
+ y = firstLineY - 20;
|
|
|
ctx.font = "12px PangMenZhengDao";
|
|
ctx.font = "12px PangMenZhengDao";
|
|
|
const timeText = props.imgData?.executeDate;
|
|
const timeText = props.imgData?.executeDate;
|
|
|
ctx.fillText(timeText, paddingX, y);
|
|
ctx.fillText(timeText, paddingX, y);
|
|
@@ -475,13 +534,26 @@ function drawReviewEffectText(ctx, w, h) {
|
|
|
ctx.shadowBlur = 0;
|
|
ctx.shadowBlur = 0;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function drawBottomMask(ctx, w, h) {
|
|
|
|
|
- const maskHeight = 66; // 和 3 行文字 + padding 精确匹配
|
|
|
|
|
|
|
+function drawBottomMask(ctx, w, h, topY) {
|
|
|
|
|
+ let maskHeight;
|
|
|
|
|
+ let maskTop;
|
|
|
|
|
+
|
|
|
|
|
+ if (topY !== undefined) {
|
|
|
|
|
+ // 遮罩从第一行文字上方一点开始,到图片底部
|
|
|
|
|
+ // 第一行文字是 12px 字体,实际高度约 12-14px,留出一些空间
|
|
|
|
|
+ const textHeight = 14; // 第一行文字的实际高度
|
|
|
|
|
+ maskTop = topY - textHeight; // 从文字上方开始
|
|
|
|
|
+ maskHeight = h - maskTop; // 从 maskTop 到图片底部
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 默认高度(用于 drawReviewEffectText 等情况)
|
|
|
|
|
+ maskHeight = 66;
|
|
|
|
|
+ maskTop = h - maskHeight;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
ctx.fillStyle = "rgba(0,0,0,0.45)";
|
|
ctx.fillStyle = "rgba(0,0,0,0.45)";
|
|
|
ctx.fillRect(
|
|
ctx.fillRect(
|
|
|
0,
|
|
0,
|
|
|
- h - maskHeight, // ✅ 绝对贴底
|
|
|
|
|
|
|
+ maskTop,
|
|
|
w,
|
|
w,
|
|
|
maskHeight
|
|
maskHeight
|
|
|
);
|
|
);
|