pdfZyb.vue 34 KB

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