pdfZyb.vue 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823
  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="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" style="margin-top: 5px;">
  13. <view class="fm2-texts-pre adf" style="margin-top: 10px;">
  14. <view class="fm2-texts-pre-span">客户名称:</view>
  15. <view class="fm2-texts-pre-val">{{ reportData.teamInfo.enterpriseName||'' }}</view>
  16. </view>
  17. <view class="fm2-texts-pre adf" style="margin-top: 10px;">
  18. <view class="fm2-texts-pre-span">团队名称:</view>
  19. <view class="fm2-texts-pre-val">{{ reportData.teamInfo.teamName||'' }}</view>
  20. </view>
  21. <view class="fm2-texts-pre adf" style="margin-top: 10px;">
  22. <view class="fm2-texts-pre-span">评估发起人:</view>
  23. <view class="fm2-texts-pre-val">{{ reportData.teamInfo.initiator||'' }}</view>
  24. </view>
  25. <view class="fm2-texts-pre adf" style="margin-top: 10px;">
  26. <view class="fm2-texts-pre-span">报告生成时间:</view>
  27. <view class="fm2-texts-pre-val">{{ reportData.teamInfo.reportDate||'' }}</view>
  28. </view>
  29. <view class="fm2-texts-pre adf" style="margin-top: 10px;">
  30. <view class="fm2-texts-pre-span">团队类型:</view>
  31. <view class="fm2-texts-pre-val">{{ reportData.teamInfo.functionName||'' }}</view>
  32. </view>
  33. <view class="fm2-texts-pre adf" style="margin-top: 10px;">
  34. <view class="fm2-texts-pre-span">团队层级:</view>
  35. <view class="fm2-texts-pre-val">{{ reportData.teamInfo.hierarchy||'' }}</view>
  36. </view>
  37. </view>
  38. </view>
  39. <!-- 介绍 -->
  40. <view class="cd_box">
  41. <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)'}">
  42. <view class="vt-left" style="color: #FFFFFF;">介绍<span>PERILL模型简介</span></view>
  43. <view class="vt-right">团队发展动态评估报告(专业版)</view>
  44. </view>
  45. <view class="v2-box">
  46. <img class="vb-img1" :src="'https://gitee.com/hw_0302/chuang-heng-wechat-images/raw/master/versionTwo/'+'intro'+'_box_img1.png'">
  47. <img class="vb-img2" :src="'https://gitee.com/hw_0302/chuang-heng-wechat-images/raw/master/versionTwo/'+'intro'+'_box_img2.png'">
  48. <view class="v2-p">PERILL团队发展动态评估源于团队教练辅导领域的先驱、管理思维与团队绩效领域的权威、全球顶尖团队教练David Clutterbuck教授及其团队通过深入研究,提炼出影响团队绩效的140多个基于实证的因素,整合而成的团队评估和提升工具-PERILL模型。</view>
  49. <view class="v2-p" style="margin-top: 8px;">创衡国际基于10多年来在全球与200多家具有前瞻性的国际公司以及国内具有行业代表性公司的合作经验,结合CCMI的PERILL评估工具,在中国推出的团队发展动态评估系统,旨在帮助团队更全面、更有效地从六个维度评估团队的发展现状,为支持团队成为高价值团队提供全景式的客观评估。</view>
  50. <view class="v2-p" style="margin-top: 8px;">PERILL团队发展动态评估的主体内容由<span>{{reportData.teamInfo.questionCount||0}}</span>个关于团队的描述组成。</view>
  51. </view>
  52. <view class="v2-six">
  53. <view class="vsix-title">PERILL六大纬度</view>
  54. <view class="vsix-p">PERILL评估提供了一个复杂的团队系统概览,它并非针对孤立的问题,也不是简单的优缺点,而是着眼于团队系统的复杂性。它 通过6个影响因素(如下所述)提出问题,以揭示团队系统各要素之间的联系,以及这些联系如何影响团队的高效运作能力。</view>
  55. <view class="vsix-boxs">
  56. <view class="vsb adfac" v-for="(item,index) in sixWd" :key="index">
  57. <img class="vsb-img" :src="item.image"/>
  58. <view class="vsb-right">
  59. <view class="vsbr-top adfac">
  60. <view class="vsbrt-type" :style="{'background':item.color}">{{ item.type }}</view>
  61. <view class="vsbrt-title" :style="{'color':item.color}">{{ item.title }}</view>
  62. </view>
  63. <view class="vsbr-desc">{{ item.desc }}</view>
  64. </view>
  65. </view>
  66. </view>
  67. </view>
  68. </view>
  69. <!-- 总体评估分析 -->
  70. <view class="cd_box adffc">
  71. <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)'}">
  72. <view class="vt-left">总体评估分析</view>
  73. <view class="vt-right">团队发展动态评估报告(专业版)</view>
  74. </view>
  75. <view class="v2-box" @click="downloadZtzdfxImg">
  76. <img class="vb-img1" :src="'https://gitee.com/hw_0302/chuang-heng-wechat-images/raw/master/versionTwo/'+'intro'+'_box_img1.png'">
  77. <img class="vb-img2" :src="'https://gitee.com/hw_0302/chuang-heng-wechat-images/raw/master/versionTwo/'+'intro'+'_box_img2.png'">
  78. <view class="v2-p2">报告的核心是PERILL评估分析下的整体团队表现。这包括团队在PERILL模型每个关键要素上的综合得分,通过结合得分和置信指数,我们能够展示出高功能领域和低功能域。</view>
  79. <view class="v2-p2" style="margin-top: 16px;">下面图中的位置标记显示了团队按主题划分的总分。</view>
  80. <view class="vb-category">
  81. <view class="vbc-pre adfac">
  82. <view class="vbcp-yuan y1"></view>
  83. <view class="vbcp-text">团队Leader</view>
  84. </view>
  85. <view class="vbc-pre adfac">
  86. <view class="vbcp-yuan y2"></view>
  87. <view class="vbcp-text">团队Member</view>
  88. </view>
  89. <view class="vbc-pre adfac">
  90. <view class="vbcp-yuan y3"></view>
  91. <view class="vbcp-text">利益相关方Stakeholder</view>
  92. </view>
  93. <view class="vbc-pre adfac">
  94. <view class="vbcp-yuan y4"></view>
  95. <view class="vbcp-text">赞助人Sponsor</view>
  96. </view>
  97. </view>
  98. <view style="width:100%;height:360px;margin: 0 auto;" class="pdfEchart">
  99. <l-echart ref="ztzdfxRef" :canvas2d="true" @finished="initZtzdfxChart" style="width: 100%;height: 100%;"></l-echart>
  100. </view>
  101. </view>
  102. <view class="v2-data">
  103. <view class="vd-title" :style="{'background-image':'url(https://gitee.com/hw_0302/chuang-heng-wechat-images/raw/master/versionTwo/'+'intro'+'_title_bg1.png)'}">评估结果</view>
  104. <view class="v2-p" v-html="(reportData.totalDiagnosticOutput||'').replaceAll('\n\n','<br>')"></view>
  105. </view>
  106. <view class="v2-data" style="flex: 1;margin-top: 20px;">
  107. <view class="vd-title vt2" :style="{'background-image':'url(https://gitee.com/hw_0302/chuang-heng-wechat-images/raw/master/versionTwo/'+'intro'+'_title_bg2.png)'}">团队提升&教练建议</view>
  108. <view class="v2-p" v-html="(reportData.totalDiagnosisSuggest||'').replaceAll('\n\n','<br>')"></view>
  109. </view>
  110. </view>
  111. <!-- 多维度 -->
  112. <canvas type="2d" id="table-canvas" canvas-id="table-canvas" class="offscreen-canvas"></canvas>
  113. <template v-if="reportData&&reportData.dimensionAnalysis&&reportData.dimensionAnalysis.length">
  114. <view class="cd_box adffc" style="border: none;" :style="{'background-image':'url('+'https://gitee.com/hw_0302/chuang-heng-wechat-images/raw/master/versionTwo/'+typeDict[item.title]+'_bg.png)'}" v-for="(item,index) in reportData.dimensionAnalysis" :key="index">
  115. <view class="v2-top adfacjb" :style="{'background-image':'url('+'https://gitee.com/hw_0302/chuang-heng-wechat-images/raw/master/versionTwo/'+typeDict[item.title]+'_title_bg.png)'}">
  116. <view class="vt-left" :class="{'black':(item.title=='人际关系'||item.title=='学习')}">{{ item.title }}</view>
  117. <view class="vt-right">团队发展动态评估报告(专业版)</view>
  118. </view>
  119. <view class="v2-box" :style="{'border':'1px solid '+item.bcolor}">
  120. <img class="vb-img1" :src="'https://gitee.com/hw_0302/chuang-heng-wechat-images/raw/master/versionTwo/'+typeDict[item.title]+'_box_img1.png'">
  121. <img class="vb-img2" :src="'https://gitee.com/hw_0302/chuang-heng-wechat-images/raw/master/versionTwo/'+typeDict[item.title]+'_box_img2.png'">
  122. <view class="v2-p2">{{ item.desc }}</view>
  123. <view class="v2-p2" style="margin-top: 16px;">评分总体分布</view>
  124. <view class="vb-table" :style="{'border':'1px solid '+item.bcolor,'margin-top':'22px'}">
  125. <view class="vbt-th adfac" :class="{'black':(item.title=='人际关系'||item.title=='学习')}" :style="{'background':item.thcolor}">
  126. <view class="vbtt-w1">主题</view>
  127. <view class="vbtt-w2">最低分</view>
  128. <view class="vbtt-w2">平均分</view>
  129. <view class="vbtt-w2">最高分</view>
  130. <view class="vbtt-w3">问卷陈述</view>
  131. </view>
  132. <view class="vbt-pre adfac" v-for="(ss,si) in item.scoreSpreads" :key="si">
  133. <view class="vbtp-left vbtt-w1 adfacjc" :class="{'black':(item.title=='人际关系'||item.title=='学习'||item.title=='内部流程及系统与架构')}" :style="{'background':item.titlecolor,'padding':'0 16px'}">{{ ss.theme||'' }}</view>
  134. <view class="vbtp-num vbtt-w2" :style="{'border-bottom':'1px solid '+item.bcolor}">{{ ss.minScore||0 }}</view>
  135. <view class="vbtp-num vbtt-w2 green" :style="{'border-bottom':'1px solid '+item.bcolor}">{{ ss.avgScore||0 }}</view>
  136. <view class="vbtp-num vbtt-w2" :style="{'border-bottom':'1px solid '+item.bcolor}">{{ ss.maxScore||0 }}</view>
  137. <view class="vbtp-desc" :style="{'border-bottom':'1px solid '+item.bcolor}">
  138. <view class="vbtpd-title">{{ ss.question||'' }}</view>
  139. <view class="xr_tb adfac">
  140. <view class="xt_pre p1"></view>
  141. <view class="xt_pre p2"></view>
  142. <view class="xt_pre p3"></view>
  143. <view class="xt_score adfac" :style="{'left':ss.left,'width':(((ss.maxScore>25?25:ss.maxScore)-(ss.minScore>25?25:ss.minScore))*4)+'%'}">
  144. <view class="xts_num adfacjc red">{{ ss.minScore>25?25:ss.minScore }}</view>
  145. <view class="xts_box"></view>
  146. <view class="xts_num adfacjc green">{{ ss.maxScore>25?25:ss.maxScore }}</view>
  147. </view>
  148. </view>
  149. </view>
  150. </view>
  151. </view>
  152. </view>
  153. <view class="v2-data" :style="{'border':'1px solid '+item.bcolor}">
  154. <view class="vd-title vt3" :class="{'black':(item.title=='人际关系'||item.title=='学习')}" :style="{'background-image':'url(https://gitee.com/hw_0302/chuang-heng-wechat-images/raw/master/versionTwo/'+typeDict[item.title]+'_title_bg1.png)'}">评估结果</view>
  155. <view class="v2-p3">纬度得分</view>
  156. <view class="vd-wd adfac" :style="{'background':item.wddf}">
  157. <view class="vdwd-pre">维度加权总分:<span>{{ item.weightedTotal||0 }}</span></view>
  158. <view class="vdwd-pre vp">维度同意度总分(未加权):<span>{{ item.consentTotal||0 }}</span></view>
  159. <view class="vdwd-pre">维度权重:<span>{{ item.weight||0 }}</span></view>
  160. </view>
  161. <view class="v2-p" style="margin-top: 8px;" v-html="(item.diagnosisOutput||'').replaceAll('\n\n','<br>')"></view>
  162. </view>
  163. <view class="v2-data" :style="{'border':'1px solid '+item.bcolor}" style="flex: 1;margin-top: 15px;">
  164. <view class="vd-title vt3" :class="{'black':(item.title=='人际关系'||item.title=='学习')}" :style="{'background-image':'url(https://gitee.com/hw_0302/chuang-heng-wechat-images/raw/master/versionTwo/'+typeDict[item.title]+'_title_bg1.png)'}">评估建议</view>
  165. <view class="v2-p" v-html="(item.diagnosisSuggest||'').replaceAll('\n\n','<br>')"></view>
  166. </view>
  167. </view>
  168. </template>
  169. </view>
  170. <view class="pdf_btn" @click="createPdf">生成PDF</view>
  171. </view>
  172. </template>
  173. <script name="">
  174. import { BaseApi } from '@/http/baseApi.js';
  175. import * as echarts from '@/pagesHome/components/lime-echart/static/echarts.min.js'
  176. import lEchart from '@/pagesHome/components/lime-echart/components/l-echart/l-echart.vue'
  177. export default {
  178. name: 'ZtzdfxChart',
  179. components:{ lEchart },
  180. data() {
  181. return {
  182. reportId:'',
  183. reportData: null,
  184. isChartReady: false,
  185. scale:1,
  186. originalContainerHeight: 0,
  187. containerScaledHeight: 'auto',
  188. typeDict: {
  189. '宗旨与动机': 'zzdj',
  190. '外部流程及系统与架构': 'wbjg',
  191. '人际关系': 'rjgx',
  192. '内部流程及系统与架构': 'nbjg',
  193. '学习': 'xx',
  194. '领导力': 'ldl'
  195. },
  196. sixWd: [
  197. {
  198. image: 'https://gitee.com/hw_0302/chuang-heng-wechat-images/raw/master/versionTwo/intro_img_p.png',
  199. type: 'P',
  200. title: '宗旨与动机',
  201. desc: '指团队共享的目的和存在的意义, 包含对共同的愿景,目标和优先级的清晰度。',
  202. color: '#761E6A'
  203. },
  204. {
  205. image: 'https://gitee.com/hw_0302/chuang-heng-wechat-images/raw/master/versionTwo/intro_img_e.png',
  206. type: 'E',
  207. title: '外部流程、系统与结构',
  208. desc: '指团队与其外部利益相关者 - 客户,供应商,股东,组织内的其他团队等的互动关联。',
  209. color: '#009191'
  210. },
  211. {
  212. image: 'https://gitee.com/hw_0302/chuang-heng-wechat-images/raw/master/versionTwo/intro_img_r.png',
  213. type: 'R',
  214. title: '人际关系',
  215. desc: '指团队成员如何共同工作–他们是否相互尊重对方的能力,足够心理安全以能够坦诚相对,真正关心彼此的幸福感。',
  216. color: '#FFD750'
  217. },
  218. {
  219. image: 'https://gitee.com/hw_0302/chuang-heng-wechat-images/raw/master/versionTwo/intro_img_i.png',
  220. type: 'I',
  221. title: '内部流程、系统与结构',
  222. desc: '指团队如何管理工作流程,互相支持和高质量的沟通和决策(包括工作任务和团队感情)。',
  223. color: '#4EB2B2'
  224. },
  225. {
  226. image: 'https://gitee.com/hw_0302/chuang-heng-wechat-images/raw/master/versionTwo/intro_img_l.png',
  227. type: 'L',
  228. title: '学习',
  229. desc: '指团队应对多变的环境和保持持续的进步和成长的能力。团队如何提高绩效(如何完成今天的任务)、能力(如何提高技能和资源以处理明天的任务)和容量(长期的愿景, 如何用更少的资源做更多的事情)',
  230. color: '#AFCDF5'
  231. },
  232. {
  233. image: 'https://gitee.com/hw_0302/chuang-heng-wechat-images/raw/master/versionTwo/intro_img_l2.png',
  234. type: 'L',
  235. title: '领导力',
  236. desc: '指团队认为需要怎样的领导行为能够让他们,作为个人或者团队做到最好。团队可以和他们的领导者讨论他们的责任,以帮助领导者成为他们需要的领导者。',
  237. color: '#002846'
  238. }
  239. ],
  240. pdfImages:[],
  241. };
  242. },
  243. onLoad(options) {
  244. this.reportId = options.reportId;
  245. this.getReportData();
  246. },
  247. mounted() {
  248. this.calculateScaleAndPosition();
  249. uni.onWindowResize(() => {
  250. this.calculateScaleAndPosition();
  251. });
  252. },
  253. methods: {
  254. getReportData(){
  255. this.$api.get(`/core/report/previewReport/${this.reportId}`).then(({data:res})=>{
  256. if(res.code!==0) return this.$showToast(res.msg)
  257. this.reportData = res.data;
  258. const tempDimensionAnalysis = [
  259. {title:'宗旨与动机',bcolor:'#E4D2E1',thcolor:'#761E6A',titlecolor:'#BA8EB4',wddf:'linear-gradient( 90deg, #F5EFF5 0%, #FAF2F9 100%)',
  260. desc:`「宗旨与动机」维度,我们旨在探究是否存在一个清晰的存在理由和明确的方向,能够激发团队成员的动力并吸引他们的想象力,以及个人与集体的身份认同是否围绕共同的目标,并达成一致。`},
  261. {title:'外部流程及系统与架构',bcolor:'#B3DEDE',thcolor:'#009191',titlecolor:'#80C8C8',wddf:'linear-gradient( 90deg, #E8F5F5 0%, #F0F8F8 100%)',
  262. desc:`「外部流程、系统与结构」维度,我们旨在探究团队如何与各种利益相关者互动,他们与团队的利益相关方各自如何寻求了解对方,以及现有系统和流程的有效性,以帮助管理不同的期望和需求。`},
  263. {title:'人际关系',bcolor:'#FFDF73',thcolor:'#FFD750',titlecolor:'#FFEBA8',wddf:'linear-gradient( 90deg, rgba(255,215,80,0.34) 0%, rgba(251,225,130,0.09) 100%)',
  264. desc:`「人际关系」维度,我们旨在探究团队成员如何相互交流、信任程度、尊重和关心的程度,以及团队成员之间的关系如何促进(或破坏)协作。`},
  265. {title:'内部流程及系统与架构',bcolor:'#B3DEDE',thcolor:'#4EB2B2',titlecolor:'#CDE9EA',wddf:'linear-gradient( 90deg, #E8F5F5 0%, #F0F8F8 100%)',
  266. desc:`「内部流程、系统与结构」维度,我们旨在探究团队如何在平衡责任与自主权方面进行协作。我们关注团队的敏捷程度、沟通方式以及决策过程的有效性。`},
  267. {title:'学习',bcolor:'#C7DCF8',thcolor:'#AFCDF5',titlecolor:'#D7E5FA',wddf:'linear-gradient( 270deg, #F2F5F9 0%, #E3ECF8 100%)',
  268. desc:`「学习」维度,我们旨在探究团队如何提高其绩效、技能和资源以应对当前和未来的任务。我们还希望了解团队如何管理能力和提高效率。`},
  269. {title:'领导力',bcolor:'#E6EAED',thcolor:'#002846',titlecolor:'#8093A3',wddf:'linear-gradient( 270deg, #F2F4F6 0%, #EDF0F2 100%)',
  270. desc:`「领导力」维度,我们旨在探究领导素质和行为如何对团队功能和其他因素产生调节影响,以及这是积极的还是消极的。`}
  271. ]
  272. this.reportData.dimensionAnalysis.forEach((d,i)=>{
  273. d.scoreSpreads.forEach(s=>{
  274. s.theme = s.theme.replaceAll(',','').replaceAll(',','');
  275. s.range = [s.minScore>25?25:s.minScore,s.maxScore>25?25:s.maxScore];
  276. if(s.minScore==0) s.left = '0%';
  277. else if(s.minScore>=25) s.left = 'calc(100% - 48px)';
  278. else s.left = s.minScore*4+'%';
  279. })
  280. this.reportData.dimensionAnalysis[i] = {...d,...tempDimensionAnalysis[i]}
  281. })
  282. })
  283. },
  284. async createPdf(){
  285. uni.showLoading({
  286. title:'正在生成PDF所需的图片...'
  287. })
  288. try {
  289. const ztzdfxImgPromise = this.downloadZtzdfxImg();
  290. const dimensionImagePromises = this.reportData.dimensionAnalysis.map(d => {
  291. return this.generateTableImage(d,d.scoreSpreads);
  292. });
  293. const allImageUrls = await Promise.all([
  294. ztzdfxImgPromise,
  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. * @description 使用 Canvas 绘制表格并生成图片
  319. * @param {Object} dimensionData 维度数据
  320. * @param {Array} tableData 表格数据
  321. * @returns {Promise<string>} 返回生成的图片临时文件路径
  322. */
  323. generateTableImage(dimensionData, tableData) {
  324. return new Promise((resolve, reject) => {
  325. const query = uni.createSelectorQuery().in(this);
  326. query.select('#table-canvas')
  327. .fields({ node: true, size: true })
  328. .exec(async (res) => {
  329. if (!res || !res[0] || !res[0].node) {
  330. return reject('获取Canvas节点失败');
  331. }
  332. const canvasNode = res[0].node;
  333. const ctx = canvasNode.getContext('2d');
  334. const dpr = uni.getSystemInfoSync().pixelRatio;
  335. // --- 1. 定义布局和尺寸常量
  336. const TABLE_WIDTH = 548;
  337. const HEADER_HEIGHT = 38;
  338. const ROW_HEIGHT = 49;
  339. const FONT_FAMILY = 'sans-serif';
  340. const COL_WIDTHS = { theme: 72, min: 49, avg: 49, max: 49, statement: 329 };
  341. const COL_POSITIONS = {
  342. theme: 0,
  343. min: COL_WIDTHS.theme,
  344. avg: COL_WIDTHS.theme + COL_WIDTHS.min,
  345. max: COL_WIDTHS.theme + COL_WIDTHS.min + COL_WIDTHS.avg,
  346. statement: COL_WIDTHS.theme + COL_WIDTHS.min * 3
  347. };
  348. const CANVAS_HEIGHT = HEADER_HEIGHT + tableData.length * ROW_HEIGHT;
  349. const CANVAS_WIDTH = TABLE_WIDTH;
  350. canvasNode.width = CANVAS_WIDTH * dpr;
  351. canvasNode.height = CANVAS_HEIGHT * dpr;
  352. ctx.scale(dpr, dpr);
  353. ctx.fillStyle = '#FFFFFF';
  354. ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
  355. ctx.strokeStyle = dimensionData.bcolor;
  356. ctx.lineWidth = 1;
  357. ctx.strokeRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
  358. // --- 4. 绘制表头 ---
  359. const isBlackHeader = (dimensionData.title == '人际关系'||dimensionData.title == '学习');
  360. ctx.fillStyle = dimensionData.thcolor;
  361. ctx.fillRect(1, 1, CANVAS_WIDTH - 2, HEADER_HEIGHT - 1);
  362. ctx.fillStyle = isBlackHeader ? '#002846' : '#FFFFFF';
  363. ctx.font = `bold 10px ${FONT_FAMILY}`;
  364. ctx.textAlign = 'center';
  365. ctx.textBaseline = 'middle';
  366. ctx.fillText('主题', COL_POSITIONS.theme + COL_WIDTHS.theme / 2, HEADER_HEIGHT / 2);
  367. ctx.fillText('最低分', COL_POSITIONS.min + COL_WIDTHS.min / 2, HEADER_HEIGHT / 2);
  368. ctx.fillText('平均分', COL_POSITIONS.avg + COL_WIDTHS.avg / 2, HEADER_HEIGHT / 2);
  369. ctx.fillText('最高分', COL_POSITIONS.max + COL_WIDTHS.max / 2, HEADER_HEIGHT / 2);
  370. ctx.fillText('问卷陈述', COL_POSITIONS.statement + COL_WIDTHS.statement / 2, HEADER_HEIGHT / 2);
  371. // --- 5. 循环绘制每一行---
  372. tableData.forEach((row, index) => {
  373. const y = HEADER_HEIGHT + index * ROW_HEIGHT;
  374. const isBlackLeftTitle = (dimensionData.title == '人际关系' || dimensionData.title == '学习' || dimensionData.title == '内部流程及系统与架构');
  375. ctx.fillStyle = dimensionData.titlecolor;
  376. ctx.fillRect(1, y, COL_WIDTHS.theme - 1, ROW_HEIGHT);
  377. // 绘制主题文字
  378. ctx.textAlign = 'center';
  379. ctx.textBaseline = 'middle';
  380. ctx.fillStyle = isBlackLeftTitle ? '#002846' : '#FFFFFF';
  381. ctx.font = `10px ${FONT_FAMILY}`;
  382. this.drawWrappedText(ctx, row.theme, COL_POSITIONS.theme + COL_WIDTHS.theme / 2, y + ROW_HEIGHT / 2, 12, COL_WIDTHS.theme - 32);
  383. // 绘制主题单元格的白色下边框
  384. ctx.strokeStyle = '#FFFFFF';
  385. ctx.lineWidth = 1;
  386. ctx.beginPath();
  387. ctx.moveTo(1, y + ROW_HEIGHT - 1);
  388. ctx.lineTo(COL_WIDTHS.theme - 1, y + ROW_HEIGHT - 1);
  389. ctx.stroke();
  390. // 绘制其他单元格的下边框
  391. ctx.strokeStyle = dimensionData.bcolor;
  392. ['min', 'avg', 'max', 'statement'].forEach(key => {
  393. ctx.beginPath();
  394. ctx.moveTo(COL_POSITIONS[key], y + ROW_HEIGHT);
  395. ctx.lineTo(COL_POSITIONS[key] + COL_WIDTHS[key], y + ROW_HEIGHT);
  396. ctx.stroke();
  397. });
  398. ctx.font = `bold 14px ${FONT_FAMILY}`;
  399. ctx.fillStyle = '#667E90';
  400. ctx.fillText(row.minScore, COL_POSITIONS.min + COL_WIDTHS.min / 2, y + ROW_HEIGHT / 2);
  401. ctx.fillStyle = '#27AE60';
  402. ctx.fillText(row.avgScore, COL_POSITIONS.avg + COL_WIDTHS.avg / 2, y + ROW_HEIGHT / 2);
  403. ctx.fillStyle = '#667E90';
  404. ctx.fillText(row.maxScore, COL_POSITIONS.max + COL_WIDTHS.max / 2, y + ROW_HEIGHT / 2);
  405. // 5.3 绘制问卷陈述列
  406. const statementX = COL_POSITIONS.statement;
  407. const statementPadding = 10;
  408. ctx.textAlign = 'left';
  409. ctx.textBaseline = 'top';
  410. ctx.fillStyle = '#193D59';
  411. ctx.font = `9px ${FONT_FAMILY}`;
  412. this.drawWrappedText(ctx, row.question, statementX + statementPadding, y + 8, 10, COL_WIDTHS.statement - statementPadding * 2);
  413. // 绘制范围指示器
  414. const rangeBarY = y + 33;
  415. const rangeBarWidth = COL_WIDTHS.statement - statementPadding * 2;
  416. const rangeBarHeight = 4;
  417. const rangeBarX = statementX + statementPadding;
  418. // 绘制三段色背景
  419. const segWidth = rangeBarWidth / 3;
  420. ctx.fillStyle = '#BA8EB4';
  421. ctx.fillRect(rangeBarX, rangeBarY, segWidth, rangeBarHeight);
  422. ctx.fillStyle = '#66BDBD';
  423. ctx.fillRect(rangeBarX + segWidth, rangeBarY, segWidth, rangeBarHeight);
  424. ctx.fillStyle = '#AFCDF5';
  425. ctx.fillRect(rangeBarX + segWidth * 2, rangeBarY, segWidth, rangeBarHeight);
  426. // --- 开始绘制滑块 ---
  427. const scaleFactor = rangeBarWidth / 25;
  428. const minVal = row.range[0];
  429. const maxVal = row.range[1];
  430. const rangeLeft = minVal * scaleFactor;
  431. const rangeWidth = (maxVal - minVal) * scaleFactor;
  432. // 绘制中间的连接条
  433. const connectorY = rangeBarY - (8 - rangeBarHeight) / 2;
  434. const connectorHeight = 8;
  435. ctx.fillStyle = '#199C9C';
  436. ctx.fillRect(rangeBarX + rangeLeft, connectorY, rangeWidth, connectorHeight);
  437. // 绘制左右数字框
  438. const numBoxPadding = { h: 7, v: 4 };
  439. const numBoxFont = `bold 12px ${FONT_FAMILY}`;
  440. // 封装一个绘制数字框的函数
  441. const drawNumberBox = (text, side) => {
  442. ctx.font = numBoxFont;
  443. const metrics = ctx.measureText(text);
  444. const boxWidth = metrics.width + numBoxPadding.h * 2;
  445. const boxHeight = 12 + numBoxPadding.v * 2;
  446. const value = parseFloat(text);
  447. const centerPointX = rangeBarX + value * scaleFactor;
  448. let x;
  449. // 核心逻辑:判断两值是否相等
  450. if (minVal === maxVal) {
  451. // 如果相等,左右滑块并排显示,整体居中
  452. if (side === 'left') {
  453. x = centerPointX - boxWidth;
  454. } else { // 'right'
  455. x = centerPointX;
  456. }
  457. // --- 边界检查(针对组合滑块)---
  458. let combinedLeftX = centerPointX - boxWidth;
  459. if (combinedLeftX < rangeBarX) {
  460. const shift = rangeBarX - combinedLeftX;
  461. x += shift; // 将两个滑块一起向右移动
  462. }
  463. let combinedRightX = centerPointX + boxWidth;
  464. if (combinedRightX > rangeBarX + rangeBarWidth) {
  465. const shift = combinedRightX - (rangeBarX + rangeBarWidth);
  466. x -= shift; // 将两个滑块一起向左移动
  467. }
  468. } else {
  469. // 如果不相等,各自居中显示
  470. x = centerPointX - boxWidth / 2;
  471. // --- 边界检查(针对单个滑块)---
  472. if (x < rangeBarX) {
  473. x = rangeBarX;
  474. }
  475. if (x + boxWidth > rangeBarX + rangeBarWidth) {
  476. x = rangeBarX + rangeBarWidth - boxWidth;
  477. }
  478. }
  479. const boxY = connectorY + (connectorHeight - boxHeight) / 2;
  480. // 绘制阴影
  481. ctx.shadowColor = 'rgba(118, 30, 106, 0.08)';
  482. ctx.shadowBlur = 10;
  483. ctx.shadowOffsetY = 4;
  484. // 绘制圆角矩形背景
  485. ctx.fillStyle = '#FFFFFF';
  486. ctx.beginPath();
  487. ctx.moveTo(x + 4, boxY);
  488. ctx.arcTo(x + boxWidth, boxY, x + boxWidth, boxY + boxHeight, 4);
  489. ctx.arcTo(x + boxWidth, boxY + boxHeight, x, boxY + boxHeight, 4);
  490. ctx.arcTo(x, boxY + boxHeight, x, boxY, 4);
  491. ctx.arcTo(x, boxY, x + boxWidth, boxY, 4);
  492. ctx.closePath();
  493. ctx.fill();
  494. // 重置阴影
  495. ctx.shadowColor = 'transparent';
  496. ctx.shadowBlur = 0;
  497. ctx.shadowOffsetY = 0;
  498. // 绘制边框
  499. ctx.strokeStyle = 'rgba(131, 52, 120, 0.19)';
  500. ctx.lineWidth = 1;
  501. ctx.stroke();
  502. // 绘制文字
  503. ctx.fillStyle = side === 'left' ? '#904A87' : '#199C9C';
  504. ctx.textAlign = 'center';
  505. ctx.textBaseline = 'middle';
  506. ctx.fillText(text, x + boxWidth / 2, boxY + boxHeight / 2);
  507. };
  508. drawNumberBox(minVal.toString(), 'left');
  509. drawNumberBox(maxVal.toString(), 'right');
  510. });
  511. // --- 6. 生成图片文件 ---
  512. uni.canvasToTempFilePath({
  513. canvas: canvasNode,
  514. success: async (result) => {
  515. const fileurl = await this.uploadFilePromise(result.tempFilePath);
  516. resolve(fileurl);
  517. },
  518. fail: (err) => {
  519. console.error('图片生成失败', err);
  520. uni.showToast({ title: '图片生成失败', icon: 'none' });
  521. reject(err);
  522. }
  523. }, this);
  524. });
  525. });
  526. },
  527. /**
  528. * @description 辅助函数:在Canvas中绘制可自动换行的文本
  529. * @param {CanvasRenderingContext2D} ctx
  530. * @param {string} text 要绘制的文本
  531. * @param {number} x 起始x坐标
  532. * @param {number} y 起始y坐标(对于居中对齐,这是中心y;对于top对齐,这是第一行的y)
  533. * @param {number} lineHeight 行高
  534. * @param {number} maxWidth 最大宽度
  535. */
  536. drawWrappedText(ctx, text, x, y, lineHeight, maxWidth) {
  537. let words = text.split('');
  538. let line = '';
  539. let lines = [];
  540. for (let n = 0; n < words.length; n++) {
  541. let testLine = line + words[n];
  542. let metrics = ctx.measureText(testLine);
  543. if (metrics.width > maxWidth && n > 0) {
  544. lines.push(line);
  545. line = words[n];
  546. } else {
  547. line = testLine;
  548. }
  549. }
  550. lines.push(line);
  551. let startY;
  552. if (ctx.textBaseline === 'middle') {
  553. startY = y - (lineHeight * (lines.length - 1)) / 2;
  554. } else { // top
  555. startY = y;
  556. }
  557. for (let i = 0; i < lines.length; i++) {
  558. ctx.fillText(lines[i], x, startY + (i * lineHeight));
  559. }
  560. },
  561. calculateScaleAndPosition() {
  562. uni.getSystemInfo({
  563. success: (res) => {
  564. const screenWidth = res.windowWidth; // 手机屏幕的宽度
  565. const pcContentWidth = 630; // PC端内容的原始宽度
  566. this.scale = screenWidth / pcContentWidth;
  567. this.$nextTick(() => {
  568. if (this.$refs.ztzdfxRef) {
  569. this.initZtzdfxChart();
  570. }
  571. });
  572. }
  573. });
  574. },
  575. calculatePdfContainerHeight() {
  576. uni.createSelectorQuery().in(this).select('#pdfContainer').boundingClientRect(rect => {
  577. if (rect) {
  578. this.originalContainerHeight = rect.height;
  579. this.containerScaledHeight = this.originalContainerHeight * this.scale;
  580. // console.log('原始高度:', this.originalContainerHeight, '缩放比例:', this.scale, '缩放后高度:', this.containerScaledHeight);
  581. }
  582. }).exec();
  583. },
  584. downloadZtzdfxImg(){
  585. return new Promise(resolve=>{
  586. if (!this.isChartReady) return console.log('图表尚未准备好');
  587. const chartRef = this.$refs.ztzdfxRef;
  588. if (!chartRef) return console.log('无法找到图表组件');
  589. chartRef.canvasToTempFilePath({
  590. success: async (res) => {
  591. const imgUrl = await this.uploadFilePromise(res.tempFilePath);
  592. console.log(imgUrl,'imgUrl');
  593. resolve(imgUrl)
  594. },
  595. fail: (err) => {
  596. console.log('生成图片失败:', err);
  597. }
  598. });
  599. })
  600. },
  601. uploadFilePromise(url) {
  602. return new Promise((resolve, reject) => {
  603. let a = uni.uploadFile({
  604. url: BaseApi+'/uploadFile',
  605. filePath: url,
  606. name: 'file',
  607. success: (res) => {
  608. setTimeout(() => {
  609. let data = JSON.parse(res.data)
  610. if(data&&data.code===0){
  611. resolve(data.data);
  612. }else this.$showToast(data?.msg)
  613. }, 1000);
  614. },
  615. fail: err =>{
  616. resolve('');
  617. }
  618. });
  619. });
  620. },
  621. async initZtzdfxChart() {
  622. let dataSum = this.reportData.overall.length*this.reportData.overall[0].themeTotalSpreads.length;
  623. const leaderData = [],memberData = [],stakeholderData=[],sponsorData=[];
  624. const overall = this.reportData.overall||[];
  625. overall.forEach(o=>{
  626. let themeTotalSpreads = o.themeTotalSpreads||[];
  627. if(['内部流程、系统与结构','学习','领导力'].includes(o.dimension)) themeTotalSpreads = o.themeTotalSpreads.reverse()||[];
  628. themeTotalSpreads.forEach(t=>{
  629. leaderData.push(t.scoreLeader||0);
  630. memberData.push(t.scoreMember||0);
  631. stakeholderData.push(t.scoreStakeholder||0);
  632. sponsorData.push(t.scoreSponsor||0);
  633. })
  634. })
  635. const sumArr = leaderData.concat(memberData).concat(stakeholderData).concat(sponsorData);
  636. const maxValue = sumArr.reduce((a,b)=>Math.max(a,b));
  637. const minValue = sumArr.reduce((a,b)=>Math.min(a,b));
  638. const chart = await this.$refs.ztzdfxRef.init(echarts);
  639. let option = {
  640. graphic: [
  641. {
  642. type: 'image',
  643. id: 'radar-bg',
  644. z: -1,
  645. bounding: 'raw',
  646. left: 'center',
  647. top: 'center',
  648. style: {
  649. image:'https://gitee.com/hw_0302/chuang-heng-wechat-images/raw/master/six_chart_bg.png',
  650. width: 360*this.scale-20,
  651. height: 360*this.scale-20,
  652. opacity: 1
  653. }
  654. }
  655. ],
  656. radar: {
  657. // shape: 'circle',
  658. indicator: new Array(dataSum).fill({ max:maxValue, min:minValue }),
  659. axisName: {
  660. show: false
  661. },
  662. splitArea:{
  663. show:false
  664. },
  665. splitLine: {
  666. show: false
  667. },
  668. axisLine: {
  669. show: false
  670. },
  671. startAngle: 95
  672. },
  673. series: [
  674. {
  675. type: 'radar',
  676. data: [
  677. {
  678. value: leaderData,
  679. itemStyle: {
  680. color: '#9F6196'
  681. },
  682. lineStyle: {
  683. color: '#9F6196',
  684. width:2
  685. },
  686. areaStyle: {
  687. color: 'rgba(255, 255, 255, 0.4)'
  688. },
  689. symbolSize: 4
  690. },
  691. {
  692. value: memberData,
  693. itemStyle: {
  694. color: '#AFCDF5'
  695. },
  696. lineStyle: {
  697. color: '#AFCDF5',
  698. width:2
  699. },
  700. areaStyle: {
  701. color: 'rgba(255, 255, 255, 0.4)'
  702. },
  703. symbolSize: 4
  704. },
  705. {
  706. value: stakeholderData,
  707. itemStyle: {
  708. color: '#FFD650'
  709. },
  710. lineStyle: {
  711. color: '#FFD650',
  712. width:2
  713. },
  714. areaStyle: {
  715. color: 'rgba(255, 255, 255, 0.4)'
  716. },
  717. symbolSize: 4
  718. },
  719. {
  720. value: sponsorData,
  721. itemStyle: {
  722. color: '#012846'
  723. },
  724. lineStyle: {
  725. color: '#012846',
  726. width:2
  727. },
  728. areaStyle: {
  729. color: 'rgba(255, 255, 255, 0.4)'
  730. },
  731. symbolSize: 4
  732. }
  733. ]
  734. }
  735. ]
  736. };
  737. chart.setOption(option);
  738. this.isChartReady = true;
  739. this.$nextTick(() => {
  740. this.calculatePdfContainerHeight();
  741. });
  742. },
  743. }
  744. };
  745. </script>
  746. <style scoped lang="scss">
  747. .page-wrappe{
  748. width: 100%;
  749. background: #FFFFFF;
  750. overflow-x: hidden;
  751. overflow-y: auto;
  752. .pdf-container{
  753. width: 630px;
  754. padding: 0 20rpx;
  755. box-sizing: border-box;
  756. transform-origin: top left;
  757. }
  758. }
  759. .offscreen-canvas {
  760. position: fixed;
  761. top: -9999px;
  762. left: -9999px;
  763. }
  764. .pdf_btn{
  765. padding: 15rpx 20rpx;
  766. border-radius: 20rpx;
  767. font-size: 28rpx;
  768. color: #FFFFFF;
  769. background: #189B9B;
  770. position: fixed;
  771. right: 30rpx;
  772. bottom: 100rpx;
  773. z-index: 1000;
  774. }
  775. @import '../static/pdf.scss';
  776. </style>