|
@@ -1,377 +1,586 @@
|
|
|
<template>
|
|
<template>
|
|
|
- <photo-consumer
|
|
|
|
|
- class="carousel-item"
|
|
|
|
|
- :src="watermark || getPhotoSrc(photo)"
|
|
|
|
|
- >
|
|
|
|
|
- <img
|
|
|
|
|
- v-if="Math.abs(current - index) < 3"
|
|
|
|
|
- crossorigin="anonymous"
|
|
|
|
|
- loading="lazy"
|
|
|
|
|
- @load="drawWatermark($event)"
|
|
|
|
|
- :src="watermark || getPhotoSrc(photo)"
|
|
|
|
|
- style="width: 100%; height: 255px; object-fit: cover; display: block; border-radius: 8px;"
|
|
|
|
|
- />
|
|
|
|
|
- <canvas
|
|
|
|
|
- ref="canvasRef"
|
|
|
|
|
- style="position: absolute; left: 0; top: 0; width: 100%; height: 100%; pointer-events: none;border-radius: 8px;"
|
|
|
|
|
- ></canvas>
|
|
|
|
|
- <div class="tag-box right" v-if="isShowNum" :class="{'leftTop': 'leftTop'}">{{ index+1 }}/{{ length }}</div>
|
|
|
|
|
-<!-- <div class="center-mark">mark</div>-->
|
|
|
|
|
- </photo-consumer>
|
|
|
|
|
-
|
|
|
|
|
|
|
+ <photo-consumer class="carousel-item" :src="watermark || getPhotoSrc(photo)">
|
|
|
|
|
+ <img
|
|
|
|
|
+ v-if="Math.abs(current - index) < 3"
|
|
|
|
|
+ crossorigin="anonymous"
|
|
|
|
|
+ loading="lazy"
|
|
|
|
|
+ @load="drawWatermark($event)"
|
|
|
|
|
+ :src="watermark || getPhotoSrc(photo)"
|
|
|
|
|
+ style="width: 100%; height: 255px; object-fit: cover; display: block; border-radius: 8px"
|
|
|
|
|
+ />
|
|
|
|
|
+ <canvas
|
|
|
|
|
+ ref="canvasRef"
|
|
|
|
|
+ @click="handleClick"
|
|
|
|
|
+ style="position: absolute; left: 0; top: 0; width: 100%; height: 100%; border-radius: 8px"
|
|
|
|
|
+ ></canvas>
|
|
|
|
|
+ <div class="tag-box right" v-if="isShowNum" :class="{ leftTop: 'leftTop' }">{{ index + 1 }}/{{ length }}</div>
|
|
|
|
|
+ <!-- <div class="center-mark">mark</div>-->
|
|
|
|
|
+ </photo-consumer>
|
|
|
|
|
+
|
|
|
|
|
+ <popup class="cavans-popup" v-model:show="showPopup">
|
|
|
|
|
+ <div class="cavans-content">
|
|
|
|
|
+ <img class="current-img" :src="watermarkWithQRCode || watermark" alt="" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <!-- 底部操作按钮 -->
|
|
|
|
|
+ <div class="bottom-actions" @click.stop="showPopup = false">
|
|
|
|
|
+ <div class="action-buttons">
|
|
|
|
|
+ <div class="action-btn green-btn" @click.stop="handleWechat">
|
|
|
|
|
+ <div class="icon-circle">
|
|
|
|
|
+ <img src="@/assets/img/home/wechat.png" alt="" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <span class="btn-label">微信</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="action-btn orange-btn" @click.stop="handleSaveImage">
|
|
|
|
|
+ <div class="icon-circle">
|
|
|
|
|
+ <el-icon :size="24"><Download /></el-icon>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <span class="btn-label">保存图片</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="cancel-btn" @click="handleCancel">取消</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </popup>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<script setup>
|
|
<script setup>
|
|
|
|
|
+import { Popup } from "vant";
|
|
|
import { ref, onMounted, onBeforeUnmount, defineProps } from "vue";
|
|
import { ref, onMounted, onBeforeUnmount, defineProps } from "vue";
|
|
|
import { base_img_url2 } from "@/api/config";
|
|
import { base_img_url2 } from "@/api/config";
|
|
|
-import {imageCache,loadImage} from "./cacheImg.js"
|
|
|
|
|
-import {dateFormat} from "@/utils/date_util.js"
|
|
|
|
|
-
|
|
|
|
|
-import {drawTextInRect, drawBorderImageInRect, drawImageInRect, drawRectInRect, drawHorizontalTextList} from "./utils"
|
|
|
|
|
|
|
+import { imageCache, loadImage } from "./cacheImg.js";
|
|
|
|
|
+import { dateFormat } from "@/utils/date_util.js";
|
|
|
|
|
+
|
|
|
|
|
+import {
|
|
|
|
|
+ drawTextInRect,
|
|
|
|
|
+ drawBorderImageInRect,
|
|
|
|
|
+ drawImageInRect,
|
|
|
|
|
+ drawRectInRect,
|
|
|
|
|
+ drawHorizontalTextList,
|
|
|
|
|
+} from "./utils";
|
|
|
// const resize = "?x-oss-process=image/resize,p_30/format,webp/quality,q_40";
|
|
// const resize = "?x-oss-process=image/resize,p_30/format,webp/quality,q_40";
|
|
|
const resize = "";
|
|
const resize = "";
|
|
|
|
|
|
|
|
const canvasRef = ref(null);
|
|
const canvasRef = ref(null);
|
|
|
-const watermark = ref(null)
|
|
|
|
|
-const baseMapBig = ref(false)
|
|
|
|
|
|
|
+const watermark = ref(null);
|
|
|
|
|
+const baseMapBig = ref(false);
|
|
|
|
|
|
|
|
const props = defineProps({
|
|
const props = defineProps({
|
|
|
- photo:{
|
|
|
|
|
- required: true
|
|
|
|
|
- },
|
|
|
|
|
- index:{
|
|
|
|
|
- type: Number,
|
|
|
|
|
- required: true
|
|
|
|
|
- },
|
|
|
|
|
- length:{
|
|
|
|
|
- type: Number,
|
|
|
|
|
- required: true
|
|
|
|
|
- },
|
|
|
|
|
- current:{
|
|
|
|
|
- type: Number,
|
|
|
|
|
- required: true
|
|
|
|
|
- },
|
|
|
|
|
- isShowNum:{
|
|
|
|
|
- type: Number,
|
|
|
|
|
- required: true
|
|
|
|
|
- }
|
|
|
|
|
-})
|
|
|
|
|
|
|
+ photo: {
|
|
|
|
|
+ required: true,
|
|
|
|
|
+ },
|
|
|
|
|
+ index: {
|
|
|
|
|
+ type: Number,
|
|
|
|
|
+ required: true,
|
|
|
|
|
+ },
|
|
|
|
|
+ length: {
|
|
|
|
|
+ type: Number,
|
|
|
|
|
+ required: true,
|
|
|
|
|
+ },
|
|
|
|
|
+ current: {
|
|
|
|
|
+ type: Number,
|
|
|
|
|
+ required: true,
|
|
|
|
|
+ },
|
|
|
|
|
+ isShowNum: {
|
|
|
|
|
+ type: Number,
|
|
|
|
|
+ required: true,
|
|
|
|
|
+ },
|
|
|
|
|
+});
|
|
|
let img = null;
|
|
let img = null;
|
|
|
let ctx = null;
|
|
let ctx = null;
|
|
|
|
|
+// 保存原始图片引用,用于重新绘制
|
|
|
|
|
+let cachedSourceImg = null;
|
|
|
|
|
+let cachedDisplayImg = null;
|
|
|
|
|
|
|
|
function getWatermarkKey(photo) {
|
|
function getWatermarkKey(photo) {
|
|
|
- return photo?.resFilename || photo?.cloudFilename || photo
|
|
|
|
|
|
|
+ return photo?.resFilename || photo?.cloudFilename || photo;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function getPhotoSrc(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
|
|
|
|
|
|
|
+ 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;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+const showPopup = ref(false);
|
|
|
|
|
+const watermarkWithQRCode = ref(null); // 带二维码的图片,用于弹窗显示
|
|
|
|
|
|
|
|
-async function drawWatermark(event) {
|
|
|
|
|
- const displayImg = event.target
|
|
|
|
|
|
|
+async function handleClick() {
|
|
|
|
|
+ // 点击时生成带二维码的图片用于弹窗显示
|
|
|
|
|
+ await generateImageWithQRCode();
|
|
|
|
|
+ showPopup.value = true;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 加载二维码图片
|
|
|
|
|
+async function loadQRCodeImage() {
|
|
|
|
|
+ // 先检查缓存
|
|
|
|
|
+ if (imageCache.has("qrcode")) {
|
|
|
|
|
+ return imageCache.get("qrcode");
|
|
|
|
|
+ }
|
|
|
|
|
+ // 使用 loadImage 加载
|
|
|
|
|
+ try {
|
|
|
|
|
+ await loadImage(require("@/assets/img/home/qrcode.png"), "qrcode");
|
|
|
|
|
+ return imageCache.get("qrcode");
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error("加载二维码失败:", error);
|
|
|
|
|
+ // 如果 require 失败,尝试直接创建图片
|
|
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
|
|
+ const img = new Image();
|
|
|
|
|
+ img.crossOrigin = "anonymous";
|
|
|
|
|
+ img.onload = () => {
|
|
|
|
|
+ imageCache.set("qrcode", img);
|
|
|
|
|
+ resolve(img);
|
|
|
|
|
+ };
|
|
|
|
|
+ img.onerror = reject;
|
|
|
|
|
+ img.src = require("@/assets/img/home/qrcode.png");
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 生成带二维码的图片(用于弹窗显示,不影响原图)
|
|
|
|
|
+async function generateImageWithQRCode() {
|
|
|
|
|
+ if (!cachedSourceImg || !cachedDisplayImg) return;
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 加载二维码图片
|
|
|
|
|
+ const qrCodeImg = await loadQRCodeImage();
|
|
|
|
|
+
|
|
|
|
|
+ // 获取显示图片的尺寸
|
|
|
|
|
+ const rect = cachedDisplayImg.getBoundingClientRect();
|
|
|
|
|
+ const w = rect.width;
|
|
|
|
|
+ const h = rect.height;
|
|
|
|
|
+
|
|
|
|
|
+ // 创建一个临时的 canvas 用于生成带二维码的图片
|
|
|
|
|
+ const tempCanvas = document.createElement("canvas");
|
|
|
|
|
+ const tempCtx = tempCanvas.getContext("2d");
|
|
|
|
|
+
|
|
|
|
|
+ const dpr = window.devicePixelRatio || 1;
|
|
|
|
|
+
|
|
|
|
|
+ // 设置 canvas 尺寸
|
|
|
|
|
+ tempCanvas.width = w * dpr;
|
|
|
|
|
+ tempCanvas.height = h * dpr;
|
|
|
|
|
+
|
|
|
|
|
+ tempCtx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
|
|
|
+ tempCtx.imageSmoothingEnabled = true;
|
|
|
|
|
+ tempCtx.imageSmoothingQuality = "high";
|
|
|
|
|
+
|
|
|
|
|
+ // 清空 canvas
|
|
|
|
|
+ tempCtx.clearRect(0, 0, w, h);
|
|
|
|
|
+
|
|
|
|
|
+ // 重新绘制图片
|
|
|
|
|
+ drawImageCoverByNatural(tempCtx, cachedSourceImg, w, h);
|
|
|
|
|
+
|
|
|
|
|
+ // 绘制底部遮罩和文字
|
|
|
|
|
+ drawBottomMask(tempCtx, w, h);
|
|
|
|
|
+ drawBottomTextOverlay(tempCtx, w, h);
|
|
|
|
|
+
|
|
|
|
|
+ // 二维码尺寸和位置(参考 albumCarouselItem 的样式)
|
|
|
|
|
+ const qrSize = 40;
|
|
|
|
|
+ const qrX = w - qrSize - 12; // 距离右边 12px
|
|
|
|
|
+ const qrY = 12; // 距离顶部 12px
|
|
|
|
|
+
|
|
|
|
|
+ // 绘制二维码到右上角
|
|
|
|
|
+ tempCtx.drawImage(qrCodeImg, qrX, qrY, qrSize, qrSize);
|
|
|
|
|
+
|
|
|
|
|
+ // 保存为带二维码的图片(不影响原来的 watermark)
|
|
|
|
|
+ watermarkWithQRCode.value = tempCanvas.toDataURL("image/jpeg", 0.85);
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error("生成带二维码的图片失败:", error);
|
|
|
|
|
+ // 如果失败,使用原来的 watermark
|
|
|
|
|
+ watermarkWithQRCode.value = watermark.value;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
- const key = getWatermarkKey(props.photo)
|
|
|
|
|
- if (watermarkCache.has(key)) {
|
|
|
|
|
- watermark.value = watermarkCache.get(key)
|
|
|
|
|
- return
|
|
|
|
|
- }
|
|
|
|
|
|
|
+async function drawWatermark(event) {
|
|
|
|
|
+ const displayImg = event.target;
|
|
|
|
|
+
|
|
|
|
|
+ const key = getWatermarkKey(props.photo);
|
|
|
|
|
+ if (watermarkCache.has(key)) {
|
|
|
|
|
+ watermark.value = watermarkCache.get(key);
|
|
|
|
|
+ // 从缓存中恢复图片引用
|
|
|
|
|
+ const cachedData = watermarkCache.get(key + "_refs");
|
|
|
|
|
+ if (cachedData) {
|
|
|
|
|
+ cachedSourceImg = cachedData.sourceImg;
|
|
|
|
|
+ cachedDisplayImg = cachedData.displayImg;
|
|
|
|
|
+ }
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- // ✅ 用原始图片重新创建一个 Image
|
|
|
|
|
- const sourceImg = await loadOriginalImage(displayImg.src)
|
|
|
|
|
|
|
+ // ✅ 用原始图片重新创建一个 Image
|
|
|
|
|
+ const sourceImg = await loadOriginalImage(displayImg.src);
|
|
|
|
|
+
|
|
|
|
|
+ // 保存引用
|
|
|
|
|
+ cachedSourceImg = sourceImg;
|
|
|
|
|
+ cachedDisplayImg = displayImg;
|
|
|
|
|
|
|
|
- drawWatermark2(sourceImg, displayImg)
|
|
|
|
|
|
|
+ drawWatermark2(sourceImg, displayImg);
|
|
|
|
|
|
|
|
- watermarkCache.set(key, watermark.value)
|
|
|
|
|
|
|
+ watermarkCache.set(key, watermark.value);
|
|
|
|
|
+ // 同时保存图片引用
|
|
|
|
|
+ watermarkCache.set(key + "_refs", { sourceImg, displayImg });
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-const watermarkCache = new Map()
|
|
|
|
|
-
|
|
|
|
|
|
|
+const watermarkCache = new Map();
|
|
|
|
|
|
|
|
function loadOriginalImage(src) {
|
|
function loadOriginalImage(src) {
|
|
|
- return new Promise((resolve) => {
|
|
|
|
|
- const img = new Image()
|
|
|
|
|
- img.crossOrigin = 'anonymous'
|
|
|
|
|
- img.onload = () => resolve(img)
|
|
|
|
|
- img.src = src
|
|
|
|
|
- })
|
|
|
|
|
|
|
+ return new Promise((resolve) => {
|
|
|
|
|
+ const img = new Image();
|
|
|
|
|
+ img.crossOrigin = "anonymous";
|
|
|
|
|
+ img.onload = () => resolve(img);
|
|
|
|
|
+ img.src = src;
|
|
|
|
|
+ });
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function drawWatermark2(sourceImg, displayImg) {
|
|
function drawWatermark2(sourceImg, displayImg) {
|
|
|
- const canvas = canvasRef.value
|
|
|
|
|
- const ctx = canvas.getContext('2d')
|
|
|
|
|
|
|
+ const canvas = canvasRef.value;
|
|
|
|
|
+ const ctx = canvas.getContext("2d");
|
|
|
|
|
|
|
|
- const rect = displayImg.getBoundingClientRect()
|
|
|
|
|
- const w = rect.width
|
|
|
|
|
- const h = rect.height
|
|
|
|
|
|
|
+ const rect = displayImg.getBoundingClientRect();
|
|
|
|
|
+ const w = rect.width;
|
|
|
|
|
+ const h = rect.height;
|
|
|
|
|
|
|
|
- const dpr = window.devicePixelRatio || 1
|
|
|
|
|
|
|
+ const dpr = window.devicePixelRatio || 1;
|
|
|
|
|
|
|
|
- canvas.width = w * dpr
|
|
|
|
|
- canvas.height = h * dpr
|
|
|
|
|
- canvas.style.width = w + 'px'
|
|
|
|
|
- canvas.style.height = h + 'px'
|
|
|
|
|
|
|
+ canvas.width = w * dpr;
|
|
|
|
|
+ canvas.height = h * dpr;
|
|
|
|
|
+ canvas.style.width = w + "px";
|
|
|
|
|
+ canvas.style.height = h + "px";
|
|
|
|
|
|
|
|
- ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
|
|
|
|
|
- ctx.imageSmoothingEnabled = true
|
|
|
|
|
- ctx.imageSmoothingQuality = 'high'
|
|
|
|
|
|
|
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
|
|
|
+ ctx.imageSmoothingEnabled = true;
|
|
|
|
|
+ ctx.imageSmoothingQuality = "high";
|
|
|
|
|
|
|
|
- ctx.clearRect(0, 0, w, h)
|
|
|
|
|
|
|
+ ctx.clearRect(0, 0, w, h);
|
|
|
|
|
|
|
|
- // ✅ 用「原始像素图」做 cover
|
|
|
|
|
- drawImageCoverByNatural(ctx, sourceImg, w, h)
|
|
|
|
|
|
|
+ // ✅ 用「原始像素图」做 cover
|
|
|
|
|
+ drawImageCoverByNatural(ctx, sourceImg, w, h);
|
|
|
|
|
|
|
|
- drawBottomMask(ctx, w, h)
|
|
|
|
|
- drawBottomTextOverlay(ctx, w, h)
|
|
|
|
|
|
|
+ drawBottomMask(ctx, w, h);
|
|
|
|
|
+ drawBottomTextOverlay(ctx, w, h);
|
|
|
|
|
|
|
|
- watermark.value = canvas.toDataURL('image/jpeg', 0.85)
|
|
|
|
|
|
|
+ watermark.value = canvas.toDataURL("image/jpeg", 0.85);
|
|
|
}
|
|
}
|
|
|
function drawImageCoverByNatural(ctx, img, w, h) {
|
|
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)
|
|
|
|
|
|
|
+ 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 drawImageCover(ctx, img, w, h) {
|
|
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
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- ctx.drawImage(img, sx, sy, sw, sh, 0, 0, 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 drawBottomTextOverlay(ctx, 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
|
|
|
|
|
- )
|
|
|
|
|
-
|
|
|
|
|
- // 第二行
|
|
|
|
|
- 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
|
|
|
|
|
|
|
+ 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);
|
|
|
|
|
+
|
|
|
|
|
+ // 第二行
|
|
|
|
|
+ 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;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-
|
|
|
|
|
function drawBottomMask(ctx, w, h) {
|
|
function drawBottomMask(ctx, w, h) {
|
|
|
- const maskHeight = 60 // 和 3 行文字 + padding 精确匹配
|
|
|
|
|
-
|
|
|
|
|
- ctx.fillStyle = 'rgba(0,0,0,0.45)'
|
|
|
|
|
- ctx.fillRect(
|
|
|
|
|
- 0,
|
|
|
|
|
- h - maskHeight, // ✅ 绝对贴底
|
|
|
|
|
- w,
|
|
|
|
|
- maskHeight
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ const maskHeight = 60; // 和 3 行文字 + padding 精确匹配
|
|
|
|
|
+
|
|
|
|
|
+ ctx.fillStyle = "rgba(0,0,0,0.45)";
|
|
|
|
|
+ ctx.fillRect(
|
|
|
|
|
+ 0,
|
|
|
|
|
+ h - maskHeight, // ✅ 绝对贴底
|
|
|
|
|
+ w,
|
|
|
|
|
+ maskHeight
|
|
|
|
|
+ );
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-
|
|
|
|
|
-
|
|
|
|
|
-
|
|
|
|
|
const showTagBox = ref(true); // 控制 tag-box 的显示状态
|
|
const showTagBox = ref(true); // 控制 tag-box 的显示状态
|
|
|
const hideTagBox = (event) => {
|
|
const hideTagBox = (event) => {
|
|
|
- event.stopPropagation();
|
|
|
|
|
- showTagBox.value = false; // 隐藏 tag-box
|
|
|
|
|
|
|
+ event.stopPropagation();
|
|
|
|
|
+ showTagBox.value = false; // 隐藏 tag-box
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
const formatDate = (date) => {
|
|
const formatDate = (date) => {
|
|
|
- const year = date.getFullYear();
|
|
|
|
|
- const month = String(date.getMonth() + 1).padStart(2, '0'); // 月份从0开始,需要加1
|
|
|
|
|
- const day = String(date.getDate()).padStart(2, '0');
|
|
|
|
|
- return `${(year+"").substring(2)}${month}${day}`;
|
|
|
|
|
|
|
+ const year = date.getFullYear();
|
|
|
|
|
+ const month = String(date.getMonth() + 1).padStart(2, "0"); // 月份从0开始,需要加1
|
|
|
|
|
+ const day = String(date.getDate()).padStart(2, "0");
|
|
|
|
|
+ return `${(year + "").substring(2)}${month}${day}`;
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
|
|
+const handleSaveImage = () => {
|
|
|
|
|
+ // 保存带二维码的图片
|
|
|
|
|
+ downloadImage(watermarkWithQRCode.value || watermark.value, "执行照片");
|
|
|
|
|
+};
|
|
|
|
|
|
|
|
|
|
+const handleWechat = () => {
|
|
|
|
|
+ // 微信分享功能(可以后续实现)
|
|
|
|
|
+ console.log("微信分享");
|
|
|
|
|
+};
|
|
|
|
|
|
|
|
|
|
+const handleCancel = () => {
|
|
|
|
|
+ showPopup.value = false;
|
|
|
|
|
+};
|
|
|
|
|
|
|
|
|
|
+function downloadImage(dataUrl, filename) {
|
|
|
|
|
+ const link = document.createElement("a");
|
|
|
|
|
+ link.href = dataUrl;
|
|
|
|
|
+ link.download = filename;
|
|
|
|
|
+ document.body.appendChild(link);
|
|
|
|
|
+ link.click();
|
|
|
|
|
+ document.body.removeChild(link);
|
|
|
|
|
+}
|
|
|
</script>
|
|
</script>
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
<style lang="scss" scoped>
|
|
|
.canvas-container {
|
|
.canvas-container {
|
|
|
- width: 100%;
|
|
|
|
|
- height: 100%;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- justify-content: center;
|
|
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: center;
|
|
|
}
|
|
}
|
|
|
.carousel-item {
|
|
.carousel-item {
|
|
|
- min-width: 100%;
|
|
|
|
|
- max-height: 100%;
|
|
|
|
|
- flex-shrink: 0;
|
|
|
|
|
- width: 100%;
|
|
|
|
|
- pointer-events: auto;
|
|
|
|
|
- position: relative;
|
|
|
|
|
- .tag-box {
|
|
|
|
|
- position: absolute;
|
|
|
|
|
- bottom: 30%;
|
|
|
|
|
- left: 50%;
|
|
|
|
|
- transform: translate(-50%, 50%); // 确保在高二分之一的位置水平居中
|
|
|
|
|
- height: 18px;
|
|
|
|
|
- padding: 0 6px;
|
|
|
|
|
- background: rgba(108, 108, 108, 0.67);
|
|
|
|
|
- border-radius: 10px;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- color: #FFFFFF;
|
|
|
|
|
- font-size: 12px;
|
|
|
|
|
-
|
|
|
|
|
- &.right {
|
|
|
|
|
- left: auto;
|
|
|
|
|
- right: 10px;
|
|
|
|
|
|
|
+ min-width: 100%;
|
|
|
|
|
+ max-height: 100%;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ pointer-events: auto;
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ .tag-box {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ bottom: 30%;
|
|
|
|
|
+ left: 50%;
|
|
|
|
|
+ transform: translate(-50%, 50%); // 确保在高二分之一的位置水平居中
|
|
|
|
|
+ height: 18px;
|
|
|
|
|
+ padding: 0 6px;
|
|
|
|
|
+ background: rgba(108, 108, 108, 0.67);
|
|
|
|
|
+ border-radius: 10px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ color: #ffffff;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+
|
|
|
|
|
+ &.right {
|
|
|
|
|
+ left: auto;
|
|
|
|
|
+ right: 10px;
|
|
|
|
|
+ }
|
|
|
|
|
+ &.leftTop {
|
|
|
|
|
+ height: 25px;
|
|
|
|
|
+ line-height: 26px;
|
|
|
|
|
+ padding: 0 8px;
|
|
|
|
|
+ border-radius: 16px;
|
|
|
|
|
+ background: rgba(0, 0, 0, 0.6);
|
|
|
|
|
+ bottom: auto;
|
|
|
|
|
+ top: 6px;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
- &.leftTop {
|
|
|
|
|
- height: 25px;
|
|
|
|
|
- line-height: 26px;
|
|
|
|
|
- padding: 0 8px;
|
|
|
|
|
- border-radius: 16px;
|
|
|
|
|
- background: rgba(0, 0, 0, 0.6);
|
|
|
|
|
- bottom: auto;
|
|
|
|
|
- top: 6px;
|
|
|
|
|
|
|
+ .tag-text {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ bottom: 31%;
|
|
|
|
|
+ left: 50%;
|
|
|
|
|
+ width: 80%;
|
|
|
|
|
+ transform: translate(-50%, 50%); // 确保在高二分之一的位置水平居中
|
|
|
|
|
+ height: 24px;
|
|
|
|
|
+ padding: 10px 0px 10px 0px;
|
|
|
|
|
+ background: rgba(0, 0, 0, 0.67);
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ color: #ffffff;
|
|
|
|
|
+ font-size: 12px;
|
|
|
}
|
|
}
|
|
|
- }
|
|
|
|
|
- .tag-text {
|
|
|
|
|
- position: absolute;
|
|
|
|
|
- bottom: 31%;
|
|
|
|
|
- left: 50%;
|
|
|
|
|
- width: 80%;
|
|
|
|
|
- transform: translate(-50%, 50%); // 确保在高二分之一的位置水平居中
|
|
|
|
|
- height: 24px;
|
|
|
|
|
- padding: 10px 0px 10px 0px;
|
|
|
|
|
- background: rgba(0, 0, 0, 0.67);
|
|
|
|
|
- border-radius: 6px;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- justify-content: center;
|
|
|
|
|
- text-align: center;
|
|
|
|
|
- color: #FFFFFF;
|
|
|
|
|
- font-size: 12px;
|
|
|
|
|
- }
|
|
|
|
|
|
|
|
|
|
- .center-mark {
|
|
|
|
|
- position: absolute;
|
|
|
|
|
- bottom: 10px;
|
|
|
|
|
- left: 50%;
|
|
|
|
|
- transform: translateX(-50%);
|
|
|
|
|
- color: #36402c;
|
|
|
|
|
- font-size: rpx(24);
|
|
|
|
|
- font-weight: bold;
|
|
|
|
|
- padding: rpx(14) rpx(30);
|
|
|
|
|
- background: linear-gradient(
|
|
|
|
|
|
|
+ .center-mark {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ bottom: 10px;
|
|
|
|
|
+ left: 50%;
|
|
|
|
|
+ transform: translateX(-50%);
|
|
|
|
|
+ color: #36402c;
|
|
|
|
|
+ font-size: rpx(24);
|
|
|
|
|
+ font-weight: bold;
|
|
|
|
|
+ padding: rpx(14) rpx(30);
|
|
|
|
|
+ background: linear-gradient(
|
|
|
90deg,
|
|
90deg,
|
|
|
rgba(255, 255, 255, 0) 0%,
|
|
rgba(255, 255, 255, 0) 0%,
|
|
|
rgba(255, 255, 255, 0.6) 24%,
|
|
rgba(255, 255, 255, 0.6) 24%,
|
|
|
rgba(255, 255, 255, 0.6) 76%,
|
|
rgba(255, 255, 255, 0.6) 76%,
|
|
|
rgba(255, 255, 255, 0) 100%
|
|
rgba(255, 255, 255, 0) 100%
|
|
|
- );
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
.carousel-item img {
|
|
.carousel-item img {
|
|
|
- width: 100%;
|
|
|
|
|
- display: block;
|
|
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ display: block;
|
|
|
}
|
|
}
|
|
|
canvas {
|
|
canvas {
|
|
|
- position: absolute;
|
|
|
|
|
|
|
+ position: absolute;
|
|
|
}
|
|
}
|
|
|
.close-button {
|
|
.close-button {
|
|
|
- background: transparent;
|
|
|
|
|
- border: none;
|
|
|
|
|
- color: #FFFFFF;
|
|
|
|
|
- cursor: pointer;
|
|
|
|
|
- font-size: 10px; // 可以根据需求调整大小
|
|
|
|
|
- position: absolute;
|
|
|
|
|
- top: -1px;
|
|
|
|
|
- right: -9px; // 调整为合适的间距
|
|
|
|
|
- transform: translateY(-50%);
|
|
|
|
|
|
|
+ background: transparent;
|
|
|
|
|
+ border: none;
|
|
|
|
|
+ color: #ffffff;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ font-size: 10px; // 可以根据需求调整大小
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: -1px;
|
|
|
|
|
+ right: -9px; // 调整为合适的间距
|
|
|
|
|
+ transform: translateY(-50%);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.floating-img {
|
|
.floating-img {
|
|
|
- position: absolute;
|
|
|
|
|
- bottom: 0;
|
|
|
|
|
- right: 0;
|
|
|
|
|
- width: auto !important;
|
|
|
|
|
- height: 25% !important;
|
|
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ bottom: 0;
|
|
|
|
|
+ right: 0;
|
|
|
|
|
+ width: auto !important;
|
|
|
|
|
+ height: 25% !important;
|
|
|
}
|
|
}
|
|
|
.floating-img-big {
|
|
.floating-img-big {
|
|
|
- position: fixed !important;
|
|
|
|
|
- z-index: 99999 !important;
|
|
|
|
|
- top: 50% !important;
|
|
|
|
|
- left: 50% !important;
|
|
|
|
|
- width: auto !important;
|
|
|
|
|
- height: 100% !important;
|
|
|
|
|
- transform: translate(-50%, -50%) !important;
|
|
|
|
|
|
|
+ position: fixed !important;
|
|
|
|
|
+ z-index: 99999 !important;
|
|
|
|
|
+ top: 50% !important;
|
|
|
|
|
+ left: 50% !important;
|
|
|
|
|
+ width: auto !important;
|
|
|
|
|
+ height: 100% !important;
|
|
|
|
|
+ transform: translate(-50%, -50%) !important;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+.cavans-popup {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ max-width: 100%;
|
|
|
|
|
+ max-height: 90vh;
|
|
|
|
|
+ background: none;
|
|
|
|
|
+ border-radius: 12px;
|
|
|
|
|
+ overflow: auto;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ backdrop-filter: 4px;
|
|
|
|
|
+ .cavans-content {
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ padding: 16px;
|
|
|
|
|
+ .current-img {
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
-
|
|
|
|
|
-
|
|
|
|
|
|
|
+ // 底部操作按钮
|
|
|
|
|
+ .bottom-actions {
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+
|
|
|
|
|
+ .action-buttons {
|
|
|
|
|
+ padding: 16px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: space-around;
|
|
|
|
|
+
|
|
|
|
|
+ .action-btn {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+
|
|
|
|
|
+ .icon-circle {
|
|
|
|
|
+ width: 48px;
|
|
|
|
|
+ height: 48px;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+ margin-bottom: 4px;
|
|
|
|
|
+
|
|
|
|
|
+ .el-icon {
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+ }
|
|
|
|
|
+ img {
|
|
|
|
|
+ width: 50px;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &.blue-btn .icon-circle {
|
|
|
|
|
+ background: #2199f8;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &.green-btn .icon-circle {
|
|
|
|
|
+ background: #07c160;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &.orange-btn .icon-circle {
|
|
|
|
|
+ background: #ff790b;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .btn-label {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .cancel-btn {
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ font-size: 18px;
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
</style>
|
|
</style>
|