pdf.vue 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775
  1. <template>
  2. <view class="page-wrappe">
  3. <cus-header title=' ' bgColor="transparent"></cus-header>
  4. <view id="pdfContainer" class="pdf-container" :style="{'transform':'scale('+scale+')', 'height': containerScaledHeight + 'px'}" v-if="reportData">
  5. <!-- 封面 -->
  6. <view class="cd_box fm2 adffc" style="border: none;margin-top: 20px;height: 868px;">
  7. <img class="fm2-logo" src="https://gitee.com/hw_0302/chuang-heng-wechat-images/raw/master/versionTwo/fm_logo.png">
  8. <img class="fm2-logo2" src="https://gitee.com/hw_0302/chuang-heng-wechat-images/raw/master/versionTwo/fm_logo2.png">
  9. <img class="fm2-perill" src="https://gitee.com/hw_0302/chuang-heng-wechat-images/raw/master/versionTwo/fm_perill.png">
  10. <view class="fm2-line"></view>
  11. <view class="fm2-p">团队发展动态评估报告(个人版)</view>
  12. <view class="fm2-texts adf">
  13. <view class="fm2-texts-pre"><span>客户公司:</span>{{ reportData.teamInfo.enterpriseName||'' }}</view>
  14. <view class="fm2-texts-pre"><span>团队名称:</span>{{ reportData.teamInfo.teamName||'' }}</view>
  15. <view class="fm2-texts-pre"><span>评估发起人:</span>{{ reportData.teamInfo.initiator||'' }}</view>
  16. <view class="fm2-texts-pre"><span>报告生成时间:</span>{{ reportData.teamInfo.reportDate||'' }}</view>
  17. </view>
  18. <view class="fm2-tip">免责声明:本团队测评报告基于您方团队填写的测评数据及相关信息生成,深圳创衡管理顾问有限公司不对报告内容的真实性、准确性和完整性负责。本报告仅供您了解团队情况、优化管理决策提供参考。报告结论不构成任何法律、商业或投资建议,亦不替代专业咨询意见。我方不对因使用本报告内容而产生的任何直接或间接损失承担责任。</view>
  19. </view>
  20. <!-- 介绍 -->
  21. <view class="cd_box" style="border: none;">
  22. <view class="v2-top adfacjb" :style="{'background-image':'url(https://gitee.com/hw_0302/chuang-heng-wechat-images/raw/master/versionTwo/intro_title_bg.png)'}">
  23. <view class="vt-left" style="color: #FFFFFF;">介绍<span>PERILL模型简介</span></view>
  24. <view class="vt-right">团队发展动态评估报告(个人版)</view>
  25. </view>
  26. <view class="v2-box">
  27. <img class="vb-img1" :src="'https://gitee.com/hw_0302/chuang-heng-wechat-images/raw/master/versionTwo/'+'intro'+'_box_img1.png'">
  28. <view class="v2-p">PERILL团队发展动态评估源于团队教练辅导领域的先驱、管理思维与团队绩效领域的权威、全球顶尖团队教练David Clutterbuck教授及其团队通过深入研究,提炼出影响团队绩效的140多个基于实证的因素,整合而成的团队评估和提升工具-PERILL模型。</view>
  29. <view class="v2-p" style="margin-top: 8px;">创衡国际基于10多年来在全球与200多家具有前瞻性的国际公司以及国内具有行业代表性公司的合作经验,结合CCMI的PERILL评估工具,在中国推出的团队发展动态评估系统,旨在帮助团队更全面、更有效地从六个维度评估团队的发展现状,为支持团队成为高价值创造团队提供全景式的客观评估。</view>
  30. <view class="v2-p" style="margin-top: 8px;">PERILL团队发展动态评估(个人版)的主体内容由<span>{{ reportData.teamInfo.questionCount||0 }}</span>个关于团队的描述组成。</view>
  31. </view>
  32. <view class="v2-six">
  33. <view class="vsix-title">PERILL六大纬度</view>
  34. <view class="vsix-p">PERILL评估提供了一个复杂的团队系统概览,它并非针对孤立的问题,也不是简单的优缺点,而是着眼于团队系统的复杂性。它通过6个影响因素(如下所述)提出问题,以揭示团队系统各要素之间的联系,以及这些联系如何影响团队的高效运作能力。</view>
  35. <view class="vsix-boxs">
  36. <view class="vsb adfac" v-for="(item,index) in sixWd" :key="index">
  37. <img class="vsb-img" :src="item.img"/>
  38. <view class="vsb-right">
  39. <view class="vsbr-top adfac">
  40. <view class="vsbrt-type" :style="{'background':item.color}">{{ item.type }}</view>
  41. <view class="vsbrt-title" :style="{'color':item.color}">{{ item.title }}</view>
  42. </view>
  43. <view class="vsbr-desc">{{ item.desc }}</view>
  44. </view>
  45. </view>
  46. </view>
  47. </view>
  48. </view>
  49. <!-- 总体评估分析 -->
  50. <view class="cd_box adffc" style="border: none;">
  51. <view class="v2-top adfacjb" :style="{'background-image':'url(https://gitee.com/hw_0302/chuang-heng-wechat-images/raw/master/versionTwo/'+'intro'+'_top_title_bg2.png)'}">
  52. <view class="vt-left">总体评估分析</view>
  53. <view class="vt-right">团队发展动态评估报告(个人版)</view>
  54. </view>
  55. <view class="v2-box">
  56. <img class="vb-img1" :src="'https://gitee.com/hw_0302/chuang-heng-wechat-images/raw/master/versionTwo/'+'intro'+'_box_img1.png'">
  57. <view class="v2-p2">报告的核心是PERILL评估分析下从评估发起者个人维度来看其所在整体团队当前状态下的表现。这包括团队在PERILL模型每个关键要素上的综合得分,通过结合得分和置信指数,我们能够展示出高功能和低功能领域。</view>
  58. <view class="v2-p2" style="margin-top: 16px;">下面的条形图上的位置标记显示了团队在各个维度上的影响力评分。</view>
  59. <view style="width:100%;height:200px;" class="pdfEchart">
  60. <l-echart ref="ztzdfxRef" :canvas2d="true" @finished="initZtzdfxChart"></l-echart>
  61. </view>
  62. <view class="v2b-tip-title">注: 关于认同度、重要性分、影响力分的定义</view>
  63. <view class="v2b-tip-memo">
  64. a.“认同度分”,指标逻辑为评估发起者个人对当前主题所对应各问卷题目认同度评分的均值,用来表征其对这一主题问卷所陈述内容与团队情况的相符合程度的平均认知,最高分:5分,最低分:0分;<br>
  65. b.“重要分”,指标逻辑为根据评估发起者个人对当前主题所对应各问卷题目重要性评分的均值,用来表征其对这一主题问卷所陈述内容对于团队重要性的平均认知,最高分:5分,最低分:0分;<br>
  66. c.“影响力分”,指标逻辑为评估发起者个人对当前主题所对应各问卷题目的认同度评分与重要性评分乘积的均值,用来表征其对这一主题问卷所陈述内容对于团队的影响力水平,最高分:25分,最低分:0分;
  67. </view>
  68. <view class="v2b-title">评估结果</view>
  69. <view class="v2b-p" v-html="(reportData.totalDiagnosticOutput||'').replaceAll('\n\n','<br>')"></view>
  70. <view class="v2b-title">评估建议</view>
  71. <view class="v2b-p" v-html="(reportData.totalDiagnosisSuggest||'').replaceAll('\n\n','<br>')"></view>
  72. </view>
  73. </view>
  74. <!-- 多维度 -->
  75. <canvas type="2d" canvas-id="score-canvas" id="score-canvas" class="offscreen-canvas"></canvas>
  76. <template v-if="reportData&&reportData.dimensionAnalysis&&reportData.dimensionAnalysis.length">
  77. <view class="cd_box adffc" style="border: none;" v-for="(n,i) in [[0,2],[2,4],[4,6]]" :key="i">
  78. <view class="v2-top" :style="{'background-image':'url(https://gitee.com/hw_0302/chuang-heng-wechat-images/raw/master/versionTwo/person_title_bg.png)'}"></view>
  79. <view v-for="(item,index) in reportData.dimensionAnalysis.slice(n[0],n[1])" :key="index">
  80. <view class="v2-title2">{{ item.title||'' }}</view>
  81. <view class="v2-box" :style="{'border':'none','padding':0}">
  82. <view class="v2-p2">{{ item.desc }}</view>
  83. <view class="vb-table adf" :style="{'border':'1px solid #E6EAED','margin-top':'12px'}">
  84. <view class="vbt2-left">
  85. <view class="vbt2l-th adf" :style="{'background':item.thbgcolor,'color':item.thtextcolor}">
  86. <view class="vbt2l-th-title adfac">主题</view>
  87. <view class="vbt2l-th-score adfacjc">影响力分</view>
  88. </view>
  89. <view class="vbt2l-pre" v-for="(ss,si) in item.scoreSpreads" :key="si">
  90. <view class="vbt2l-pre-l">
  91. <view class="vbt2l-pre-l-title" :style="{'color':item.titlecolor}">{{ ss.theme||'' }}</view>
  92. <view class="vbt2l-pre-l-tip">{{ ss.question||'' }}</view>
  93. </view>
  94. <view class="vbt2l-pre-r adfacjc">{{ ss.memeber.avgScore||0 }}</view>
  95. </view>
  96. </view>
  97. <view class="vbt2-right adffc">
  98. <view class="vbt2r-th adf" :style="{'background':item.thbgcolor,'color':item.thtextcolor}">
  99. <view class="vbt2r-pre adfacjc">认同度分</view>
  100. <view class="vbt2r-pre adfacjc">重要性分</view>
  101. </view>
  102. <view class="vbt2r-tb adf">
  103. <view class="vbt2r-tb-lines adf">
  104. <view class="vbt2r-tb-lines-pre" v-for="item in 12" :key="item"></view>
  105. </view>
  106. <view class="vbt2r-tb-l adffc">
  107. <view class="vbt2r-tb-l-pre red adfac" v-for="(ss,si) in item.scoreSpreads" :key="si">
  108. <view class="vbt2r-tb-l-pre-zzt red" :style="{'width':(ss.memeber.avgAgreement/6*100)+'%'}"></view>
  109. </view>
  110. </view>
  111. <view class="vbt2r-tb-l">
  112. <view class="vbt2r-tb-l-pre green adfac" v-for="(ss,si) in item.scoreSpreads" :key="si">
  113. <view class="vbt2r-tb-l-pre-zzt green" :style="{'width':(ss.memeber.avgVital/6*100)+'%'}"></view>
  114. </view>
  115. </view>
  116. <view class="vbt2r-num adf">
  117. <view class="vbt2r-num-pre" v-for="(item,index) in [5,4,3,2,1,0,1,2,3,4,5]" :key="index">{{ item }}</view>
  118. </view>
  119. </view>
  120. </view>
  121. </view>
  122. </view>
  123. </view>
  124. </view>
  125. </template>
  126. <!-- 封底页 -->
  127. <view class="cd_box fdy" style="border: none;">
  128. <view class="v2-top adfacjb" :style="{'background-image':'url(https://gitee.com/hw_0302/chuang-heng-wechat-images/raw/master/versionTwo/fdy_title_bg.png)'}"></view>
  129. <view class="fdy-title">致关注团队发展的您:从评估到行动,开启团队进化跃迁之旅。</view>
  130. <view class="fdy-p">当您阅读至此,首先恭喜您完成了一次专业的团队评估!这不仅是您对所在团队的一次深度观察,也为团队发展开启了一个充满可能性的新起点。</view>
  131. <view class="fdy-p">作为团队教练,我们希望与您分享这次评估的深层价值,并为您描绘从个人视角走向团队共识的可能路径。</view>
  132. <view class="fdy-subtitle">一、这次个人评估为您理解团队提供了两个独特视角:</view>
  133. <view class="fdy-subtitle2">1. 个人观察的整合</view>
  134. <view class="fdy-p" style="margin-top: 6px;">本报告整合了您个人对团队当前状态(认同度)和未来期待(重要性)的评价,如同一面镜子,映照出您所感知的团队运作状态。它帮助您看见那些可能被忽略的惯性模式、盲点与认知差异。无论是您看到的优势、共识,还是待发展的领域,这些都构成了您个人视角下的团队画像。 </view>
  135. <view class="fdy-subtitle2">2. 进化方向的个人洞察</view>
  136. <view class="fdy-p" style="margin-top: 6px;">基于您的观察,报告勾勒出团队持续进化的潜在方向。这并非标准答案,而是帮助您识别那些对团队成功可能最关键的发展维度——无论是战略对齐、信任深化、协同效率、创新学习还是领导力进化。它将您个人的感受与对团队的期待连接起来,把模糊的感觉转化为可讨论、可聚焦的议题,为您后续参与团队对话提供事实依据。</view>
  137. <view class="fdy-subtitle">二、个人评估是重要的第一步,但团队的真实面貌往往需要更多视角。</view>
  138. <view class="fdy-p">本次评估仅代表您个人的观察,而团队作为一个系统,其全貌需要整合团队领导者、成员、赞助人及利益相关方等多方视角。这正是PERILL团队评估系统的核心价值:通过科学的、结构化的评估,帮助团队获得系统性洞察,实现高杠杆改进。</view>
  139. <view class="fdy-subtitle">三、我们邀请您:</view>
  140. <view class="fdy-subtitle2">1. 发起一次坦诚的团队对话</view>
  141. <view class="fdy-p" style="margin-top: 6px;">邀请关键成员,基于您的观察分享感受,倾听他人视角,开启团队共识的第一步。</view>
  142. <view class="fdy-subtitle2">2. 进行一次完整的团队PERILL评估</view>
  143. <view class="fdy-p" style="margin-top: 6px;">邀请团队负责人、成员、赞助人及利益相关方共同参与,获得真实、立体的团队诊断报告,为团队发展奠定共同事实基础。</view>
  144. <view class="fdy-subtitle2">3. 考虑获取专业支持</view>
  145. <view class="fdy-p" style="margin-top: 6px;">团队教练可作为中立的思考伙伴,帮助团队整合多元视角,建立共同语言,制定切实可行的进化路径。</view>
  146. <view class="fdy-p" style="margin-top: 20px;">每一支超越高绩效的团队,都始于对现状清醒的认知与持续进化的共同勇气。现在,您已拥有个人视角的洞察,下一步是走向团队共识——让每一次对话,都成为团队蜕变的契机。</view>
  147. <view class="fdy-p">PERILL不止于评估,更在于赋能。期待与您和团队同行,见证未来的更多可能。</view>
  148. <view class="fdy-p">具体需求可联系您的团队教练或扫码联系“⼤衡同学”。</view>
  149. <view class="fdy-code adfacjc"><img :src="'https://gitee.com/hw_0302/chuang-heng-wechat-images/raw/master/versionTwo/fdy_code.png'"></view>
  150. </view>
  151. </view>
  152. <view class="pdf_btn" @click="createPdf">生成PDF</view>
  153. </view>
  154. </template>
  155. <script name="">
  156. import { BaseApi } from '@/http/baseApi.js';
  157. import * as echarts from '@/pagesHome/components/lime-echart/static/echarts.min.js'
  158. import lEchart from '@/pagesHome/components/lime-echart/components/l-echart/l-echart.vue'
  159. export default {
  160. name: 'ZtzdfxChart',
  161. components:{ lEchart },
  162. data() {
  163. return {
  164. reportId:'',
  165. reportData: null,
  166. isChartReady: false,
  167. scale:1,
  168. originalContainerHeight: 0,
  169. containerScaledHeight: 'auto',
  170. sixWd: [
  171. {
  172. img:'https://gitee.com/hw_0302/chuang-heng-wechat-images/raw/master/versionTwo/intro_img_p.png',
  173. type:'P',
  174. title:'宗旨与动机',
  175. desc:'指团队共享的目的和存在的意义, 包含对共同的愿景,目标和优先级的清晰度。',
  176. color:'#761E6A'
  177. },
  178. {
  179. img:'https://gitee.com/hw_0302/chuang-heng-wechat-images/raw/master/versionTwo/intro_img_e.png',
  180. type:'E',
  181. title:'外部流程、系统与结构',
  182. desc:'指团队与其外部利益相关者 - 客户,供应商,股东,组织内的上级及其他团队的互动关系和协作机制。',
  183. color:'#009191'
  184. },
  185. {
  186. img:'https://gitee.com/hw_0302/chuang-heng-wechat-images/raw/master/versionTwo/intro_img_r.png',
  187. type:'R',
  188. title:'人际关系',
  189. desc:'指团队成员共同工作时的关系状态–他们是否相互尊重和信任对方的能力,是否足够心理安全以能够坦诚沟通,是否真正关心彼此的幸福感等。',
  190. color:'#FFD750'
  191. },
  192. {
  193. img:'https://gitee.com/hw_0302/chuang-heng-wechat-images/raw/master/versionTwo/intro_img_i.png',
  194. type:'I',
  195. title:'内部流程、系统与结构',
  196. desc:'指团队如何管理工作任务和流程(包括但不限于会议、任务分配和团队情绪等),互相支持以及高质量的沟通和决策。',
  197. color:'#4EB2B2'
  198. },
  199. {
  200. img:'https://gitee.com/hw_0302/chuang-heng-wechat-images/raw/master/versionTwo/intro_img_l.png',
  201. type:'L',
  202. title:'学习',
  203. desc:'指团队如何应对多变的环境和保持持续的进步和成⻓,能够从经验中反思、提炼并应用知识的能力。',
  204. color:'#AFCDF5'
  205. },
  206. {
  207. img:'https://gitee.com/hw_0302/chuang-heng-wechat-images/raw/master/versionTwo/intro_img_l2.png',
  208. type:'L',
  209. title:'领导力',
  210. desc:'指团队认为需要怎样的领导行为能够让他们,作为个人或者团队做到最好。团队可以和他们的领导者讨论他们的责任及承担方式,以帮助领导者成为他们需要的领导者。',
  211. color:'#002846'
  212. }
  213. ],
  214. pdfImages:[],
  215. };
  216. },
  217. onLoad(options) {
  218. this.reportId = options.reportId;
  219. this.getReportData();
  220. },
  221. mounted() {
  222. this.calculateScaleAndPosition();
  223. uni.onWindowResize(() => {
  224. this.calculateScaleAndPosition();
  225. });
  226. },
  227. methods: {
  228. getReportData(){
  229. this.$api.get(`/core/report/previewReport/${this.reportId}`).then(({data:res})=>{
  230. if(res.code!==0) return this.$showToast(res.msg)
  231. this.reportData = res.data;
  232. const tempDimensionAnalysis = [
  233. {
  234. title:'P-宗旨与动机',thbgcolor:'#761E6A',thtextcolor:'#FFFFFF',titlecolor:'#761E6A',dimensionCode:"purpose",
  235. desc:`「宗旨与动机」维度,我们旨在探究团队是否有清晰的存在理由和明确的方向,能够激发团队成员的动力并吸引他们的想象力,以及个人与集体的身份认同是否围绕共同的目标,并在实现目标的优先次序上达成一致。`
  236. },
  237. {
  238. title:'E-外部流程、系统与结构',thbgcolor:'#009191',thtextcolor:'#FFFFFF',titlecolor:'#009191',dimensionCode:"external",
  239. desc:`「外部流程、系统与结构」维度,我们旨在探究团队如何与各种利益相关者互动,他们与团队的利益相关方各自如何寻求了解对方,以及现有系统和流程的有效性,以帮助管理不同利益相关者的期望和需求。`
  240. },
  241. {
  242. title:'R-人际关系',thbgcolor:'#FFD750',thtextcolor:'#002846',titlecolor:'#002846',dimensionCode:"relationship",
  243. desc:`「人际关系」维度,我们旨在探究团队成员相互沟通交流的状态,团队成员的信任程度、尊重和关心的程度、心理安全度,以及团队成员之间的关系如何促进(或破坏)协作。`
  244. },
  245. {
  246. title:'I-内部流程、系统与结构',thbgcolor:'#4EB2B2',thtextcolor:'#FFFFFF',titlecolor:'#33A7A7',dimensionCode:"internal",
  247. desc:`「内部流程、系统与结构」维度,我们旨在探究团队在管理工作任务和流程中如何平衡责任与自主权进行协作。主要关注团队应对变化时的敏捷程度、日常沟通方式以及内部决策过程的有效性。`
  248. },
  249. {
  250. title:'L-学习',thbgcolor:'#AFCDF5',thtextcolor:'#002846',titlecolor:'#002846',dimensionCode:"learning",
  251. desc:`「学习」维度,我们旨在探究团队如何提高其绩效(完成当下的任务)、能力(提高技能和资源以处理明天的任务)和容量(⻓期的愿景,用更少的资源做更多的事情)以应对当前和未来的任务。同时还关注团队如何管理这些能力和提高效率。`
  252. },
  253. {
  254. title:'L-领导力',thbgcolor:'#002846',thtextcolor:'#FFFFFF',titlecolor:'#002846',dimensionCode:"leadership",
  255. desc:`「领导力」维度,我们旨在探究团队领导者的素质和行为(包括但不限于正式与非正式的引导、赋能与责任承担方式)如何对团队功能和其他因素产生影响,以及这是积极的还是消极的。`
  256. }
  257. ]
  258. if(this.reportData&&this.reportData.dimensionAnalysis){
  259. this.reportData.dimensionAnalysis.forEach((d,i)=>{
  260. d.scoreSpreads.forEach(s=>{
  261. s.theme = s.theme.replaceAll(',',' ').replaceAll(',',' ');
  262. })
  263. let temp = tempDimensionAnalysis.find(t=>t.dimensionCode === d.dimensionCode)||{};
  264. this.reportData.dimensionAnalysis[i] = {...d,...temp}
  265. })
  266. }
  267. })
  268. },
  269. // async createPdf(){
  270. // uni.showLoading({
  271. // title:'正在生成PDF所需的图片...'
  272. // })
  273. // const imageUrl = await this.downloadZtzdfximage();
  274. // this.pdfImages.push(imageUrl);
  275. // for(const d of this.reportData.dimensionAnalysis){
  276. // const fileurl = await this.generateScoreImage(d);
  277. // this.pdfImages.push(fileurl)
  278. // }
  279. // this.$showToast(`生成成功,共计${this.pdfImages.length}张`);
  280. // uni.hideLoading();
  281. // console.log(this.pdfImages);
  282. // },
  283. // Promise.all方法,性能更高
  284. async createPdf(){
  285. uni.showLoading({
  286. title:'正在生成PDF所需的图片...'
  287. })
  288. try {
  289. const ztzdfximagePromise = this.downloadZtzdfximage();
  290. const dimensionImagePromises = this.reportData.dimensionAnalysis.map(d => {
  291. return this.generateScoreImage(d,d.scoreSpreads||[]);
  292. });
  293. const allImageUrls = await Promise.all([
  294. ztzdfximagePromise,
  295. ...dimensionImagePromises
  296. ]);
  297. this.pdfImages = allImageUrls;
  298. this.$api.post('/core/report/reportToPdf',{
  299. images:this.pdfImages,
  300. reportId:this.reportId
  301. }).then(({data:res})=>{
  302. if(res.code!==0) return this.$showToast(res.msg)
  303. uni.hideLoading();
  304. this.$showToast('生成成功');
  305. setTimeout(()=>{
  306. uni.redirectTo({
  307. url:'/pagesHome/report'
  308. })
  309. },1500)
  310. })
  311. } catch (error) {
  312. uni.hideLoading();
  313. console.error('生成图片过程中发生错误:', error);
  314. uni.showToast({ title: '生成图片失败,请重试', icon: 'none' });
  315. }
  316. },
  317. // 绘制主函数
  318. async generateScoreImage(dimensionData,scoreData) {
  319. return new Promise(resolve=>{
  320. console.log('开始生成图片...');
  321. // --- 1. 定义尺寸和样式 ---
  322. const canvasWidth = 588; // .v2-box 的宽度大约是 630 - 20*2(padding) - 1*2(border) = 588
  323. const itemHeight = 49; // 每个评估项的高度
  324. const totalHeight = itemHeight * scoreData.length;
  325. // 调整为整数,避免边框模糊
  326. const canvasHeight = totalHeight;
  327. // --- 2. 获取 Canvas 节点 ---
  328. // 使用 ID 选择器更精确
  329. const query = uni.createSelectorQuery().in(this);
  330. query.select('#score-canvas')
  331. .fields({ node: true, size: true })
  332. .exec(async (res) => {
  333. // 【重要】增加健壮性检查
  334. if (!res || !res[0] || !res[0].node) {
  335. console.error('获取 Canvas 节点失败,请检查 canvas-id 和 type="2d" 是否正确设置。');
  336. uni.showToast({ title: '组件初始化失败', icon: 'none' });
  337. return;
  338. }
  339. const canvasNode = res[0].node;
  340. const context = canvasNode.getContext('2d');
  341. const dpr = uni.getSystemInfoSync().pixelRatio;
  342. // --- 3. 设置画布尺寸和缩放以适应高分屏 ---
  343. canvasNode.width = canvasWidth * dpr;
  344. canvasNode.height = canvasHeight * dpr;
  345. context.scale(dpr, dpr);
  346. // --- 4. 开始绘制 ---
  347. // 绘制大背景
  348. context.fillStyle = '#FFFFFF';
  349. context.fillRect(0, 0, canvasWidth, canvasHeight);
  350. // --- 5. 循环绘制每一项 ---
  351. for (let i = 0; i < scoreData.length; i++) {
  352. const item = scoreData[i];
  353. const yPos = i * itemHeight;
  354. // 注意:这里不再需要 await,因为 canvas 2d 绘图是同步的
  355. this.drawScoreItem(context, item, yPos, canvasWidth, itemHeight, dimensionData);
  356. }
  357. // 【补充】绘制最外层的上下边框,避免被循环内的矩形覆盖
  358. context.strokeStyle = dimensionData.bcolor;
  359. context.lineWidth = 1;
  360. context.strokeRect(0, 0, canvasWidth, canvasHeight);
  361. // --- 6. 生成图片 ---
  362. uni.hideLoading();
  363. uni.canvasToTempFilePath({
  364. canvas: canvasNode,
  365. x: 0,
  366. y: 0,
  367. width: canvasWidth,
  368. height: canvasHeight,
  369. destWidth: canvasWidth * dpr,
  370. destHeight: canvasHeight * dpr,
  371. success: async (result) => {
  372. console.log('图片生成成功!', result.tempFilePath);
  373. const fileurl = await this.uploadFilePromise(result.tempFilePath);
  374. console.log(fileurl, 'fileurl');
  375. resolve(fileurl)
  376. },
  377. fail: (err) => {
  378. console.error('图片生成失败', err);
  379. uni.showToast({ title: '图片生成失败', icon: 'none' });
  380. }
  381. }, this);
  382. });
  383. })
  384. },
  385. // 辅助函数:计算自动换行文字的总高度
  386. calculateWrappedTextHeight(ctx, text, lineHeight, maxWidth) {
  387. let words = text.split('');
  388. let line = '';
  389. let height = lineHeight; // 至少有一行
  390. for (let n = 0; n < words.length; n++) {
  391. let testLine = line + words[n];
  392. let metrics = ctx.measureText(testLine);
  393. let testWidth = metrics.width;
  394. if (testWidth > maxWidth && n > 0) {
  395. line = words[n];
  396. height += lineHeight;
  397. } else {
  398. line = testLine;
  399. }
  400. }
  401. return height;
  402. },
  403. // 辅助函数:绘制单个评估项
  404. drawScoreItem(ctx, scoreItem, y, width, height, dimensionData) {
  405. const leftBoxWidth = 72;
  406. const rightBoxX = leftBoxWidth;
  407. const rightBoxWidth = width - leftBoxWidth;
  408. const rightPadding = 10; // 右侧内容的通用内边距
  409. // 1. --- 绘制左侧部分 ---
  410. ctx.fillStyle = dimensionData.titlecolor;
  411. ctx.fillRect(0, y, leftBoxWidth, height);
  412. // 绘制白色下边框
  413. if (y + height < ctx.canvas.height / (uni.getSystemInfoSync().pixelRatio)) {
  414. ctx.strokeStyle = '#FFFFFF';
  415. ctx.lineWidth = 1;
  416. ctx.beginPath();
  417. ctx.moveTo(0, y + height - 1);
  418. ctx.lineTo(leftBoxWidth, y + height - 1);
  419. ctx.stroke();
  420. }
  421. // 绘制左侧标题文字 (要求 1)
  422. let theme = (scoreItem.theme||'').replaceAll(',','').replaceAll(',','');
  423. const isBlackLeftTitle = (dimensionData.title == '人际关系' || dimensionData.title == '学习' || dimensionData.title == '内部流程、系统与结构');
  424. ctx.fillStyle = isBlackLeftTitle ? '#002846' : '#FFFFFF';
  425. ctx.font = '10px sans-serif';
  426. ctx.textAlign = 'center';
  427. ctx.textBaseline = 'middle';
  428. this.drawWrappedText(ctx, theme, leftBoxWidth / 2, y + height / 2, 12, leftBoxWidth - 32); // 左右留16px边距
  429. // 2. --- 绘制右侧部分 ---
  430. // 绘制右侧外边框
  431. ctx.strokeStyle = dimensionData.bcolor;
  432. ctx.lineWidth = 1;
  433. ctx.strokeRect(rightBoxX, y, rightBoxWidth, height);
  434. // --- 计算右侧内容垂直居中需要的参数 (要求 2) ---
  435. const descLineHeight = 16;
  436. const descMaxWidth = rightBoxWidth - rightPadding * 2;
  437. ctx.font = '9px sans-serif'; // 设置好字体用于计算
  438. const descHeight = this.calculateWrappedTextHeight(ctx, scoreItem.question, descLineHeight, descMaxWidth);
  439. const spacing = 7; // 文字与进度条间距 (要求 4)
  440. const progressBarHeight = 6; // 进度条高度 (要求 3)
  441. // 计算右侧所有内容的总高度
  442. const totalContentHeight = descHeight + spacing + progressBarHeight;
  443. // 计算内容块的起始 Y 坐标,使其在右侧框内垂直居中
  444. const contentStartY = y + (height - totalContentHeight) / 2;
  445. // --- 开始绘制右侧内容 ---
  446. // 绘制右侧描述文字 (要求 2)
  447. ctx.fillStyle = '#193D59';
  448. ctx.font = '9px sans-serif';
  449. ctx.textAlign = 'left';
  450. ctx.textBaseline = 'top'; // 基线设为 top 方便计算
  451. this.drawWrappedText(ctx, scoreItem.question, rightBoxX + rightPadding, contentStartY, descLineHeight, descMaxWidth);
  452. // 绘制进度条 (要求 3)
  453. const progressBarY = contentStartY + descHeight + spacing;
  454. const progressBarWidth = rightBoxWidth - rightPadding * 2;
  455. const scoreWidth = ((scoreItem.avgScore>25?25:scoreItem.avgScore)/ 25) * progressBarWidth;
  456. const barRadius = 3;
  457. // 绘制灰色背景
  458. ctx.fillStyle = '#F0F2F8';
  459. ctx.beginPath();
  460. ctx.moveTo(rightBoxX + rightPadding + barRadius, progressBarY);
  461. ctx.arcTo(rightBoxX + rightPadding + progressBarWidth, progressBarY, rightBoxX + rightPadding + progressBarWidth, progressBarY + progressBarHeight, barRadius);
  462. ctx.arcTo(rightBoxX + rightPadding + progressBarWidth, progressBarY + progressBarHeight, rightBoxX + rightPadding, progressBarY + progressBarHeight, barRadius);
  463. ctx.arcTo(rightBoxX + rightPadding, progressBarY + progressBarHeight, rightBoxX + rightPadding, progressBarY, barRadius);
  464. ctx.arcTo(rightBoxX + rightPadding, progressBarY, rightBoxX + rightPadding + progressBarWidth, progressBarY, barRadius);
  465. ctx.closePath();
  466. ctx.fill();
  467. // 绘制实际得分的渐变色进度条
  468. const gradient = ctx.createLinearGradient(rightBoxX + rightPadding, 0, rightBoxX + rightPadding + progressBarWidth, 0);
  469. const gradientColors = this.parseGradient(dimensionData.pfztfb);
  470. gradientColors.forEach(c => gradient.addColorStop(c.stop, c.color));
  471. ctx.fillStyle = gradient;
  472. ctx.save();
  473. ctx.clip(); // 使用上面的圆角矩形路径进行裁剪
  474. ctx.fillRect(rightBoxX + rightPadding, progressBarY, scoreWidth, progressBarHeight);
  475. ctx.restore();
  476. // 绘制分数框 (要求 4)
  477. const scoreFontSize = 12;
  478. const scorePaddingY = 4; // 上下内边距
  479. const scorePaddingX = 7; // 左右内边距
  480. const scoreBoxRadius = 4;
  481. ctx.font = `bold ${scoreFontSize}px sans-serif`;
  482. const scoreTextMetrics = ctx.measureText(scoreItem.avgScore>25?25:scoreItem.avgScore);
  483. const scoreBoxWidth = scoreTextMetrics.width + scorePaddingX * 2;
  484. const scoreBoxHeight = scoreFontSize + scorePaddingY * 2;
  485. // 计算分数框的位置,使其右端对齐在进度条的末端
  486. const scoreBoxX = rightBoxX + rightPadding + scoreWidth - scoreBoxWidth;
  487. // 垂直居中于进度条
  488. const scoreBoxY = progressBarY + (progressBarHeight / 2) - (scoreBoxHeight / 2);
  489. // 绘制阴影
  490. ctx.save();
  491. ctx.shadowColor = dimensionData.bcolor;
  492. ctx.shadowBlur = 6;
  493. ctx.shadowOffsetX = 0;
  494. ctx.shadowOffsetY = 2;
  495. // 绘制分数框背景和边框
  496. ctx.fillStyle = '#FFFFFF';
  497. ctx.strokeStyle = dimensionData.bcolor;
  498. ctx.lineWidth = 1;
  499. ctx.beginPath();
  500. ctx.moveTo(scoreBoxX + scoreBoxRadius, scoreBoxY);
  501. ctx.arcTo(scoreBoxX + scoreBoxWidth, scoreBoxY, scoreBoxX + scoreBoxWidth, scoreBoxY + scoreBoxHeight, scoreBoxRadius);
  502. ctx.arcTo(scoreBoxX + scoreBoxWidth, scoreBoxY + scoreBoxHeight, scoreBoxX, scoreBoxY + scoreBoxHeight, scoreBoxRadius);
  503. ctx.arcTo(scoreBoxX, scoreBoxY + scoreBoxHeight, scoreBoxX, scoreBoxY, scoreBoxRadius);
  504. ctx.arcTo(scoreBoxX, scoreBoxY, scoreBoxX + scoreBoxWidth, scoreBoxY, scoreBoxRadius);
  505. ctx.closePath();
  506. ctx.fill();
  507. ctx.stroke();
  508. ctx.restore();
  509. // 绘制分数文字
  510. ctx.fillStyle = '#002846';
  511. ctx.font = `bold ${scoreFontSize}px sans-serif`;
  512. ctx.textAlign = 'center';
  513. ctx.textBaseline = 'middle';
  514. ctx.fillText((scoreItem.avgScore>25?25:scoreItem.avgScore), scoreBoxX + scoreBoxWidth / 2, scoreBoxY + scoreBoxHeight / 2);
  515. },
  516. // 辅助函数:绘制自动换行的文字
  517. // 辅助函数:绘制自动换行且垂直居中的文字
  518. drawWrappedText(ctx, text, x, y_center, lineHeight, maxWidth) {
  519. // 1. 将文本分割成多行
  520. let words = text.split('');
  521. let lines = [];
  522. let currentLine = '';
  523. for (let n = 0; n < words.length; n++) {
  524. let testLine = currentLine + words[n];
  525. let metrics = ctx.measureText(testLine);
  526. let testWidth = metrics.width;
  527. if (testWidth > maxWidth && n > 0) {
  528. lines.push(currentLine);
  529. currentLine = words[n];
  530. } else {
  531. currentLine = testLine;
  532. }
  533. }
  534. lines.push(currentLine); // 加入最后一行
  535. // 2. 计算文本块的总高度
  536. const totalTextHeight = lines.length * lineHeight;
  537. // 3. 计算绘制第一行文本的起始 Y 坐标
  538. // y_center 是外部传入的容器中心点
  539. // 我们从容器中心点上移一半文本总高度,得到文本块的顶部位置
  540. let startY = y_center - totalTextHeight / 2;
  541. // 4. 逐行绘制
  542. // ctx.textBaseline = 'middle' 是在外部设置的,所以我们绘制每一行时,
  543. // Y坐标需要是该行所在矩形区域的垂直中心。
  544. for (let i = 0; i < lines.length; i++) {
  545. // 计算当前行文本的中心Y坐标
  546. const lineY = startY + (i * lineHeight) + (lineHeight / 2);
  547. ctx.fillText(lines[i], x, lineY);
  548. }
  549. },
  550. // 辅助函数:解析 CSS linear-gradient 字符串
  551. parseGradient(gradientString) {
  552. const colorStops = [];
  553. // 简化解析,仅适用于 "linear-gradient(90deg, #RRGGBB 0%, #RRGGBB 100%)" 格式
  554. const matches = gradientString.match(/#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})\s+(\d+)%/g);
  555. if (matches) {
  556. matches.forEach(match => {
  557. const parts = match.split(' ');
  558. colorStops.push({ color: parts[0], stop: parseInt(parts[1]) / 100 });
  559. });
  560. }
  561. return colorStops;
  562. },
  563. calculateScaleAndPosition() {
  564. uni.getSystemInfo({
  565. success: (res) => {
  566. const screenWidth = res.windowWidth; // 手机屏幕的宽度
  567. const pcContentWidth = 630; // PC端内容的原始宽度
  568. this.scale = screenWidth / pcContentWidth;
  569. this.$nextTick(() => {
  570. if (this.$refs.ztzdfxRef) {
  571. this.initZtzdfxChart();
  572. }
  573. });
  574. }
  575. });
  576. },
  577. calculatePdfContainerHeight() {
  578. uni.createSelectorQuery().in(this).select('#pdfContainer').boundingClientRect(rect => {
  579. if (rect) {
  580. this.originalContainerHeight = rect.height;
  581. this.containerScaledHeight = this.originalContainerHeight * this.scale;
  582. // console.log('原始高度:', this.originalContainerHeight, '缩放比例:', this.scale, '缩放后高度:', this.containerScaledHeight);
  583. }
  584. }).exec();
  585. },
  586. downloadZtzdfximage(){
  587. return new Promise(resolve=>{
  588. if (!this.isChartReady) return console.log('图表尚未准备好');
  589. const chartRef = this.$refs.ztzdfxRef;
  590. if (!chartRef) return console.log('无法找到图表组件');
  591. chartRef.canvasToTempFilePath({
  592. success: async (res) => {
  593. const imageUrl = await this.uploadFilePromise(res.tempFilePath);
  594. console.log(imageUrl,'imageUrl');
  595. resolve(imageUrl)
  596. },
  597. fail: (err) => {
  598. console.log('生成图片失败:', err);
  599. }
  600. });
  601. })
  602. },
  603. uploadFilePromise(url) {
  604. return new Promise((resolve, reject) => {
  605. let a = uni.uploadFile({
  606. url: BaseApi+'/uploadFile',
  607. filePath: url,
  608. name: 'file',
  609. success: (res) => {
  610. setTimeout(() => {
  611. let data = JSON.parse(res.data)
  612. if(data&&data.code===0){
  613. resolve(data.data);
  614. }else this.$showToast(data?.msg)
  615. }, 1000);
  616. },
  617. fail: err =>{
  618. resolve('');
  619. }
  620. });
  621. });
  622. },
  623. async initZtzdfxChart() {
  624. let xdata = ['宗旨与动机', '外部流程、系统与结构', '关系', '内部流程、系统与结构', '学习', '领导力'].reverse();
  625. let yvalue = [40, 25, 30, 35, 33, 47].reverse();
  626. let ycolor = [['#771E6A','#771E6A'],['#009191','#009191'],['#FFD750','#FFD750'],['#4EB2B2','#4EB2B2'],['#AFCDF5','#AFCDF5'],['#002846','#002846']].reverse();
  627. let yData = yvalue.map((v, i) => {
  628. return {
  629. value: v,
  630. itemStyle: {
  631. color: new echarts.graphic.LinearGradient(
  632. 0, 0, 1, 0,
  633. [
  634. { offset: 0, color: ycolor[i][0] },
  635. { offset: 1, color: ycolor[i][1] }
  636. ]
  637. ),
  638. borderRadius: [0, 4, 4, 0]
  639. }
  640. };
  641. });
  642. const chart = await this.$refs.ztzdfxRef.init(echarts);
  643. let option = {
  644. grid: {
  645. left: '2%',
  646. right: '10%',
  647. bottom: '0%',
  648. top: '10%',
  649. containLabel: true
  650. },
  651. xAxis: {
  652. type: 'value',
  653. boundaryGap: [0, 0.01],
  654. splitLine: {
  655. show: false
  656. },
  657. // 隐藏X轴轴线和标签,使图表更干净
  658. axisLine: {
  659. show: false
  660. },
  661. axisLabel: {
  662. show: true,
  663. color: '#193D59',
  664. fontSize: 10*this.scale // X轴刻度值也不显示
  665. }
  666. },
  667. yAxis: {
  668. type: 'category',
  669. data: xdata,
  670. axisLine: {
  671. show: true, // 设置为 true 来显示Y轴的轴线
  672. lineStyle: {
  673. color: '#ECEEF5',
  674. width: 1
  675. }
  676. },
  677. // 隐藏Y轴上的小刻度线(如果你想保留可以设为true)
  678. axisTick: {
  679. show: false
  680. },
  681. axisLabel: {
  682. color: '#193D59',
  683. fontSize: 10*this.scale
  684. }
  685. },
  686. series: [
  687. {
  688. type: 'bar',
  689. barWidth: `${8*this.scale}px`,
  690. data: yData
  691. }
  692. ]
  693. };
  694. chart.setOption(option);
  695. this.isChartReady = true;
  696. this.$nextTick(() => {
  697. this.calculatePdfContainerHeight();
  698. });
  699. },
  700. }
  701. };
  702. </script>
  703. <style scoped lang="scss">
  704. .offscreen-canvas {
  705. position: fixed;
  706. top: -9999px;
  707. left: -9999px;
  708. }
  709. .pdf_btn{
  710. padding: 15rpx 20rpx;
  711. border-radius: 20rpx;
  712. font-size: 28rpx;
  713. color: #FFFFFF;
  714. background: #189B9B;
  715. position: fixed;
  716. right: 30rpx;
  717. bottom: 100rpx;
  718. z-index: 1000;
  719. }
  720. .page-wrappe{
  721. width: 100%;
  722. background: #FFFFFF;
  723. overflow-x: hidden;
  724. overflow-y: auto;
  725. .pdf-container{
  726. width: 630px;
  727. padding: 0 20rpx;
  728. box-sizing: border-box;
  729. transform-origin: top left;
  730. }
  731. }
  732. @import '../static/pdf.scss';
  733. </style>