|
|
@@ -272,7 +272,7 @@
|
|
|
console.log('开始生成图片...');
|
|
|
// --- 1. 定义尺寸和样式 ---
|
|
|
const canvasWidth = 588; // .v2-box 的宽度大约是 630 - 20*2(padding) - 1*2(border) = 588
|
|
|
- const itemHeight = 70; // 每个评估项的高度
|
|
|
+ const itemHeight = 49; // 每个评估项的高度
|
|
|
const totalHeight = itemHeight * this.scoreData.length;
|
|
|
// 调整为整数,避免边框模糊
|
|
|
const canvasHeight = totalHeight;
|
|
|
@@ -347,80 +347,151 @@
|
|
|
});
|
|
|
})
|
|
|
},
|
|
|
+ // 辅助函数:计算自动换行文字的总高度
|
|
|
+ calculateWrappedTextHeight(ctx, text, lineHeight, maxWidth) {
|
|
|
+ let words = text.split('');
|
|
|
+ let line = '';
|
|
|
+ let height = lineHeight; // 至少有一行
|
|
|
+ for (let n = 0; n < words.length; n++) {
|
|
|
+ let testLine = line + words[n];
|
|
|
+ let metrics = ctx.measureText(testLine);
|
|
|
+ let testWidth = metrics.width;
|
|
|
+ if (testWidth > maxWidth && n > 0) {
|
|
|
+ line = words[n];
|
|
|
+ height += lineHeight;
|
|
|
+ } else {
|
|
|
+ line = testLine;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return height;
|
|
|
+ },
|
|
|
// 辅助函数:绘制单个评估项
|
|
|
drawScoreItem(ctx, scoreItem, y, width, height, dimensionData) {
|
|
|
- const leftBoxWidth = 110;
|
|
|
- const padding = 10;
|
|
|
+ const leftBoxWidth = 110;
|
|
|
const rightBoxX = leftBoxWidth;
|
|
|
const rightBoxWidth = width - leftBoxWidth;
|
|
|
-
|
|
|
- // 绘制左侧标题背景
|
|
|
+ const rightPadding = 10; // 右侧内容的通用内边距
|
|
|
+
|
|
|
+ // 1. --- 绘制左侧部分 ---
|
|
|
ctx.fillStyle = dimensionData.titlecolor;
|
|
|
ctx.fillRect(0, y, leftBoxWidth, height);
|
|
|
|
|
|
- // 绘制左侧标题文字
|
|
|
- ctx.fillStyle = dimensionData.title === '目的与动机' ? '#000000' : '#FFFFFF';
|
|
|
+ // 绘制白色下边框
|
|
|
+ if (y + height < ctx.canvas.height / (uni.getSystemInfoSync().pixelRatio)) {
|
|
|
+ ctx.strokeStyle = '#FFFFFF';
|
|
|
+ ctx.lineWidth = 1;
|
|
|
+ ctx.beginPath();
|
|
|
+ ctx.moveTo(0, y + height - 1);
|
|
|
+ ctx.lineTo(leftBoxWidth, y + height - 1);
|
|
|
+ ctx.stroke();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 绘制左侧标题文字 (要求 1)
|
|
|
+ ctx.fillStyle = '#002846';
|
|
|
ctx.font = '10px sans-serif';
|
|
|
ctx.textAlign = 'center';
|
|
|
ctx.textBaseline = 'middle';
|
|
|
- // 实现文字自动换行
|
|
|
- this.drawWrappedText(ctx, scoreItem.title, leftBoxWidth / 2, y + height / 2, 20, leftBoxWidth - 10);
|
|
|
-
|
|
|
- // 绘制右侧边框
|
|
|
+ this.drawWrappedText(ctx, scoreItem.title, leftBoxWidth / 2, y + height / 2, 20, leftBoxWidth - 20); // 左右留10px边距
|
|
|
+
|
|
|
+ // 2. --- 绘制右侧部分 ---
|
|
|
+ // 绘制右侧外边框
|
|
|
ctx.strokeStyle = dimensionData.bcolor;
|
|
|
ctx.lineWidth = 1;
|
|
|
ctx.strokeRect(rightBoxX, y, rightBoxWidth, height);
|
|
|
-
|
|
|
- // 绘制右侧描述文字
|
|
|
+
|
|
|
+ // --- 计算右侧内容垂直居中需要的参数 (要求 2) ---
|
|
|
+ const descLineHeight = 16;
|
|
|
+ const descMaxWidth = rightBoxWidth - rightPadding * 2;
|
|
|
+ ctx.font = '9px sans-serif'; // 设置好字体用于计算
|
|
|
+ const descHeight = this.calculateWrappedTextHeight(ctx, scoreItem.desc, descLineHeight, descMaxWidth);
|
|
|
+
|
|
|
+ const spacing = 7; // 文字与进度条间距 (要求 4)
|
|
|
+ const progressBarHeight = 6; // 进度条高度 (要求 3)
|
|
|
+
|
|
|
+ // 计算右侧所有内容的总高度
|
|
|
+ const totalContentHeight = descHeight + spacing + progressBarHeight;
|
|
|
+ // 计算内容块的起始 Y 坐标,使其在右侧框内垂直居中
|
|
|
+ const contentStartY = y + (height - totalContentHeight) / 2;
|
|
|
+
|
|
|
+ // --- 开始绘制右侧内容 ---
|
|
|
+ // 绘制右侧描述文字 (要求 2)
|
|
|
ctx.fillStyle = '#193D59';
|
|
|
ctx.font = '9px sans-serif';
|
|
|
ctx.textAlign = 'left';
|
|
|
- ctx.textBaseline = 'top';
|
|
|
- this.drawWrappedText(ctx, scoreItem.desc, rightBoxX + 10, y + 12, 16, rightBoxWidth - 20);
|
|
|
-
|
|
|
- // 绘制进度条
|
|
|
- const progressBarY = y + 45;
|
|
|
- const progressBarWidth = rightBoxWidth - 20;
|
|
|
+ ctx.textBaseline = 'top'; // 基线设为 top 方便计算
|
|
|
+ this.drawWrappedText(ctx, scoreItem.desc, rightBoxX + rightPadding, contentStartY, descLineHeight, descMaxWidth);
|
|
|
+
|
|
|
+ // 绘制进度条 (要求 3)
|
|
|
+ const progressBarY = contentStartY + descHeight + spacing;
|
|
|
+ const progressBarWidth = rightBoxWidth - rightPadding * 2;
|
|
|
const scoreWidth = (scoreItem.score / 5) * progressBarWidth;
|
|
|
-
|
|
|
- // 创建渐变
|
|
|
- const gradient = ctx.createLinearGradient(rightBoxX + 10, 0, rightBoxX + 10 + progressBarWidth, 0);
|
|
|
- // 解析渐变色
|
|
|
+ const barRadius = 3;
|
|
|
+
|
|
|
+ // 绘制灰色背景
|
|
|
+ ctx.fillStyle = '#F0F2F8';
|
|
|
+ ctx.beginPath();
|
|
|
+ ctx.moveTo(rightBoxX + rightPadding + barRadius, progressBarY);
|
|
|
+ ctx.arcTo(rightBoxX + rightPadding + progressBarWidth, progressBarY, rightBoxX + rightPadding + progressBarWidth, progressBarY + progressBarHeight, barRadius);
|
|
|
+ ctx.arcTo(rightBoxX + rightPadding + progressBarWidth, progressBarY + progressBarHeight, rightBoxX + rightPadding, progressBarY + progressBarHeight, barRadius);
|
|
|
+ ctx.arcTo(rightBoxX + rightPadding, progressBarY + progressBarHeight, rightBoxX + rightPadding, progressBarY, barRadius);
|
|
|
+ ctx.arcTo(rightBoxX + rightPadding, progressBarY, rightBoxX + rightPadding + progressBarWidth, progressBarY, barRadius);
|
|
|
+ ctx.closePath();
|
|
|
+ ctx.fill();
|
|
|
+
|
|
|
+ // 绘制实际得分的渐变色进度条
|
|
|
+ const gradient = ctx.createLinearGradient(rightBoxX + rightPadding, 0, rightBoxX + rightPadding + progressBarWidth, 0);
|
|
|
const gradientColors = this.parseGradient(dimensionData.pfztfb);
|
|
|
gradientColors.forEach(c => gradient.addColorStop(c.stop, c.color));
|
|
|
ctx.fillStyle = gradient;
|
|
|
- ctx.fillRect(rightBoxX + 10, progressBarY, scoreWidth, 6);
|
|
|
-
|
|
|
- // 绘制分数气泡
|
|
|
- const bubbleSize = 24;
|
|
|
- const bubbleX = rightBoxX + 10 + scoreWidth - (bubbleSize / 2);
|
|
|
- const bubbleY = progressBarY - (bubbleSize / 2) + 3; // +3 微调居中
|
|
|
+ ctx.save();
|
|
|
+ ctx.clip(); // 使用上面的圆角矩形路径进行裁剪
|
|
|
+ ctx.fillRect(rightBoxX + rightPadding, progressBarY, scoreWidth, progressBarHeight);
|
|
|
+ ctx.restore();
|
|
|
+
|
|
|
+ // 绘制分数框 (要求 4)
|
|
|
+ const scoreFontSize = 12;
|
|
|
+ const scorePaddingY = 4; // 上下内边距
|
|
|
+ const scorePaddingX = 7; // 左右内边距
|
|
|
+ const scoreBoxRadius = 4;
|
|
|
|
|
|
- // 气泡阴影
|
|
|
+ ctx.font = `bold ${scoreFontSize}px sans-serif`;
|
|
|
+ const scoreTextMetrics = ctx.measureText(scoreItem.score);
|
|
|
+ const scoreBoxWidth = scoreTextMetrics.width + scorePaddingX * 2;
|
|
|
+ const scoreBoxHeight = scoreFontSize + scorePaddingY * 2;
|
|
|
+
|
|
|
+ // 计算分数框的位置,使其右端对齐在进度条的末端
|
|
|
+ const scoreBoxX = rightBoxX + rightPadding + scoreWidth - scoreBoxWidth;
|
|
|
+ // 垂直居中于进度条
|
|
|
+ const scoreBoxY = progressBarY + (progressBarHeight / 2) - (scoreBoxHeight / 2);
|
|
|
+
|
|
|
+ // 绘制阴影
|
|
|
ctx.save();
|
|
|
ctx.shadowColor = dimensionData.bcolor;
|
|
|
ctx.shadowBlur = 6;
|
|
|
ctx.shadowOffsetX = 0;
|
|
|
ctx.shadowOffsetY = 2;
|
|
|
|
|
|
- // 气泡背景和边框
|
|
|
+ // 绘制分数框背景和边框
|
|
|
ctx.fillStyle = '#FFFFFF';
|
|
|
ctx.strokeStyle = dimensionData.bcolor;
|
|
|
- ctx.borderRadius= 4;
|
|
|
ctx.lineWidth = 1;
|
|
|
ctx.beginPath();
|
|
|
- ctx.arc(bubbleX + bubbleSize / 2, bubbleY + bubbleSize / 2, bubbleSize / 2, 0, Math.PI * 2);
|
|
|
+ ctx.moveTo(scoreBoxX + scoreBoxRadius, scoreBoxY);
|
|
|
+ ctx.arcTo(scoreBoxX + scoreBoxWidth, scoreBoxY, scoreBoxX + scoreBoxWidth, scoreBoxY + scoreBoxHeight, scoreBoxRadius);
|
|
|
+ ctx.arcTo(scoreBoxX + scoreBoxWidth, scoreBoxY + scoreBoxHeight, scoreBoxX, scoreBoxY + scoreBoxHeight, scoreBoxRadius);
|
|
|
+ ctx.arcTo(scoreBoxX, scoreBoxY + scoreBoxHeight, scoreBoxX, scoreBoxY, scoreBoxRadius);
|
|
|
+ ctx.arcTo(scoreBoxX, scoreBoxY, scoreBoxX + scoreBoxWidth, scoreBoxY, scoreBoxRadius);
|
|
|
+ ctx.closePath();
|
|
|
ctx.fill();
|
|
|
ctx.stroke();
|
|
|
- ctx.closePath();
|
|
|
ctx.restore();
|
|
|
-
|
|
|
- // 气泡内分数文字
|
|
|
- ctx.fillStyle = '#333333';
|
|
|
- ctx.font = 'bold 12px sans-serif';
|
|
|
+
|
|
|
+ // 绘制分数文字
|
|
|
+ ctx.fillStyle = '#002846';
|
|
|
+ ctx.font = `bold ${scoreFontSize}px sans-serif`;
|
|
|
ctx.textAlign = 'center';
|
|
|
ctx.textBaseline = 'middle';
|
|
|
- ctx.fillText(scoreItem.score, bubbleX + bubbleSize / 2, bubbleY + bubbleSize / 2);
|
|
|
+ ctx.fillText(scoreItem.score, scoreBoxX + scoreBoxWidth / 2, scoreBoxY + scoreBoxHeight / 2);
|
|
|
},
|
|
|
// 辅助函数:绘制自动换行的文字
|
|
|
drawWrappedText(ctx, text, x, y, lineHeight, maxWidth) {
|