fa-poster.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469
  1. <template>
  2. <view class="fa-poster">
  3. <view class="" v-if="showCanvas"><canvas canvas-id="poster" class="poster_canvas" :style="[{ width: width + 'px', height: height + 'px' }]"></canvas></view>
  4. <u-popup v-model="value" :popup="false" mode="center" @close="hide">
  5. <view class="">
  6. <!-- #ifdef H5 -->
  7. <view class="poster-img"><image class="" :src="posterImgs" mode="aspectFit"></image></view>
  8. <!-- #endif -->
  9. <!-- #ifndef H5 -->
  10. <view class="poster-img"><image class="" :src="posterImgs" mode="aspectFit" @longpress="save"></image></view>
  11. <!-- #endif -->
  12. <view class="u-flex u-row-between u-m-t-15">
  13. <view class="btn" @click="hide">取消</view>
  14. <view class="btn" @click="save">长按保存到本地</view>
  15. </view>
  16. </view>
  17. </u-popup>
  18. </view>
  19. </template>
  20. <script>
  21. let settingWritePhotosAlbum = false;
  22. export default {
  23. name: 'fa-poster',
  24. props: {
  25. value: {
  26. type: Boolean,
  27. default: false
  28. },
  29. goods: {
  30. type: Object,
  31. default() {
  32. return {};
  33. }
  34. }
  35. },
  36. watch: {
  37. value(newValue, oldValue) {
  38. if (newValue) {
  39. this.initCanvas();
  40. }
  41. }
  42. },
  43. data() {
  44. return {
  45. width: 750,
  46. height: 1334,
  47. imgWidth: 0,
  48. imgHeight: 0,
  49. multiple: 1, //倍数
  50. showCanvas: false,
  51. posterImgs: ''
  52. };
  53. },
  54. methods: {
  55. hide() {
  56. this.$emit('input', false);
  57. },
  58. //获取小程序码或二维码
  59. getWxCode() {
  60. return new Promise((resolve, reject) => {
  61. let version = 'release';
  62. // #ifdef MP-WEIXIN
  63. let info = uni.getAccountInfoSync();
  64. version = info.miniProgram.envVersion || 'release';
  65. // #endif
  66. this.$api.getWxCode({ goods_id: this.goods.id, version: version }).then(({ code, data: res, msg }) => {
  67. if (code) {
  68. resolve(res);
  69. } else {
  70. reject(msg);
  71. }
  72. });
  73. });
  74. },
  75. //下载图片
  76. downloadImg(path) {
  77. return new Promise((resolve, reject) => {
  78. uni.downloadFile({
  79. url: path,
  80. success: res => {
  81. if (res.statusCode === 200) {
  82. resolve(res.tempFilePath);
  83. } else {
  84. reject('图片下载失败');
  85. }
  86. },
  87. fail: err => {
  88. reject(err);
  89. }
  90. });
  91. });
  92. },
  93. //获取图片信息
  94. getImageInfo(path) {
  95. return new Promise((resolve, reject) => {
  96. uni.getImageInfo({
  97. src: path,
  98. success: function(res) {
  99. resolve(res);
  100. },
  101. fail(err) {
  102. reject(err);
  103. }
  104. });
  105. });
  106. },
  107. // 文字换行
  108. drawtext(text, maxWidth) {
  109. let textArr = text.split('');
  110. let len = textArr.length;
  111. // 上个节点
  112. let previousNode = 0;
  113. // 记录节点宽度
  114. let nodeWidth = 0;
  115. // 文本换行数组
  116. let rowText = [];
  117. // 如果是字母,侧保存长度
  118. let letterWidth = 0;
  119. // 汉字宽度
  120. let chineseWidth = 21 * this.multiple;
  121. // otherFont宽度
  122. let otherWidth = 10.5 * this.multiple;
  123. for (let i = 0; i < len; i++) {
  124. if (/[\u4e00-\u9fa5]|[\uFE30-\uFFA0]/g.test(textArr[i])) {
  125. if (letterWidth > 0) {
  126. if (nodeWidth + chineseWidth + letterWidth * otherWidth > maxWidth) {
  127. rowText.push({
  128. type: 'text',
  129. content: text.substring(previousNode, i)
  130. });
  131. previousNode = i;
  132. nodeWidth = chineseWidth;
  133. letterWidth = 0;
  134. } else {
  135. nodeWidth += chineseWidth + letterWidth * otherWidth;
  136. letterWidth = 0;
  137. }
  138. } else {
  139. if (nodeWidth + chineseWidth > maxWidth) {
  140. rowText.push({
  141. type: 'text',
  142. content: text.substring(previousNode, i)
  143. });
  144. previousNode = i;
  145. nodeWidth = chineseWidth;
  146. } else {
  147. nodeWidth += chineseWidth;
  148. }
  149. }
  150. } else {
  151. if (/\n/g.test(textArr[i])) {
  152. rowText.push({
  153. type: 'break',
  154. content: text.substring(previousNode, i)
  155. });
  156. previousNode = i + 1;
  157. nodeWidth = 0;
  158. letterWidth = 0;
  159. } else if (textArr[i] == '\\' && textArr[i + 1] == 'n') {
  160. rowText.push({
  161. type: 'break',
  162. content: text.substring(previousNode, i)
  163. });
  164. previousNode = i + 2;
  165. nodeWidth = 0;
  166. letterWidth = 0;
  167. } else if (/[a-zA-Z0-9]/g.test(textArr[i])) {
  168. letterWidth += 1;
  169. if (nodeWidth + letterWidth * otherWidth > maxWidth) {
  170. rowText.push({
  171. type: 'text',
  172. content: text.substring(previousNode, i + 1 - letterWidth)
  173. });
  174. previousNode = i + 1 - letterWidth;
  175. nodeWidth = letterWidth * otherWidth;
  176. letterWidth = 0;
  177. }
  178. } else {
  179. if (nodeWidth + otherWidth > maxWidth) {
  180. rowText.push({
  181. type: 'text',
  182. content: text.substring(previousNode, i)
  183. });
  184. previousNode = i;
  185. nodeWidth = otherWidth;
  186. } else {
  187. nodeWidth += otherWidth;
  188. }
  189. }
  190. }
  191. }
  192. if (previousNode < len) {
  193. rowText.push({
  194. type: 'text',
  195. content: text.substring(previousNode, len)
  196. });
  197. }
  198. return rowText;
  199. },
  200. //init
  201. async initCanvas() {
  202. //先取下载图片
  203. const goodsImgPath = await this.downloadImg(this.goods.image);
  204. //取图片信息
  205. const { width, height } = await this.getImageInfo(goodsImgPath);
  206. console.log(width, height);
  207. //最小375
  208. if (width > height && height > 375) {
  209. this.width = 2 * height;
  210. this.height = (2 * height * 1334) / 750;
  211. this.imgWidth = width;
  212. } else if (height > width && width > 375) {
  213. this.width = 2 * width;
  214. this.height = (2 * width * 1334) / 750;
  215. this.imgHeight = height;
  216. }
  217. //倍数
  218. this.multiple = (this.width / 750).toFixed(2);
  219. //加载画布
  220. this.showCanvas = true;
  221. this.$nextTick(() => {
  222. this.createPoster(goodsImgPath);
  223. });
  224. },
  225. // 创建海报
  226. async createPoster(goodsImgPath) {
  227. try {
  228. if (this.posterImgs) {
  229. return;
  230. }
  231. uni.showLoading({
  232. title: '海报生成中'
  233. });
  234. const ctx = uni.createCanvasContext('poster', this);
  235. ctx.fillRect(0, 0, this.width / 2, this.height / 2);
  236. ctx.setFillStyle('#FFF');
  237. ctx.fillRect(0, 0, this.width / 2, this.height / 2);
  238. this.drawImage(ctx, goodsImgPath);
  239. const codeUrl = await this.getWxCode();
  240. const left_gap = 17 * this.multiple;
  241. // 商品标题
  242. let drawtextList = this.drawtext(this.goods.title, this.width / 2 - 30 * this.multiple);
  243. let textTop = 0,
  244. len = drawtextList.length;
  245. if (len == 1) {
  246. ctx.setFontSize(22 * this.multiple);
  247. } else {
  248. ctx.setFontSize(21 * this.multiple);
  249. }
  250. ctx.setFillStyle('#333');
  251. for (let [index, item] of drawtextList.entries()) {
  252. if (len == 1) {
  253. textTop = this.width / 2 + (index + 1) * 50 * this.multiple;
  254. ctx.fillText(item.content, left_gap, textTop);
  255. textTop += 10;
  256. } else if (index < 2) {
  257. textTop = this.width / 2 + (index + 1) * 35 * this.multiple;
  258. ctx.fillText(item.content, left_gap, textTop);
  259. }
  260. }
  261. // 商品价格
  262. ctx.setFontSize(26 * this.multiple);
  263. ctx.setFillStyle('#f00');
  264. ctx.fillText('¥' + this.goods.price, left_gap, textTop + 47 * this.multiple);
  265. // 商品门市价
  266. ctx.setFontSize(18 * this.multiple);
  267. ctx.setFillStyle('#999');
  268. let textLeft = (60 + (this.goods.price + '').length * 13) * this.multiple;
  269. ctx.fillText('¥' + this.goods.marketprice, textLeft, textTop + 45 * this.multiple);
  270. // // 商品门市价横线
  271. ctx.beginPath();
  272. ctx.setLineWidth(1 * this.multiple);
  273. ctx.moveTo(textLeft - 1, textTop + 38 * this.multiple);
  274. ctx.lineTo(textLeft + 20 + (this.goods.marketprice + '').length * 9, textTop + 38 * this.multiple);
  275. ctx.setStrokeStyle('#999');
  276. ctx.stroke();
  277. // // 商品分割线
  278. ctx.beginPath();
  279. ctx.setLineWidth(1 * this.multiple);
  280. ctx.moveTo(left_gap, textTop + 70 * this.multiple);
  281. ctx.lineTo(this.width / 2 - 20, textTop + 70 * this.multiple);
  282. ctx.setStrokeStyle('#eee');
  283. ctx.stroke();
  284. // // 长按识别二维码访问
  285. ctx.setFontSize(19 * this.multiple);
  286. ctx.setFillStyle('#333');
  287. ctx.fillText('长按识别二维码访问', left_gap, textTop + 110 * this.multiple);
  288. // // 平台名称
  289. ctx.setFontSize(16 * this.multiple);
  290. ctx.setFillStyle('#999');
  291. ctx.fillText(this.vuex_config.sitename, left_gap, textTop + 150 * this.multiple);
  292. // // 二维码
  293. // #ifdef MP-WEIXIN
  294. let code_path = await this.writefile(codeUrl);
  295. ctx.drawImage(code_path, this.width / 2 - 150 * this.multiple, textTop + 88 * this.multiple, 120 * this.multiple, 120 * this.multiple);
  296. // #endif
  297. // #ifndef MP-WEIXIN
  298. ctx.drawImage(codeUrl, this.width / 2 - 150 * this.multiple, textTop + 88 * this.multiple, 120 * this.multiple, 120 * this.multiple);
  299. // #endif
  300. ctx.draw(true, () => {
  301. // canvas画布转成图片并返回图片地址
  302. uni.canvasToTempFilePath(
  303. {
  304. canvasId: 'poster',
  305. width: this.width / 2,
  306. height: this.height / 2,
  307. success: res => {
  308. this.posterImgs = res.tempFilePath;
  309. console.log('海报制作成功!');
  310. },
  311. fail: err => {
  312. console.log(err);
  313. }
  314. },
  315. this
  316. );
  317. uni.hideLoading();
  318. });
  319. } catch (e) {
  320. this.hide();
  321. this.$u.toast(e);
  322. }
  323. },
  324. // #ifdef MP-WEIXIN
  325. writefile(base64data) {
  326. return new Promise((resolve, reject) => {
  327. const [, format, bodyData] = /data:image\/(\w+);base64,(.*)/.exec(base64data) || [];
  328. if (!format) {
  329. reject(new Error('ERROR_BASE64SRC_PARSE'));
  330. }
  331. const fsm = wx.getFileSystemManager();
  332. const FILE_BASE_NAME = 'tmp_base64src';
  333. const filePath = `${wx.env.USER_DATA_PATH}/${FILE_BASE_NAME}.${format}`;
  334. const buffer = wx.base64ToArrayBuffer(bodyData);
  335. fsm.writeFile({
  336. filePath,
  337. data: buffer,
  338. encoding: 'binary',
  339. success() {
  340. resolve(filePath);
  341. },
  342. fail() {
  343. reject(new Error('ERROR_BASE64SRC_WRITE'));
  344. }
  345. });
  346. });
  347. },
  348. // #endif
  349. //适配图片绘图
  350. drawImage(ctx, path) {
  351. if (this.imgWidth) {
  352. let w = (this.imgWidth - this.width / 2) / 2;
  353. ctx.drawImage(path, 0, 0, this.width / 2 + w, this.width / 2, -w, 0, this.width / 2 + w, this.width / 2);
  354. } else if (this.imgHeight) {
  355. let h = (this.imgHeight - this.width / 2) / 2 + 30;
  356. ctx.drawImage(path, 0, 0, this.width / 2, this.width / 2 + h, 0, -h, this.width / 2, this.width / 2 + h);
  357. } else {
  358. ctx.drawImage(path, 0, 0, this.width / 2, this.width / 2);
  359. }
  360. },
  361. //保存海报
  362. save() {
  363. // #ifdef H5
  364. this.$u.toast('长按保存到本地');
  365. // #endif
  366. // #ifdef MP-WEIXIN
  367. uni.showLoading({
  368. title: '海报下载中'
  369. });
  370. if (settingWritePhotosAlbum) {
  371. uni.getSetting({
  372. success: res => {
  373. if (res.authSetting['scope.writePhotosAlbum']) {
  374. uni.saveImageToPhotosAlbum({
  375. filePath: this.posterImgs,
  376. success: () => {
  377. uni.hideLoading();
  378. uni.showToast({
  379. title: '保存成功'
  380. });
  381. }
  382. });
  383. } else {
  384. uni.showModal({
  385. title: '提示',
  386. content: '请先在设置页面打开“保存相册”使用权限',
  387. confirmText: '去设置',
  388. cancelText: '算了',
  389. success: data => {
  390. if (data.confirm) {
  391. uni.hideLoading();
  392. uni.openSetting();
  393. }
  394. }
  395. });
  396. }
  397. }
  398. });
  399. } else {
  400. settingWritePhotosAlbum = true;
  401. uni.authorize({
  402. scope: 'scope.writePhotosAlbum',
  403. success: () => {
  404. uni.saveImageToPhotosAlbum({
  405. filePath: this.posterImgs,
  406. success: () => {
  407. uni.hideLoading();
  408. uni.showToast({
  409. title: '保存成功'
  410. });
  411. }
  412. });
  413. }
  414. });
  415. }
  416. // #endif
  417. // #ifdef APP-PLUS
  418. uni.showLoading({
  419. title: '海报下载中'
  420. });
  421. uni.saveImageToPhotosAlbum({
  422. filePath: this.posterImgs,
  423. success: () => {
  424. uni.hideLoading();
  425. uni.showToast({
  426. title: '保存成功'
  427. });
  428. }
  429. });
  430. // #endif
  431. }
  432. }
  433. };
  434. </script>
  435. <style lang="scss" scoped>
  436. .poster_canvas {
  437. position: fixed;
  438. top: -10000px;
  439. left: 0px;
  440. }
  441. .poster-img {
  442. width: calc(750rpx * 0.7);
  443. height: calc(1334rpx * 0.7);
  444. border-radius: 15rpx;
  445. overflow: hidden;
  446. image {
  447. width: 100%;
  448. height: 100%;
  449. }
  450. }
  451. .btn {
  452. background-color: #ffffff;
  453. padding: 15rpx 30rpx;
  454. border-radius: 10rpx;
  455. text-align: center;
  456. &:first-child {
  457. width: 30%;
  458. }
  459. &:last-child {
  460. margin-left: 15rpx;
  461. flex: 1;
  462. }
  463. }
  464. </style>