cm-share-popup.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500
  1. <template>
  2. <view class="share-popup">
  3. <!-- 弹窗 -->
  4. <uni-popup ref="sharePopup" type="bottom" :safe-area="safeArea" :is-mask-click="false">
  5. <view class="popup-content" :class="{ 'no-safe-area': !safeArea }">
  6. <view class="title">
  7. <text class="title-text" v-if="title" v-text="title"></text>
  8. <template v-else>
  9. <slot name="title"></slot>
  10. </template>
  11. </view>
  12. <view class="content">
  13. <view class="row">
  14. <button class="share item" open-type="share" @click="share">
  15. <view class="icon-image"><image :src="staticUrl + 'icon-share-wechat.png'"></image></view>
  16. <text class="label">微信</text>
  17. </button>
  18. <view class="poster item" @click="createPoster" v-if="!posterUrl">
  19. <view class="icon-image"><image :src="staticUrl + 'icon-poster.png'"></image></view>
  20. <text class="label">生成海报</text>
  21. </view>
  22. <view class="poster item" @click="savePoster" v-else>
  23. <view class="icon-image"><image :src="staticUrl + 'icon-download.png'"></image></view>
  24. <text class="label">保存相册</text>
  25. </view>
  26. </view>
  27. <tui-divider :height="64"></tui-divider>
  28. <view class="cancel" @click="cancel">取消</view>
  29. </view>
  30. </view>
  31. </uni-popup>
  32. <canvas canvas-id="poster" id="poster" class="canvas"></canvas>
  33. <!-- 海报 -->
  34. <view class="poster-container" v-show="visiable">
  35. <!-- 画布 -->
  36. <image :src="posterUrl" class="poster-image"></image>
  37. <!-- 下载按钮 -->
  38. <view class="poster item" @click="savePoster" v-if="downType === 'fixed'">
  39. <view class="icon-image"><image :src="staticUrl + 'icon-download.png'"></image></view>
  40. <text class="label">保存相册</text>
  41. </view>
  42. </view>
  43. <!-- 海报遮罩层 -->
  44. <view class="poster-mask" @click="cancel" v-if="downType === 'fixed' && posterUrl && visiable"></view>
  45. </view>
  46. </template>
  47. <script>
  48. import { mapGetters } from 'vuex'
  49. import { getUserProfile } from '@/common/auth.js'
  50. import { generateWxUnlimited } from '@/common/share.helper.js'
  51. export default {
  52. props: {
  53. title: {
  54. type: String,
  55. default: ''
  56. },
  57. data: {
  58. type: Object,
  59. default: () => {}
  60. },
  61. type: {
  62. type: String,
  63. default: 'normal',
  64. validator: value => {
  65. return ['product', 'normal', 'activity'].indexOf(value) > -1
  66. }
  67. },
  68. safeArea: {
  69. type: Boolean,
  70. default: true
  71. },
  72. // 海报下载类型
  73. downType: {
  74. type: String,
  75. default: 'fixed'
  76. }
  77. },
  78. data() {
  79. return {
  80. visiable: false,
  81. imageList: [],
  82. posterUrl: '',
  83. // 海报数据信息
  84. posterData: {
  85. query: '', // 查询参数字符串
  86. path: '', // 页面路径
  87. avatar: '', // 用户头像
  88. username: '', // 用户名
  89. porductName: '', // 产品名
  90. productPrice: '', // 产品价格
  91. productOriginPrice: '', // 产品原价
  92. productImage: '', // 产品图片
  93. qrCodeImage: '' //页面二维码,小程序二维码
  94. }
  95. }
  96. },
  97. computed: {
  98. ...mapGetters(['systemInfo'])
  99. },
  100. methods: {
  101. // 绘制海报 初始化
  102. initDrawPoster() {
  103. this.downLoadImageTask()
  104. },
  105. open() {
  106. this.$refs.sharePopup.open()
  107. this.$emit('open')
  108. },
  109. close() {
  110. this.$refs.sharePopup?.close()
  111. this.$emit('close')
  112. },
  113. share() {
  114. this.$emit('share', this.posterUrl)
  115. this.visiable = false
  116. this.posterUrl = ''
  117. this.close()
  118. },
  119. cancel() {
  120. this.visiable = false
  121. this.posterUrl = ''
  122. this.close()
  123. },
  124. savePoster() {
  125. uni.saveImageToPhotosAlbum({
  126. filePath: this.posterUrl,
  127. success: res => {
  128. this.share()
  129. }
  130. })
  131. },
  132. // 下载图片任务
  133. async downLoadImageTask() {
  134. // 执行下载图片
  135. try {
  136. const { avatar, productImage, qrCodeImage } = this.posterData
  137. // 背景图片
  138. const bgImageUrl = this.staticUrl + 'bg-share-01.png'
  139. // 用户头像
  140. const avatarUrl = avatar || this.staticUrl + 'icon-join-us.png'
  141. // 分享封面
  142. let coverUrl = productImage || this.staticUrl + 'icon-share.png'
  143. // 分享二维码
  144. let ewmUrl = qrCodeImage || this.staticUrl + 'icon-ewm-hehe.jpg'
  145. // 默认分享封面二维码
  146. if (this.type === 'normal') {
  147. coverUrl = this.staticUrl + 'icon-share.png' // 默认封面
  148. ewmUrl = this.staticUrl + 'icon-ewm-hehe.jpg' // 默认二维码
  149. } else {
  150. const { data: iamgeUrl } = await generateWxUnlimited({
  151. pagePath: this.posterData.path,
  152. queryStr: this.posterData.query
  153. })
  154. ewmUrl = iamgeUrl
  155. }
  156. // 下载图片任务
  157. const taskList = [
  158. this.downloadImage(bgImageUrl),
  159. this.downloadImage(avatarUrl),
  160. this.downloadImage(coverUrl),
  161. this.downloadImage(ewmUrl)
  162. ]
  163. this.imageList = await Promise.all(taskList)
  164. this.drawPoster()
  165. } catch (e) {
  166. this.cancel()
  167. uni.hideLoading()
  168. setTimeout(() => {
  169. this.$toast('生成海报失败,请重试')
  170. }, 200)
  171. }
  172. },
  173. // 绘制海报
  174. drawPoster() {
  175. const windowWidth = this.systemInfo.windowWidth
  176. // const scale = this.systemInfo.windowWidth / 750
  177. const scale = 1
  178. var ctx = uni.createCanvasContext('poster', this)
  179. // 绘制背景
  180. ctx.drawImage(this.imageList[0].tempFilePath, 0, 0, 540 * scale, 878 * scale)
  181. ctx.save()
  182. // 绘制用户信息
  183. this.drawPosterHead(ctx, scale)
  184. // 绘制白色矩形区域
  185. ctx.beginPath()
  186. ctx.rect(20 * scale, 154 * scale, 500 * scale, 704 * scale)
  187. ctx.setFillStyle('#FFFFFF')
  188. ctx.fill()
  189. ctx.clip()
  190. const type = this.type
  191. // 绘制底部内容
  192. if (!type || type === 'normal') {
  193. this.drawPosterFoot(ctx, scale)
  194. } else {
  195. this.drawGoodsInfo(ctx, scale)
  196. }
  197. ctx.restore()
  198. ctx.draw(true, () => {
  199. uni.canvasToTempFilePath(
  200. {
  201. canvasId: 'poster',
  202. success: res => {
  203. this.posterUrl = res.tempFilePath
  204. this.visiable = true
  205. if (this.downType === 'fixed') {
  206. this.close()
  207. }
  208. },
  209. complete: () => {
  210. uni.hideLoading()
  211. }
  212. },
  213. this
  214. )
  215. })
  216. },
  217. // 绘制海报头部
  218. drawPosterHead(ctx, scale) {
  219. const { username } = this.posterData
  220. // 绘制头像
  221. ctx.beginPath()
  222. ctx.arc(40 * scale + (90 * scale) / 2, 32 * scale + (90 * scale) / 2, (90 * scale) / 2, 0, 2 * Math.PI)
  223. ctx.setFillStyle('#fff')
  224. ctx.fill()
  225. ctx.clip()
  226. ctx.drawImage(this.imageList[1].tempFilePath, 40 * scale, 32 * scale, 90 * scale, 90 * scale)
  227. ctx.restore()
  228. ctx.save()
  229. // 绘制用户名和推荐语
  230. ctx.setFillStyle('#FFFFFF')
  231. ctx.font = 'bold 10px sans-serif'
  232. ctx.setFontSize(30 * scale)
  233. ctx.fillText(username, 146 * scale, (30 + 34) * scale, 350 * scale)
  234. ctx.font = 'normal 10px sans-serif'
  235. ctx.setFontSize(24 * scale)
  236. let txt = '强烈为你推荐该商城'
  237. if (this.type === 'product') {
  238. txt = '强烈为你推荐该商品'
  239. } else if (this.type === 'activity') {
  240. txt = '强烈为你推荐该活动'
  241. }
  242. ctx.fillText(txt, 146 * scale, (87 + 24) * scale, 350 * scale)
  243. },
  244. // 绘制海报底部
  245. drawPosterFoot(ctx, scale) {
  246. // 绘制中心图片
  247. ctx.drawImage(this.imageList[2].tempFilePath, 40 * scale, 174 * scale, 460 * scale, 460 * scale)
  248. // 绘制底部
  249. ctx.setFontSize(24 * scale)
  250. ctx.setFillStyle('#333')
  251. ctx.fillText('护肤上颜选,正品', 60 * scale, (710 + 24) * scale)
  252. ctx.fillText('有好货', 60 * scale, (710 + 24 + 40) * scale)
  253. // 绘制二维码
  254. ctx.drawImage(this.imageList[3].tempFilePath, 364 * scale, 690 * scale, 116 * scale, 116 * scale)
  255. },
  256. // 绘制商品信息
  257. drawGoodsInfo(ctx, scale) {
  258. // 参数处理
  259. let { porductName, productPrice, productOriginPrice } = this.posterData
  260. const porductNames = this.getProductNames(porductName)
  261. console.log(porductNames)
  262. // 绘制中心图片
  263. ctx.drawImage(this.imageList[2].tempFilePath, 40 * scale, 174 * scale, 460 * scale, 460 * scale)
  264. if (this.type === 'product') {
  265. productPrice = productPrice.toFixed(2)
  266. if (productOriginPrice) {
  267. productOriginPrice = '¥' + productOriginPrice.toFixed(2)
  268. }
  269. // 绘制价格符号
  270. ctx.setFillStyle('#FF457B')
  271. ctx.setFontSize(24 * scale)
  272. ctx.fillText('¥', 40 * scale, (680 + 24) * scale)
  273. // 绘制购买价格
  274. ctx.setFontSize(40 * scale)
  275. ctx.fillText(productPrice, 62 * scale, (665 + 40) * scale)
  276. // 绘制原价
  277. if (productOriginPrice) {
  278. ctx.setFillStyle('#999')
  279. ctx.setFontSize(24 * scale)
  280. ctx.fillText(productOriginPrice, 200 * scale, (680 + 24) * scale)
  281. let m = ctx.measureText(productOriginPrice)
  282. ctx.fillRect(200 * scale, (680 + 16) * scale, parseInt(m.width), 1)
  283. }
  284. }
  285. // 绘制商品标题
  286. ctx.setFillStyle('#333')
  287. ctx.setFontSize(26 * scale)
  288. porductNames.forEach((text, index) => {
  289. text = index === 1 ? text.slice(0, text.length - 1) + '...' : text
  290. ctx.fillText(text, 40 * scale, (732 + 26 + 40 * index) * scale)
  291. })
  292. // 绘制商品二维码
  293. ctx.drawImage(this.imageList[3].tempFilePath, 384 * scale, 702 * scale, 116 * scale, 116 * scale)
  294. },
  295. // 处理产品名称
  296. getProductNames(porductName) {
  297. const list = []
  298. if (porductName.length > 12) {
  299. for (let i = 0; i < porductName.length; i += 12) {
  300. if (list.length < 2) {
  301. list.push(porductName.slice(i, i + 12))
  302. }
  303. }
  304. } else {
  305. list.push(porductName)
  306. }
  307. return list
  308. },
  309. // 下载图片
  310. downloadImage(url) {
  311. return new Promise((resolve, reject) => {
  312. uni.downloadFile({
  313. url: url,
  314. success(data) {
  315. resolve(data)
  316. },
  317. fail(err) {
  318. reject(err)
  319. }
  320. })
  321. })
  322. },
  323. async createPoster() {
  324. // 合并海报数据
  325. this.posterData = { ...this.posterData, ...this.data }
  326. try {
  327. // 从本地缓存中获取微信用户基本信息
  328. let userProfile = this.$getStorage('USER_PROFILE')
  329. if (!userProfile) {
  330. userProfile = await getUserProfile()
  331. this.$setStorage('USER_PROFILE', userProfile)
  332. }
  333. this.posterData.avatar = userProfile.avatarUrl
  334. this.posterData.username = userProfile.nickName
  335. uni.showLoading({
  336. mask: true,
  337. title: '正在为您生成海报'
  338. })
  339. // 绘制海报
  340. this.initDrawPoster()
  341. } catch (e) {
  342. //TODO handle the exception
  343. uni.hideLoading()
  344. }
  345. }
  346. }
  347. }
  348. </script>
  349. <style lang="scss" scoped>
  350. .canvas {
  351. position: fixed;
  352. left: -600px;
  353. top: 0;
  354. width: 540px;
  355. height: 878px;
  356. opacity: 0;
  357. }
  358. .poster-mask {
  359. position: fixed;
  360. top: 0;
  361. left: 0;
  362. z-index: 96;
  363. width: 100%;
  364. height: 100vh;
  365. background-color: rgba(0, 0, 0, 0.4);
  366. }
  367. .poster-container {
  368. z-index: 99;
  369. position: fixed;
  370. top: 80rpx;
  371. left: 50%;
  372. transform: translateX(-50%);
  373. width: 540rpx;
  374. overflow: hidden;
  375. text-align: center;
  376. .poster-image {
  377. display: block;
  378. width: 540rpx;
  379. height: 878rpx;
  380. }
  381. .poster {
  382. display: inline-block;
  383. margin: 0 auto;
  384. margin-top: 48rpx;
  385. .label {
  386. color: #fff !important;
  387. }
  388. }
  389. }
  390. .popup-content {
  391. position: relative;
  392. padding: 40rpx;
  393. padding-bottom: 0;
  394. border-radius: 16rpx 16rpx 0 0;
  395. background-color: #fff;
  396. &.no-safe-area {
  397. transform: translateY(34px);
  398. }
  399. &::after {
  400. position: absolute;
  401. content: '';
  402. width: 100%;
  403. height: 80rpx;
  404. bottom: -80rpx;
  405. left: 0;
  406. background-color: #fff;
  407. }
  408. .title {
  409. .title-text {
  410. font-size: 34rpx;
  411. color: #666;
  412. text-align: center;
  413. padding-bottom: 28rpx;
  414. }
  415. }
  416. }
  417. .popup-content,
  418. .poster-container {
  419. .row {
  420. @extend .cm-flex-around;
  421. }
  422. .item {
  423. @extend .cm-flex-center;
  424. flex-direction: column;
  425. .label {
  426. color: #333;
  427. font-size: 26rpx;
  428. margin-top: 16rpx;
  429. }
  430. .icon-image {
  431. @extend .cm-flex-center;
  432. width: 100rpx;
  433. height: 100rpx;
  434. background-color: #f7f7f7;
  435. border-radius: 50%;
  436. image {
  437. width: 64rpx;
  438. height: 64rpx;
  439. display: block;
  440. }
  441. }
  442. &.share {
  443. line-height: inherit;
  444. padding: 0;
  445. margin: 0;
  446. border: 0;
  447. background: transparent;
  448. &::after {
  449. border: 0;
  450. }
  451. }
  452. }
  453. .cancel {
  454. font-size: 28rpx;
  455. color: #666;
  456. font-weight: bold;
  457. text-align: center;
  458. padding-bottom: 32rpx;
  459. }
  460. }
  461. </style>