index.vue 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626
  1. <template>
  2. <div class="achievement-report-page">
  3. <custom-header name="生成成果报告"></custom-header>
  4. <div class="report-content-wrap" v-loading="loading">
  5. <div class="report-content" ref="reportDom">
  6. <img src="@/assets/img/home/qrcode.png" alt="" class="code-icon" />
  7. <div class="report-header">
  8. <img class="header-book" src="@/assets/img/home/book.png" alt="" />
  9. <div class="time-tag">{{ workItem?.executeDate }}</div>
  10. <div class="report-title">成果报告</div>
  11. <div class="report-info">
  12. <div class="info-item">
  13. <img class="info-icon" src="@/assets/img/home/nz.png" alt="" />
  14. <span class="info-text">执行组织:{{ workItem?.executeMain }}</span>
  15. </div>
  16. <div class="info-item">
  17. <img class="info-icon" src="@/assets/img/home/nz.png" alt="" />
  18. <span class="info-text">服务农场:{{ workItem?.farmName }}</span>
  19. </div>
  20. </div>
  21. </div>
  22. <div class="report-box">
  23. <div class="report-box-item">
  24. <div class="item-content">{{ workItem?.farmWorkName || "--" }}</div>
  25. <div class="item-title">作业农事</div>
  26. </div>
  27. <div class="report-box-item">
  28. <div class="item-content">{{ formatArea(workItem?.area) || "--" }}</div>
  29. <div class="item-title">作业面积</div>
  30. </div>
  31. <div class="report-box-item">
  32. <div class="item-content">{{ reportData?.workCycle ? (reportData?.workCycle + '天') : "--" }}</div>
  33. <div class="item-title">作业周期</div>
  34. </div>
  35. <div class="report-box-item">
  36. <div class="item-content">{{ reportData?.deviceName || "--" }}</div>
  37. <div class="item-title">使用设备</div>
  38. </div>
  39. </div>
  40. <!-- <div class="report-box">
  41. <div class="box-title">精准施治,智慧护航</div>
  42. <div class="box-text">
  43. {{ reportData?.resultInfo || "--" }}
  44. </div>
  45. </div> -->
  46. <div class="report-excute" v-for="(item, index) in workItem?.executeEvidence" :key="index">
  47. <div class="tag-label">执行照片</div>
  48. <album-draw-box :key="paramsPage.id" :isShowNum="0" :imgData="workItem" :photo="item" :current="index" :index="index" :length="workItem?.executeEvidence?.length"></album-draw-box>
  49. </div>
  50. <div class="report-excute" v-for="(item, index) in workItem?.reviewImage" :key="index">
  51. <div class="tag-label">复核照片</div>
  52. <album-draw-box :key="paramsPage.id+'复核'" :isShowNum="0" :isAchievementImgs="true" :imgData="workItem" :photo="item" :current="index" :index="index" :length="workItem?.reviewImage?.length"></album-draw-box>
  53. </div>
  54. <!-- <div class="report-excute" v-for="(item, index) in workItem?.reviewImage" :key="index">
  55. <div class="tag-label">复核照片</div>
  56. <album-draw-box :isShowNum="0" :photo="item" :current="index" :index="'review'+index" :length="workItem?.reviewImage?.length"></album-draw-box>
  57. </div> -->
  58. <!-- <div class="report-excute">
  59. <album-carousel
  60. :key="93"
  61. :isAchievementImgs="true"
  62. :images="combinedReviewImages"
  63. ></album-carousel>
  64. </div> -->
  65. </div>
  66. <div class="bottom-btn">
  67. <div class="btn-item second" @click="handleDownload">保存图片</div>
  68. <div class="btn-item primay">转发</div>
  69. </div>
  70. </div>
  71. <!-- 组合照片(用于生成合成图片) -->
  72. <div class="review-hide-box">
  73. <div class="review-image" ref="reviewComboRef">
  74. <div class="review-mask">
  75. <div class="review-text">复核成效</div>
  76. <div class="review-content">
  77. {{ workItem?.reCheckText }}
  78. </div>
  79. </div>
  80. <div class="vs-wrap" v-if="workItem?.reviewImage && workItem?.reviewImage?.length">
  81. <img src="@/assets/img/home/vs.png" alt="" />
  82. </div>
  83. <div class="review-image-item" v-if="workItem?.executeEvidence?.length">
  84. <div class="review-image-item-title">复核照片</div>
  85. <img
  86. class="review-image-item-img left-img"
  87. :src="leftCoverImg"
  88. style="width: 100%; height: 255px; display: block; image-rendering: auto"
  89. />
  90. </div>
  91. <div class="review-image-item" v-if="workItem?.reviewImage?.length">
  92. <img
  93. class="review-image-item-img right-img"
  94. :src="rightCoverImg"
  95. style="width: 100%; height: 255px; display: block; image-rendering: auto"
  96. />
  97. </div>
  98. </div>
  99. </div>
  100. </div>
  101. </template>
  102. <script setup>
  103. import CustomHeader from "@/components/customHeader.vue";
  104. import AlbumCarousel from "@/components/album_compoents/albumCarousel";
  105. import { ref, onActivated, onDeactivated, onUnmounted, nextTick, watch } from "vue";
  106. import html2canvas from "html2canvas";
  107. import { uploadBase64 } from "@/common/uploadImg";
  108. import { detectRuntimeEnvironment } from "@/common/commonFun";
  109. import { useRoute } from "vue-router";
  110. import { base_img_url2 } from "@/api/config";
  111. import AlbumDrawBox from "@/components/album_compoents/albumDrawBox.vue";
  112. const route = useRoute();
  113. const loading = ref(false);
  114. const workItem = ref({});
  115. function formatArea(val) {
  116. const num = typeof val === "number" ? val : parseFloat(val);
  117. if (Number.isNaN(num)) return val;
  118. return Number.isInteger(num) ? num : num.toFixed(2) + "亩";
  119. }
  120. const paramsPage = ref({});
  121. onActivated(() => {
  122. window.scrollTo(0, 0);
  123. paramsPage.value = route.query.miniJson ? JSON.parse(route.query.miniJson) : {};
  124. console.log('paramsPage', paramsPage.value);
  125. getDetail();
  126. getResultReport();
  127. });
  128. const reportData = ref({});
  129. const getResultReport = () => {
  130. VE_API.z_farm_work_record.resultReport({ recordId: paramsPage.value.id }).then((res) => {
  131. if (res.code === 0) {
  132. reportData.value = res.data;
  133. }
  134. });
  135. };
  136. const getDetail = () => {
  137. if (!paramsPage.value.id) return;
  138. loading.value = true;
  139. VE_API.z_farm_work_record
  140. .getDetail({ id: paramsPage.value.id })
  141. .then(({ data }) => {
  142. workItem.value = data[0];
  143. })
  144. .finally(() => {
  145. loading.value = false;
  146. });
  147. };
  148. const isDowload = ref(true);
  149. const reportDom = ref(null);
  150. async function handleDownload() {
  151. isDowload.value = false;
  152. setTimeout(async () => {
  153. // 获取要截图的DOM元素
  154. const element = reportDom.value;
  155. try {
  156. const canvas = await html2canvas(element, {
  157. scrollY: -window.scrollY, // 处理滚动条位置
  158. allowTaint: true, // 允许跨域图片
  159. useCORS: true, // 使用CORS
  160. scale: 2, // 提高分辨率(2倍)
  161. height: element.scrollHeight, // 设置完整高度
  162. width: element.scrollWidth, // 设置完整宽度
  163. logging: true, // 开启日志(调试用)
  164. });
  165. // 转换为图片并下载
  166. const image = canvas.toDataURL("image/png");
  167. const process = detectRuntimeEnvironment();
  168. if (process === "wechat-webview") {
  169. const imgUrl = await uploadBase64(image, false);
  170. const params = {
  171. miniUserId: 766,
  172. key: "report",
  173. };
  174. // VE_API.garden.editPopSave({ ...params, text: imgUrl }).then((res) => {
  175. // if (res.success) {
  176. // wx.miniProgram.navigateTo({
  177. // url: `/pages/subPages/report_page/index`,
  178. // });
  179. // } else {
  180. // ElMessage.error("保存失败");
  181. // }
  182. // });
  183. } else {
  184. downloadImage(image, "成果报告");
  185. }
  186. isDowload.value = true;
  187. } catch (error) {
  188. isDowload.value = true;
  189. }
  190. });
  191. }
  192. function downloadImage(dataUrl, filename) {
  193. const link = document.createElement("a");
  194. link.href = dataUrl;
  195. link.download = filename;
  196. document.body.appendChild(link);
  197. link.click();
  198. document.body.removeChild(link);
  199. }
  200. const reviewComboRef = ref(null);
  201. const combinedReviewImages = ref([]);
  202. // 生成组合照片,传给相册组件
  203. const generateCombinedReviewImage = async () => {
  204. try {
  205. await prepareCoverImages();
  206. await nextTick();
  207. const canvas = await html2canvas(reviewComboRef.value, {
  208. backgroundColor: null,
  209. useCORS: true,
  210. allowTaint: true,
  211. scale: window.devicePixelRatio || 2,
  212. });
  213. combinedReviewImages.value = [canvas.toDataURL("image/png")];
  214. } catch (e) {
  215. console.error("生成组合照片失败", e);
  216. }
  217. };
  218. const prepareCoverImages = async () => {
  219. await nextTick();
  220. const itemEl = reviewComboRef.value.querySelector(".review-image-item");
  221. const cssWidth = itemEl.offsetWidth;
  222. const cssHeight = 255;
  223. if (workItem.value?.executeEvidence?.length) {
  224. leftCoverImg.value = await coverImageToBase64HD(
  225. base_img_url2 + workItem.value.executeEvidence.at(-1),
  226. cssWidth,
  227. cssHeight
  228. );
  229. }
  230. if (workItem.value?.reviewImage?.length) {
  231. rightCoverImg.value = await coverImageToBase64HD(
  232. base_img_url2 + workItem.value.reviewImage.at(-1),
  233. cssWidth,
  234. cssHeight
  235. );
  236. }
  237. };
  238. const leftCoverImg = ref("");
  239. const rightCoverImg = ref("");
  240. function coverImageToBase64HD(imgUrl, cssWidth, cssHeight) {
  241. return new Promise((resolve, reject) => {
  242. const dpr = window.devicePixelRatio || 2;
  243. const img = new Image();
  244. img.crossOrigin = "anonymous";
  245. img.src = imgUrl;
  246. img.onload = () => {
  247. // ⚠️ 用“物理像素”创建 canvas
  248. const canvas = document.createElement("canvas");
  249. canvas.width = cssWidth * dpr;
  250. canvas.height = cssHeight * dpr;
  251. const ctx = canvas.getContext("2d");
  252. ctx.scale(dpr, dpr);
  253. const imgRatio = img.width / img.height;
  254. const targetRatio = cssWidth / cssHeight;
  255. let sx = 0,
  256. sy = 0,
  257. sw = img.width,
  258. sh = img.height;
  259. if (imgRatio > targetRatio) {
  260. sw = img.height * targetRatio;
  261. sx = (img.width - sw) / 2;
  262. } else {
  263. sh = img.width / targetRatio;
  264. sy = (img.height - sh) / 2;
  265. }
  266. ctx.drawImage(img, sx, sy, sw, sh, 0, 0, cssWidth, cssHeight);
  267. resolve(canvas.toDataURL("image/png"));
  268. };
  269. img.onerror = reject;
  270. });
  271. }
  272. watch(
  273. () => [workItem.value.executeEvidence, workItem.value.reviewImage],
  274. ([preImgs, reviewImgs]) => {
  275. if (preImgs && preImgs.length && reviewImgs && reviewImgs.length) {
  276. generateCombinedReviewImage();
  277. }
  278. },
  279. { deep: true }
  280. );
  281. // 清理数据的函数
  282. const clearData = () => {
  283. workItem.value = {};
  284. reportData.value = {};
  285. paramsPage.value = {};
  286. loading.value = false;
  287. isDowload.value = true;
  288. combinedReviewImages.value = [];
  289. leftCoverImg.value = "";
  290. rightCoverImg.value = "";
  291. };
  292. onDeactivated(() => {
  293. clearData();
  294. });
  295. onUnmounted(() => {
  296. clearData();
  297. });
  298. </script>
  299. <style lang="scss" scoped>
  300. .achievement-report-page {
  301. width: 100%;
  302. height: 100vh;
  303. background: linear-gradient(195.35deg, #d4e4ff 16.34%, rgba(93, 189, 255, 0) 50.3%),
  304. linear-gradient(156.64deg, rgba(255, 255, 255, 0.16) 27.7%, rgba(255, 255, 255, 0) 72.82%);
  305. .report-content-wrap {
  306. height: calc(100% - 40px);
  307. padding-bottom: 60px;
  308. overflow: auto;
  309. box-sizing: border-box;
  310. position: relative;
  311. .bottom-btn {
  312. z-index: 2;
  313. position: fixed;
  314. bottom: 0;
  315. left: 0;
  316. width: 100%;
  317. background: #fff;
  318. height: 60px;
  319. display: flex;
  320. align-items: center;
  321. justify-content: space-between;
  322. padding: 0 12px;
  323. box-sizing: border-box;
  324. box-shadow: 2px 2px 4.5px 0px rgba(0, 0, 0, 0.4);
  325. .btn-item {
  326. height: 40px;
  327. line-height: 40px;
  328. padding: 0 24px;
  329. border-radius: 20px;
  330. font-size: 14px;
  331. &.second {
  332. color: #666666;
  333. border: 1px solid rgba(153, 153, 153, 0.5);
  334. }
  335. &.primay {
  336. padding: 0 34px;
  337. background: linear-gradient(180deg, #76c3ff, #2199f8);
  338. color: #fff;
  339. }
  340. }
  341. }
  342. }
  343. .code-icon {
  344. position: absolute;
  345. right: 10px;
  346. top: 12px;
  347. width: 48px;
  348. }
  349. .report-content {
  350. background: url("@/assets/img/home/report_bg.png") no-repeat center center;
  351. background-size: 100% auto;
  352. background-position: top center;
  353. padding: 24px 16px 16px;
  354. box-sizing: border-box;
  355. position: relative;
  356. .report-header {
  357. position: relative;
  358. .header-book {
  359. position: absolute;
  360. right: 0;
  361. bottom: -6px;
  362. height: 88px;
  363. z-index: 10;
  364. }
  365. .time-tag {
  366. background: linear-gradient(137.86deg, #9fd5ff 5.87%, #2199f8 82.98%);
  367. border-radius: 5px 0 5px 0;
  368. height: 23px;
  369. line-height: 23px;
  370. font-size: 13px;
  371. font-weight: 500;
  372. color: #fff;
  373. padding: 0 9px;
  374. width: fit-content;
  375. }
  376. .report-title {
  377. font-family: "PangMenZhengDao";
  378. font-size: 34px;
  379. line-height: 38px;
  380. color: #000000;
  381. }
  382. .report-info {
  383. padding: 10px 0 16px 0;
  384. .info-item {
  385. width: fit-content;
  386. display: flex;
  387. height: 33px;
  388. align-items: center;
  389. padding: 0 6px 0 3px;
  390. background: linear-gradient(90deg, rgba(255, 255, 255, 0.58) 0%, rgba(255, 255, 255, 0.0696) 100%);
  391. border-radius: 20px;
  392. border: 0.5px solid rgba(33, 153, 248, 0.35);
  393. gap: 3px;
  394. .info-icon {
  395. width: 26px;
  396. height: 26px;
  397. object-fit: cover;
  398. border-radius: 50%;
  399. }
  400. .info-text {
  401. font-size: 14px;
  402. color: #2199f8;
  403. }
  404. }
  405. .info-item + .info-item {
  406. margin-top: 5px;
  407. }
  408. }
  409. }
  410. .report-box {
  411. display: flex;
  412. align-items: center;
  413. padding: 8px;
  414. background: linear-gradient(0deg, #ffffff 86.32%, #2199f8 136.87%);
  415. border: 1px solid #ffffff;
  416. border-radius: 8px;
  417. gap: 5px;
  418. position: relative;
  419. .report-box-item {
  420. flex: 1;
  421. padding: 10px 2px;
  422. background: rgba(33, 153, 248, 0.1);
  423. border-radius: 8px;
  424. .item-content {
  425. color: #2199f8;
  426. font-size: 14px;
  427. text-align: center;
  428. }
  429. .item-title {
  430. color: #000000;
  431. font-size: 10px;
  432. text-align: center;
  433. padding-top: 5px;
  434. }
  435. }
  436. .box-title {
  437. position: absolute;
  438. top: -8px;
  439. left: 0;
  440. height: 32px;
  441. line-height: 26px;
  442. padding: 0 10px;
  443. color: #ffffff;
  444. background: url("@/assets/img/home/title-bg.png") no-repeat center center / 100% 100%;
  445. }
  446. .box-text {
  447. padding: 22px 0 12px 0;
  448. }
  449. }
  450. .report-box + .report-box {
  451. margin-top: 20px;
  452. }
  453. .report-excute {
  454. position: relative;
  455. margin-top: 12px;
  456. .tag-label {
  457. position: absolute;
  458. top: 0;
  459. left: 0;
  460. padding: 4px 10px;
  461. background: rgba(54, 52, 52, 0.8);
  462. color: #fff;
  463. font-size: 12px;
  464. border-radius: 8px 0 8px 0;
  465. z-index: 1;
  466. }
  467. ::v-deep {
  468. .carousel-container .carousel-wrapper .carousel-img {
  469. min-width: calc(100vw - 32px);
  470. width: calc(100vw - 32px);
  471. }
  472. }
  473. }
  474. }
  475. .download-btn {
  476. position: fixed;
  477. bottom: 20px;
  478. left: 50%;
  479. // background: #fff;
  480. // box-shadow: 2px 2px 4.5px 0px #00000066;
  481. // width: 100%;
  482. transform: translateX(-50%);
  483. }
  484. .review-hide-box {
  485. position: absolute;
  486. left: 0;
  487. width: 100%;
  488. height: 100%;
  489. z-index: -1;
  490. bottom: 0;
  491. }
  492. .review-image {
  493. position: relative;
  494. display: flex;
  495. align-items: center;
  496. justify-content: center;
  497. gap: 8px;
  498. margin: 12px;
  499. background: #fff;
  500. border-radius: 8px;
  501. .review-mask {
  502. z-index: 1;
  503. pointer-events: none;
  504. position: absolute;
  505. left: 0;
  506. top: 0;
  507. width: 100%;
  508. height: 100%;
  509. border-radius: 8px;
  510. background: linear-gradient(
  511. 360deg,
  512. rgba(0, 0, 0, 0.78) 0%,
  513. rgba(0, 0, 0, 0.437208) 19.87%,
  514. rgba(0, 0, 0, 0) 33.99%
  515. );
  516. display: flex;
  517. flex-direction: column;
  518. align-items: baseline;
  519. justify-content: end;
  520. padding: 12px;
  521. box-sizing: border-box;
  522. color: #fff;
  523. .review-text {
  524. font-family: "PangMenZhengDao";
  525. font-size: 16px;
  526. margin-bottom: 1px;
  527. }
  528. .review-content {
  529. font-size: 10px;
  530. line-height: 15px;
  531. }
  532. }
  533. .vs-wrap {
  534. position: absolute;
  535. left: 50%;
  536. top: 50%;
  537. transform: translate(-50%, -50%);
  538. width: 40px;
  539. height: 40px;
  540. z-index: 10;
  541. img {
  542. width: 100%;
  543. height: 100%;
  544. object-fit: cover;
  545. }
  546. }
  547. .review-image-item {
  548. position: relative;
  549. flex: 1;
  550. .review-image-item-title {
  551. position: absolute;
  552. top: 0;
  553. left: 0;
  554. background: rgba(54, 52, 52, 0.6);
  555. padding: 4px 10px;
  556. border-radius: 8px 0 8px 0;
  557. backdrop-filter: 4px;
  558. font-size: 12px;
  559. color: #fff;
  560. }
  561. // .review-image-item-img {
  562. // width: 100%;
  563. // height: 250px;
  564. // object-fit: cover;
  565. // }
  566. .review-image-item-img {
  567. width: 100%;
  568. height: 100%;
  569. object-fit: cover;
  570. object-position: center;
  571. }
  572. .left-img {
  573. border-radius: 8px 0 0 8px;
  574. }
  575. .right-img {
  576. border-radius: 0 8px 8px 0;
  577. }
  578. }
  579. }
  580. }
  581. </style>