share.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636
  1. <template>
  2. <div class="share" :style="{ 'z-index': zIndex }">
  3. <!-- canvasImageUrl canvase 渲染成功后删除原始元素,避免当前页面元素太多出现卡顿 -->
  4. <div class="share-content" ref="shareContent">
  5. <!-- <div class="share-content" ref="shareContent" v-if="!canvasImageUrl"> -->
  6. <div class="header">
  7. <div class="left-icon">
  8. <img :src="require('@/assets/shareLeft.png')" />
  9. </div>
  10. <div class="right-icon">
  11. <img :src="require('@/assets/shareRight.png')" />
  12. </div>
  13. </div>
  14. <!-- 点评 -->
  15. <!-- <template v-for="item in reportTarget.reportRemarks"> -->
  16. <div
  17. class="comment summaryDay"
  18. v-if="
  19. reportRemarksIndex != -1 &&
  20. reportTarget.reportRemarks &&
  21. reportTarget.reportRemarks.length
  22. ">
  23. <div class="title-box">
  24. <div class="title">
  25. <div class="name">
  26. {{ reportTarget.reportRemarks[reportRemarksIndex].nickName }}点评
  27. </div>
  28. <div class="date">
  29. {{
  30. reportTarget.reportRemarks[reportRemarksIndex].createTime
  31. ? formatChineseDate(
  32. reportTarget.reportRemarks[reportRemarksIndex].createTime.split(' ')[0]
  33. )
  34. : ''
  35. }}
  36. </div>
  37. </div>
  38. </div>
  39. <div class="commentMessage">
  40. {{ reportTarget.reportRemarks[reportRemarksIndex].remarkContent }}
  41. </div>
  42. </div>
  43. <!-- </template> -->
  44. <!-- 内容 -->
  45. <div class="summaryDay">
  46. <div class="title-box">
  47. <div class="title">
  48. <div class="name">{{ reportTarget.nickName }}的日报</div>
  49. <div class="date">
  50. {{
  51. reportTarget.commitTime
  52. ? formatChineseDate(reportTarget.commitTime.split(' ')[0])
  53. : ''
  54. }}
  55. </div>
  56. </div>
  57. </div>
  58. <!-- <template
  59. v-for="(item, index) in reportTarget.reportContents"
  60. v-if="reportTarget.reportContents && reportTarget.reportContents.length > 0">
  61. <div :class="['text']">{{ filterText(index) }}</div>
  62. <div class="content">{{ item.dayContent }}</div>
  63. </template> -->
  64. <template v-if="checkedPlan">
  65. <div :class="['text']">今日机会与挑战总结</div>
  66. <div class="content">
  67. {{
  68. reportTarget.reportContents.length ? reportTarget.reportContents[0].dayContent : ''
  69. }}
  70. </div>
  71. <div :class="['text']">明日工作计划</div>
  72. <div class="content">
  73. {{
  74. reportTarget.reportContents.length ? reportTarget.reportContents[1].dayContent : ''
  75. }}
  76. </div>
  77. </template>
  78. <template v-if="photosData && photosData.length">
  79. <div class="text">今日拜访照片</div>
  80. <div class="content-photos">
  81. <template v-for="item in photosData">
  82. <img
  83. :src="'data:image/jpg;base64,' + item"
  84. crossorigin="anonymous"
  85. referrerpolicy="no-referrer"
  86. alt=""
  87. style="display: block" />
  88. </template>
  89. <!-- <template v-for="item in urlList">
  90. <img
  91. :src="item + '&' + new Date().getTime()"
  92. crossOrigin="anonymous"
  93. referrerpolicy="no-referrer"
  94. alt=""
  95. style="display: block" />
  96. </template> -->
  97. </div>
  98. </template>
  99. </div>
  100. <div class="footerShare">
  101. <div class="right-text">
  102. <div>长按识别二维码查看详情&点评</div>
  103. <div></div>
  104. </div>
  105. <vue-qr
  106. class="QRcodes"
  107. :callback="QRCallback"
  108. :text="vueQrText"
  109. :dotScale="0.8"
  110. :margin="8"
  111. :logoSrc="require('@/assets/logo1.png')"
  112. :logoMargin="2"></vue-qr>
  113. </div>
  114. </div>
  115. <div
  116. class="share-mask"
  117. style="background: #000; position: fixed; top: 0; width: 100%; height: 100%"></div>
  118. <div class="html2canvasBox">
  119. <div id="html2canvas" ref="html2canvas">
  120. <div class="scroll-container">
  121. <div style="position: relative">
  122. <div class="shareHeader">
  123. <div class="shareTips">
  124. 长按图片可下载、收藏、转发
  125. <!-- <img :src="require('@/assets/shareTips.png')" alt="" /> -->
  126. </div>
  127. <!-- <div class="closeShare" @click="closeShare"><van-icon name="close" /></div> -->
  128. <div class="closeShare" @click="closeShare">返回</div>
  129. </div>
  130. <img :src="canvasImageUrl" width="100%" class="canvasImage" />
  131. </div>
  132. </div>
  133. </div>
  134. </div>
  135. </div>
  136. </template>
  137. <script>
  138. import html2canvas from 'html2canvas';
  139. import { imgToBase64 } from '@/api/index';
  140. import VueQr from 'vue-qr';
  141. export default {
  142. name: 'share',
  143. components: { VueQr },
  144. props: {
  145. // 日报数据
  146. reportTarget: {
  147. type: Object,
  148. default() {
  149. return {};
  150. },
  151. },
  152. //日报id
  153. reportId: {
  154. type: [String, Number],
  155. },
  156. // 点评数据
  157. reportRemarksIndex: {
  158. type: Number,
  159. default: -1,
  160. },
  161. // 图片转换列表
  162. urlList: {
  163. type: Array,
  164. default() {
  165. return [];
  166. },
  167. },
  168. // 是否显示 今日总结&明日规划
  169. checkedPlan: {
  170. type: Boolean,
  171. default: true,
  172. },
  173. },
  174. data() {
  175. return {
  176. canvasImageUrl: '',
  177. retryCount: 0,
  178. photosData: [],
  179. zIndex: -1,
  180. qrcode: null,
  181. vueQrText: '',
  182. heightFlag: 'start', //图片高度不足,居中显示
  183. };
  184. },
  185. created() {
  186. this.zIndex = -1;
  187. this.canvasImageUrl = '';
  188. this.vueQrText = 'http://1.npz.cn/2/' + this.reportId;
  189. this.toastLoading(0, '生成中...', true);
  190. },
  191. methods: {
  192. // 初始化会先执行二维码生成,成功后回调
  193. QRCallback() {
  194. // 二维码
  195. if (this.urlList.length) {
  196. imgToBase64({ urlList: this.urlList }).then((res) => {
  197. if (res.data && res.code == 200) {
  198. this.photosData = res.data;
  199. this.$nextTick(async () => {
  200. await this.htmlToCanvas();
  201. });
  202. } else {
  203. this.$toast(res.msg);
  204. this.$nextTick(async () => {
  205. await this.htmlToCanvas();
  206. });
  207. }
  208. });
  209. } else {
  210. this.$nextTick(async () => {
  211. await this.htmlToCanvas();
  212. });
  213. }
  214. },
  215. //异步执行
  216. imageUrlToBase64(imageUrl) {
  217. return new Promise((resolve, reject) => {
  218. //一定要设置为let,不然图片不显示
  219. let image = new Image();
  220. //解决跨域问题
  221. image.setAttribute('crossOrigin', 'anonymous');
  222. image.src = imageUrl;
  223. image.onload = () => {
  224. var canvas = document.createElement('canvas');
  225. canvas.width = image.width;
  226. canvas.height = image.height;
  227. var context = canvas.getContext('2d');
  228. context.drawImage(image, 0, 0, image.width, image.height);
  229. var quality = 1;
  230. //这里的dataurl就是base64类型
  231. let dataURL = canvas.toDataURL('image/png', quality); //使用toDataUrl将图片转换成jpeg的格式,不要把图片压缩成png,因为压缩成png后base64的字符串可能比不转换前的长!
  232. resolve(dataURL);
  233. };
  234. image.onerror = () => {
  235. reject(new Error('图像加载失败'));
  236. };
  237. });
  238. },
  239. filterText(index) {
  240. if (index == 0) {
  241. return '明日工作计划';
  242. } else {
  243. return '今日机会与挑战总结';
  244. }
  245. },
  246. htmlToCanvas() {
  247. html2canvas(this.$refs.shareContent, {
  248. scale: window.devicePixelRatio || 1,
  249. useCORS: true,
  250. allowTaint: false,
  251. backgroundColor: null,
  252. logging: false, // 关闭日志提升性能
  253. // onclone: (clonedDoc) => {
  254. // // 确保克隆的DOM保持原始样式
  255. // clonedDoc.getElementById('html2canvas').style.overflow = 'auto';
  256. // },
  257. // ignoreElements: (e) => {
  258. // if (
  259. // e.contains(element) ||
  260. // element.contains(e) ||
  261. // e.tagName === 'STYLE' ||
  262. // e.tagName === 'LINK' ||
  263. // e.getAttribute('data-html2canvas') != null // header里面的样式不能筛掉
  264. // ) {
  265. // console.log(e);
  266. // return false;
  267. // }
  268. // return true;
  269. // },
  270. })
  271. .then((canvas) => {
  272. this.toastLoading().clear();
  273. let imageUrl = canvas.toDataURL('image/png');
  274. this.canvasImageUrl = imageUrl;
  275. // 图片加载完成后设置滚动容器高度
  276. const img = new Image();
  277. img.onload = () => {
  278. const canvasImage = this.$refs.html2canvas.querySelector('.canvasImage');
  279. const scrollContainer = this.$refs.html2canvas.querySelector('.scroll-container');
  280. const shareHeader = this.$refs.html2canvas.querySelector('.shareHeader');
  281. console.log(scrollContainer.style);
  282. if (canvasImage.height < window.innerHeight) {
  283. scrollContainer.style.alignItems = 'center';
  284. shareHeader.style.position = 'absolute';
  285. } else {
  286. scrollContainer.style.alignItems = 'start';
  287. shareHeader.style.position = 'fixed';
  288. }
  289. this.$forceUpdate();
  290. // // 根据图片实际高度设置容器高度(增加20px缓冲)
  291. // // 根据设备像素比调整图片高度
  292. // // 使用实际渲染高度而非原始图片高度
  293. // const imgHeight = img.offsetHeight + 20;
  294. // // 设置容器最小高度保证内容显示,同时允许自动扩展
  295. // scrollContainer.style.minHeight = `${imgHeight}px`;
  296. // // 保持父容器为可见滚动
  297. // scrollContainer.parentElement.style.overflow = 'visible';
  298. // // 强制浏览器重排
  299. // scrollContainer.style.display = 'none';
  300. // scrollContainer.offsetHeight; // 触发重排
  301. // scrollContainer.style.display = 'block';
  302. // // 添加移动端滚动优化
  303. // requestAnimationFrame(() => {
  304. // scrollContainer.style.overflow = 'auto';
  305. // scrollContainer.style.overflowScrolling = 'touch';
  306. // scrollContainer.style.webkitOverflowScrolling = 'touch';
  307. // scrollContainer.style.overscrollBehavior = 'contain';
  308. // scrollContainer.style.touchAction = 'pan-y';
  309. // });
  310. };
  311. img.src = imageUrl;
  312. this.zIndex = 9;
  313. this.$emit('setShareImg', true);
  314. this.$emit('setDailyDetailsBox', false);
  315. })
  316. .catch((error) => {
  317. this.toastLoading().clear();
  318. console.error('html2canvas error:', error);
  319. this.$toast('生成分享图失败,请稍后重试');
  320. });
  321. },
  322. closeShare() {
  323. this.canvasImageUrl = '';
  324. this.$emit('setShareImg', false);
  325. this.$emit('setDailyDetailsBox', true);
  326. },
  327. },
  328. beforeDestroy() {
  329. // 组件销毁时清除实例
  330. if (this.qrcode) {
  331. this.$refs.qrcode.innerHTML = '';
  332. }
  333. },
  334. };
  335. </script>
  336. <style lang="scss" scoped>
  337. .share {
  338. width: 100%;
  339. height: 100%;
  340. overflow: hidden;
  341. background: #fff;
  342. position: fixed;
  343. top: 0;
  344. .share-content {
  345. background: url('../assets/shareBack.webp') no-repeat center;
  346. background-size: 100% 100%;
  347. // background-attachment: local;
  348. width: 100%;
  349. // min-height: 100%;
  350. padding: vw(30);
  351. position: absolute;
  352. // padding-bottom: vw(30);
  353. top: 0;
  354. z-index: -10;
  355. }
  356. .header {
  357. display: flex;
  358. justify-content: space-between;
  359. padding-bottom: vw(40);
  360. .left-icon {
  361. width: vw(114);
  362. height: vw(71);
  363. }
  364. .right-icon {
  365. width: vw(230);
  366. height: vw(78);
  367. }
  368. img {
  369. width: 100%;
  370. height: 100%;
  371. }
  372. }
  373. .title-box {
  374. position: relative;
  375. width: 100%;
  376. height: auto;
  377. display: flex;
  378. justify-content: center;
  379. align-items: center;
  380. top: -6px;
  381. .title {
  382. // width: vw(262);
  383. // height: vw(115);
  384. display: flex;
  385. flex-direction: column;
  386. justify-content: center;
  387. align-items: center;
  388. color: #600d0e;
  389. font-weight: bold;
  390. text-align: center;
  391. // background: url('../assets/titleBack.png') no-repeat;
  392. // background-size: 100% 100%;
  393. padding: vw(20) vw(30);
  394. background-color: #ecdd9a;
  395. border-radius: 6px 6px 25px 25px;
  396. .name {
  397. font-size: vw(36);
  398. }
  399. .date {
  400. font-size: vw(28);
  401. }
  402. }
  403. }
  404. .summaryDay {
  405. background: url('../assets/summaryDay.png') no-repeat;
  406. background-size: 100%;
  407. padding: vw(24);
  408. position: relative;
  409. margin-top: vw(55);
  410. border-radius: 0 0 vw(35) vw(35);
  411. padding-top: 0;
  412. .text {
  413. background: url('../assets/textBack.png') no-repeat;
  414. width: 100%;
  415. height: vw(94);
  416. background-size: cover;
  417. color: #7d0207;
  418. font-size: vw(36);
  419. text-align: center;
  420. line-height: vw(94);
  421. // margin-top: vw(152);
  422. // margin-bottom: vw(45);
  423. margin: vw(35) 0;
  424. font-weight: bold;
  425. }
  426. .content {
  427. font-size: vw(28);
  428. font-weight: bold;
  429. color: #ffff;
  430. line-height: vw(63);
  431. white-space: pre-wrap;
  432. }
  433. .content-photos {
  434. display: flex;
  435. flex-wrap: wrap;
  436. // justify-content: space-between;
  437. img {
  438. width: 30%;
  439. height: vw(188);
  440. margin-bottom: 15px;
  441. margin-right: 5%;
  442. background-color: rgba(255, 255, 255, 0.1);
  443. &:nth-child(3n) {
  444. margin-right: 0;
  445. }
  446. }
  447. }
  448. }
  449. .comment {
  450. .title {
  451. // background: url('../assets/comment.png') no-repeat;
  452. // background-size: 100% 100%;
  453. background-color: #032371;
  454. border-radius: 6px 6px 25px 25px;
  455. color: #e6cd78;
  456. }
  457. .commentMessage {
  458. padding: vw(60) 0;
  459. font-family: PingFang-SC-Medium;
  460. font-size: vw(28);
  461. font-weight: bold;
  462. color: #ffffff;
  463. }
  464. }
  465. .footerShare {
  466. // position: absolute;
  467. display: flex;
  468. // align-items: center;
  469. justify-content: flex-end;
  470. margin-top: vw(30);
  471. margin-right: 2px;
  472. .right-text {
  473. // margin-left: vw(12.5);
  474. display: flex;
  475. flex-direction: column;
  476. justify-content: space-between;
  477. margin-right: vw(24);
  478. div {
  479. color: #ffffff;
  480. font-size: vw(26);
  481. // font-weight: bold;
  482. padding: 6px 0;
  483. }
  484. }
  485. // display: flex;
  486. // flex-direction: column;
  487. // justify-content: center;
  488. // align-items: center;
  489. // margin-top: vw(100);
  490. // .right-text {
  491. // // margin-left: vw(12.5);
  492. // display: flex;
  493. // // flex-direction: column;
  494. // // justify-content: space-between;
  495. // align-items: center;
  496. // justify-content: center;
  497. // padding: vw(10) 0;
  498. // div {
  499. // color: #ffffff;
  500. // font-size: vw(30);
  501. // font-weight: bold;
  502. // }
  503. // }
  504. }
  505. .html2canvasBox {
  506. width: 100%;
  507. height: 100%;
  508. min-height: auto;
  509. min-height: 100vh;
  510. overflow: hidden !important;
  511. display: flex;
  512. flex-direction: column;
  513. }
  514. #html2canvas {
  515. width: 100%;
  516. height: 100%;
  517. position: static; /* 改为静态定位 */
  518. .scroll-container {
  519. width: 100%;
  520. height: 100%;
  521. flex: 1;
  522. overflow-y: auto !important;
  523. -webkit-overflow-scrolling: touch;
  524. overscroll-behavior: contain;
  525. touch-action: pan-y;
  526. // padding: 20px 0;
  527. box-sizing: border-box;
  528. /* 修复iOS弹性滚动 */
  529. overflow-scrolling: touch;
  530. max-height: 100vh; /* 添加最大高度限制 */
  531. position: relative; /* 修复定位上下文 */
  532. display: flex;
  533. // align-items: start;
  534. img {
  535. width: 100%;
  536. display: block;
  537. }
  538. .shareHeader {
  539. position: absolute;
  540. z-index: 1;
  541. top: 10px;
  542. width: 100%;
  543. display: flex;
  544. align-items: center;
  545. padding: 0 vw(30);
  546. .closeShare {
  547. // position: absolute;
  548. // z-index: 1;
  549. // right: 10px;
  550. // top: 10px;
  551. color: #fff;
  552. font-size: vw(30);
  553. background: #a2819c;
  554. text-align: center;
  555. line-height: 30px;
  556. border-radius: 6px;
  557. padding: 0 7px;
  558. border: 2px solid #11224e;
  559. .van-icon {
  560. font-size: vw(40);
  561. }
  562. }
  563. .shareTips {
  564. display: flex;
  565. align-items: center;
  566. justify-content: center;
  567. color: #f8f8fa;
  568. width: 82%;
  569. // flex: 1;
  570. margin-right: 10px;
  571. background: rgba(157, 157, 188, 0.8);
  572. height: vw(60);
  573. font-weight: 600;
  574. font-size: vw(26);
  575. img {
  576. width: 100%;
  577. // height: vw(60);
  578. }
  579. }
  580. }
  581. }
  582. }
  583. }
  584. </style>
  585. <style lang="scss">
  586. .share {
  587. .QRcodes {
  588. width: 45px; /* 固定像素尺寸 */
  589. height: 45px;
  590. position: relative;
  591. background: #ffffff;
  592. image-rendering: crisp-edges;
  593. image-rendering: -webkit-optimize-contrast; /* 安卓浏览器兼容 */
  594. // padding: 2px;
  595. display: flex;
  596. align-items: center;
  597. justify-content: center;
  598. border-radius: 3px;
  599. .logo {
  600. position: absolute;
  601. top: 50%;
  602. left: 50%;
  603. transform: translate(-50%, -50%);
  604. width: 10px;
  605. height: 10px;
  606. img {
  607. object-fit: contain;
  608. }
  609. z-index: 3;
  610. }
  611. img {
  612. width: 100%;
  613. height: 100%;
  614. }
  615. }
  616. }
  617. </style>