share.vue 18 KB

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