albumDrawBox.vue 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377
  1. <template>
  2. <photo-consumer
  3. class="carousel-item"
  4. :src="watermark || getPhotoSrc(photo)"
  5. >
  6. <img
  7. v-if="Math.abs(current - index) < 3"
  8. crossorigin="anonymous"
  9. loading="lazy"
  10. @load="drawWatermark($event)"
  11. :src="watermark || getPhotoSrc(photo)"
  12. style="width: 100%; height: 255px; object-fit: cover; display: block; border-radius: 8px;"
  13. />
  14. <canvas
  15. ref="canvasRef"
  16. style="position: absolute; left: 0; top: 0; width: 100%; height: 100%; pointer-events: none;border-radius: 8px;"
  17. ></canvas>
  18. <div class="tag-box right" v-if="isShowNum" :class="{'leftTop': 'leftTop'}">{{ index+1 }}/{{ length }}</div>
  19. <!-- <div class="center-mark">mark</div>-->
  20. </photo-consumer>
  21. </template>
  22. <script setup>
  23. import { ref, onMounted, onBeforeUnmount, defineProps } from "vue";
  24. import { base_img_url2 } from "@/api/config";
  25. import {imageCache,loadImage} from "./cacheImg.js"
  26. import {dateFormat} from "@/utils/date_util.js"
  27. import {drawTextInRect, drawBorderImageInRect, drawImageInRect, drawRectInRect, drawHorizontalTextList} from "./utils"
  28. // const resize = "?x-oss-process=image/resize,p_30/format,webp/quality,q_40";
  29. const resize = "";
  30. const canvasRef = ref(null);
  31. const watermark = ref(null)
  32. const baseMapBig = ref(false)
  33. const props = defineProps({
  34. photo:{
  35. required: true
  36. },
  37. index:{
  38. type: Number,
  39. required: true
  40. },
  41. length:{
  42. type: Number,
  43. required: true
  44. },
  45. current:{
  46. type: Number,
  47. required: true
  48. },
  49. isShowNum:{
  50. type: Number,
  51. required: true
  52. }
  53. })
  54. let img = null;
  55. let ctx = null;
  56. function getWatermarkKey(photo) {
  57. return photo?.resFilename || photo?.cloudFilename || photo
  58. }
  59. function getPhotoSrc(photo) {
  60. const key = photo?.resFilename || photo?.cloudFilename || photo
  61. if (typeof key === 'string' && (key.startsWith('http') || key.startsWith('data:'))) {
  62. return key + resize
  63. }
  64. return base_img_url2 + key + resize
  65. }
  66. async function drawWatermark(event) {
  67. const displayImg = event.target
  68. const key = getWatermarkKey(props.photo)
  69. if (watermarkCache.has(key)) {
  70. watermark.value = watermarkCache.get(key)
  71. return
  72. }
  73. // ✅ 用原始图片重新创建一个 Image
  74. const sourceImg = await loadOriginalImage(displayImg.src)
  75. drawWatermark2(sourceImg, displayImg)
  76. watermarkCache.set(key, watermark.value)
  77. }
  78. const watermarkCache = new Map()
  79. function loadOriginalImage(src) {
  80. return new Promise((resolve) => {
  81. const img = new Image()
  82. img.crossOrigin = 'anonymous'
  83. img.onload = () => resolve(img)
  84. img.src = src
  85. })
  86. }
  87. function drawWatermark2(sourceImg, displayImg) {
  88. const canvas = canvasRef.value
  89. const ctx = canvas.getContext('2d')
  90. const rect = displayImg.getBoundingClientRect()
  91. const w = rect.width
  92. const h = rect.height
  93. const dpr = window.devicePixelRatio || 1
  94. canvas.width = w * dpr
  95. canvas.height = h * dpr
  96. canvas.style.width = w + 'px'
  97. canvas.style.height = h + 'px'
  98. ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
  99. ctx.imageSmoothingEnabled = true
  100. ctx.imageSmoothingQuality = 'high'
  101. ctx.clearRect(0, 0, w, h)
  102. // ✅ 用「原始像素图」做 cover
  103. drawImageCoverByNatural(ctx, sourceImg, w, h)
  104. drawBottomMask(ctx, w, h)
  105. drawBottomTextOverlay(ctx, w, h)
  106. watermark.value = canvas.toDataURL('image/jpeg', 0.85)
  107. }
  108. function drawImageCoverByNatural(ctx, img, w, h) {
  109. const imgRatio = img.naturalWidth / img.naturalHeight
  110. const canvasRatio = w / h
  111. let sx, sy, sw, sh
  112. if (imgRatio > canvasRatio) {
  113. sh = img.naturalHeight
  114. sw = sh * canvasRatio
  115. sx = (img.naturalWidth - sw) / 2
  116. sy = 0
  117. } else {
  118. sw = img.naturalWidth
  119. sh = sw / canvasRatio
  120. sx = 0
  121. sy = (img.naturalHeight - sh) / 2
  122. }
  123. ctx.drawImage(img, sx, sy, sw, sh, 0, 0, w, h)
  124. }
  125. function drawImageCover(ctx, img, w, h) {
  126. const imgRatio = img.naturalWidth / img.naturalHeight
  127. const canvasRatio = w / h
  128. let sx, sy, sw, sh
  129. if (imgRatio > canvasRatio) {
  130. sh = img.naturalHeight
  131. sw = sh * canvasRatio
  132. sx = (img.naturalWidth - sw) / 2
  133. sy = 0
  134. } else {
  135. sw = img.naturalWidth
  136. sh = sw / canvasRatio
  137. sx = 0
  138. sy = (img.naturalHeight - sh) / 2
  139. }
  140. ctx.drawImage(img, sx, sy, sw, sh, 0, 0, w, h)
  141. }
  142. function drawBottomTextOverlay(ctx, w, h) {
  143. const paddingX = 12
  144. const paddingBottom = 8
  145. const lineHeight = 16
  146. ctx.textBaseline = 'alphabetic'
  147. ctx.fillStyle = '#fff'
  148. ctx.shadowColor = 'rgba(0,0,0,0.6)'
  149. ctx.shadowBlur = 2
  150. // ⬇️ 从底部开始,一行一行往上
  151. let y = h - paddingBottom
  152. // 第三行(最底)
  153. ctx.font = '10px sans-serif'
  154. console.log('paddingX', paddingX, y)
  155. ctx.drawImage(imageCache.get("address"), paddingX, y - 9, 9, 10);
  156. ctx.fillText(
  157. '荔博园(广东省广州市从化区)',
  158. paddingX + 12,
  159. y
  160. )
  161. // 第二行
  162. y -= 15
  163. ctx.font = '16px PangMenZhengDao'
  164. const workNameText = '梢期杀虫'
  165. const prescriptionText = '药物处方:乙烯利'
  166. ctx.fillText(
  167. workNameText,
  168. paddingX,
  169. y
  170. )
  171. ctx.font = '10px sans-serif'
  172. ctx.fillText(
  173. prescriptionText,
  174. paddingX + workNameText.length * 20,
  175. y
  176. )
  177. // 第一行(最上)
  178. y -= 17
  179. ctx.font = '12px PangMenZhengDao'
  180. const timeText = '2025.12.25'
  181. ctx.fillText(timeText, paddingX, y)
  182. const executorText = '执行人:张三李四'
  183. ctx.font = '10px sans-serif'
  184. ctx.fillText(executorText, paddingX + 80, y)
  185. ctx.shadowBlur = 0
  186. }
  187. function drawBottomMask(ctx, w, h) {
  188. const maskHeight = 60 // 和 3 行文字 + padding 精确匹配
  189. ctx.fillStyle = 'rgba(0,0,0,0.45)'
  190. ctx.fillRect(
  191. 0,
  192. h - maskHeight, // ✅ 绝对贴底
  193. w,
  194. maskHeight
  195. )
  196. }
  197. const showTagBox = ref(true); // 控制 tag-box 的显示状态
  198. const hideTagBox = (event) => {
  199. event.stopPropagation();
  200. showTagBox.value = false; // 隐藏 tag-box
  201. };
  202. const formatDate = (date) => {
  203. const year = date.getFullYear();
  204. const month = String(date.getMonth() + 1).padStart(2, '0'); // 月份从0开始,需要加1
  205. const day = String(date.getDate()).padStart(2, '0');
  206. return `${(year+"").substring(2)}${month}${day}`;
  207. };
  208. </script>
  209. <style lang="scss" scoped>
  210. .canvas-container {
  211. width: 100%;
  212. height: 100%;
  213. display: flex;
  214. justify-content: center;
  215. }
  216. .carousel-item {
  217. min-width: 100%;
  218. max-height: 100%;
  219. flex-shrink: 0;
  220. width: 100%;
  221. pointer-events: auto;
  222. position: relative;
  223. .tag-box {
  224. position: absolute;
  225. bottom: 30%;
  226. left: 50%;
  227. transform: translate(-50%, 50%); // 确保在高二分之一的位置水平居中
  228. height: 18px;
  229. padding: 0 6px;
  230. background: rgba(108, 108, 108, 0.67);
  231. border-radius: 10px;
  232. display: flex;
  233. align-items: center;
  234. color: #FFFFFF;
  235. font-size: 12px;
  236. &.right {
  237. left: auto;
  238. right: 10px;
  239. }
  240. &.leftTop {
  241. height: 25px;
  242. line-height: 26px;
  243. padding: 0 8px;
  244. border-radius: 16px;
  245. background: rgba(0, 0, 0, 0.6);
  246. bottom: auto;
  247. top: 6px;
  248. }
  249. }
  250. .tag-text {
  251. position: absolute;
  252. bottom: 31%;
  253. left: 50%;
  254. width: 80%;
  255. transform: translate(-50%, 50%); // 确保在高二分之一的位置水平居中
  256. height: 24px;
  257. padding: 10px 0px 10px 0px;
  258. background: rgba(0, 0, 0, 0.67);
  259. border-radius: 6px;
  260. display: flex;
  261. align-items: center;
  262. justify-content: center;
  263. text-align: center;
  264. color: #FFFFFF;
  265. font-size: 12px;
  266. }
  267. .center-mark {
  268. position: absolute;
  269. bottom: 10px;
  270. left: 50%;
  271. transform: translateX(-50%);
  272. color: #36402c;
  273. font-size: rpx(24);
  274. font-weight: bold;
  275. padding: rpx(14) rpx(30);
  276. background: linear-gradient(
  277. 90deg,
  278. rgba(255, 255, 255, 0) 0%,
  279. rgba(255, 255, 255, 0.6) 24%,
  280. rgba(255, 255, 255, 0.6) 76%,
  281. rgba(255, 255, 255, 0) 100%
  282. );
  283. }
  284. }
  285. .carousel-item img {
  286. width: 100%;
  287. display: block;
  288. }
  289. canvas {
  290. position: absolute;
  291. }
  292. .close-button {
  293. background: transparent;
  294. border: none;
  295. color: #FFFFFF;
  296. cursor: pointer;
  297. font-size: 10px; // 可以根据需求调整大小
  298. position: absolute;
  299. top: -1px;
  300. right: -9px; // 调整为合适的间距
  301. transform: translateY(-50%);
  302. }
  303. .floating-img {
  304. position: absolute;
  305. bottom: 0;
  306. right: 0;
  307. width: auto !important;
  308. height: 25% !important;
  309. }
  310. .floating-img-big {
  311. position: fixed !important;
  312. z-index: 99999 !important;
  313. top: 50% !important;
  314. left: 50% !important;
  315. width: auto !important;
  316. height: 100% !important;
  317. transform: translate(-50%, -50%) !important;
  318. }
  319. </style>