XQGeneratePoster.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403
  1. <template>
  2. <view class="content">
  3. <u-mask :show="share_qrcode_flag" :zoom="false" :custom-style="{ background: 'rgba(0,0,0,.8)' }" :duration="0">
  4. <view class="sq_box">
  5. <view class="tz_box qrcode_box">
  6. <view class="close_box" @click="share_qrcode_flag = false"><u-icon name="close" color="#ffffff"></u-icon></view>
  7. <view class="share_qrcode">
  8. <canvas canvas-id="myCanvas" style="width: 690px;height:1040px; position: fixed;top: -10000px;"></canvas>
  9. <image @longpress="showSaveImgWin = true" style="width: 100%; height: 100%;" :src="canvasToTempFilePath"></image>
  10. </view>
  11. </view>
  12. </view>
  13. </u-mask>
  14. <u-modal
  15. v-model="showSaveImgWin"
  16. content="确定要保存图片吗"
  17. @confirm="saveShareImg(canvasToTempFilePath)"
  18. @cancel="showSaveImgWin = false"
  19. :show-cancel-button="true"
  20. ></u-modal>
  21. </view>
  22. </template>
  23. <script>
  24. export default {
  25. name: 'XQGeneratePoster',
  26. data() {
  27. return {
  28. ratio: 1,
  29. ctx: null, // 创建canvas对象
  30. canvasToTempFilePath: null, // 保存最终生成的导出的图片地址
  31. openStatus: true, // 声明一个全局变量判断是否授权保存到相册
  32. share_qrcode_flag: false,
  33. showSaveImgWin: false //保存图片到相册
  34. };
  35. },
  36. methods: {
  37. share_qrcode(option) {
  38. if (option) {
  39. if (!this.canvasToTempFilePath) {
  40. this.createCanvasImage(option);
  41. }
  42. this.share_qrcode_flag = true;
  43. }
  44. },
  45. //获取图片信息
  46. downloadFileImg(url) {
  47. return new Promise(resolve => {
  48. uni.getImageInfo({
  49. src: url,
  50. success: res => {
  51. resolve(res.path);
  52. },
  53. fail: err => {
  54. console.log(err);
  55. uni.showToast({
  56. title: 'Lỗi mạng Vui lòng thử lại',
  57. icon: 'loading'
  58. });
  59. }
  60. });
  61. });
  62. },
  63. // 生成海报
  64. async createCanvasImage(option) {
  65. // 点击生成海报数据埋点
  66. if (!this.ctx) {
  67. uni.showLoading({
  68. title: '生成中...'
  69. });
  70. let code = this.downloadFileImg(option.codeUrl);
  71. let cover = this.downloadFileImg(option.coverUrl);
  72. let headImg = this.downloadFileImg(option.headUrl);
  73. let bgUrl = '';
  74. if (option.bgUrl) {
  75. bgUrl = new Promise(resolve => {
  76. uni.downloadFile({
  77. //生成临时地址
  78. url: option.bgUrl,
  79. success: res => {
  80. resolve(res.tempFilePath);
  81. },
  82. fail: erros => {
  83. uni.showToast({
  84. title: 'Lỗi mạng Vui lòng thử lại',
  85. icon: 'loading'
  86. });
  87. }
  88. });
  89. });
  90. }
  91. Promise.all([headImg, code, cover, bgUrl]).then(result => {
  92. const ctx = uni.createCanvasContext('myCanvas', this);
  93. let canvasWidthPx = 640 * this.ratio,
  94. canvasHeightPx = 1040 * this.ratio,
  95. avatarurl_width = 120, //绘制的头像宽度
  96. avatarurl_heigth = 120, //绘制的头像高度
  97. avatarurl_x = 40, //绘制的头像在画布上的位置
  98. avatarurl_y = 28, //绘制的头像在画布上的位置
  99. codeurl_width = 180, //绘制的二维码宽度
  100. codeurl_heigth = 180, //绘制的二维码高度
  101. codeurl_x = 70, //绘制的二维码在画布上的位置
  102. codeurl_y = 800, //绘制的二维码在画布上的位置
  103. coverurl_width = 610, //绘制的封面宽度
  104. coverurl_heigth = 350, //绘制的封面高度
  105. coverurl_x = 40, //绘制的封面在画布上的位置
  106. coverurl_y = 190; //绘制的封面在画布上的位置
  107. if (option.bgUrl) {
  108. ctx.drawImage(result[3], 0, 0, 690, 1040); // 背景图片需要本地
  109. } else {
  110. //绘制圆角矩形
  111. ctx.save();
  112. ctx.translate(0, 0);
  113. //绘制圆角矩形的各个边
  114. this.drawRoundRectPath(ctx, 690, 1040, 14);
  115. ctx.fillStyle = option.fillStyle || '#0688ff'; //若是给定了值就用给定的值否则给予默认值
  116. ctx.fill();
  117. ctx.restore();
  118. }
  119. // 白底
  120. ctx.setFillStyle('#ffffff');
  121. ctx.fillRect(25, 175, 640, 840);
  122. ctx.save(); // 先保存状态 已便于画完圆再用
  123. ctx.beginPath(); //开始绘制
  124. //先画个圆 前两个参数确定了圆心 (x,y) 坐标 第三个参数是圆的半径 四参数是绘图方向 默认是false,即顺时针
  125. ctx.arc(avatarurl_width / 2 + avatarurl_x, avatarurl_heigth / 2 + avatarurl_y, avatarurl_width / 2, 0, Math.PI * 2, false);
  126. ctx.clip(); //画了圆 再剪切 原始画布中剪切任意形状和尺寸。一旦剪切了某个区域,则所有之后的绘图都会被限制在被剪切的区域内
  127. ctx.drawImage(result[0], avatarurl_x, avatarurl_y, avatarurl_width, avatarurl_heigth); // 推进去图片
  128. ctx.restore(); //恢复之前保存的绘图上下文状态 可以继续绘制
  129. ctx.font = 'normal bold 45px sans-serif';
  130. ctx.setFillStyle('#ffffff'); // 文字颜色
  131. if (option.nickName) {
  132. this.dealWords({
  133. ctx: ctx, //画布上下文
  134. fontSize: 45, //字体大小
  135. word: option.nickName, //需要处理的文字
  136. maxWidth: 480, //一行文字最大宽度
  137. x: 190, //文字在x轴要显示的位置
  138. y: 40, //文字在y轴要显示的位置
  139. maxLine: 1 //文字最多显示的行数
  140. });
  141. }
  142. ctx.setFillStyle('#ffffff'); // 文字颜色
  143. ctx.setFontSize(30); // 文字字号
  144. ctx.fillText(option.miniName, 190, 130); // 绘制文字
  145. ctx.setFillStyle('#222222');
  146. if (option.tkName) {
  147. this.dealWords({
  148. ctx: ctx, //画布上下文
  149. fontSize: 32, //字体大小
  150. word: option.tkName, //需要处理的文字
  151. maxWidth: 610, //一行文字最大宽度
  152. x: 40, //文字在x轴要显示的位置
  153. y: 550, //文字在y轴要显示的位置
  154. maxLine: 2 //文字最多显示的行数
  155. });
  156. }
  157. ctx.font = 'normal normal 26px sans-serif';
  158. ctx.setFillStyle('#555555'); // 文字颜色
  159. ctx.fillText('题库作者:', 40, 670); // 绘制文字
  160. ctx.font = 'normal normal 26px sans-serif';
  161. ctx.setFillStyle('#555555'); // 文字颜色
  162. if (option.tkAuthor) {
  163. this.dealWords({
  164. ctx: ctx, //画布上下文
  165. fontSize: 26, //字体大小
  166. word: option.tkAuthor, //需要处理的文字
  167. maxWidth: 490, //一行文字最大宽度
  168. x: 170, //文字在x轴要显示的位置
  169. y: 630, //文字在y轴要显示的位置
  170. maxLine: 1 //文字最多显示的行数
  171. });
  172. }
  173. // 白底
  174. ctx.setFillStyle('#CBE6FE');
  175. ctx.fillRect(40, 690, 125, 40);
  176. ctx.font = 'normal normal 24px sans-serif';
  177. ctx.setFillStyle('#1F8DFE'); // 文字颜色
  178. ctx.fillText(option.tkType, 78, 720); // 绘制文字
  179. // 白底
  180. ctx.setFillStyle('#FDE5D2');
  181. ctx.fillRect(180, 690, 125, 40);
  182. ctx.setFillStyle('#F37F26'); // 文字颜色
  183. ctx.fillText(option.cost, 218, 720); // 绘制文字
  184. // 白底
  185. ctx.setFillStyle('#D2F1EF');
  186. ctx.fillRect(320, 690, 125, 40);
  187. ctx.setFillStyle('#2EBBB4'); // 文字颜色
  188. ctx.fillText(option.isPub, 360, 720); // 绘制文字
  189. ctx.beginPath();
  190. // 设置线宽
  191. ctx.lineWidth = 1;
  192. // 设置间距(参数为无限数组,虚线的样式会随数组循环)
  193. ctx.setLineDash([10, 10]);
  194. // 移动画笔至坐标 x20 y20 的位置
  195. ctx.moveTo(30, 760);
  196. // 绘制到坐标 x20 y100 的位置
  197. ctx.lineTo(660, 760);
  198. // 填充颜色
  199. ctx.strokeStyle = '#aaaaaa';
  200. // 开始填充
  201. ctx.stroke();
  202. ctx.closePath();
  203. ctx.font = 'normal normal 36px sans-serif';
  204. ctx.setFillStyle('#E65449'); // 文字颜色
  205. ctx.fillText('长按识别', 300, 870); // 绘制孩子百分比
  206. ctx.font = 'normal normal 36px sans-serif';
  207. ctx.setFillStyle('#222222'); // 文字颜色
  208. ctx.fillText('小程序码', 444, 870); // 绘制孩子百分比
  209. ctx.font = 'normal normal 36px sans-serif';
  210. ctx.setFillStyle('#222222'); // 文字颜色
  211. ctx.fillText('查看题库详细信息', 300, 920); // 绘制孩子百分比
  212. ctx.drawImage(result[2], coverurl_x, coverurl_y, coverurl_width, coverurl_heigth); // 绘制封面
  213. ctx.drawImage(result[1], codeurl_x, codeurl_y, codeurl_width, codeurl_heigth); // 绘制头像
  214. ctx.draw(false, () => {
  215. // canvas画布转成图片并返回图片地址
  216. uni.canvasToTempFilePath(
  217. {
  218. canvasId: 'myCanvas',
  219. width: 690,
  220. height: 1040,
  221. destWidth: 690,
  222. destHeight: 1040,
  223. success: res => {
  224. this.canvasToTempFilePath = res.tempFilePath;
  225. this.showShareImg = true;
  226. uni.showToast({
  227. title: 'Vẽ thành công'
  228. });
  229. },
  230. fail: err => {
  231. uni.showToast({
  232. title: 'Vẽ thất bại'
  233. });
  234. },
  235. complete: () => {
  236. uni.hideLoading();
  237. uni.hideToast();
  238. }
  239. },
  240. this
  241. );
  242. });
  243. });
  244. }
  245. },
  246. drawRoundRectPath(cxt, width, height, radius) {
  247. cxt.beginPath(0);
  248. //从右下角顺时针绘制,弧度从0到1/2PI
  249. cxt.arc(width - radius, height - radius, radius, 0, Math.PI / 2);
  250. //矩形下边线
  251. cxt.lineTo(radius, height);
  252. //左下角圆弧,弧度从1/2PI到PI
  253. cxt.arc(radius, height - radius, radius, Math.PI / 2, Math.PI);
  254. //矩形左边线
  255. cxt.lineTo(0, radius);
  256. //左上角圆弧,弧度从PI到3/2PI
  257. cxt.arc(radius, radius, radius, Math.PI, (Math.PI * 3) / 2);
  258. //上边线
  259. cxt.lineTo(width - radius, 0);
  260. //右上角圆弧
  261. cxt.arc(width - radius, radius, radius, (Math.PI * 3) / 2, Math.PI * 2);
  262. //右边线
  263. cxt.lineTo(width, height - radius);
  264. cxt.closePath();
  265. },
  266. //处理文字多出省略号显示
  267. dealWords(options) {
  268. options.ctx.setFontSize(options.fontSize); //设置字体大小
  269. let allRow = Math.ceil(options.ctx.measureText(options.word).width / options.maxWidth); //实际总共能分多少行
  270. let count = allRow >= options.maxLine ? options.maxLine : allRow; //实际能分多少行与设置的最大显示行数比,谁小就用谁做循环次数
  271. let endPos = 0; //当前字符串的截断点
  272. for (let j = 0; j < count; j++) {
  273. let nowStr = options.word.slice(endPos); //当前剩余的字符串
  274. let rowWid = 0; //每一行当前宽度
  275. if (options.ctx.measureText(nowStr).width > options.maxWidth) {
  276. //如果当前的字符串宽度大于最大宽度,然后开始截取
  277. for (let m = 0; m < nowStr.length; m++) {
  278. rowWid += options.ctx.measureText(nowStr[m]).width; //当前字符串总宽度
  279. if (rowWid > options.maxWidth) {
  280. if (j === options.maxLine - 1) {
  281. //如果是最后一行
  282. options.ctx.fillText(nowStr.slice(0, m - 1) + '...', options.x, options.y + (j + 1) * 40); //(j+1)*18这是每一行的高度
  283. } else {
  284. options.ctx.fillText(nowStr.slice(0, m), options.x, options.y + (j + 1) * 40);
  285. }
  286. endPos += m; //下次截断点
  287. break;
  288. }
  289. }
  290. } else {
  291. //如果当前的字符串宽度小于最大宽度就直接输出
  292. options.ctx.fillText(nowStr.slice(0), options.x, options.y + (j + 1) * 40);
  293. }
  294. }
  295. },
  296. // 保存到系统相册
  297. saveShareImg(canvasToTempFilePath) {
  298. uni.saveImageToPhotosAlbum({
  299. filePath: canvasToTempFilePath,
  300. success: () => {
  301. this.$u.toast('保存成功,快去分享到朋友圈吧~');
  302. },
  303. fail: () => {
  304. this.$u.toast('保存失败~');
  305. }
  306. });
  307. }
  308. }
  309. };
  310. </script>
  311. <style lang="scss">
  312. .sq_box {
  313. display: flex;
  314. height: 100%;
  315. justify-content: center;
  316. color: #333333;
  317. padding: 60rpx;
  318. .tz_box {
  319. background-color: #ffffff;
  320. border-radius: 14rpx;
  321. padding: 40rpx;
  322. display: flex;
  323. flex-direction: column;
  324. align-items: center;
  325. position: relative;
  326. width: 100%;
  327. text-align: center;
  328. line-height: 50rpx;
  329. height: 80vh;
  330. .close_box {
  331. position: absolute;
  332. left: 50%;
  333. bottom: -100rpx;
  334. border: 4rpx solid #ffffff;
  335. width: 60rpx;
  336. height: 60rpx;
  337. margin-left: -30rpx;
  338. border-radius: 50%;
  339. display: flex;
  340. justify-content: center;
  341. align-items: center;
  342. }
  343. .share_qrcode {
  344. width: 100%;
  345. height: 100%;
  346. }
  347. .tz_title {
  348. font-size: 34rpx;
  349. font-weight: 600;
  350. }
  351. .content_box {
  352. margin: 50rpx 0;
  353. text-align: left;
  354. .inf {
  355. font-weight: bold;
  356. font-size: 36rpx;
  357. .copy {
  358. font-weight: 0;
  359. color: rgb(85, 104, 147);
  360. font-size: 30rpx;
  361. }
  362. }
  363. }
  364. }
  365. .qrcode_box {
  366. padding: 0;
  367. }
  368. }
  369. </style>