tui-poster.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652
  1. <template>
  2. <canvas :style="{ width: cv_width + 'px', height: cv_height + 'px' }" :canvas-id="posterId" :id="posterId"
  3. class="tui-poster__cv"></canvas>
  4. </template>
  5. <script>
  6. // #ifdef MP-WEIXIN
  7. const posterId = `poster_${Math.ceil(Math.random() * 10e5).toString(36)}`
  8. // #endif
  9. export default {
  10. name: "tui-poster",
  11. emits: ['ready'],
  12. props: {
  13. //海报宽度,单位rpx
  14. width: {
  15. type: [Number, String],
  16. default: 800
  17. },
  18. //海报高度,单位rpx
  19. height: {
  20. type: [Number, String],
  21. default: 1200
  22. },
  23. //像素比
  24. pixel: {
  25. type: [Number, String],
  26. default: 3
  27. }
  28. },
  29. watch: {
  30. width(val) {
  31. this.cv_width = this.getPX(this.width)
  32. },
  33. height(val) {
  34. this.cv_height = this.getPX(this.height)
  35. }
  36. },
  37. data() {
  38. // #ifndef MP-WEIXIN
  39. const posterId = `poster_${Math.ceil(Math.random() * 10e5).toString(36)}`
  40. // #endif
  41. return {
  42. posterId: posterId,
  43. cv_width: 400,
  44. cv_height: 600
  45. };
  46. },
  47. created() {
  48. this.cv_width = this.getPX(this.width)
  49. this.cv_height = this.getPX(this.height)
  50. this.ctx = null
  51. },
  52. mounted() {
  53. setTimeout(() => {
  54. this.ctx = uni.createCanvasContext(this.posterId, this)
  55. this.$emit('ready', {})
  56. }, 50)
  57. },
  58. methods: {
  59. toast(msg) {
  60. uni.showToast({
  61. title: msg,
  62. icon: 'none'
  63. })
  64. },
  65. getPX(val) {
  66. return uni.upx2px(Number(val) * Number(this.pixel))
  67. },
  68. getTextWidth(context, text, fontSize) {
  69. let width = 0;
  70. // #ifndef MP-ALIPAY || MP-BAIDU
  71. width = context.measureText(text).width
  72. // #endif
  73. // #ifdef MP-ALIPAY || MP-BAIDU
  74. let sum = 0;
  75. for (let i = 0, len = text.length; i < len; i++) {
  76. if (text.charCodeAt(i) >= 0 && text.charCodeAt(i) <= 255)
  77. sum = sum + 1;
  78. else
  79. sum = sum + 2;
  80. }
  81. width = sum / 2 * this.getPX(fontSize)
  82. // #endif
  83. return width
  84. },
  85. getWrapText(text, fontSize, textWidth, width, ctx, rows = 2) {
  86. let textArr = [];
  87. if (textWidth > width) {
  88. let fillText = '';
  89. let lines = 1;
  90. let arr = text.split('')
  91. for (let i = 0, len = arr.length; i < len; i++) {
  92. fillText = fillText + arr[i];
  93. if (this.getTextWidth(ctx, fillText, fontSize) >= width) {
  94. if (lines === rows && rows !== -1) {
  95. if (i !== arr.length - 1) {
  96. fillText = fillText.substring(0, fillText.length - 1) + '...';
  97. }
  98. textArr.push(fillText);
  99. break;
  100. }
  101. textArr.push(fillText);
  102. fillText = '';
  103. lines++;
  104. } else if (i === arr.length - 1) {
  105. textArr.push(fillText);
  106. }
  107. }
  108. } else {
  109. textArr.push(text)
  110. }
  111. return textArr;
  112. },
  113. startDrawText(ctx, param) {
  114. let styles = param.style || {}
  115. let {
  116. left,
  117. top,
  118. fontSize,
  119. color,
  120. baseLine = 'normal',
  121. textAlign = 'left',
  122. frontSize,
  123. spacing,
  124. opacity = 1,
  125. lineThrough = false,
  126. width = 600,
  127. rows = 1,
  128. lineHeight = 0,
  129. fontWeight = 'normal',
  130. fontStyle = 'normal',
  131. fontFamily = "sans-serif"
  132. } = styles;
  133. ctx.save();
  134. ctx.beginPath();
  135. ctx.font = fontStyle + " " + fontWeight + " " + this.getPX(fontSize) + "px " + fontFamily
  136. ctx.setGlobalAlpha(opacity);
  137. // ctx.setFontSize(this.getPX(fontSize));
  138. ctx.setFillStyle(color);
  139. ctx.setTextBaseline(baseLine);
  140. ctx.setTextAlign(textAlign);
  141. let textWidth = this.getTextWidth(ctx, param.text, fontSize);
  142. width = this.getPX(width);
  143. let textArr = this.getWrapText(param.text, fontSize, textWidth, width, ctx, rows)
  144. if (param.frontText) {
  145. ctx.setFontSize(this.getPX(frontSize));
  146. left = this.getTextWidth(ctx, param.frontText, frontSize) + this.getPX(left + spacing);
  147. ctx.setFontSize(this.getPX(fontSize));
  148. } else {
  149. left = this.getPX(left)
  150. }
  151. textArr.forEach((item, index) => {
  152. ctx.fillText(item, left, this.getPX(top + (lineHeight || fontSize) * index))
  153. })
  154. ctx.restore();
  155. if (lineThrough) {
  156. let lineY = top;
  157. switch (baseLine) {
  158. case 'top':
  159. lineY += fontSize / 2 + 4;
  160. break;
  161. case 'middle':
  162. break;
  163. case 'bottom':
  164. lineY -= fontSize / 2 + 4;
  165. break;
  166. default:
  167. // #ifdef MP-WEIXIN
  168. lineY -= fontSize / 2 - 3;
  169. // #endif
  170. // #ifndef MP-WEIXIN
  171. lineY -= fontSize / 2 - 4;
  172. // #endif
  173. break;
  174. }
  175. ctx.save();
  176. ctx.moveTo(left, this.getPX(lineY));
  177. ctx.lineTo(left + textWidth + 2, this.getPX(lineY));
  178. ctx.setStrokeStyle(color);
  179. ctx.stroke();
  180. ctx.restore();
  181. }
  182. },
  183. drawRadiusRect(ctx, styles) {
  184. let {
  185. left,
  186. top,
  187. width,
  188. height,
  189. borderRadius
  190. } = styles;
  191. let r = this.getPX(borderRadius / 2);
  192. left = this.getPX(left)
  193. top = this.getPX(top)
  194. width = this.getPX(width)
  195. height = this.getPX(height)
  196. ctx.beginPath();
  197. ctx.arc(left + r, top + r, r, Math.PI, Math.PI * 1.5);
  198. ctx.moveTo(left + r, top);
  199. ctx.lineTo(left + width - r, top);
  200. ctx.lineTo(left + width, top + r);
  201. ctx.arc(left + width - r, top + r, r, Math.PI * 1.5, Math.PI * 2);
  202. ctx.lineTo(left + width, top + height - r);
  203. ctx.lineTo(left + width - r, top + height);
  204. ctx.arc(left + width - r, top + height - r, r, 0, Math.PI * 0.5);
  205. ctx.lineTo(left + r, top + height);
  206. ctx.lineTo(left, top + height - r);
  207. ctx.arc(left + r, top + height - r, r, Math.PI * 0.5, Math.PI);
  208. ctx.lineTo(left, top + r);
  209. ctx.lineTo(left + r, top);
  210. },
  211. startDrawImage(ctx, param) {
  212. let styles = param.style || {}
  213. let {
  214. left,
  215. top,
  216. width,
  217. height,
  218. borderRadius = 0,
  219. borderWidth = 0,
  220. borderColor
  221. } = styles;
  222. ctx.save();
  223. if (borderRadius > 0) {
  224. this.drawRadiusRect(ctx, styles);
  225. ctx.strokeStyle = 'rgba(0,0,0,0)'
  226. // #ifndef MP-BAIDU || MP-TOUTIAO
  227. ctx.stroke();
  228. // #endif
  229. ctx.clip();
  230. }
  231. ctx.drawImage(param.src, this.getPX(left), this.getPX(top), this.getPX(width), this.getPX(
  232. height))
  233. if (borderWidth && borderWidth > 0) {
  234. ctx.setStrokeStyle(borderColor);
  235. ctx.setLineWidth(this.getPX(borderWidth));
  236. ctx.stroke();
  237. }
  238. ctx.restore();
  239. },
  240. startDrawRect(ctx, param) {
  241. let styles = param.style || {}
  242. let {
  243. width,
  244. height,
  245. left,
  246. top,
  247. borderWidth,
  248. backgroundColor,
  249. gradientColor,
  250. gradientType = 1,
  251. borderColor,
  252. borderRadius = 0,
  253. opacity = 1,
  254. shadow
  255. } = styles;
  256. if (backgroundColor) {
  257. ctx.save();
  258. ctx.setGlobalAlpha(opacity);
  259. if (gradientColor) {
  260. let grd = null;
  261. if (gradientType == 1) {
  262. grd = ctx.createLinearGradient(0, 0, this.getPX(width), this.getPX(height))
  263. } else {
  264. grd = ctx.createLinearGradient(0, this.getPX(width), this.getPX(height), 0)
  265. }
  266. grd.addColorStop(0, backgroundColor)
  267. grd.addColorStop(1, gradientColor)
  268. ctx.setFillStyle(grd);
  269. } else {
  270. ctx.setFillStyle(backgroundColor);
  271. }
  272. if (shadow) {
  273. const {
  274. offsetX,
  275. offsetY,
  276. blur,
  277. color
  278. } = shadow;
  279. ctx.shadowOffsetX = this.getPX(offsetX)
  280. ctx.shadowOffsetY = this.getPX(offsetY)
  281. ctx.shadowBlur = blur
  282. ctx.shadowColor = color
  283. }
  284. if (borderRadius > 0) {
  285. this.drawRadiusRect(ctx, styles);
  286. ctx.fill();
  287. } else {
  288. ctx.fillRect(this.getPX(left), this.getPX(top), this.getPX(width), this.getPX(height));
  289. }
  290. ctx.restore();
  291. }
  292. if (borderWidth) {
  293. ctx.save();
  294. ctx.setGlobalAlpha(opacity);
  295. ctx.setStrokeStyle(borderColor);
  296. ctx.setLineWidth(this.getPX(borderWidth));
  297. if (borderRadius > 0) {
  298. this.drawRadiusRect(ctx, styles);
  299. ctx.stroke();
  300. } else {
  301. ctx.strokeRect(this.getPX(left), this.getPX(top), this.getPX(width), this.getPX(height));
  302. }
  303. ctx.restore();
  304. }
  305. },
  306. startDrawLine(ctx, param) {
  307. let styles = param.style
  308. let {
  309. left,
  310. top,
  311. endLeft,
  312. endTop,
  313. color,
  314. width = 1
  315. } = styles;
  316. ctx.save();
  317. ctx.beginPath();
  318. ctx.setStrokeStyle(color);
  319. ctx.setLineWidth(this.getPX(width));
  320. ctx.moveTo(this.getPX(left), this.getPX(top));
  321. ctx.lineTo(this.getPX(endLeft), this.getPX(endTop));
  322. ctx.stroke();
  323. ctx.closePath();
  324. ctx.restore();
  325. },
  326. judgeIosPermissionPhotoLibrary() {
  327. // #ifdef APP-PLUS
  328. var result = 0;
  329. var PHPhotoLibrary = plus.ios.import("PHPhotoLibrary");
  330. var authStatus = PHPhotoLibrary.authorizationStatus();
  331. if (authStatus === 0) {
  332. result = -1;
  333. } else if (authStatus == 3) {
  334. result = 1;
  335. console.log("相册权限已经开启");
  336. } else {
  337. result = 0;
  338. console.log("相册权限没有开启");
  339. }
  340. plus.ios.deleteObject(PHPhotoLibrary);
  341. return result;
  342. // #endif
  343. },
  344. requestAndroidPermission(permissionID) {
  345. // #ifdef APP-PLUS
  346. return new Promise((resolve, reject) => {
  347. plus.android.requestPermissions(
  348. [permissionID],
  349. function(resultObj) {
  350. var result = 0;
  351. for (var i = 0; i < resultObj.granted.length; i++) {
  352. var grantedPermission = resultObj.granted[i];
  353. result = 1
  354. }
  355. for (var i = 0; i < resultObj.deniedPresent.length; i++) {
  356. var deniedPresentPermission = resultObj.deniedPresent[i];
  357. result = 0
  358. }
  359. for (var i = 0; i < resultObj.deniedAlways.length; i++) {
  360. var deniedAlwaysPermission = resultObj.deniedAlways[i];
  361. result = -1
  362. }
  363. resolve(result);
  364. },
  365. function(error) {
  366. resolve({
  367. code: error.code,
  368. message: error.message
  369. });
  370. }
  371. );
  372. });
  373. // #endif
  374. },
  375. gotoAppPermissionSetting(isAndroid) {
  376. // #ifdef APP-PLUS
  377. if (!isAndroid) {
  378. var UIApplication = plus.ios.import("UIApplication");
  379. var application2 = UIApplication.sharedApplication();
  380. var NSURL2 = plus.ios.import("NSURL");
  381. var setting2 = NSURL2.URLWithString("app-settings:");
  382. application2.openURL(setting2);
  383. plus.ios.deleteObject(setting2);
  384. plus.ios.deleteObject(NSURL2);
  385. plus.ios.deleteObject(application2);
  386. } else {
  387. var Intent = plus.android.importClass("android.content.Intent");
  388. var Settings = plus.android.importClass("android.provider.Settings");
  389. var Uri = plus.android.importClass("android.net.Uri");
  390. var mainActivity = plus.android.runtimeMainActivity();
  391. var intent = new Intent();
  392. intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
  393. var uri = Uri.fromParts("package", mainActivity.getPackageName(), null);
  394. intent.setData(uri);
  395. mainActivity.startActivity(intent);
  396. }
  397. // #endif
  398. },
  399. judgePermissionPhotoLibrary: async function(callback) {
  400. // #ifndef APP-PLUS || MP-WEIXIN || MP-QQ
  401. callback && callback(true)
  402. // #endif
  403. // #ifdef APP-PLUS
  404. const res = uni.getSystemInfoSync();
  405. let result;
  406. let isAndroid = res.platform.toLocaleLowerCase() == "android";
  407. if (isAndroid) {
  408. result = await this.requestAndroidPermission('android.permission.WRITE_EXTERNAL_STORAGE')
  409. } else {
  410. result = this.judgeIosPermissionPhotoLibrary()
  411. }
  412. if (result == 1) {
  413. callback && callback(true)
  414. } else {
  415. if (!(!isAndroid && result == -1)) {
  416. uni.showModal({
  417. title: '提示',
  418. content: '您还没有开启相册权限,是否立即开启?',
  419. showCancel: true,
  420. success: (res) => {
  421. if (res.confirm) {
  422. this.gotoAppPermissionSetting(isAndroid)
  423. }
  424. }
  425. })
  426. } else {
  427. callback && callback(true)
  428. }
  429. }
  430. // #endif
  431. // #ifdef MP-WEIXIN || MP-QQ
  432. uni.authorize({
  433. scope: 'scope.writePhotosAlbum',
  434. success() {
  435. callback && callback(true)
  436. },
  437. fail() {
  438. uni.showModal({
  439. title: '提示',
  440. content: '您还没有开启相册权限,是否立即开启?',
  441. showCancel: true,
  442. success: (res) => {
  443. if (res.confirm) {
  444. wx.openSetting({
  445. success(res) {}
  446. });
  447. }
  448. }
  449. })
  450. }
  451. })
  452. // #endif
  453. },
  454. imgDownload(url) {
  455. return new Promise((resolve, reject) => {
  456. uni.downloadFile({
  457. url: url,
  458. success: res => {
  459. resolve(res.tempFilePath);
  460. },
  461. fail: err => {
  462. reject(false)
  463. }
  464. })
  465. })
  466. },
  467. base64ToImg(base64) {
  468. const uniqueId = `poster_${Math.ceil(Math.random() * 10e5).toString(36)}`
  469. return new Promise((resolve, reject) => {
  470. // #ifdef MP-WEIXIN
  471. const [, format, bodyData] = /data:image\/(\w+);base64,(.*)/.exec(base64) || [];
  472. let arrayBuffer = wx.base64ToArrayBuffer(bodyData)
  473. const filePath = `${wx.env.USER_DATA_PATH}/${uniqueId}.${format}`;
  474. wx.getFileSystemManager().writeFile({
  475. filePath,
  476. data: arrayBuffer,
  477. encoding: 'binary',
  478. success() {
  479. resolve(filePath);
  480. },
  481. fail() {
  482. reject(false)
  483. }
  484. })
  485. // #endif
  486. // #ifdef APP-PLUS
  487. let bitmap = new plus.nativeObj.Bitmap(uniqueId);
  488. bitmap.loadBase64Data(base64, function() {
  489. bitmap.save(`_doc/${uniqueId}.png`, {}, function(e) {
  490. let target = e.target;
  491. resolve(target);
  492. }, function(e) {
  493. reject(false)
  494. });
  495. }, function() {
  496. reject(false)
  497. });
  498. // #endif
  499. // #ifdef H5
  500. resolve(base64);
  501. // #endif
  502. // #ifndef MP-WEIXIN || APP-PLUS || H5
  503. reject(false)
  504. // #endif
  505. })
  506. },
  507. startDraw(data, callback) {
  508. let ctx = this.ctx
  509. if (ctx) {
  510. ctx.clearRect(0, 0, this.cv_width, this.cv_height)
  511. data.forEach((item) => {
  512. if (item.type === 'image') {
  513. this.startDrawImage(ctx, item)
  514. } else if (item.type === 'text') {
  515. this.startDrawText(ctx, item)
  516. } else if (item.type === 'rect') {
  517. this.startDrawRect(ctx, item)
  518. } else if (item.type === 'line') {
  519. this.startDrawLine(ctx, item)
  520. }
  521. });
  522. const platform = uni.getSystemInfoSync().platform;
  523. let time = 80;
  524. if (platform === 'android') {
  525. time = 300;
  526. }
  527. setTimeout(() => {
  528. ctx.draw(false, () => {
  529. setTimeout(() => {
  530. // #ifdef MP-ALIPAY
  531. ctx.toTempFilePath({
  532. success: res => {
  533. callback && callback(res.apFilePath)
  534. },
  535. fail: err => {
  536. callback && callback(false)
  537. }
  538. });
  539. // #endif
  540. // #ifndef MP-ALIPAY
  541. uni.canvasToTempFilePath({
  542. x: 0,
  543. y: 0,
  544. canvasId: this.posterId,
  545. fileType: 'png',
  546. quality: 1,
  547. success: function(res) {
  548. callback && callback(res.tempFilePath)
  549. },
  550. fail() {
  551. callback && callback(false)
  552. }
  553. }, this)
  554. // #endif
  555. }, time)
  556. })
  557. }, 50)
  558. } else {
  559. callback && callback(false)
  560. }
  561. },
  562. draw(data, callback) {
  563. // text(文本)、image(图片)、rect(矩形),line(线条)
  564. // {
  565. // type:'image',
  566. // src:'',
  567. // style:{
  568. // }
  569. // }
  570. if (!data || data.length === 0) return;
  571. let func = [],
  572. idxes = [];
  573. data.forEach((item, index) => {
  574. if (item.type === 'image') {
  575. //图片类型:1-本地图片(需要平台支持);2-网络图片; 3- base64 图片(仅App,微信小程序,H5支持)
  576. if (item.imgType == 2) {
  577. func.push(this.imgDownload(item.src))
  578. idxes.push(index)
  579. }
  580. // #ifdef APP-PLUS || H5 || MP-WEIXIN
  581. if (item.imgType == 3) {
  582. func.push(this.base64ToImg(item.src))
  583. idxes.push(index)
  584. }
  585. // #endif
  586. }
  587. })
  588. if (func.length > 0) {
  589. Promise.all(func).then(res => {
  590. res.forEach((imgRes, idx) => {
  591. let item = data[idxes[idx]]
  592. item.src = imgRes
  593. })
  594. this.startDraw(data, callback)
  595. }).catch(err => {
  596. console.log(err)
  597. this.toast('图片处理失败!')
  598. })
  599. } else {
  600. this.startDraw(data, callback)
  601. }
  602. },
  603. save(file) {
  604. // #ifdef H5
  605. //H5无法直接保存到相册,预览后长按保存
  606. uni.previewImage({
  607. urls: [file]
  608. });
  609. // #endif
  610. // #ifndef H5
  611. this.judgePermissionPhotoLibrary((res) => {
  612. if (res) {
  613. uni.saveImageToPhotosAlbum({
  614. filePath: file,
  615. success: (res) => {
  616. this.toast('图片已保存到相册')
  617. },
  618. fail: (res) => {
  619. this.toast('图片保存失败')
  620. }
  621. })
  622. }
  623. })
  624. // #endif
  625. }
  626. }
  627. }
  628. </script>
  629. <style scoped>
  630. .tui-poster__cv {
  631. position: fixed;
  632. left: -9999px;
  633. bottom: 0;
  634. }
  635. </style>