pdf.vue 34 KB

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