|
@@ -1,19 +1,18 @@
|
|
|
<template>
|
|
<template>
|
|
|
<photo-consumer
|
|
<photo-consumer
|
|
|
class="carousel-item"
|
|
class="carousel-item"
|
|
|
- :src="watermark || base_img_url2 + (photo.resFilename ? photo.resFilename : photo.filename) + resize"
|
|
|
|
|
|
|
+ :src="watermark || getPhotoSrc(photo)"
|
|
|
>
|
|
>
|
|
|
<img
|
|
<img
|
|
|
v-if="Math.abs(current - index) < 3"
|
|
v-if="Math.abs(current - index) < 3"
|
|
|
crossorigin="anonymous"
|
|
crossorigin="anonymous"
|
|
|
loading="lazy"
|
|
loading="lazy"
|
|
|
- @load="drawWatermark($event)"
|
|
|
|
|
- :src="watermark || (base_img_url2 + (photo.resFilename ? photo.resFilename : photo.filename) + resize)"
|
|
|
|
|
- style="width: 100%; height: 255px; object-fit: cover; display: block;"
|
|
|
|
|
|
|
+ :src="watermark || getPhotoSrc(photo)"
|
|
|
|
|
+ style="width: 100%; height: auto; object-fit: cover; display: block;border-radius: 8px;"
|
|
|
/>
|
|
/>
|
|
|
<canvas
|
|
<canvas
|
|
|
ref="canvasRef"
|
|
ref="canvasRef"
|
|
|
- style="position: absolute; left: 0; top: 0; width: 100%; height: 100%; pointer-events: none;"
|
|
|
|
|
|
|
+ style="position: absolute; left: 0; top: 0; width: 100%; height: 100%; pointer-events: none;border-radius: 8px;"
|
|
|
></canvas>
|
|
></canvas>
|
|
|
<div class="tag-box right" v-if="isShowNum" :class="{'leftTop': 'leftTop'}">{{ index+1 }}/{{ length }}</div>
|
|
<div class="tag-box right" v-if="isShowNum" :class="{'leftTop': 'leftTop'}">{{ index+1 }}/{{ length }}</div>
|
|
|
<!-- <div class="center-mark">mark</div>-->
|
|
<!-- <div class="center-mark">mark</div>-->
|
|
@@ -36,12 +35,7 @@ const watermark = ref(null)
|
|
|
const baseMapBig = ref(false)
|
|
const baseMapBig = ref(false)
|
|
|
|
|
|
|
|
const props = defineProps({
|
|
const props = defineProps({
|
|
|
- farmId:{
|
|
|
|
|
- type: [Number,String],
|
|
|
|
|
- required: true
|
|
|
|
|
- },
|
|
|
|
|
photo:{
|
|
photo:{
|
|
|
- type: Object,
|
|
|
|
|
required: true
|
|
required: true
|
|
|
},
|
|
},
|
|
|
index:{
|
|
index:{
|
|
@@ -63,158 +57,187 @@ const props = defineProps({
|
|
|
})
|
|
})
|
|
|
let img = null;
|
|
let img = null;
|
|
|
let ctx = null;
|
|
let ctx = null;
|
|
|
-let data = {year:props.photo.uploadDate.substring(0,4),
|
|
|
|
|
- monthDay:dateFormat(new Date(props.photo.uploadDate),'mm/dd'),
|
|
|
|
|
- address:props.photo.district.replaceAll("\"","") + props.photo.gardenName,
|
|
|
|
|
- tempImg:imageCache.get("temp"),temp:"10°C-20°C",wendu:"适宜",
|
|
|
|
|
- feiniao:imageCache.get("feiniao"),
|
|
|
|
|
- baseMap:imageCache.get("base_map_"+props.photo.treeId),
|
|
|
|
|
- fusheImg:imageCache.get("fushe"),fushe:"光照优",
|
|
|
|
|
- shiduImg:imageCache.get("shidu"),shidu:"湿度适宜",
|
|
|
|
|
- text:"病害风险,及时喷药",
|
|
|
|
|
- shotCode:props.photo.shotCode,
|
|
|
|
|
- treeCode:props.photo.treeCode,
|
|
|
|
|
- pingzhong:props.photo.pingzhong,
|
|
|
|
|
- uploadDate:props.photo.uploadDate,
|
|
|
|
|
|
|
+
|
|
|
|
|
+function getWatermarkKey(photo) {
|
|
|
|
|
+ return photo?.resFilename || photo?.cloudFilename || photo
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function getPhotoSrc(photo) {
|
|
|
|
|
+ const key = photo?.resFilename || photo?.cloudFilename || photo
|
|
|
|
|
+ if (typeof key === 'string' && (key.startsWith('http') || key.startsWith('data:'))) {
|
|
|
|
|
+ return key + resize
|
|
|
|
|
+ }
|
|
|
|
|
+ return base_img_url2 + key + resize
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+
|
|
|
async function drawWatermark(event) {
|
|
async function drawWatermark(event) {
|
|
|
- img = event.target
|
|
|
|
|
- const weather = {
|
|
|
|
|
- "tempMax": 17,
|
|
|
|
|
- "tempMin": 10,
|
|
|
|
|
- "tempSuitability": "适宜",
|
|
|
|
|
- "humiditySuitability": "适宜",
|
|
|
|
|
- "vindexSuitability": "寡照"
|
|
|
|
|
|
|
+ const displayImg = event.target
|
|
|
|
|
+
|
|
|
|
|
+ const key = getWatermarkKey(props.photo)
|
|
|
|
|
+ if (watermarkCache.has(key)) {
|
|
|
|
|
+ watermark.value = watermarkCache.get(key)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ✅ 用原始图片重新创建一个 Image
|
|
|
|
|
+ const sourceImg = await loadOriginalImage(displayImg.src)
|
|
|
|
|
+
|
|
|
|
|
+ drawWatermark2(sourceImg, displayImg)
|
|
|
|
|
+
|
|
|
|
|
+ watermarkCache.set(key, watermark.value)
|
|
|
}
|
|
}
|
|
|
- drawWatermark2(img,null)
|
|
|
|
|
- // await loadImage(props.photo.baseMap,"base_map_"+props.photo.treeId)
|
|
|
|
|
- // data.baseMap = imageCache.get("base_map_"+props.photo.treeId)
|
|
|
|
|
- // if(!watermark.value){
|
|
|
|
|
- // let param = {farmId:props.farmId, date: props.photo.uploadDate}
|
|
|
|
|
- // let weather = null
|
|
|
|
|
- // VE_API.weather7d.findSuitabilityByPoint(param).then((res)=>{
|
|
|
|
|
- // if(res.code === 0){
|
|
|
|
|
- // weather = res.data
|
|
|
|
|
- // drawWatermark2(img,weather)
|
|
|
|
|
- // }else{
|
|
|
|
|
- // drawWatermark2(img,null)
|
|
|
|
|
- // }
|
|
|
|
|
- // })
|
|
|
|
|
- // }
|
|
|
|
|
|
|
+
|
|
|
|
|
+const watermarkCache = new Map()
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+function loadOriginalImage(src) {
|
|
|
|
|
+ return new Promise((resolve) => {
|
|
|
|
|
+ const img = new Image()
|
|
|
|
|
+ img.crossOrigin = 'anonymous'
|
|
|
|
|
+ img.onload = () => resolve(img)
|
|
|
|
|
+ img.src = src
|
|
|
|
|
+ })
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// function drawWatermark2(img,weather) {
|
|
|
|
|
-// const canvas = canvasRef.value;
|
|
|
|
|
-// let scale = 3
|
|
|
|
|
-// canvas.width = img.width * scale;
|
|
|
|
|
-// canvas.height = img.height * scale;
|
|
|
|
|
-// ctx = canvas.getContext('2d');
|
|
|
|
|
-// ctx.scale(scale, scale)
|
|
|
|
|
-// ctx.drawImage(img, 0, 0, img.width, img.height);
|
|
|
|
|
-// drawBottom(img.width, img.height, weather)
|
|
|
|
|
-// // watermark.value = canvas.toDataURL();
|
|
|
|
|
-// }
|
|
|
|
|
-
|
|
|
|
|
-function drawWatermark2(img, weather) {
|
|
|
|
|
|
|
+function drawWatermark2(sourceImg, displayImg) {
|
|
|
const canvas = canvasRef.value
|
|
const canvas = canvasRef.value
|
|
|
|
|
+ const ctx = canvas.getContext('2d')
|
|
|
|
|
|
|
|
- // 1️⃣ 固定容器尺寸(关键)
|
|
|
|
|
- const canvasWidth = 750
|
|
|
|
|
- const canvasHeight = 1334
|
|
|
|
|
|
|
+ const rect = displayImg.getBoundingClientRect()
|
|
|
|
|
+ const w = rect.width
|
|
|
|
|
+ const h = rect.height
|
|
|
|
|
|
|
|
- canvas.width = canvasWidth
|
|
|
|
|
- canvas.height = canvasHeight
|
|
|
|
|
|
|
+ const dpr = window.devicePixelRatio || 1
|
|
|
|
|
|
|
|
- // 2️⃣ CSS 尺寸必须一致(防止二次拉伸)
|
|
|
|
|
- canvas.style.width = canvasWidth + 'px'
|
|
|
|
|
- canvas.style.height = canvasHeight + 'px'
|
|
|
|
|
|
|
+ canvas.width = w * dpr
|
|
|
|
|
+ canvas.height = h * dpr
|
|
|
|
|
+ canvas.style.width = w + 'px'
|
|
|
|
|
+ canvas.style.height = h + 'px'
|
|
|
|
|
|
|
|
- // 将 ctx 赋值给全局变量,供 drawBottom 使用
|
|
|
|
|
- ctx = canvas.getContext('2d')
|
|
|
|
|
|
|
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
|
|
|
|
|
+ ctx.imageSmoothingEnabled = true
|
|
|
|
|
+ ctx.imageSmoothingQuality = 'high'
|
|
|
|
|
|
|
|
- // 3️⃣ 绝对不能再有 ctx.scale
|
|
|
|
|
- // ctx.scale(...) ❌
|
|
|
|
|
|
|
+ ctx.clearRect(0, 0, w, h)
|
|
|
|
|
|
|
|
- // 4️⃣ 等比按宽度缩放
|
|
|
|
|
- const scale = canvasWidth / img.width
|
|
|
|
|
- const drawHeight = img.height * scale
|
|
|
|
|
|
|
+ // ✅ 用「原始像素图」做 cover
|
|
|
|
|
+ drawImageCoverByNatural(ctx, sourceImg, w, h)
|
|
|
|
|
|
|
|
- // 5️⃣ 裁剪(居中)
|
|
|
|
|
- const dy = (canvasHeight - drawHeight) / 2
|
|
|
|
|
|
|
+ drawBottomMask(ctx, w, h)
|
|
|
|
|
+ drawBottomTextOverlay(ctx, w, h)
|
|
|
|
|
|
|
|
- ctx.clearRect(0, 0, canvasWidth, canvasHeight)
|
|
|
|
|
- ctx.drawImage(img, 0, dy, canvasWidth, drawHeight)
|
|
|
|
|
-
|
|
|
|
|
- // 传入图片实际绘制的尺寸,而不是 canvas 的尺寸
|
|
|
|
|
- // 图片绘制区域:从 (0, dy) 开始,宽度 canvasWidth,高度 drawHeight
|
|
|
|
|
- console.log('canvasWidth',canvasWidth)
|
|
|
|
|
- console.log('drawHeight',drawHeight)
|
|
|
|
|
- console.log('dy',dy)
|
|
|
|
|
- // drawBottom(375, drawHeight, weather, 0, dy)
|
|
|
|
|
|
|
+ watermark.value = canvas.toDataURL('image/jpeg', 0.85)
|
|
|
}
|
|
}
|
|
|
|
|
+function drawImageCoverByNatural(ctx, img, w, h) {
|
|
|
|
|
+ const imgRatio = img.naturalWidth / img.naturalHeight
|
|
|
|
|
+ const canvasRatio = w / h
|
|
|
|
|
+
|
|
|
|
|
+ let sx, sy, sw, sh
|
|
|
|
|
+
|
|
|
|
|
+ if (imgRatio > canvasRatio) {
|
|
|
|
|
+ sh = img.naturalHeight
|
|
|
|
|
+ sw = sh * canvasRatio
|
|
|
|
|
+ sx = (img.naturalWidth - sw) / 2
|
|
|
|
|
+ sy = 0
|
|
|
|
|
+ } else {
|
|
|
|
|
+ sw = img.naturalWidth
|
|
|
|
|
+ sh = sw / canvasRatio
|
|
|
|
|
+ sx = 0
|
|
|
|
|
+ sy = (img.naturalHeight - sh) / 2
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
|
|
+ ctx.drawImage(img, sx, sy, sw, sh, 0, 0, w, h)
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
-function drawImageWidth100Cover(ctx, img, canvasWidth, canvasHeight) {
|
|
|
|
|
- // 以宽度为基准缩放
|
|
|
|
|
- const scale = canvasWidth / img.width
|
|
|
|
|
- const drawHeight = img.height * scale
|
|
|
|
|
|
|
+function drawImageCover(ctx, img, w, h) {
|
|
|
|
|
+ const imgRatio = img.naturalWidth / img.naturalHeight
|
|
|
|
|
+ const canvasRatio = w / h
|
|
|
|
|
+
|
|
|
|
|
+ let sx, sy, sw, sh
|
|
|
|
|
+
|
|
|
|
|
+ if (imgRatio > canvasRatio) {
|
|
|
|
|
+ sh = img.naturalHeight
|
|
|
|
|
+ sw = sh * canvasRatio
|
|
|
|
|
+ sx = (img.naturalWidth - sw) / 2
|
|
|
|
|
+ sy = 0
|
|
|
|
|
+ } else {
|
|
|
|
|
+ sw = img.naturalWidth
|
|
|
|
|
+ sh = sw / canvasRatio
|
|
|
|
|
+ sx = 0
|
|
|
|
|
+ sy = (img.naturalHeight - sh) / 2
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- // 计算裁剪(居中)
|
|
|
|
|
- const dy = (canvasHeight - drawHeight) / 2
|
|
|
|
|
|
|
+ ctx.drawImage(img, sx, sy, sw, sh, 0, 0, w, h)
|
|
|
|
|
+}
|
|
|
|
|
+function drawBottomTextOverlay(ctx, w, h) {
|
|
|
|
|
+ const paddingX = 12
|
|
|
|
|
+ const paddingBottom = 8
|
|
|
|
|
+ const lineHeight = 16
|
|
|
|
|
+
|
|
|
|
|
+ ctx.textBaseline = 'alphabetic'
|
|
|
|
|
+ ctx.fillStyle = '#fff'
|
|
|
|
|
+ ctx.shadowColor = 'rgba(0,0,0,0.6)'
|
|
|
|
|
+ ctx.shadowBlur = 2
|
|
|
|
|
+
|
|
|
|
|
+ // ⬇️ 从底部开始,一行一行往上
|
|
|
|
|
+ let y = h - paddingBottom
|
|
|
|
|
+
|
|
|
|
|
+ // 第三行(最底)
|
|
|
|
|
+ ctx.font = '10px sans-serif'
|
|
|
|
|
+ console.log('paddingX', paddingX, y)
|
|
|
|
|
+ ctx.drawImage(imageCache.get("address"), paddingX, y - 9, 9, 10);
|
|
|
|
|
+ ctx.fillText(
|
|
|
|
|
+ '荔博园(广东省广州市从化区)',
|
|
|
|
|
+ paddingX + 12,
|
|
|
|
|
+ y
|
|
|
|
|
+ )
|
|
|
|
|
|
|
|
- ctx.drawImage(
|
|
|
|
|
- img,
|
|
|
|
|
- 0,
|
|
|
|
|
- dy,
|
|
|
|
|
- canvasWidth,
|
|
|
|
|
- drawHeight
|
|
|
|
|
|
|
+ // 第二行
|
|
|
|
|
+ y -= 15
|
|
|
|
|
+ ctx.font = '16px PangMenZhengDao'
|
|
|
|
|
+ const workNameText = '梢期杀虫'
|
|
|
|
|
+ const prescriptionText = '药物处方:乙烯利'
|
|
|
|
|
+ ctx.fillText(
|
|
|
|
|
+ workNameText,
|
|
|
|
|
+ paddingX,
|
|
|
|
|
+ y
|
|
|
|
|
+ )
|
|
|
|
|
+ ctx.font = '10px sans-serif'
|
|
|
|
|
+ ctx.fillText(
|
|
|
|
|
+ prescriptionText,
|
|
|
|
|
+ paddingX + workNameText.length * 20,
|
|
|
|
|
+ y
|
|
|
)
|
|
)
|
|
|
|
|
+
|
|
|
|
|
+ // 第一行(最上)
|
|
|
|
|
+ y -= 17
|
|
|
|
|
+ ctx.font = '12px PangMenZhengDao'
|
|
|
|
|
+ const timeText = '2025.12.25'
|
|
|
|
|
+ ctx.fillText(timeText, paddingX, y)
|
|
|
|
|
+ const executorText = '执行人:张三李四'
|
|
|
|
|
+ ctx.font = '10px sans-serif'
|
|
|
|
|
+ ctx.fillText(executorText, paddingX + 80, y)
|
|
|
|
|
+
|
|
|
|
|
+ ctx.shadowBlur = 0
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// console.log(data)
|
|
|
|
|
-const drawBottom = (imgWidth, imgHeight, weather, offsetX = 0, offsetY = 0) => {
|
|
|
|
|
- // 设置文本样式
|
|
|
|
|
- ctx.font = "10px Arial";
|
|
|
|
|
- ctx.textAlign = "left"; // 设置为左对齐
|
|
|
|
|
- // imgRect 应该对应图片在 canvas 上的实际绘制区域
|
|
|
|
|
- const imgRect = { x: offsetX, y: offsetY, width: imgWidth, height: imgHeight };
|
|
|
|
|
-
|
|
|
|
|
- // 绘制底部黑色半透明遮罩
|
|
|
|
|
- const bottomRect = drawRectInRect(
|
|
|
|
|
- ctx,
|
|
|
|
|
- imgRect,
|
|
|
|
|
|
|
+
|
|
|
|
|
+function drawBottomMask(ctx, w, h) {
|
|
|
|
|
+ const maskHeight = 60 // 和 3 行文字 + padding 精确匹配
|
|
|
|
|
+
|
|
|
|
|
+ ctx.fillStyle = 'rgba(0,0,0,0.45)'
|
|
|
|
|
+ ctx.fillRect(
|
|
|
0,
|
|
0,
|
|
|
- (7 / 9) * 100,
|
|
|
|
|
- 100,
|
|
|
|
|
- (2 / 9) * 100,
|
|
|
|
|
- "rgba(0, 0, 0, 0.6)"
|
|
|
|
|
- );
|
|
|
|
|
-
|
|
|
|
|
- ctx.fillStyle = "#FFFFFF"; // 文本颜色为白色
|
|
|
|
|
- const startXPercent = 2;
|
|
|
|
|
-
|
|
|
|
|
- // 第一行:日期 + 执行人
|
|
|
|
|
- const line1 = "2025.12.03";
|
|
|
|
|
- drawTextInRect(ctx, bottomRect, line1, startXPercent, 30, 22);
|
|
|
|
|
- drawTextInRect(ctx, bottomRect,'执行人:张三,李四',startXPercent + 22, 30, 22)
|
|
|
|
|
-
|
|
|
|
|
- // 第二行:农事名称 + 药物处方
|
|
|
|
|
- const line2 = "梢期杀虫";
|
|
|
|
|
- // 使用 PangMenZhengDao 字体绘制农事名称
|
|
|
|
|
- ctx.font = "12px PangMenZhengDao";
|
|
|
|
|
- drawTextInRect(ctx, bottomRect, line2, startXPercent, 60, 22);
|
|
|
|
|
- // 还原为默认字体,用于后续文字
|
|
|
|
|
- ctx.font = "10px Arial";
|
|
|
|
|
- drawTextInRect(ctx, bottomRect, '药物处方:乙烯利乙烯利', startXPercent+22, 60, 22);
|
|
|
|
|
-
|
|
|
|
|
- // 第三行:地址
|
|
|
|
|
- const line3 = "荔博园(广东省广州市从化区)";
|
|
|
|
|
- drawImageInRect(ctx, bottomRect, imageCache.get("address"), startXPercent, 68, 3, 22)
|
|
|
|
|
- drawTextInRect(ctx, bottomRect, line3, startXPercent+4, 90, 22);
|
|
|
|
|
|
|
+ h - maskHeight, // ✅ 绝对贴底
|
|
|
|
|
+ w,
|
|
|
|
|
+ maskHeight
|
|
|
|
|
+ )
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
const showTagBox = ref(true); // 控制 tag-box 的显示状态
|
|
const showTagBox = ref(true); // 控制 tag-box 的显示状态
|
|
|
const hideTagBox = (event) => {
|
|
const hideTagBox = (event) => {
|
|
|
event.stopPropagation();
|
|
event.stopPropagation();
|