share.vue 18 KB

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