index.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473
  1. <template>
  2. <view class="share">
  3. <!-- 海报内容区域 -->
  4. <view class="bg_color_fff border_radius_20 save-area" id="saveArea">
  5. <view class="merchant-info">
  6. <image class="merchant-logo" :src="merchantInfo.merchantLogo" mode="aspectFill"></image>
  7. <view class="merchant-detail">
  8. <view class="merchant-name">{{merchantInfo.merchantNameNew}}</view>
  9. </view>
  10. </view>
  11. <view class="big-merchant-logo">
  12. <image class="merchant-logo" :src="merchantInfo.merchantLogo" mode="aspectFill"></image>
  13. </view>
  14. <view class="qrCodeInfo">
  15. <view class="left">
  16. <view class="title">{{ merchantInfo.merchantDescribeNew }}</view>
  17. <view class="subTitle">扫码加入<text class="dot"></text>{{merchantInfo.merchantNameNew}}</view>
  18. </view>
  19. <view class="right">
  20. <image class="qrCode" :src="qrCode" mode="widthFix"></image>
  21. </view>
  22. </view>
  23. </view>
  24. <!-- 操作按钮 -->
  25. <view class="flex-center-between padding30">
  26. <view class="order_btn flex-center save-btn" @click="saveToAlbum">
  27. <view class="flex-center-between">
  28. <image
  29. src="/static/img/xiazai.png"
  30. mode="widthFix"
  31. class="share_img mr20"
  32. ></image>
  33. <text>保存海报</text>
  34. </view>
  35. </view>
  36. <button class="order_btn flex-center share-btn" open-type="share">
  37. <view class="flex-center-between">
  38. <image
  39. src="/static/img/fenxiang_w.png"
  40. mode="widthFix"
  41. class="share_img mr20"
  42. ></image>
  43. <text>分享海报</text>
  44. </view>
  45. </button>
  46. </view>
  47. <!-- 用于绘制海报的Canvas -->
  48. <canvas
  49. canvas-id="posterCanvas"
  50. id="posterCanvas"
  51. style="position: fixed; top: -9999px; left: -9999px;"
  52. :style="{ width: canvasWidth + 'px', height: canvasHeight + 'px' }"
  53. ></canvas>
  54. </view>
  55. </template>
  56. <script setup>
  57. import { ref, reactive, onMounted, getCurrentInstance } from 'vue'
  58. import { onLoad } from "@dcloudio/uni-app";
  59. import { getQrcode } from "@/api/user.js"
  60. // 响应式数据
  61. const merchantInfo = ref({})
  62. const qrCode = ref("")
  63. const canvasWidth = ref(600) // 根据参考图片调整宽度
  64. const canvasHeight = ref(800) // 根据参考图片调整高度
  65. // 获取二维码方法
  66. const QRCodeGenerationFn = async () => {
  67. uni.showLoading({ title: "正在获取二维码..." })
  68. try {
  69. let data = {
  70. id: merchantInfo.value.id
  71. }
  72. const response = await getQrcode(data)
  73. console.log("getQrcode", response)
  74. qrCode.value = response.message
  75. } catch (err) {
  76. console.error("获取二维码失败", err)
  77. uni.showToast({ title: "获取二维码失败", icon: "none" })
  78. } finally {
  79. uni.hideLoading()
  80. }
  81. }
  82. // 绘制圆角矩形
  83. const drawRoundRect = (ctx, x, y, width, height, radius) => {
  84. ctx.beginPath()
  85. ctx.moveTo(x + radius, y)
  86. ctx.arcTo(x + width, y, x + width, y + height, radius)
  87. ctx.arcTo(x + width, y + height, x, y + height, radius)
  88. ctx.arcTo(x, y + height, x, y, radius)
  89. ctx.arcTo(x, y, x + width, y, radius)
  90. ctx.closePath()
  91. }
  92. // 绘制圆角图片
  93. const drawRoundImage = (ctx, imgPath, x, y, width, height, radius) => {
  94. // 保存当前状态
  95. ctx.save()
  96. // 创建圆角路径
  97. drawRoundRect(ctx, x, y, width, height, radius)
  98. ctx.clip()
  99. // 绘制图片
  100. ctx.drawImage(imgPath, x, y, width, height)
  101. // 恢复状态
  102. ctx.restore()
  103. }
  104. // 保存海报到相册
  105. const saveToAlbum = async () => {
  106. uni.showLoading({ title: "正在生成海报..." })
  107. try {
  108. // 检查必要的图片资源
  109. if (!merchantInfo.value.merchantLogo) {
  110. throw new Error("商家Logo不能为空")
  111. }
  112. if (!qrCode.value) {
  113. throw new Error("二维码不能为空")
  114. }
  115. // 创建 Canvas 上下文
  116. const ctx = uni.createCanvasContext('posterCanvas', getCurrentInstance())
  117. // 1. 绘制白色背景
  118. ctx.setFillStyle("#ffffff")
  119. ctx.fillRect(0, 0, canvasWidth.value, canvasHeight.value)
  120. // 3. 加载并绘制商家Logo(小头像)
  121. await new Promise((resolve, reject) => {
  122. uni.getImageInfo({
  123. src: merchantInfo.value.merchantLogo,
  124. success: (logoRes) => {
  125. // 绘制圆形商家头像
  126. const avatarSize = 80
  127. const avatarX = 60
  128. const avatarY = 60
  129. const radius = 10 // 设置圆角半径为10
  130. // ctx.drawImage(logoRes.path, avatarX, avatarY, avatarSize, avatarSize)
  131. // ctx.restore()
  132. drawRoundImage(ctx, logoRes.path, avatarX, avatarY, avatarSize, avatarSize, radius)
  133. resolve()
  134. },
  135. fail: (err) => {
  136. reject(new Error('获取商家Logo失败: ' + err.errMsg))
  137. }
  138. })
  139. })
  140. // 4. 绘制商家名称
  141. const merchantName = merchantInfo.value.merchantNameNew || "商家名称"
  142. ctx.setFontSize(28)
  143. ctx.setFillStyle("#333333")
  144. ctx.setTextAlign("left")
  145. ctx.fillText(merchantName, 160, 110)
  146. // 5. 绘制主体内容区域
  147. const contentStartY = 200
  148. // 6. 加载并绘制商家Logo(大头像)
  149. await new Promise((resolve, reject) => {
  150. uni.getImageInfo({
  151. src: merchantInfo.value.merchantLogo,
  152. success: (logoRes) => {
  153. // 绘制圆形商家头像
  154. const avatarSize = 400
  155. const avatarX = 100
  156. const avatarY = contentStartY
  157. const radius = 10
  158. // ctx.drawImage(logoRes.path, avatarX, contentStartY, avatarSize, avatarSize)
  159. // ctx.restore()
  160. drawRoundImage(ctx, logoRes.path, avatarX, avatarY, avatarSize, avatarSize, radius)
  161. resolve()
  162. },
  163. fail: (err) => {
  164. reject(new Error('获取商家Logo失败: ' + err.errMsg))
  165. }
  166. })
  167. })
  168. // 9. 绘制"好久不见"标题
  169. const bottomStartY = contentStartY + 80+400
  170. ctx.setFontSize(32)
  171. ctx.setFillStyle("#333333")
  172. ctx.setTextAlign("left")
  173. ctx.fillText(merchantInfo.value.merchantDescribeNew, 40, bottomStartY)
  174. // 10. 绘制副标题
  175. ctx.setFontSize(20)
  176. ctx.setFillStyle("#787878")
  177. const subTitle = `扫码加入 · ${merchantInfo.value.merchantNameNew}`
  178. ctx.fillText(subTitle, 40, bottomStartY + 50)
  179. // 11. 加载并绘制二维码
  180. await new Promise((resolve, reject) => {
  181. uni.getImageInfo({
  182. src: qrCode.value,
  183. success: (qrRes) => {
  184. const qrSize = 150
  185. const qrX = canvasWidth.value-210;
  186. const qrY = contentStartY+400+20
  187. ctx.drawImage(qrRes.path, qrX, qrY, qrSize, qrSize)
  188. ctx.restore()
  189. resolve()
  190. },
  191. fail: (err) => {
  192. reject(new Error('获取二维码失败: ' + err.errMsg))
  193. }
  194. })
  195. })
  196. // 等待绘制完成
  197. await new Promise((resolve) => {
  198. ctx.draw(false, () => {
  199. setTimeout(() => {
  200. resolve()
  201. }, 800) // 增加等待时间确保绘制完成
  202. })
  203. })
  204. // 13. 导出Canvas为临时文件
  205. const tempFilePath = await new Promise((resolve, reject) => {
  206. uni.canvasToTempFilePath({
  207. canvasId: 'posterCanvas',
  208. fileType: 'png',
  209. quality: 1,
  210. width: canvasWidth.value,
  211. height: canvasHeight.value,
  212. success: (res) => {
  213. resolve(res.tempFilePath)
  214. },
  215. fail: (err) => {
  216. reject(new Error('生成图片失败: ' + err.errMsg))
  217. }
  218. }, getCurrentInstance())
  219. })
  220. // 14. 保存到相册
  221. await new Promise((resolve, reject) => {
  222. uni.saveImageToPhotosAlbum({
  223. filePath: tempFilePath,
  224. success: () => {
  225. resolve()
  226. },
  227. fail: (err) => {
  228. if (err.errMsg.includes('auth deny') || err.errMsg.includes('auth denied')) {
  229. uni.showModal({
  230. title: '提示',
  231. content: '需要授权保存图片到相册',
  232. showCancel: false,
  233. confirmText: '去设置',
  234. success: () => {
  235. uni.openSetting()
  236. }
  237. })
  238. }
  239. reject(new Error('保存失败: ' + err.errMsg))
  240. }
  241. })
  242. })
  243. uni.hideLoading()
  244. uni.showToast({ title: '保存成功', icon: 'success' })
  245. } catch (error) {
  246. uni.hideLoading()
  247. console.error('保存海报失败:', error)
  248. uni.showToast({
  249. title: error.message || '保存失败',
  250. icon: 'none',
  251. duration: 3000
  252. })
  253. }
  254. }
  255. // 页面加载时获取二维码
  256. onLoad((options) => {
  257. if (options.merchantInfo) {
  258. merchantInfo.value = JSON.parse(decodeURIComponent(options.merchantInfo))
  259. merchantInfo.value.merchantName="一号商家一号商家一号商家一号商家一号商家一号商家一号商家一号商家一号商家一号商家一号商家一号商家"
  260. merchantInfo.value.merchantDescribe="一号商家一号商家一号商家一号商家一号商家一号商家一号商家一号商家一号商家一号商家一号商家一号商家"
  261. // 修正语法错误
  262. merchantInfo.value.merchantNameNew = truncateText(merchantInfo.value.merchantName, 10) || ''
  263. merchantInfo.value.merchantDescribeNew = truncateText(merchantInfo.value.merchantDescribe, 10) || ''
  264. qrCode.value = merchantInfo.value.merchantCodeUrl || ''
  265. if (!qrCode.value) {
  266. QRCodeGenerationFn()
  267. }
  268. }
  269. })
  270. // 分享功能
  271. const onShareAppMessage = () => {
  272. let keyInfo = `merchantCode=${merchantInfo.value?.merchantCode}`
  273. keyInfo = encodeURIComponent(keyInfo)
  274. return {
  275. title: `快来加入${merchantInfo.value?.merchantName || '我们'}~`,
  276. desc: merchantInfo.value.merchantDescribeNew,
  277. path: `/pages/index/index?scene=${keyInfo}`
  278. }
  279. }
  280. // 添加文本截取函数
  281. const truncateText = (text, maxLength) => {
  282. if (!text) return ''
  283. if (text.length <= maxLength) return text
  284. return text.substring(0, maxLength) + '...'
  285. }
  286. // 暴露给模板
  287. defineExpose({
  288. onShareAppMessage
  289. })
  290. </script>
  291. <style lang="scss" scoped>
  292. .share {
  293. padding: 30rpx;
  294. background-color: #f5f5f5;
  295. min-height: 100vh;
  296. box-sizing: border-box;
  297. .save-area {
  298. padding: 40rpx;
  299. box-sizing: border-box;
  300. position: relative;
  301. margin-bottom: 30rpx;
  302. }
  303. .logo_img {
  304. width: 60rpx;
  305. height: 60rpx;
  306. border-radius: 50%;
  307. }
  308. .share_img {
  309. width: 40rpx;
  310. height: 40rpx;
  311. }
  312. .order_btn {
  313. padding: 20rpx;
  314. width: 45%;
  315. text-align: center;
  316. font-size: 32rpx;
  317. color: white;
  318. border-radius: 12rpx;
  319. border: none;
  320. background: #f0ad4e;
  321. }
  322. }
  323. .merchant-info {
  324. display: flex;
  325. align-items: center;
  326. margin-bottom: 30rpx;
  327. .merchant-logo {
  328. width: 120rpx;
  329. height: 120rpx;
  330. border-radius: 12rpx;
  331. margin-right: 30rpx;
  332. }
  333. .merchant-detail {
  334. flex: 1;
  335. .merchant-name {
  336. font-size: 32rpx;
  337. font-weight: bold;
  338. color: #333;
  339. }
  340. }
  341. }
  342. .big-merchant-logo {
  343. display: flex;
  344. width: 100%;
  345. justify-content: center;
  346. align-items: center;
  347. margin: 60rpx 0;
  348. .merchant-logo {
  349. width: 400rpx;
  350. height: 400rpx;
  351. border-radius: 20rpx;
  352. box-shadow: 0 8rpx 30rpx rgba(0, 0, 0, 0.1);
  353. }
  354. }
  355. .border_radius_20 {
  356. border-radius: 20rpx;
  357. }
  358. .bg_color_fff {
  359. background-color: #ffffff;
  360. }
  361. .qrCodeInfo {
  362. display: flex;
  363. align-items: center;
  364. margin-top: 60rpx;
  365. .left {
  366. flex: 1;
  367. .title {
  368. font-size: 36rpx;
  369. font-weight: bold;
  370. color: #333;
  371. line-height: 60rpx;
  372. margin-bottom: 16rpx;
  373. }
  374. .subTitle {
  375. font-size: 28rpx;
  376. color: #666;
  377. line-height: 40rpx;
  378. .dot {
  379. display: inline-block;
  380. vertical-align: middle;
  381. height: 8rpx;
  382. width: 8rpx;
  383. border-radius: 50%;
  384. background-color: #666;
  385. margin: 0 16rpx;
  386. }
  387. }
  388. }
  389. .right {
  390. width: 160rpx;
  391. text-align: center;
  392. .qrCode {
  393. width: 100%;
  394. border-radius: 12rpx;
  395. }
  396. }
  397. }
  398. .flex-center-between {
  399. display: flex;
  400. justify-content: space-between;
  401. align-items: center;
  402. }
  403. .padding30 {
  404. padding: 30rpx 0;
  405. }
  406. .flex-center {
  407. display: flex;
  408. justify-content: center;
  409. align-items: center;
  410. }
  411. .mr20 {
  412. margin-right: 20rpx;
  413. }
  414. </style>