Explorar el Código

fix: 绘制复核成效水印

lxf hace 2 días
padre
commit
0383928117

+ 9 - 0
src/api/modules/container_phenology.js

@@ -0,0 +1,9 @@
+const config = require("../config")
+const url = config.base_dev_url + "container_phenology_sample_files_speak_title"
+
+module.exports = {
+    getFarmSpeakInfo: {
+        url: url + "/getFarmSpeakInfo",
+        type: "get",
+    },
+}

+ 1 - 1
src/components/album_compoents/albumCarouselItem.vue

@@ -22,7 +22,7 @@
                     </template>
             </photo-provider>
         </div>
-        <div class="label-text">{{ labelText }}</div>
+        <div class="label-text" v-if="labelText">{{ labelText }}</div>
 
         <!-- 左右箭头 -->
         <div @click.stop="prev" v-if="currentIndex !== 0" class="arrow left-arrow">

+ 158 - 135
src/components/album_compoents/albumDrawBox.vue

@@ -1,19 +1,18 @@
 <template>
   <photo-consumer
       class="carousel-item"
-      :src="watermark || base_img_url2 + (photo.resFilename ? photo.resFilename : photo.filename) + resize"
+      :src="watermark || getPhotoSrc(photo)"
   >
     <img
       v-if="Math.abs(current - index) < 3"
       crossorigin="anonymous"
       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
       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>
     <div class="tag-box right" v-if="isShowNum" :class="{'leftTop': 'leftTop'}">{{ index+1 }}/{{ length }}</div>
 <!--    <div class="center-mark">mark</div>-->
@@ -36,12 +35,7 @@ const watermark = ref(null)
 const baseMapBig = ref(false)
 
 const props = defineProps({
-  farmId:{
-    type: [Number,String],
-    required: true
-  },
   photo:{
-    type: Object,
     required: true
   },
   index:{
@@ -63,158 +57,187 @@ const props = defineProps({
 })
 let img = 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) {
-  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 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,
-    (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 hideTagBox = (event) => {
   event.stopPropagation();

+ 2 - 2
src/views/old_mini/achievement_report/index.vue

@@ -86,8 +86,8 @@ const executeViewImage = ref([
   "baseMap": "https://birdseye-img.sysuimars.com/birdseye-look-mini/base_map/v2/111594.jpg",
   "blueZoneId": null,
   "district": "东莞市",
-  "filename": "birdseye-look-mini/91429/1763371316207.jpg",
-//   "filename": "birdseye-look-mini/91429/1763461501781.png",
+//   "filename": "birdseye-look-mini/91429/1763371316207.jpg",
+  "filename": "birdseye-look-mini/91429/1763461501781.png",
 //   "filename": "3f27e127-6497-4175-8efb-ba18d703852b/b1f6d99e-826d-4468-a6dd-83f9e7a12ea3/DJI_202512131000_001_b1f6d99e-826d-4468-a6dd-83f9e7a12ea3/DJI_20251213100724_0070_V_code-ws0fsmghvf91.jpeg",
   "fosterCode": "LCGW-DGJH-GLY0253",
   "gardenId": null,

+ 190 - 25
src/views/old_mini/modify_work/reviewWork.vue

@@ -1,7 +1,11 @@
 <template>
     <div class="work-wrap">
         <custom-header name="农事详情" :isClose="paramsPage.goBack ? false : true"></custom-header>
-        <div class="work-content recheck-title" :class="{ 'no-bottom': curRole == '0' && (!workItem.reviewImage ||!workItem.reviewImage.length) }" v-loading="loading">
+        <div
+            class="work-content recheck-title"
+            :class="{ 'no-bottom': curRole == '0' && (!workItem.reviewImage || !workItem.reviewImage.length) }"
+            v-loading="loading"
+        >
             <div class="tabs-content-item">
                 <div class="common-card-title">
                     <img class="icon" src="@/assets/img/home/label-icon.png" alt="" />
@@ -82,7 +86,12 @@
                                 <album-carousel :key="1" labelText="农事前" :images="triggerImg"></album-carousel>
                             </div>
                             <div class="img-list over-img-box">
-                                <album-carousel class="execute-img" :key="1" labelText="执行照片" :images="workItem.executeEvidence"></album-carousel>
+                                <album-carousel
+                                    class="execute-img"
+                                    :key="1"
+                                    labelText="执行照片"
+                                    :images="workItem.executeEvidence"
+                                ></album-carousel>
                             </div>
                             <div
                                 class="img-list over-img-box"
@@ -94,6 +103,9 @@
                                     :images="workItem.reviewImage"
                                 ></album-carousel>
                             </div>
+                            <div class="img-list over-img-box" v-if="combinedReviewImages.length">
+                                <album-carousel :key="3" :images="combinedReviewImages"></album-carousel>
+                            </div>
                             <div class="img-list" v-else>
                                 <div
                                     class="recheck-text-wrap active"
@@ -121,9 +133,7 @@
                                         >
                                             <img
                                                 class="img-icon"
-                                                :src="
-                                                    require(`@/assets/img/gallery/img-icon-act.png`)
-                                                "
+                                                :src="require(`@/assets/img/gallery/img-icon-act.png`)"
                                                 alt=""
                                             />
                                             <div class="recheck-text">点击上传照片</div>
@@ -234,10 +244,7 @@
                 </template>
             </div>
 
-            <div
-                class="fixed-btn-wrap"
-                 v-if="curRole == '2'"
-            >
+            <div class="fixed-btn-wrap" v-if="curRole == '2'">
                 <template v-if="workItem.reviewImage && workItem.reviewImage.length">
                     <div class="fixed-btn more" @click="handleMore">查看更多农事</div>
                     <div class="fixed-btn excute" @click="handleShare">生成成果报告</div>
@@ -248,10 +255,41 @@
             </div>
             <div
                 class="fixed-btn-wrap center"
-                 v-if="curRole == '0' && workItem.reviewImage && workItem.reviewImage.length"
+                v-if="curRole == '0' && workItem.reviewImage && workItem.reviewImage.length"
             >
                 <div class="fixed-btn excute" @click="handleShare">转发</div>
             </div>
+
+            <!-- 组合照片(用于生成合成图片) -->
+            <div class="review-hide-box">
+                <div class="review-image" ref="reviewComboRef">
+                    <div class="review-mask">
+                        <div class="review-text">复核成效</div>
+                        <div class="review-content">
+                            促进分蘖芽萌发、加快分蘖生长,同时补充氮素等关键养分,增强植株长势,为形成足够穗数、提高群体产量打基础。
+                        </div>
+                    </div>
+                    <div class="vs-wrap" v-if="workItem?.reviewImage && workItem?.reviewImage?.length">
+                        <img src="@/assets/img/home/vs.png" alt="" />
+                    </div>
+                    <div class="review-image-item" v-if="triggerImg?.length">
+                        <div class="review-image-item-title">农事前</div>
+                        <img
+                            class="review-image-item-img left-img"
+                            :src="base_img_url2 + triggerImg[triggerImg.length - 1].cloudFilename"
+                            alt=""
+                        />
+                    </div>
+                    <div class="review-image-item" v-if="workItem?.reviewImage?.length">
+                        <div class="review-image-item-title">农事后</div>
+                        <img
+                            class="review-image-item-img right-img"
+                            :src="base_img_url2 + workItem.reviewImage[workItem.reviewImage.length - 1]"
+                            alt=""
+                        />
+                    </div>
+                </div>
+            </div>
         </div>
         <!-- 上传图片弹窗 -->
         <upload-popup :executionData="workItem"></upload-popup>
@@ -266,7 +304,7 @@
 <script setup>
 import { Tab, Tabs } from "vant";
 import customHeader from "@/components/customHeader.vue";
-import { onMounted, ref, onDeactivated, onActivated, onUnmounted } from "vue";
+import { onMounted, ref, onDeactivated, onActivated, onUnmounted, nextTick, watch } from "vue";
 import { useRoute, useRouter } from "vue-router";
 import upload from "@/components/upload";
 import AlbumCarousel from "@/components/album_compoents/albumCarousel";
@@ -275,6 +313,7 @@ import uploadPopup from "@/components/popup/uploadPopup.vue";
 import { base_img_url2 } from "@/api/config";
 import reviewPopup from "@/views/old_mini/task_condition/components/reviewPopup.vue";
 import uploadExecute from "@/views/old_mini/task_condition/components/uploadExecute.vue";
+import html2canvas from "html2canvas";
 
 const route = useRoute();
 const router = useRouter();
@@ -284,7 +323,8 @@ const curRole = ref("");
 // 农事规划页面-显示上传农事凭证按钮
 const isPlan = ref(false);
 const loading = ref(false);
-
+const reviewComboRef = ref(null);
+const combinedReviewImages = ref([]);
 
 const diffInDays = (date, type = "minus") => {
     const targetDate = new Date(date);
@@ -328,6 +368,36 @@ const getTriggerImg = (farmWorkRecordId) => {
     });
 };
 
+// 生成组合照片,传给相册组件
+const generateCombinedReviewImage = async () => {
+    if (!reviewComboRef.value) return;
+    try {
+        await nextTick();
+        const el = reviewComboRef.value;
+        const canvas = await html2canvas(el, {
+            backgroundColor: "#ffffff00",
+            allowTaint: true, // 允许跨域图片
+            useCORS: true, // 使用CORS
+            scale: 2, // 提高分辨率(2倍)
+            logging: true, // 开启日志(调试用)
+        });
+        const dataUrl = canvas.toDataURL("image/png");
+        combinedReviewImages.value = [dataUrl];
+    } catch (e) {
+        console.error("生成组合照片失败", e);
+    }
+};
+
+watch(
+    () => [triggerImg.value, workItem.value.reviewImage],
+    ([preImgs, reviewImgs]) => {
+        if (preImgs && preImgs.length && reviewImgs && reviewImgs.length) {
+            generateCombinedReviewImage();
+        }
+    },
+    { deep: true }
+);
+
 //确认上传
 const handleSubmit = () => {
     const params = {
@@ -345,18 +415,22 @@ const handleSubmit = () => {
 
 const reviewPopupRef = ref(null);
 const handleShare = () => {
-    const preImg = triggerImg.value.length ? base_img_url2 + triggerImg.value[triggerImg.value.length - 1].cloudFilename : '';
-    const resImg = workItem.value?.reviewImage?.length ? base_img_url2 + workItem.value.reviewImage[workItem.value.reviewImage.length - 1] : '';
+    const preImg = triggerImg.value.length
+        ? base_img_url2 + triggerImg.value[triggerImg.value.length - 1].cloudFilename
+        : "";
+    const resImg = workItem.value?.reviewImage?.length
+        ? base_img_url2 + workItem.value.reviewImage[workItem.value.reviewImage.length - 1]
+        : "";
     reviewPopupRef.value.handleShowPopup(workItem.value.id, preImg, resImg);
-}
+};
 
 const handleRemindUser = () => {
-    uploadExecuteRef.value.showPopup({...workItem.value, type: 'remindUser'});
-}
+    uploadExecuteRef.value.showPopup({ ...workItem.value, type: "remindUser" });
+};
 
 const handleMore = () => {
     router.push(`/service_detail?farmId=${workItem.value.farmId}`);
-}
+};
 
 // 清理数据的函数
 const clearData = () => {
@@ -452,7 +526,6 @@ const handleUpload = ({ imgArr }) => {
             }
         }
 
-        
         .fixed-btn-wrap {
             position: fixed;
             z-index: 10;
@@ -488,17 +561,17 @@ const handleUpload = ({ imgArr }) => {
                     background: #fff;
                 }
                 &.excute {
-                    background: linear-gradient(180deg, #FFD887, #ED9E1E);
+                    background: linear-gradient(180deg, #ffd887, #ed9e1e);
                 }
                 &.more {
-                    background: #FFFFFF;
+                    background: #ffffff;
                     border: 1px solid rgba(153, 153, 153, 0.5);
                     color: #666666;
                 }
                 &.second {
-                    background: #FFFFFF;
-                    border: 1px solid #2199F8;
-                    color: #2199F8;
+                    background: #ffffff;
+                    border: 1px solid #2199f8;
+                    color: #2199f8;
                 }
             }
         }
@@ -509,7 +582,7 @@ const handleUpload = ({ imgArr }) => {
             background: #fff;
             margin-top: 12px;
             position: relative;
-            .execute-img{
+            .execute-img {
                 margin-top: 12px;
             }
             .card-title {
@@ -830,5 +903,97 @@ const handleUpload = ({ imgArr }) => {
             }
         }
     }
+
+    .review-hide-box {
+        position: absolute;
+        left: 0;
+        width: 100%;
+        height: 100%;
+        z-index: -1;
+        bottom: 0;
+    }
+
+    .review-image {
+        position: relative;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        gap: 8px;
+        margin: 12px;
+        background: #fff;
+        border-radius: 8px;
+        .review-mask {
+            z-index: 1;
+            pointer-events: none;
+            position: absolute;
+            left: 0;
+            top: 0;
+            width: 100%;
+            height: 100%;
+            border-radius: 8px;
+            background: linear-gradient(
+                360deg,
+                rgba(0, 0, 0, 0.78) 0%,
+                rgba(0, 0, 0, 0.437208) 19.87%,
+                rgba(0, 0, 0, 0) 33.99%
+            );
+            display: flex;
+            flex-direction: column;
+            align-items: baseline;
+            justify-content: end;
+            padding: 12px;
+            box-sizing: border-box;
+            color: #fff;
+            .review-text {
+                font-family: "PangMenZhengDao";
+                font-size: 16px;
+                margin-bottom: 1px;
+            }
+            .review-content {
+                font-size: 10px;
+                line-height: 15px;
+            }
+        }
+        .vs-wrap {
+            position: absolute;
+            left: 50%;
+            top: 50%;
+            transform: translate(-50%, -50%);
+            width: 40px;
+            height: 40px;
+            z-index: 10;
+            img {
+                width: 100%;
+                height: 100%;
+                object-fit: cover;
+            }
+        }
+        .review-image-item {
+            position: relative;
+            flex: 1;
+            .review-image-item-title {
+                position: absolute;
+                top: 0;
+                left: 0;
+                background: rgba(54, 52, 52, 0.6);
+                padding: 4px 10px;
+                border-radius: 8px 0 8px 0;
+                backdrop-filter: 4px;
+                font-size: 12px;
+                color: #fff;
+            }
+            .review-image-item-img {
+                width: 100%;
+                height: 250px;
+                object-fit: cover;
+            }
+            .left-img {
+                border-radius: 8px 0 0 8px;
+            }
+            .right-img {
+                border-radius: 0 8px 8px 0;
+            }
+        }
+    }
 }
 </style>