|
|
@@ -169,48 +169,48 @@
|
|
|
scale:1,
|
|
|
originalContainerHeight: 0,
|
|
|
containerScaledHeight: 'auto',
|
|
|
- sixWd: [
|
|
|
- {
|
|
|
- img:'https://gitee.com/hw_0302/chuang-heng-wechat-images/raw/master/versionTwo/intro_img_p.png',
|
|
|
- type:'P',
|
|
|
- title:'宗旨与动机',
|
|
|
- desc:'指团队共享的目的和存在的意义, 包含对共同的愿景,目标和优先级的清晰度。',
|
|
|
- color:'#761E6A'
|
|
|
- },
|
|
|
- {
|
|
|
- img:'https://gitee.com/hw_0302/chuang-heng-wechat-images/raw/master/versionTwo/intro_img_e.png',
|
|
|
- type:'E',
|
|
|
- title:'外部流程、系统与结构',
|
|
|
- desc:'指团队与其外部利益相关者 - 客户,供应商,股东,组织内的上级及其他团队的互动关系和协作机制。',
|
|
|
- color:'#009191'
|
|
|
- },
|
|
|
- {
|
|
|
- img:'https://gitee.com/hw_0302/chuang-heng-wechat-images/raw/master/versionTwo/intro_img_r.png',
|
|
|
- type:'R',
|
|
|
- title:'人际关系',
|
|
|
- desc:'指团队成员共同工作时的关系状态–他们是否相互尊重和信任对方的能力,是否足够心理安全以能够坦诚沟通,是否真正关心彼此的幸福感等。',
|
|
|
- color:'#FFD750'
|
|
|
- },
|
|
|
- {
|
|
|
- img:'https://gitee.com/hw_0302/chuang-heng-wechat-images/raw/master/versionTwo/intro_img_i.png',
|
|
|
- type:'I',
|
|
|
- title:'内部流程、系统与结构',
|
|
|
- desc:'指团队如何管理工作任务和流程(包括但不限于会议、任务分配和团队情绪等),互相支持以及高质量的沟通和决策。',
|
|
|
- color:'#4EB2B2'
|
|
|
- },
|
|
|
- {
|
|
|
- img:'https://gitee.com/hw_0302/chuang-heng-wechat-images/raw/master/versionTwo/intro_img_l.png',
|
|
|
- type:'L',
|
|
|
- title:'学习',
|
|
|
- desc:'指团队如何应对多变的环境和保持持续的进步和成⻓,能够从经验中反思、提炼并应用知识的能力。',
|
|
|
- color:'#AFCDF5'
|
|
|
- },
|
|
|
- {
|
|
|
- img:'https://gitee.com/hw_0302/chuang-heng-wechat-images/raw/master/versionTwo/intro_img_l2.png',
|
|
|
- type:'L',
|
|
|
- title:'领导力',
|
|
|
- desc:'指团队认为需要怎样的领导行为能够让他们,作为个人或者团队做到最好。团队可以和他们的领导者讨论他们的责任及承担方式,以帮助领导者成为他们需要的领导者。',
|
|
|
- color:'#002846'
|
|
|
+ sixWd: [
|
|
|
+ {
|
|
|
+ img:'https://gitee.com/hw_0302/chuang-heng-wechat-images/raw/master/versionTwo/intro_img_p.png',
|
|
|
+ type:'P',
|
|
|
+ title:'宗旨与动机',
|
|
|
+ desc:'指团队共享的目的和存在的意义, 包含对共同的愿景,目标和优先级的清晰度。',
|
|
|
+ color:'#761E6A'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ img:'https://gitee.com/hw_0302/chuang-heng-wechat-images/raw/master/versionTwo/intro_img_e.png',
|
|
|
+ type:'E',
|
|
|
+ title:'外部流程、系统与结构',
|
|
|
+ desc:'指团队与其外部利益相关者 - 客户,供应商,股东,组织内的上级及其他团队的互动关系和协作机制。',
|
|
|
+ color:'#009191'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ img:'https://gitee.com/hw_0302/chuang-heng-wechat-images/raw/master/versionTwo/intro_img_r.png',
|
|
|
+ type:'R',
|
|
|
+ title:'人际关系',
|
|
|
+ desc:'指团队成员共同工作时的关系状态–他们是否相互尊重和信任对方的能力,是否足够心理安全以能够坦诚沟通,是否真正关心彼此的幸福感等。',
|
|
|
+ color:'#FFD750'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ img:'https://gitee.com/hw_0302/chuang-heng-wechat-images/raw/master/versionTwo/intro_img_i.png',
|
|
|
+ type:'I',
|
|
|
+ title:'内部流程、系统与结构',
|
|
|
+ desc:'指团队如何管理工作任务和流程(包括但不限于会议、任务分配和团队情绪等),互相支持以及高质量的沟通和决策。',
|
|
|
+ color:'#4EB2B2'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ img:'https://gitee.com/hw_0302/chuang-heng-wechat-images/raw/master/versionTwo/intro_img_l.png',
|
|
|
+ type:'L',
|
|
|
+ title:'学习',
|
|
|
+ desc:'指团队如何应对多变的环境和保持持续的进步和成⻓,能够从经验中反思、提炼并应用知识的能力。',
|
|
|
+ color:'#AFCDF5'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ img:'https://gitee.com/hw_0302/chuang-heng-wechat-images/raw/master/versionTwo/intro_img_l2.png',
|
|
|
+ type:'L',
|
|
|
+ title:'领导力',
|
|
|
+ desc:'指团队认为需要怎样的领导行为能够让他们,作为个人或者团队做到最好。团队可以和他们的领导者讨论他们的责任及承担方式,以帮助领导者成为他们需要的领导者。',
|
|
|
+ color:'#002846'
|
|
|
}
|
|
|
],
|
|
|
pdfImages:[],
|
|
|
@@ -231,38 +231,38 @@
|
|
|
this.$api.get(`/core/report/previewReport/${this.reportId}`).then(({data:res})=>{
|
|
|
if(res.code!==0) return this.$showToast(res.msg)
|
|
|
this.reportData = res.data;
|
|
|
- const tempDimensionAnalysis = [
|
|
|
- {
|
|
|
- title:'P-宗旨与动机',thbgcolor:'#761E6A',thtextcolor:'#FFFFFF',titlecolor:'#761E6A',dimensionCode:"purpose",
|
|
|
- desc:`「宗旨与动机」维度,我们旨在探究团队是否有清晰的存在理由和明确的方向,能够激发团队成员的动力并吸引他们的想象力,以及个人与集体的身份认同是否围绕共同的目标,并在实现目标的优先次序上达成一致。`
|
|
|
- },
|
|
|
- {
|
|
|
- title:'E-外部流程、系统与结构',thbgcolor:'#009191',thtextcolor:'#FFFFFF',titlecolor:'#009191',dimensionCode:"external",
|
|
|
- desc:`「外部流程、系统与结构」维度,我们旨在探究团队如何与各种利益相关者互动,他们与团队的利益相关方各自如何寻求了解对方,以及现有系统和流程的有效性,以帮助管理不同利益相关者的期望和需求。`
|
|
|
- },
|
|
|
- {
|
|
|
- title:'R-人际关系',thbgcolor:'#FFD750',thtextcolor:'#002846',titlecolor:'#002846',dimensionCode:"relationship",
|
|
|
- desc:`「人际关系」维度,我们旨在探究团队成员相互沟通交流的状态,团队成员的信任程度、尊重和关心的程度、心理安全度,以及团队成员之间的关系如何促进(或破坏)协作。`
|
|
|
- },
|
|
|
- {
|
|
|
- title:'I-内部流程、系统与结构',thbgcolor:'#4EB2B2',thtextcolor:'#FFFFFF',titlecolor:'#33A7A7',dimensionCode:"internal",
|
|
|
- desc:`「内部流程、系统与结构」维度,我们旨在探究团队在管理工作任务和流程中如何平衡责任与自主权进行协作。主要关注团队应对变化时的敏捷程度、日常沟通方式以及内部决策过程的有效性。`
|
|
|
- },
|
|
|
- {
|
|
|
- title:'L-学习',thbgcolor:'#AFCDF5',thtextcolor:'#002846',titlecolor:'#002846',dimensionCode:"learning",
|
|
|
- desc:`「学习」维度,我们旨在探究团队如何提高其绩效(完成当下的任务)、能力(提高技能和资源以处理明天的任务)和容量(⻓期的愿景,用更少的资源做更多的事情)以应对当前和未来的任务。同时还关注团队如何管理这些能力和提高效率。`
|
|
|
- },
|
|
|
- {
|
|
|
- title:'L-领导力',thbgcolor:'#002846',thtextcolor:'#FFFFFF',titlecolor:'#002846',dimensionCode:"leadership",
|
|
|
- desc:`「领导力」维度,我们旨在探究团队领导者的素质和行为(包括但不限于正式与非正式的引导、赋能与责任承担方式)如何对团队功能和其他因素产生影响,以及这是积极的还是消极的。`
|
|
|
+ const tempDimensionAnalysis = [
|
|
|
+ {
|
|
|
+ title:'P-宗旨与动机',thbgcolor:'#761E6A',thtextcolor:'#FFFFFF',titlecolor:'#761E6A',dimensionCode:"purpose",
|
|
|
+ desc:`「宗旨与动机」维度,我们旨在探究团队是否有清晰的存在理由和明确的方向,能够激发团队成员的动力并吸引他们的想象力,以及个人与集体的身份认同是否围绕共同的目标,并在实现目标的优先次序上达成一致。`
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title:'E-外部流程、系统与结构',thbgcolor:'#009191',thtextcolor:'#FFFFFF',titlecolor:'#009191',dimensionCode:"external",
|
|
|
+ desc:`「外部流程、系统与结构」维度,我们旨在探究团队如何与各种利益相关者互动,他们与团队的利益相关方各自如何寻求了解对方,以及现有系统和流程的有效性,以帮助管理不同利益相关者的期望和需求。`
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title:'R-人际关系',thbgcolor:'#FFD750',thtextcolor:'#002846',titlecolor:'#002846',dimensionCode:"relationship",
|
|
|
+ desc:`「人际关系」维度,我们旨在探究团队成员相互沟通交流的状态,团队成员的信任程度、尊重和关心的程度、心理安全度,以及团队成员之间的关系如何促进(或破坏)协作。`
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title:'I-内部流程、系统与结构',thbgcolor:'#4EB2B2',thtextcolor:'#FFFFFF',titlecolor:'#33A7A7',dimensionCode:"internal",
|
|
|
+ desc:`「内部流程、系统与结构」维度,我们旨在探究团队在管理工作任务和流程中如何平衡责任与自主权进行协作。主要关注团队应对变化时的敏捷程度、日常沟通方式以及内部决策过程的有效性。`
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title:'L-学习',thbgcolor:'#AFCDF5',thtextcolor:'#002846',titlecolor:'#002846',dimensionCode:"learning",
|
|
|
+ desc:`「学习」维度,我们旨在探究团队如何提高其绩效(完成当下的任务)、能力(提高技能和资源以处理明天的任务)和容量(⻓期的愿景,用更少的资源做更多的事情)以应对当前和未来的任务。同时还关注团队如何管理这些能力和提高效率。`
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title:'L-领导力',thbgcolor:'#002846',thtextcolor:'#FFFFFF',titlecolor:'#002846',dimensionCode:"leadership",
|
|
|
+ desc:`「领导力」维度,我们旨在探究团队领导者的素质和行为(包括但不限于正式与非正式的引导、赋能与责任承担方式)如何对团队功能和其他因素产生影响,以及这是积极的还是消极的。`
|
|
|
}
|
|
|
]
|
|
|
if(this.reportData&&this.reportData.dimensionAnalysis){
|
|
|
- this.reportData.dimensionAnalysis.forEach((d,i)=>{
|
|
|
- d.scoreSpreads.forEach(s=>{
|
|
|
- s.theme = s.theme.replaceAll(',',' ').replaceAll(',',' ');
|
|
|
- })
|
|
|
- let temp = tempDimensionAnalysis.find(t=>t.dimensionCode === d.dimensionCode)||{};
|
|
|
+ this.reportData.dimensionAnalysis.forEach((d,i)=>{
|
|
|
+ d.scoreSpreads.forEach(s=>{
|
|
|
+ s.theme = s.theme.replaceAll(',',' ').replaceAll(',',' ');
|
|
|
+ })
|
|
|
+ let temp = tempDimensionAnalysis.find(t=>t.dimensionCode === d.dimensionCode)||{};
|
|
|
this.reportData.dimensionAnalysis[i] = {...d,...temp}
|
|
|
})
|
|
|
}
|
|
|
@@ -321,267 +321,296 @@
|
|
|
}
|
|
|
},
|
|
|
// 绘制主函数
|
|
|
- async generateScoreImage(dimensionData,scoreData) {
|
|
|
- return new Promise(resolve=>{
|
|
|
+ async generateScoreImage(dimensionData, scoreData) {
|
|
|
+ return new Promise(resolve => {
|
|
|
console.log('开始生成图片...');
|
|
|
// --- 1. 定义尺寸和样式 ---
|
|
|
- const canvasWidth = 588; // .v2-box 的宽度大约是 630 - 20*2(padding) - 1*2(border) = 588
|
|
|
- const itemHeight = 49; // 每个评估项的高度
|
|
|
- const totalHeight = itemHeight * scoreData.length;
|
|
|
- // 调整为整数,避免边框模糊
|
|
|
- const canvasHeight = totalHeight;
|
|
|
-
|
|
|
+ const canvasWidth = 588;
|
|
|
+ const headerHeight = 26;
|
|
|
+ const rowHeight = 54;
|
|
|
+ const totalHeight = headerHeight + rowHeight * scoreData.length;
|
|
|
+
|
|
|
+ const leftColWidth = 280;
|
|
|
+ const rightColWidth = 308;
|
|
|
+
|
|
|
// --- 2. 获取 Canvas 节点 ---
|
|
|
- // 使用 ID 选择器更精确
|
|
|
const query = uni.createSelectorQuery().in(this);
|
|
|
query.select('#score-canvas')
|
|
|
- .fields({ node: true, size: true })
|
|
|
- .exec(async (res) => {
|
|
|
- // 【重要】增加健壮性检查
|
|
|
- if (!res || !res[0] || !res[0].node) {
|
|
|
- console.error('获取 Canvas 节点失败,请检查 canvas-id 和 type="2d" 是否正确设置。');
|
|
|
- uni.showToast({ title: '组件初始化失败', icon: 'none' });
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- const canvasNode = res[0].node;
|
|
|
- const context = canvasNode.getContext('2d');
|
|
|
- const dpr = uni.getSystemInfoSync().pixelRatio;
|
|
|
-
|
|
|
- // --- 3. 设置画布尺寸和缩放以适应高分屏 ---
|
|
|
- canvasNode.width = canvasWidth * dpr;
|
|
|
- canvasNode.height = canvasHeight * dpr;
|
|
|
- context.scale(dpr, dpr);
|
|
|
-
|
|
|
- // --- 4. 开始绘制 ---
|
|
|
- // 绘制大背景
|
|
|
- context.fillStyle = '#FFFFFF';
|
|
|
- context.fillRect(0, 0, canvasWidth, canvasHeight);
|
|
|
-
|
|
|
- // --- 5. 循环绘制每一项 ---
|
|
|
- for (let i = 0; i < scoreData.length; i++) {
|
|
|
- const item = scoreData[i];
|
|
|
- const yPos = i * itemHeight;
|
|
|
- // 注意:这里不再需要 await,因为 canvas 2d 绘图是同步的
|
|
|
- this.drawScoreItem(context, item, yPos, canvasWidth, itemHeight, dimensionData);
|
|
|
- }
|
|
|
-
|
|
|
- // 【补充】绘制最外层的上下边框,避免被循环内的矩形覆盖
|
|
|
- context.strokeStyle = dimensionData.bcolor;
|
|
|
- context.lineWidth = 1;
|
|
|
- context.strokeRect(0, 0, canvasWidth, canvasHeight);
|
|
|
+ .fields({
|
|
|
+ node: true,
|
|
|
+ size: true
|
|
|
+ })
|
|
|
+ .exec(async (res) => {
|
|
|
+ if (!res || !res[0] || !res[0].node) {
|
|
|
+ console.error('获取 Canvas 节点失败,请检查 canvas-id 和 type="2d" 是否正确设置。');
|
|
|
+ uni.showToast({
|
|
|
+ title: '组件初始化失败',
|
|
|
+ icon: 'none'
|
|
|
+ });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const canvasNode = res[0].node;
|
|
|
+ const ctx = canvasNode.getContext('2d');
|
|
|
+ const dpr = uni.getSystemInfoSync().pixelRatio;
|
|
|
+
|
|
|
+ // --- 3. 设置画布尺寸和缩放以适应高分屏 ---
|
|
|
+ canvasNode.width = canvasWidth * dpr;
|
|
|
+ canvasNode.height = totalHeight * dpr;
|
|
|
+ ctx.scale(dpr, dpr);
|
|
|
+
|
|
|
+ // --- 4. 开始绘制 ---
|
|
|
+ // 绘制大背景
|
|
|
+ ctx.fillStyle = '#FFFFFF';
|
|
|
+ ctx.fillRect(0, 0, canvasWidth, totalHeight);
|
|
|
+
|
|
|
+ // --- 5. 绘制表头 ---
|
|
|
+ // 左侧表头背景
|
|
|
+ ctx.fillStyle = dimensionData.thbgcolor;
|
|
|
+ ctx.fillRect(0, 0, leftColWidth, headerHeight);
|
|
|
+
|
|
|
+ // 右侧表头背景
|
|
|
+ ctx.fillRect(leftColWidth, 0, rightColWidth, headerHeight);
|
|
|
+
|
|
|
+ // 左侧表头文字
|
|
|
+ ctx.fillStyle = dimensionData.thtextcolor;
|
|
|
+ ctx.font = 'bold 9px sans-serif';
|
|
|
+ ctx.textBaseline = 'middle';
|
|
|
+ ctx.textAlign = 'left';
|
|
|
+ ctx.fillText('主题', 15, headerHeight / 2);
|
|
|
+
|
|
|
+ // 左侧表头分割线
|
|
|
+ ctx.strokeStyle = 'rgba(255,255,255,0.24)';
|
|
|
+ ctx.lineWidth = 1;
|
|
|
+ ctx.beginPath();
|
|
|
+ ctx.moveTo(leftColWidth - 54, 0);
|
|
|
+ ctx.lineTo(leftColWidth - 54, headerHeight);
|
|
|
+ ctx.stroke();
|
|
|
|
|
|
+ // 影响力分右侧分割线 (Header)
|
|
|
+ ctx.beginPath();
|
|
|
+ ctx.moveTo(leftColWidth, 0);
|
|
|
+ ctx.lineTo(leftColWidth, headerHeight);
|
|
|
+ ctx.stroke();
|
|
|
+
|
|
|
+ ctx.textAlign = 'center';
|
|
|
+ ctx.fillText('影响力分', leftColWidth - 27, headerHeight / 2);
|
|
|
+
|
|
|
+ // 右侧表头分割线
|
|
|
+ ctx.beginPath();
|
|
|
+ ctx.moveTo(leftColWidth + rightColWidth / 2, 0);
|
|
|
+ ctx.lineTo(leftColWidth + rightColWidth / 2, headerHeight);
|
|
|
+ ctx.stroke();
|
|
|
+
|
|
|
+ // 右侧表头文字
|
|
|
+ ctx.fillText('认同度分', leftColWidth + rightColWidth / 4, headerHeight / 2);
|
|
|
+ ctx.fillText('重要性分', leftColWidth + rightColWidth * 0.75, headerHeight / 2);
|
|
|
+
|
|
|
+
|
|
|
+ // --- 6. 循环绘制每一行 ---
|
|
|
+ for (let i = 0; i < scoreData.length; i++) {
|
|
|
+ const item = scoreData[i];
|
|
|
+ const yPos = headerHeight + i * rowHeight;
|
|
|
+ this.drawTableItem(ctx, item, yPos, leftColWidth, rightColWidth, rowHeight, dimensionData);
|
|
|
+ }
|
|
|
+
|
|
|
+ // --- 7. 绘制右侧底部的刻度 (绘制在最上层) ---
|
|
|
+ // 先绘制白色背景遮挡网格线
|
|
|
+ ctx.fillStyle = '#FFFFFF';
|
|
|
+ const axisHeight = 20; // 底部文字区域高度
|
|
|
+ ctx.fillRect(leftColWidth, totalHeight - axisHeight, rightColWidth, axisHeight);
|
|
|
|
|
|
- // --- 6. 生成图片 ---
|
|
|
- uni.hideLoading();
|
|
|
- uni.canvasToTempFilePath({
|
|
|
- canvas: canvasNode,
|
|
|
- x: 0,
|
|
|
- y: 0,
|
|
|
- width: canvasWidth,
|
|
|
- height: canvasHeight,
|
|
|
- destWidth: canvasWidth * dpr,
|
|
|
- destHeight: canvasHeight * dpr,
|
|
|
- success: async (result) => {
|
|
|
- console.log('图片生成成功!', result.tempFilePath);
|
|
|
- const fileurl = await this.uploadFilePromise(result.tempFilePath);
|
|
|
- console.log(fileurl, 'fileurl');
|
|
|
+ ctx.fillStyle = '#002846';
|
|
|
+ ctx.font = 'bold 10px sans-serif';
|
|
|
+ ctx.textAlign = 'center';
|
|
|
+ ctx.textBaseline = 'bottom';
|
|
|
+
|
|
|
+ const centerX = leftColWidth + rightColWidth / 2;
|
|
|
+ const colWidth = rightColWidth / 12;
|
|
|
+
|
|
|
+ // 刻度 0-5
|
|
|
+ for (let k = 0; k <= 5; k++) {
|
|
|
+ let xR = centerX + k * colWidth;
|
|
|
+ let xL = centerX - k * colWidth;
|
|
|
+ let yTxt = totalHeight - 5; // 距离底部一点距离
|
|
|
+
|
|
|
+ if (k === 0) {
|
|
|
+ ctx.fillText(0, centerX, yTxt);
|
|
|
+ } else {
|
|
|
+ ctx.fillText(k, xR, yTxt);
|
|
|
+ ctx.fillText(k, xL, yTxt);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // --- 8. 绘制最外层边框 ---
|
|
|
+ ctx.strokeStyle = '#E6EAED';
|
|
|
+ ctx.lineWidth = 1;
|
|
|
+ ctx.strokeRect(0, 0, canvasWidth, totalHeight);
|
|
|
+
|
|
|
+
|
|
|
+ // --- 9. 生成图片 ---
|
|
|
+ uni.hideLoading();
|
|
|
+ uni.canvasToTempFilePath({
|
|
|
+ canvas: canvasNode,
|
|
|
+ x: 0,
|
|
|
+ y: 0,
|
|
|
+ width: canvasWidth,
|
|
|
+ height: totalHeight,
|
|
|
+ destWidth: canvasWidth * dpr,
|
|
|
+ destHeight: totalHeight * dpr,
|
|
|
+ success: async (result) => {
|
|
|
+ console.log('图片生成成功!', result.tempFilePath);
|
|
|
+ const fileurl = await this.uploadFilePromise(result.tempFilePath);
|
|
|
+ console.log(fileurl, 'fileurl');
|
|
|
resolve(fileurl)
|
|
|
- },
|
|
|
- fail: (err) => {
|
|
|
- console.error('图片生成失败', err);
|
|
|
- uni.showToast({ title: '图片生成失败', icon: 'none' });
|
|
|
- }
|
|
|
- }, this);
|
|
|
- });
|
|
|
+ },
|
|
|
+ fail: (err) => {
|
|
|
+ console.error('图片生成失败', err);
|
|
|
+ uni.showToast({
|
|
|
+ title: '图片生成失败',
|
|
|
+ icon: 'none'
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }, this);
|
|
|
+ });
|
|
|
})
|
|
|
},
|
|
|
- // 辅助函数:计算自动换行文字的总高度
|
|
|
- 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 = 72;
|
|
|
- const rightBoxX = leftBoxWidth;
|
|
|
- const rightBoxWidth = width - leftBoxWidth;
|
|
|
- const rightPadding = 10; // 右侧内容的通用内边距
|
|
|
-
|
|
|
- // 1. --- 绘制左侧部分 ---
|
|
|
- ctx.fillStyle = dimensionData.titlecolor;
|
|
|
- ctx.fillRect(0, y, leftBoxWidth, height);
|
|
|
-
|
|
|
- // 绘制白色下边框
|
|
|
- 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)
|
|
|
- let theme = (scoreItem.theme||'').replaceAll(',','').replaceAll(',','');
|
|
|
- const isBlackLeftTitle = (dimensionData.title == '人际关系' || dimensionData.title == '学习' || dimensionData.title == '内部流程、系统与结构');
|
|
|
- ctx.fillStyle = isBlackLeftTitle ? '#002846' : '#FFFFFF';
|
|
|
- ctx.font = '10px sans-serif';
|
|
|
- ctx.textAlign = 'center';
|
|
|
- ctx.textBaseline = 'middle';
|
|
|
- this.drawWrappedText(ctx, theme, leftBoxWidth / 2, y + height / 2, 12, leftBoxWidth - 32); // 左右留16px边距
|
|
|
-
|
|
|
- // 2. --- 绘制右侧部分 ---
|
|
|
- // 绘制右侧外边框
|
|
|
- ctx.strokeStyle = dimensionData.bcolor;
|
|
|
+ drawTableItem(ctx, item, y, leftW, rightW, h, dimensionData) {
|
|
|
+ // 1. 左侧
|
|
|
+ // 边框
|
|
|
+ ctx.strokeStyle = '#E6EAED';
|
|
|
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.question, descLineHeight, descMaxWidth);
|
|
|
+ // 左侧整体边框
|
|
|
+ ctx.strokeRect(0, y, leftW, h);
|
|
|
+
|
|
|
+ // 竖线分割 Theme/Question 和 Score
|
|
|
+ ctx.beginPath();
|
|
|
+ ctx.moveTo(leftW - 54, y);
|
|
|
+ ctx.lineTo(leftW - 54, y + h);
|
|
|
+ ctx.stroke();
|
|
|
+
|
|
|
+ // --- 计算高度 ---
|
|
|
+ // 必须先设置字体,否则 measureText 计算不准确
|
|
|
+ ctx.font = 'bold 10px sans-serif';
|
|
|
+ let themeHeight = this.calculateWrappedTextHeight(ctx, item.theme, 14, leftW - 54 - 20);
|
|
|
|
|
|
- const spacing = 7; // 文字与进度条间距 (要求 4)
|
|
|
- const progressBarHeight = 6; // 进度条高度 (要求 3)
|
|
|
+ ctx.font = '8px sans-serif';
|
|
|
+ let questionHeight = this.calculateWrappedTextHeight(ctx, item.question, 10, leftW - 54 - 20);
|
|
|
|
|
|
- // 计算右侧所有内容的总高度
|
|
|
- const totalContentHeight = descHeight + spacing + progressBarHeight;
|
|
|
- // 计算内容块的起始 Y 坐标,使其在右侧框内垂直居中
|
|
|
- const contentStartY = y + (height - totalContentHeight) / 2;
|
|
|
-
|
|
|
- // --- 开始绘制右侧内容 ---
|
|
|
- // 绘制右侧描述文字 (要求 2)
|
|
|
- ctx.fillStyle = '#193D59';
|
|
|
- ctx.font = '9px sans-serif';
|
|
|
+ let totalTextHeight = themeHeight + 4 + questionHeight; // 4 is spacing
|
|
|
+ let startY = y + (h - totalTextHeight) / 2;
|
|
|
+
|
|
|
+ // --- 绘制左侧文字 ---
|
|
|
+ // Theme Title
|
|
|
+ ctx.fillStyle = dimensionData.titlecolor;
|
|
|
+ ctx.font = 'bold 10px sans-serif';
|
|
|
ctx.textAlign = 'left';
|
|
|
- ctx.textBaseline = 'top'; // 基线设为 top 方便计算
|
|
|
- this.drawWrappedText(ctx, scoreItem.question, rightBoxX + rightPadding, contentStartY, descLineHeight, descMaxWidth);
|
|
|
-
|
|
|
- // 绘制进度条 (要求 3)
|
|
|
- const progressBarY = contentStartY + descHeight + spacing;
|
|
|
- const progressBarWidth = rightBoxWidth - rightPadding * 2;
|
|
|
- const scoreWidth = ((scoreItem.avgScore>25?25:scoreItem.avgScore)/ 25) * progressBarWidth;
|
|
|
- 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.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.textBaseline = 'top';
|
|
|
+ this.drawWrappedTextTop(ctx, item.theme, 10, startY, 14, leftW - 54 - 20);
|
|
|
+
|
|
|
+ // Question Tip
|
|
|
+ ctx.fillStyle = '#002846';
|
|
|
+ ctx.font = '8px sans-serif';
|
|
|
+ this.drawWrappedTextTop(ctx, item.question, 10, startY + themeHeight + 4, 10, leftW - 54 - 20);
|
|
|
+
|
|
|
+ // Score
|
|
|
+ ctx.fillStyle = '#002846';
|
|
|
+ ctx.font = 'bold 12px DIN, sans-serif';
|
|
|
+ ctx.textAlign = 'center';
|
|
|
+ ctx.textBaseline = 'middle';
|
|
|
+ ctx.fillText((item.memeber && item.memeber.avgScore) || 0, leftW - 27, y + h / 2);
|
|
|
+
|
|
|
+ // 2. 右侧
|
|
|
+ const rightX = leftW;
|
|
|
|
|
|
- ctx.font = `bold ${scoreFontSize}px sans-serif`;
|
|
|
- const scoreTextMetrics = ctx.measureText(scoreItem.avgScore>25?25:scoreItem.avgScore);
|
|
|
- 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.beginPath();
|
|
|
+ ctx.rect(rightX, y, rightW, h);
|
|
|
+ ctx.clip();
|
|
|
|
|
|
- // 绘制分数框背景和边框
|
|
|
- ctx.fillStyle = '#FFFFFF';
|
|
|
- ctx.strokeStyle = dimensionData.bcolor;
|
|
|
+ // 右侧不需要横向分割线,只绘制竖向网格线
|
|
|
+ // 绘制背景网格线 (竖向,贯穿整个高度)
|
|
|
+ ctx.strokeStyle = '#F0F2F8';
|
|
|
ctx.lineWidth = 1;
|
|
|
- ctx.beginPath();
|
|
|
- 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();
|
|
|
+ const colWidth = rightW / 12;
|
|
|
+ for (let k = 1; k < 12; k++) {
|
|
|
+ ctx.beginPath();
|
|
|
+ ctx.moveTo(rightX + k * colWidth, y);
|
|
|
+ ctx.lineTo(rightX + k * colWidth, y + h);
|
|
|
+ ctx.stroke();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 绘制中间线
|
|
|
+ const centerX = rightX + rightW / 2;
|
|
|
+
|
|
|
+ // Bar Charts
|
|
|
+ const barHeight = 10;
|
|
|
+ const barY = y + (h - barHeight) / 2;
|
|
|
+
|
|
|
+ // Agreement (Left, Red/Purple)
|
|
|
+ // Width = avgAgreement / 6 * 100% of half width?
|
|
|
+ const maxVal = 6;
|
|
|
+ const halfW = rightW / 2;
|
|
|
+
|
|
|
+ const avgAgreement = (item.memeber && item.memeber.avgAgreement) || 0;
|
|
|
+ const avgVital = (item.memeber && item.memeber.avgVital) || 0;
|
|
|
+
|
|
|
+ const agreeW = (avgAgreement / maxVal) * halfW;
|
|
|
+ const vitalW = (avgVital / maxVal) * halfW;
|
|
|
+
|
|
|
+ // Draw Agreement Bar (grows from center to left)
|
|
|
+ ctx.fillStyle = '#BA8EB4';
|
|
|
+ ctx.fillRect(centerX - agreeW, barY, agreeW, barHeight);
|
|
|
+
|
|
|
+ // Draw Vital Bar (grows from center to right)
|
|
|
+ ctx.fillStyle = '#80C8C8';
|
|
|
+ ctx.fillRect(centerX, barY, vitalW, barHeight);
|
|
|
+
|
|
|
+ // 恢复裁剪
|
|
|
ctx.restore();
|
|
|
-
|
|
|
- // 绘制分数文字
|
|
|
- ctx.fillStyle = '#002846';
|
|
|
- ctx.font = `bold ${scoreFontSize}px sans-serif`;
|
|
|
- ctx.textAlign = 'center';
|
|
|
- ctx.textBaseline = 'middle';
|
|
|
- ctx.fillText((scoreItem.avgScore>25?25:scoreItem.avgScore), scoreBoxX + scoreBoxWidth / 2, scoreBoxY + scoreBoxHeight / 2);
|
|
|
},
|
|
|
- // 辅助函数:绘制自动换行的文字
|
|
|
- // 辅助函数:绘制自动换行且垂直居中的文字
|
|
|
- drawWrappedText(ctx, text, x, y_center, lineHeight, maxWidth) {
|
|
|
- // 1. 将文本分割成多行
|
|
|
- let words = text.split('');
|
|
|
- let lines = [];
|
|
|
- let currentLine = '';
|
|
|
-
|
|
|
- for (let n = 0; n < words.length; n++) {
|
|
|
- let testLine = currentLine + words[n];
|
|
|
- let metrics = ctx.measureText(testLine);
|
|
|
- let testWidth = metrics.width;
|
|
|
- if (testWidth > maxWidth && n > 0) {
|
|
|
- lines.push(currentLine);
|
|
|
- currentLine = words[n];
|
|
|
- } else {
|
|
|
- currentLine = testLine;
|
|
|
- }
|
|
|
- }
|
|
|
- lines.push(currentLine); // 加入最后一行
|
|
|
-
|
|
|
- // 2. 计算文本块的总高度
|
|
|
- const totalTextHeight = lines.length * lineHeight;
|
|
|
-
|
|
|
- // 3. 计算绘制第一行文本的起始 Y 坐标
|
|
|
- // y_center 是外部传入的容器中心点
|
|
|
- // 我们从容器中心点上移一半文本总高度,得到文本块的顶部位置
|
|
|
- let startY = y_center - totalTextHeight / 2;
|
|
|
-
|
|
|
- // 4. 逐行绘制
|
|
|
- // ctx.textBaseline = 'middle' 是在外部设置的,所以我们绘制每一行时,
|
|
|
- // Y坐标需要是该行所在矩形区域的垂直中心。
|
|
|
- for (let i = 0; i < lines.length; i++) {
|
|
|
- // 计算当前行文本的中心Y坐标
|
|
|
- const lineY = startY + (i * lineHeight) + (lineHeight / 2);
|
|
|
- ctx.fillText(lines[i], x, lineY);
|
|
|
- }
|
|
|
+ // 辅助函数:计算自动换行文字的总高度
|
|
|
+ calculateWrappedTextHeight(ctx, text, lineHeight, maxWidth) {
|
|
|
+ if (!text) return 0;
|
|
|
+ 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;
|
|
|
},
|
|
|
+ // 辅助函数:绘制自动换行文字 (从顶部开始)
|
|
|
+ drawWrappedTextTop(ctx, text, x, y, lineHeight, maxWidth) {
|
|
|
+ if (!text) return;
|
|
|
+ let words = text.split('');
|
|
|
+ let line = '';
|
|
|
+ let currentY = y;
|
|
|
+
|
|
|
+ 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) {
|
|
|
+ ctx.fillText(line, x, currentY); // 使用 currentY 作为顶部基线
|
|
|
+ line = words[n];
|
|
|
+ currentY += lineHeight;
|
|
|
+ } else {
|
|
|
+ line = testLine;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ ctx.fillText(line, x, currentY);
|
|
|
+ },
|
|
|
+
|
|
|
// 辅助函数:解析 CSS linear-gradient 字符串
|
|
|
parseGradient(gradientString) {
|
|
|
const colorStops = [];
|