Просмотр исходного кода

feat: 绘制水印,成果报告

lxf 13 часов назад
Родитель
Сommit
d1b3db23d8

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

@@ -6,4 +6,8 @@ module.exports = {
         url: url + "/getFarmSpeakInfo",
         type: "get",
     },
+    phenologyBroadcast: {
+        url: config.base_dev_url + "farm_broadcast/phenologyBroadcastByFarmId",
+        type: "get",
+    },
 }

BIN
src/assets/img/home/book.png


+ 5 - 1
src/components/album_compoents/albumCarousel.vue

@@ -1,5 +1,5 @@
 <template>
-    <album-carousel-item v-if="images" :labelText="labelText" :images="images"></album-carousel-item>
+    <album-carousel-item v-if="images" :labelText="labelText" :images="images" :isAchievementImgs="isAchievementImgs"></album-carousel-item>
 </template>
 
 <script setup>
@@ -16,6 +16,10 @@ const props =defineProps({
     type: String,
     default: '',
   },
+  isAchievementImgs:{
+    type: Boolean,
+    default: false,
+  },
 })
 const { images } = toRefs(props);
 

+ 323 - 21
src/components/album_compoents/albumCarouselItem.vue

@@ -2,27 +2,44 @@
     <div class="carousel-container">
         <!-- 图片列表 -->
         <div class="carousel-wrapper" :style="carouselStyle">
-            <photo-provider v-if="images" :photo-closable="true" @visibleChange="handleVisibleChange">
-                <!-- <photo-consumer
-                    class="carousel-img"
-                    v-for="(photo, index) in images"
-                    :key="photo.id"
-                    :src="base_img_url2 + (photo.cloudFilename ? photo.cloudFilename : photo)"
-                >
-                    <img
-                        :index="index"
-                        :src="base_img_url2 + (photo.cloudFilename ? photo.cloudFilename : photo)"
-                        alt=""
-                    />
-                </photo-consumer> -->
+            <!-- <photo-provider v-if="images" :photo-closable="true" @visibleChange="handleVisibleChange">
+                
                 <template  v-for="(photo, index) in images"
                    :key="photo.id">
                     <album-draw-box :isShowNum="0" :farmId="766" :photo="photo" :current="currentIndex" :index="index" :length="images.length"
                     ></album-draw-box>
                     </template>
-            </photo-provider>
+            </photo-provider> -->
+            <div
+                class="carousel-img"
+                :class="{ noFit: isAchievementImgs }"
+                v-for="(photo, index) in images"
+                :key="index"
+            >
+                <div class="label-text" v-if="labelText">{{ labelText }}</div>
+                <img class="img-dom" :index="index" @click="clickPhoto(photo)" :src="getPhotoSrc(photo)" alt="" />
+                <div class="carousel-img-mask" v-if="!isAchievementImgs">
+                    <div class="mask-content">
+                        <div class="mask-line line-top">
+                            <span class="date-text">2025.12.05</span>
+                            <span class="line-separator">|</span>
+                            <span class="executor-text">执行人:张某某、张某某</span>
+                        </div>
+                        <div class="mask-line line-middle">
+                            <span class="work-name">梢期杀虫</span>
+                            <span class="line-separator">|</span>
+                            <span class="prescription-text">药物处方:乙烯利乙烯利</span>
+                        </div>
+                        <div class="mask-line line-bottom">
+                            <span class="location-text">
+                                <img src="@/assets/watermark/address.png" alt="location" class="location-icon" />
+                                荔博园(广东省广州市从化区思想街道思)
+                            </span>
+                        </div>
+                    </div>
+                </div>
+            </div>
         </div>
-        <div class="label-text" v-if="labelText">{{ labelText }}</div>
 
         <!-- 左右箭头 -->
         <div @click.stop="prev" v-if="currentIndex !== 0" class="arrow left-arrow">
@@ -31,12 +48,65 @@
         <div @click.stop="next" v-if="images && currentIndex !== images.length - 1" class="arrow right-arrow">
             <el-icon color="#F0D09C"><ArrowRightBold /></el-icon>
         </div>
+
+        <div class="curren-img" v-if="currentPhoto">
+            <div ref="currentImgRef" class="carousel-img" :class="{ noFit: isAchievementImgs }">
+                <img class="img-dom" :src="currentPhoto" alt="" />
+                <img src="@/assets/img/home/qrcode.png" alt="" class="code-icon" />
+                <div class="carousel-img-mask" v-if="!isAchievementImgs">
+                    <div class="mask-content">
+                        <div class="mask-line line-top">
+                            <span class="date-text">2025.12.05</span>
+                            <span class="line-separator">|</span>
+                            <span class="executor-text">执行人:张某某、张某某</span>
+                        </div>
+                        <div class="mask-line line-middle">
+                            <span class="work-name">梢期杀虫</span>
+                            <span class="line-separator">|</span>
+                            <span class="prescription-text">药物处方:乙烯利乙烯利</span>
+                        </div>
+                        <div class="mask-line line-bottom">
+                            <span class="location-text">
+                                <img src="@/assets/watermark/address.png" alt="location" class="location-icon" />
+                                荔博园(广东省广州市从化区思想街道思)
+                            </span>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+
+        <popup class="cavans-popup" v-model:show="showPopup">
+            <div class="cavans-content">
+                <img class="current-img" :src="previewCanvas" 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>
     </div>
 </template>
 
 <script setup>
-import { toRefs, ref, computed, onMounted, onUnmounted } from "vue";
+import { Popup } from "vant";
+import { toRefs, ref, computed, onMounted, onUnmounted, nextTick } from "vue";
 import AlbumDrawBox from "./albumDrawBox.vue";
+import html2canvas from "html2canvas";
 import { base_img_url2 } from "@/api/config";
 import "./cacheImg.js";
 const props = defineProps({
@@ -48,8 +118,12 @@ const props = defineProps({
         type: String,
         default: "",
     },
+    isAchievementImgs: {
+        type: Boolean,
+        default: false,
+    },
 });
-const { images, labelText } = toRefs(props);
+const { images, labelText, isAchievementImgs } = toRefs(props);
 let timer = null;
 const currentIndex = ref(0);
 
@@ -113,6 +187,55 @@ const clearAndRestartTimer = () => {
     }
     // timer = setInterval(next, 5000);
 };
+
+const showPopup = ref(false);
+const currentPhoto = ref(null);
+const previewCanvas = ref(null);
+
+const currentImgRef = ref(null);
+const clickPhoto = (photo) => {
+    currentPhoto.value = getPhotoSrc(photo);
+    nextTick(async () => {
+        const canvas = await html2canvas(currentImgRef.value, {
+            backgroundColor: "#ffffff00",
+            scrollY: -window.scrollY, // 处理滚动条位置
+            allowTaint: true, // 允许跨域图片
+            useCORS: true, // 使用CORS
+            scale: 2, // 提高分辨率(2倍)
+            height: currentImgRef.value.scrollHeight, // 设置完整高度
+            width: currentImgRef.value.scrollWidth, // 设置完整宽度
+            logging: true, // 开启日志(调试用)
+        });
+
+        // 转换为图片并下载
+        const image = canvas.toDataURL("image/png");
+        setTimeout(() => {
+            previewCanvas.value = image;
+            showPopup.value = true;
+        }, 100);
+    });
+};
+
+const handleSaveImage = () => {
+    console.log('save');
+    downloadImage(previewCanvas.value, '执行照片');
+};
+
+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);
+}
+
+const getPhotoSrc = (photo) => {
+    if (isAchievementImgs.value) {
+        return photo;
+    }
+    return base_img_url2 + (photo.cloudFilename ? photo.cloudFilename : photo);
+};
 </script>
 
 <style lang="scss" scoped>
@@ -122,18 +245,118 @@ const clearAndRestartTimer = () => {
     width: 100%;
     overflow: hidden;
     margin: 0 auto;
+    .curren-img {
+        position: fixed;
+        bottom: 0;
+        left: 0;
+        right: 0;
+        width: 80%;
+        height: 100%;
+        margin: 0 auto;
+        z-index: -1;
+        pointer-events: none;
+        .carousel-img {
+            width: 100%;
+            position: relative;
+            overflow: hidden;
+        }
+        .img-dom {
+            width: 100%;
+            height: 100%;
+            object-fit: cover;
+            border-radius: 8px;
+        }
+    }
     .carousel-wrapper {
         display: flex;
         transition: transform 0.5s ease;
         width: 100%;
         .carousel-img {
-            min-width: 100%;
-            img {
-                min-width: 100%;
-                height: 255px;
+            width: calc(100vw - 48px);
+            min-width: calc(100vw - 48px);
+            // min-width: 312px;
+            height: 255px;
+            object-fit: cover;
+            position: relative;
+            overflow: hidden;
+            &.noFit {
+                height: auto;
+                object-fit: contain;
+            }
+            .img-dom {
+                width: 100%;
+                height: 100%;
                 object-fit: cover;
+                border-radius: 8px;
+            }
+            &.noFit {
+                .img-dom {
+                    height: auto;
+                    object-fit: contain;
+                }
+            }
+        }
+    }
+    .code-icon {
+        position: absolute;
+        right: 12px;
+        top: 12px;
+        width: 40px;
+    }
+    .carousel-img-mask {
+        position: absolute;
+        bottom: -2px;
+        left: -2px;
+        width: calc(100% + 2px);
+        height: 80%;
+        background: linear-gradient(360deg, rgba(0, 0, 0, 0.7) 0%, rgba(0, 0, 0, 0.4) 25%, rgba(0, 0, 0, 0) 40%);
+        z-index: 1;
+        pointer-events: none;
+        display: flex;
+        align-items: flex-end;
+        padding: 8px 12px;
+        box-sizing: border-box;
+        border-radius: 8px 8px 12px 12px;
+        .mask-content {
+            width: 100%;
+            color: #ffffff;
+            font-size: 10px;
+            line-height: 15px;
+        }
+        .mask-line {
+            display: flex;
+            align-items: center;
+            flex-wrap: wrap;
+        }
+        .line-middle {
+            margin-top: 4px;
+            .work-name {
+                font-family: "PangMenZhengDao", system-ui, -apple-system, BlinkMacSystemFont, "Helvetica Neue", Arial,
+                    sans-serif;
+                font-size: 17px;
             }
         }
+        .line-bottom {
+            margin-top: 4px;
+        }
+        .date-text {
+            font-family: "PangMenZhengDao";
+            font-size: 12px;
+        }
+        .date-text,
+        .executor-text,
+        .prescription-text,
+        .location-text {
+            white-space: nowrap;
+        }
+        .location-icon {
+            width: 9px;
+            height: 10px;
+            margin-right: 2px;
+        }
+        .line-separator {
+            margin: 0 6px;
+        }
     }
     .label-text {
         position: absolute;
@@ -211,4 +434,83 @@ const clearAndRestartTimer = () => {
         right: rpx(32);
     }
 }
+
+.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 {
+            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>

+ 2 - 1
src/components/album_compoents/albumDrawBox.vue

@@ -7,8 +7,9 @@
       v-if="Math.abs(current - index) < 3"
       crossorigin="anonymous"
       loading="lazy"
+      @load="drawWatermark($event)"
       :src="watermark || getPhotoSrc(photo)"
-      style="width: 100%; height: auto; object-fit: cover; display: block;border-radius: 8px;"
+      style="width: 100%; height: 255px; object-fit: cover; display: block; border-radius: 8px;"
     />
     <canvas
       ref="canvasRef"

+ 136 - 39
src/components/popup/activeUploadPopup.vue

@@ -4,7 +4,7 @@
         v-model:show="show"
         closeable
         teleport="body"
-        :overlay-style="{'z-index': 9999}"
+        :overlay-style="{ 'z-index': 9999 }"
         :close-on-click-overlay="false"
         @closed="handleClosed"
     >
@@ -14,8 +14,13 @@
                 当前物候期
             </div>
             <div class="date-input">
-                <el-select v-model="currentPhenologyId" placeholder="请选择当前物候期">
-                    <el-option v-for="item in phenologyList" :key="item.id" :label="item.name" :value="item.id"></el-option>
+                <el-select popper-class="custom-select-dropdown" v-model="currentPhenologyId" size="large" placeholder="请选择当前物候期">
+                    <el-option
+                        v-for="item in phenologyList"
+                        :key="item.id"
+                        :label="item.name"
+                        :value="item.id"
+                    ></el-option>
                 </el-select>
             </div>
         </div>
@@ -28,6 +33,7 @@
                 <el-date-picker
                     v-model="uploadDate"
                     size="large"
+                    popper-class="custom-select-dropdown"
                     style="width: 100%"
                     type="date"
                     placeholder="请选择日期"
@@ -35,15 +41,45 @@
                 />
             </div>
         </div>
-        <div class="header" v-if="needExecutor">
-            <div class="title">
-                <span class="required">*</span>
-                请确认执行人
+        <div v-if="needExecutor">
+            <div class="header">
+                <div class="title">
+                    <span class="required">*</span>
+                    请确认执行人
+                </div>
+                <div class="date-input">
+                    <el-select popper-class="custom-select-dropdown" size="large" v-model="executorId" placeholder="请选择执行人">
+                        <el-option
+                            v-for="(item, index) in executorList"
+                            :key="index"
+                            :label="item.name"
+                            :value="item.id"
+                        />
+                    </el-select>
+                </div>
             </div>
-            <div class="date-input">
-                <el-select v-model="executorId" placeholder="请选择执行人">
-                    <el-option v-for="item in executorList" :key="item.id" :label="item.name" :value="item.id"></el-option>
-                </el-select>
+            <div class="header flex-header">
+                <div class="title">
+                    <span class="required">*</span>
+                    是否需要复核?
+                </div>
+                <div class="date-input">
+                    <el-radio-group v-model="needReview">
+                        <el-radio :value="1">需要</el-radio>
+                        <el-radio :value="0">不需要</el-radio>
+                    </el-radio-group>
+                </div>
+            </div>
+            <div class="header" v-if="needReview">
+                <div class="title">
+                    <span class="required">*</span>
+                    请选择复核时间
+                </div>
+                <div class="date-input review-day-input">
+                    <el-input size="large" v-model="reviewDay" type="number" step="0.01">
+                        <template #append>天后</template>
+                    </el-input>
+                </div>
             </div>
         </div>
         <div class="tips-text img-desc" v-if="imgDesc"><span class="required">*</span>{{ imgDesc }}</div>
@@ -52,7 +88,9 @@
             <img class="example" src="@/assets/img/home/example-4.png" alt="" />
             <img class="example" src="@/assets/img/home/plus.png" alt="" />
         </upload>
-        <div class="btn" :class="{ 'disabled': isUploading }" @click="handleUpload">{{ isUploading ? '提交中...' : '确认' }}</div>
+        <div class="btn" :class="{ disabled: isUploading }" @click="handleUpload">
+            {{ isUploading ? "提交中..." : "确认" }}
+        </div>
     </popup>
 
     <!-- 上传成功提示弹窗 -->
@@ -102,8 +140,21 @@ const type = ref(null);
 const arrangeId = ref(null);
 // 选择执行人
 const needExecutor = ref(false);
-const executorList = ref([]);
+const executorList = ref([
+    {
+        id: 1,
+        name: "张三",
+        phone: "13800138000",
+    },
+    {
+        id: 2,
+        name: "李四",
+        phone: "13800138001",
+    },
+]);
 const executorId = ref(null);
+const needReview = ref(false);
+const reviewDay = ref(null);
 // 选择当前物候期
 const currentPhenologyId = ref(null);
 const phenologyList = ref([]);
@@ -111,15 +162,26 @@ const selectCurrentPhenology = ref(false);
 
 // 图片上传标题描述
 const imgDesc = ref(null);
-function handleShow({ gardenIdVal, problemTitleVal, typeVal, arrangeIdVal, executorListVal, imgDescVal, needExecutorVal, selectCurrentPhenologyVal, phenologyListVal}) {
+function handleShow({
+    gardenIdVal,
+    problemTitleVal,
+    typeVal,
+    arrangeIdVal,
+    executorListVal,
+    imgDescVal,
+    needExecutorVal,
+    selectCurrentPhenologyVal,
+    phenologyListVal,
+}) {
     images.value = [];
     gardenId.value = gardenIdVal;
     problemTitle.value = problemTitleVal || "请选择问题";
-    uploadDate.value = (new Date());
+    uploadDate.value = new Date();
     show.value = true;
     type.value = typeVal;
     arrangeId.value = arrangeIdVal;
-    executorList.value = executorListVal;
+    // executorList.value = executorListVal;
+    console.log("executorList222222", executorListVal);
     imgDesc.value = imgDescVal;
     needExecutor.value = needExecutorVal;
     selectCurrentPhenology.value = selectCurrentPhenologyVal ? true : false;
@@ -137,7 +199,7 @@ const emit = defineEmits(["handleUploadSuccess"]);
 const handleUpload = () => {
     // 如果正在上传中,直接返回,防止重复调用
     if (isUploading.value) return;
-    
+
     if (images.value.length === 0) return ElMessage.warning("请上传图片");
     const paramsObj = {
         farmId: gardenId.value,
@@ -145,45 +207,49 @@ const handleUpload = () => {
         executeDate: formatDate(uploadDate.value),
         imagePaths: images.value,
     };
-    if(type.value === "question"){
+    if (type.value === "question") {
         show.value = false;
         emit("handleUploadSuccess", paramsObj);
-        return
+        return;
     }
-    triggerFarmWork(paramsObj,true)
+    triggerFarmWork(paramsObj, true);
 };
 
-function triggerFarmWork(paramsObj,showSuccess) {
+function triggerFarmWork(paramsObj, showSuccess) {
     // 如果正在上传中,直接返回,防止重复调用
     if (isUploading.value) return;
-    
+
     // 设置上传状态为 true
     isUploading.value = true;
-    
-    VE_API.monitor.triggerFarmWork(paramsObj).then((res) => {
-        if (res.code === 0) {
-            if(showSuccess){
-                show.value = false;
-                successShow.value = true;
-                emit("handleUploadSuccess", paramsObj);
+
+    VE_API.monitor
+        .triggerFarmWork(paramsObj)
+        .then((res) => {
+            if (res.code === 0) {
+                if (showSuccess) {
+                    show.value = false;
+                    successShow.value = true;
+                    emit("handleUploadSuccess", paramsObj);
+                }
             }
-        }
-    }).catch((error) => {
-        console.error("触发农事失败:", error);
-    }).finally(() => {
-        // 无论成功或失败,都重置上传状态
-        isUploading.value = false;
-    });
+        })
+        .catch((error) => {
+            console.error("触发农事失败:", error);
+        })
+        .finally(() => {
+            // 无论成功或失败,都重置上传状态
+            isUploading.value = false;
+        });
 }
 
 function showPopup(data) {
-    handleShow(data)
+    handleShow(data);
 }
 
 defineExpose({
     triggerFarmWork,
-    showPopup
-})
+    showPopup,
+});
 
 function handleClosed() {
     eventBus.emit("upload:reset");
@@ -225,6 +291,30 @@ onUnmounted(() => {
                 }
             }
         }
+        .review-day-input {
+            border: 1px solid #dcdcdc;
+            border-radius: 3px;
+            ::v-deep {
+                .el-input__wrapper {
+                    width: 40px;
+                    flex: none;
+                    box-shadow: none;
+                }
+                .el-input-group__append {
+                    box-shadow: none;
+                    background: none;
+                }
+            }
+        }
+    }
+    .flex-header {
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+        margin-bottom: 12px;
+        .date-input {
+            margin: 0;
+        }
     }
     .required {
         color: #ff4d4f;
@@ -285,3 +375,10 @@ onUnmounted(() => {
     }
 }
 </style>
+
+<style lang="scss">
+// 全局样式,用于设置下拉框的 z-index(不受 scoped 限制)
+.custom-select-dropdown {
+    z-index: 10000 !important;
+}
+</style>

+ 41 - 10
src/components/popup/offerPopup.vue

@@ -72,6 +72,22 @@
                     <img class="plus" src="@/assets/img/home/plus.png" alt="" />
                 </uploader>
             </div>
+
+            <div class="time-wrap">
+                <div class="name"><span class="required">*</span>请选择 {{ executionData.farmWorkName }} 实际执行时间</div>
+                <div class="time-input">
+                    <el-date-picker
+                        v-model="executeTime"
+                        popper-style="z-index: 99999 !important;"
+                        :disabled-date="disabledDate"
+                        size="large"
+                        style="width: 100%"
+                        type="date"
+                        placeholder="请选择日期"
+                        :editable="false"
+                    />
+                </div>
+            </div>
         </div>
         <div class="tips-text">注:交易信息保密不公开</div>
         <div class="button-wrap" v-if="stepIndex === 1">
@@ -159,6 +175,7 @@ const rules = {
 };
 
 const stepIndex = ref(1);
+const executeTime = ref("");
 const fileList = ref([]);
 const fileArr = ref([]);
 
@@ -274,6 +291,20 @@ function openPopup(item) {
     formData.totalAmount = "";
 }
 
+const disabledDate = (time) => {
+    // 获取今天的开始时间(00:00:00)
+    const today = new Date();
+    today.setHours(0, 0, 0, 0);
+    
+    // 获取明天的开始时间(00:00:00)
+    const tomorrow = new Date(today);
+    tomorrow.setDate(tomorrow.getDate() + 1);
+    
+    // 如果时间 >= 明天的开始时间,则禁用(不能选今天之后)
+    // 可以选择今天及之前的时间
+    return time.getTime() >= tomorrow.getTime();
+}
+
 defineExpose({
     openPopup,
 });
@@ -497,6 +528,16 @@ defineExpose({
             color: #269fff;
         }
     }
+    .name {
+        color: #000000;
+        font-size: 16px;
+        font-weight: 500;
+        padding-bottom: 12px;
+        .required {
+            color: #ff4d4f;
+            margin-right: 2px;
+        }
+    }
     .upload-wrap {
         // position: relative;
         // border: 1px dashed #2199F8;
@@ -510,16 +551,6 @@ defineExpose({
                 }
             }
         }
-        .name {
-            color: #000000;
-            font-size: 16px;
-            font-weight: 500;
-            padding-bottom: 12px;
-            .required {
-                color: #ff4d4f;
-                margin-right: 2px;
-            }
-        }
         .img {
             width: 80px;
             height: 80px;

+ 20 - 60
src/components/taskItem.vue

@@ -1,48 +1,16 @@
 <template>
-    <div class="farm-item" :class="{ done: status, hall: isHall }">
+    <div class="farm-item" :class="{ done: props.status, hall: isHall }">
         <div class="item-top">
             <div class="top-l">
                 <div class="item-name">{{ itemData?.farmWorkName }}</div>
                 <div class="item-time">{{ itemData?.executeDate }}</div>
             </div>
-            <div class="top-r" @click="toDetail(status, itemData.id, itemData.farmWorkLibId)">
-                {{ status === 0 ? "查看处方" : "查看详情" }}
+            <div class="top-r" @click="toDetail(props.status, itemData.id, itemData.farmWorkLibId)">
+                {{ props.status === 0 ? "查看处方" : "查看详情" }}
             </div>
         </div>
-        <div class="item-box" v-if="status === 0">
-            <!-- <div class="title-wrap">
-                <div class="title-l">
-                    <div class="item-title">未命名农场</div>
-                    <div class="setting-text">设为特别关注</div>
-                </div>
-                <div class="title-r">最近服务过</div>
-            </div>
-            <div class="item-desc">
-                <div class="desc-info">
-                    <div class="desc-info-item">
-                        <span>农场品种:</span>
-                        <span class="value">荔枝-桂味</span>
-                    </div>
-                    <div class="desc-info-item">
-                        <span>农场面积:</span>
-                        <span class="value">500亩</span>
-                    </div>
-                </div>
-                <div class="desc-info copy-info">
-                    <div class="address van-ellipsis">
-                        <span>农场位置:</span>
-                        <span class="value">广东省广州市从化区市从化区从广东省广州市从化区市从化区从</span>
-                    </div>
-                    <span class="copy-text">点击复制</span>
-                </div>
-                <div class="desc-info report-text" @click="toPage">
-                    <el-icon class="done-icon" color="#fff" size="6"><Select /></el-icon>
-                    农场报告已经生成,请点击查看
-                    <el-icon class="right-icon" color="#FF953D" size="12"><ArrowRight /></el-icon>
-                </div>
-            </div> -->
-
-            <div class="farm-text" v-if="(status === 0 && itemIndex === 0) || isHall">
+        <div class="item-box" v-if="props.status === 0">
+            <div class="farm-text" v-if="(props.status === 0 && itemIndex === 0) || isHall">
                 <span class="text-title">农场现状:</span>{{ farmStatusText }}
                 <!-- <span class="text-more">点击查看照片</span> -->
             </div>
@@ -97,26 +65,11 @@
                     </div>
                 </div>
                 <div class="review-image" v-if="!expiredDay">
-                    <div class="review-image-item" v-if="triggerImg.length">
-                        <div class="review-image-item-title">农事前</div>
+                    <div class="review-image-item" v-if="detailItem?.executeEvidence?.length">
+                        <div class="review-image-item-title">执行照片</div>
                         <photo-provider :photo-closable="true">
                             <photo-consumer
-                                v-for="src in [triggerImg[triggerImg.length - 1].cloudFilename]"
-                                intro="农事前照片"
-                                :key="src"
-                                :src="base_img_url2 + src"
-                            >
-                                <div class="img-item">
-                                    <img :src="base_img_url2 + src + resize" class="view-box" />
-                                </div>
-                            </photo-consumer>
-                        </photo-provider>
-                    </div>
-                    <div class="review-image-item" v-if="detailItem?.reviewImage?.length">
-                        <div class="review-image-item-title">农事后</div>
-                        <photo-provider :photo-closable="true">
-                            <photo-consumer
-                                v-for="src in [detailItem.reviewImage[detailItem.reviewImage.length - 1]]"
+                                v-for="src in [detailItem.executeEvidence[detailItem.executeEvidence.length - 1]]"
                                 intro="农事后照片"
                                 :key="src"
                                 :src="base_img_url2 + src"
@@ -148,19 +101,19 @@
 
             <slot name="footer"></slot>
 
-            <div class="item-footer" v-if="status === 1">
+            <div class="item-footer" v-if="props.status === 1">
                 <div
                     class="footer-l farm-name-text van-ellipsis"
-                    :style="{ width: detailItem?.reviewImage?.length ? 'calc(100% - 85px)' : 'calc(100% - 180px)' }"
+                    :style="{ width: detailItem?.reviewImage?.length ? 'calc(100% - 100px)' : 'calc(100% - 180px)' }"
                 >
                     来自<span class="name-text">{{ itemData.farmName || "未知农场" }}</span>
                 </div>
                 <div class="footer-r">
-                    <div class="btn warning" v-if="detailItem?.reviewImage?.length" @click="shareResult">分享成果</div>
-                    <template v-else>
+                    <div class="btn warning" @click="generateReport">生成成果报告</div>
+                    <!-- <template v-else>
                         <div class="btn secondary-text" @click="remindUser">提醒用户拍照</div>
                         <div class="btn photo-text" @click="handleUploadPhoto">上传照片</div>
-                    </template>
+                    </template> -->
                 </div>
             </div>
         </div>
@@ -240,6 +193,13 @@ const handleFollow = ({ farmId, isFollow }) => {
         .catch(() => {});
 };
 
+const generateReport = () => {
+    router.push({
+        path: "/achievement_report",
+        query: { miniJson: JSON.stringify({ id: detailItem.value.id }) },
+    });
+};
+
 const shareResult = () => {
     if (detailItem.value?.reviewImage?.length) {
         const preImg = triggerImg.value.length

+ 1 - 0
src/router/globalRoutes.js

@@ -370,6 +370,7 @@ export default [
     {
         path: "/achievement_report",
         name: "AchievementReport",
+        meta: { keepAlive: true },
         component: () => import("@/views/old_mini/achievement_report/index.vue"),
     },
 ];

+ 400 - 114
src/views/old_mini/achievement_report/index.vue

@@ -1,131 +1,157 @@
 <template>
     <div class="achievement-report-page">
         <custom-header name="生成成果报告"></custom-header>
-        <div ref="reportDom" class="report-content">
-            <div class="report-header">
-                <img class="header-book" src="@/assets/img/home/book.png" alt="">
-                <div class="time-tag">2025.12.15</div>
-                <div class="report-title">成果报告</div>
-                <div class="report-info">
-                    <div class="info-item">
-                        <img class="info-icon" src="@/assets/img/home/nz.png" alt="">
-                        <span class="info-text">执行组织:大荔农资组织</span>
+        <div class="report-content-wrap" v-loading="loading">
+            <div class="report-content" ref="reportDom">
+                <img src="@/assets/img/home/qrcode.png" alt="" class="code-icon" />
+                <div class="report-header">
+                    <img class="header-book" src="@/assets/img/home/book.png" alt="" />
+                    <div class="time-tag">{{ workItem?.executeDate }}</div>
+                    <div class="report-title">成果报告</div>
+                    <div class="report-info">
+                        <div class="info-item">
+                            <img class="info-icon" src="@/assets/img/home/nz.png" alt="" />
+                            <span class="info-text">执行组织:{{ workItem?.executeMain }}</span>
+                        </div>
+                        <div class="info-item">
+                            <img class="info-icon" src="@/assets/img/home/nz.png" alt="" />
+                            <span class="info-text">服务农场:{{ workItem?.farmName }}</span>
+                        </div>
                     </div>
-                    <div class="info-item">
-                        <img class="info-icon" src="@/assets/img/home/nz.png" alt="">
-                        <span class="info-text">服务农场:荔博园</span>
+                </div>
+
+                <div class="report-box">
+                    <div class="report-box-item">
+                        <div class="item-content">{{ workItem?.farmWorkName || "--" }}</div>
+                        <div class="item-title">作业农事</div>
+                    </div>
+                    <div class="report-box-item">
+                        <div class="item-content">{{ formatArea(workItem?.area) || "--" }}</div>
+                        <div class="item-title">作业面积</div>
+                    </div>
+                    <div class="report-box-item">
+                        <div class="item-content">{{ workItem?.reviewDate || "--" }}</div>
+                        <div class="item-title">作业周期</div>
+                    </div>
+                    <div class="report-box-item">
+                        <div class="item-content">{{ workItem?.usageMode || "--" }}</div>
+                        <div class="item-title">使用设备</div>
                     </div>
                 </div>
-            </div>
 
-            <div class="report-box">
-                <div class="report-box-item" v-for="(item, index) in reportBoxList" :key="index">
-                    <div class="item-content">{{ item.content }}</div>
-                    <div class="item-title">{{ item.title }}</div>
+                <div class="report-box">
+                    <div class="box-title">精准施治,智慧护航</div>
+                    <div class="box-text">
+                        我们凭借时机精准的判断与定制化方案的核心能力,结合***高效产品与专业设备,病虫害防治率95%
+                        ,落果减少30%
+                    </div>
                 </div>
-            </div>
 
-            <div class="report-box">
-                <div class="box-title">精准施治,智慧护航</div>
-                <div class="box-text">
-                    我们凭借时机精准的判断与定制化方案的核心能力,结合***高效产品与专业设备,病虫害防治率95% ,落果减少30%
+                <div class="report-excute" v-for="(item, index) in workItem?.executeEvidence" :key="index">
+                    <!-- <album-carousel
+                        :key="index"
+                        labelText="执行照片"
+                        :images="[item]"
+                    ></album-carousel> -->
+                    <div class="tag-label">执行照片</div>
+                    <album-draw-box :isShowNum="0" :photo="item" :current="index" :index="index" :length="workItem?.executeEvidence?.length"></album-draw-box>
+                </div>
+                <div class="report-excute">
+                    <album-carousel
+                        :key="93"
+                        :isAchievementImgs="true"
+                        :images="combinedReviewImages"
+                    ></album-carousel>
                 </div>
             </div>
 
-            <div class="report-excute" v-for="(item, index) in executeViewImage" :key="index">
-                <album-carousel
-                    :key="index"
-                    labelText="执行照片"
-                    :images="[item]"
-                ></album-carousel>
-            </div>
-            <div class="report-excute">
-                <!-- <album-carousel
-                    :key="2"
-                    labelText="执行照片"
-                    :images="executeViewImage2"
-                ></album-carousel> -->
+            <div class="bottom-btn">
+                <div class="btn-item second" @click="handleDownload">保存图片</div>
+                <div class="btn-item primay">转发</div>
             </div>
         </div>
 
-        <el-button class="download-btn" type="primary" @click="handleDownload">转发</el-button>
+        <!-- 组合照片(用于生成合成图片) -->
+        <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="workItem?.executeEvidence?.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=""
+                        /> -->
+                    <img
+                        class="review-image-item-img left-img"
+                        :src="leftCoverImg"
+                        style="width: 100%; height: 255px; display: block; image-rendering: auto"
+                    />
+                </div>
+                <div class="review-image-item" v-if="workItem?.reviewImage?.length">
+                    <!-- <img
+                            class="review-image-item-img right-img"
+                            :src="base_img_url2 + workItem.reviewImage[workItem.reviewImage.length - 1]"
+                            alt=""
+                        /> -->
+                    <img
+                        class="review-image-item-img right-img"
+                        :src="rightCoverImg"
+                        style="width: 100%; height: 255px; display: block; image-rendering: auto"
+                    />
+                </div>
+            </div>
+        </div>
     </div>
 </template>
 
 <script setup>
 import CustomHeader from "@/components/customHeader.vue";
 import AlbumCarousel from "@/components/album_compoents/albumCarousel";
-import { ref } from "vue";
+import { ref, onActivated, nextTick, watch } from "vue";
 import html2canvas from "html2canvas";
 import { uploadBase64 } from "@/common/uploadImg";
 import { detectRuntimeEnvironment } from "@/common/commonFun";
+import { useRoute } from "vue-router";
+import { base_img_url2 } from "@/api/config";
+import AlbumDrawBox from "@/components/album_compoents/albumDrawBox.vue";
 
-const reportBoxList = ref([
-    {
-        title: "作业农事",
-        content: "梢期杀虫",
-    },
-    {
-        title: "作业面积",
-        content: "200亩",
-    },
-    {
-        title: "作业周期",
-        content: "2天",
-    },
-    {
-        title: "使用设备",
-        content: "无人机NI-7",
-    },
-]);
-
-const executeViewImage = ref([
-    {
-  "address": "",
-  "angle": "",
-  "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": "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,
-  "gardenName": "莞荔园",
-  "growText": "当前进入冬梢。\n\n\n\n    暂无病虫害\n\n    暂无生长异常\n",
-  "id": "787625848910909520",
-  "localPath": "",
-  "localResPath": "",
-  "location": "POINT (113.746646 22.970229)",
-  "miniUserId": null,
-  "miniUserName": "",
-  "modelId": null,
-  "phenologyId": "18",
-  "pingzhong": "",
-  "planId": null,
-  "regionId": null,
-  "regionName": "2区",
-  "resFilename": "",
-  "shotCode": "",
-  "source": "dji",
-  "sourceCode": null,
-  "status": null,
-  "sysId": null,
-  "treeCode": "莞荔园_M2_064",
-  "treeGeoHash": "ws0fsmgq9uhp",
-  "treeId": 111594,
-  "uploadDate": "2025-12-13",
-  "watermarkMsg": "",
-  "watermarkUrl": "",
-  "watermarks": [],
-  "weather": null,
-  "workspaceId": ""
-}
-]);
-
-const executeViewImage2 = ref(["birdseye-look-mini/91754/1763373487891.png"]);
+const route = useRoute();
+const loading = ref(false);
+const workItem = ref({});
 
+function formatArea(val) {
+    const num = typeof val === "number" ? val : parseFloat(val);
+    if (Number.isNaN(num)) return val;
+    return Number.isInteger(num) ? num : num.toFixed(2) + "亩";
+}
+const paramsPage = ref({});
+onActivated(() => {
+    window.scrollTo(0, 0);
+    paramsPage.value = route.query.miniJson ? JSON.parse(route.query.miniJson) : {};
+    getDetail();
+});
 
+const getDetail = () => {
+    if (!paramsPage.value.id) return;
+    loading.value = true;
+    VE_API.z_farm_work_record
+        .getDetail({ id: paramsPage.value.id })
+        .then(({ data }) => {
+            workItem.value = data[0];
+        })
+        .finally(() => {
+            loading.value = false;
+        });
+};
 
 const isDowload = ref(true);
 const reportDom = ref(null);
@@ -166,7 +192,7 @@ async function handleDownload() {
                 //     }
                 // });
             } else {
-                downloadImage(image,'果园报告')
+                downloadImage(image, "果园报告");
             }
             isDowload.value = true;
         } catch (error) {
@@ -183,6 +209,107 @@ function downloadImage(dataUrl, filename) {
     link.click();
     document.body.removeChild(link);
 }
+
+const reviewComboRef = ref(null);
+const combinedReviewImages = ref([]);
+// 生成组合照片,传给相册组件
+const generateCombinedReviewImage = async () => {
+    try {
+        await prepareCoverImages();
+        await nextTick();
+
+        const canvas = await html2canvas(reviewComboRef.value, {
+            backgroundColor: null,
+            useCORS: true,
+            allowTaint: true,
+            scale: window.devicePixelRatio || 2,
+        });
+
+        combinedReviewImages.value = [canvas.toDataURL("image/png")];
+    } catch (e) {
+        console.error("生成组合照片失败", e);
+    }
+};
+
+const prepareCoverImages = async () => {
+    await nextTick();
+
+    const itemEl = reviewComboRef.value.querySelector(".review-image-item");
+
+    const cssWidth = itemEl.offsetWidth;
+    const cssHeight = 255;
+
+    if (workItem.value?.executeEvidence?.length) {
+        leftCoverImg.value = await coverImageToBase64HD(
+            base_img_url2 + workItem.value.executeEvidence.at(-1),
+            cssWidth,
+            cssHeight
+        );
+    }
+
+    if (workItem.value?.reviewImage?.length) {
+        rightCoverImg.value = await coverImageToBase64HD(
+            base_img_url2 + workItem.value.reviewImage.at(-1),
+            cssWidth,
+            cssHeight
+        );
+    }
+};
+
+const leftCoverImg = ref("");
+const rightCoverImg = ref("");
+
+function coverImageToBase64HD(imgUrl, cssWidth, cssHeight) {
+    return new Promise((resolve, reject) => {
+        const dpr = window.devicePixelRatio || 2;
+
+        const img = new Image();
+        img.crossOrigin = "anonymous";
+        img.src = imgUrl;
+
+        img.onload = () => {
+            // ⚠️ 用“物理像素”创建 canvas
+            const canvas = document.createElement("canvas");
+            canvas.width = cssWidth * dpr;
+            canvas.height = cssHeight * dpr;
+
+            const ctx = canvas.getContext("2d");
+            ctx.scale(dpr, dpr);
+
+            const imgRatio = img.width / img.height;
+            const targetRatio = cssWidth / cssHeight;
+
+            let sx = 0,
+                sy = 0,
+                sw = img.width,
+                sh = img.height;
+
+            if (imgRatio > targetRatio) {
+                sw = img.height * targetRatio;
+                sx = (img.width - sw) / 2;
+            } else {
+                sh = img.width / targetRatio;
+                sy = (img.height - sh) / 2;
+            }
+
+            ctx.drawImage(img, sx, sy, sw, sh, 0, 0, cssWidth, cssHeight);
+
+            resolve(canvas.toDataURL("image/png"));
+        };
+
+        img.onerror = reject;
+    });
+}
+
+watch(
+    () => [workItem.value.executeEvidence, workItem.value.reviewImage],
+    ([preImgs, reviewImgs]) => {
+        if (preImgs && preImgs.length && reviewImgs && reviewImgs.length) {
+            generateCombinedReviewImage();
+        }
+    },
+    { deep: true }
+);
 </script>
 
 <style lang="scss" scoped>
@@ -192,25 +319,68 @@ function downloadImage(dataUrl, filename) {
     background: linear-gradient(195.35deg, #d4e4ff 16.34%, rgba(93, 189, 255, 0) 50.3%),
         linear-gradient(156.64deg, rgba(255, 255, 255, 0.16) 27.7%, rgba(255, 255, 255, 0) 72.82%);
 
+    .report-content-wrap {
+        height: calc(100% - 40px);
+        padding-bottom: 60px;
+        overflow: auto;
+        box-sizing: border-box;
+        position: relative;
+        .bottom-btn {
+            position: fixed;
+            bottom: 0;
+            left: 0;
+            width: 100%;
+            background: #fff;
+            height: 60px;
+            display: flex;
+            align-items: center;
+            justify-content: space-between;
+            padding: 0 12px;
+            box-sizing: border-box;
+            box-shadow: 2px 2px 4.5px 0px rgba(0, 0, 0, 0.4);
+            .btn-item {
+                height: 40px;
+                line-height: 40px;
+                padding: 0 24px;
+                border-radius: 20px;
+                font-size: 14px;
+                &.second {
+                    color: #666666;
+                    border: 1px solid rgba(153, 153, 153, 0.5);
+                }
+                &.primay {
+                    padding: 0 34px;
+                    background: linear-gradient(180deg, #76c3ff, #2199f8);
+                    color: #fff;
+                }
+            }
+        }
+    }
+    
+    .code-icon {
+        position: absolute;
+        right: 10px;
+        top: 12px;
+        width: 48px;
+    }
     .report-content {
         background: url("@/assets/img/home/report_bg.png") no-repeat center center;
         background-size: 100% auto;
         background-position: top center;
-        height: calc(100% - 40px);
-        overflow: auto;
         padding: 24px 16px 16px;
         box-sizing: border-box;
+        position: relative;
         .report-header {
             position: relative;
             .header-book {
                 position: absolute;
                 right: 0;
-                top: 0;
-                width: 160px;
-                height: 160px;
+                bottom: -6px;
+                height: 88px;
+                z-index: 10;
             }
             .time-tag {
-                background: linear-gradient(137.86deg, #9FD5FF 5.87%, #2199F8 82.98%);
+                background: linear-gradient(137.86deg, #9fd5ff 5.87%, #2199f8 82.98%);
                 border-radius: 5px 0 5px 0;
                 height: 23px;
                 line-height: 23px;
@@ -246,7 +416,7 @@ function downloadImage(dataUrl, filename) {
                     }
                     .info-text {
                         font-size: 14px;
-                        color: #2199F8;
+                        color: #2199f8;
                     }
                 }
                 .info-item + .info-item {
@@ -259,8 +429,8 @@ function downloadImage(dataUrl, filename) {
             display: flex;
             align-items: center;
             padding: 8px;
-            background: linear-gradient(0deg, #FFFFFF 86.32%, #2199F8 136.87%);
-            border: 1px solid #FFFFFF;
+            background: linear-gradient(0deg, #ffffff 86.32%, #2199f8 136.87%);
+            border: 1px solid #ffffff;
             border-radius: 8px;
             gap: 5px;
             position: relative;
@@ -270,7 +440,7 @@ function downloadImage(dataUrl, filename) {
                 background: rgba(33, 153, 248, 0.1);
                 border-radius: 8px;
                 .item-content {
-                    color: #2199F8;
+                    color: #2199f8;
                     font-size: 14px;
                     text-align: center;
                 }
@@ -289,8 +459,8 @@ function downloadImage(dataUrl, filename) {
                 height: 32px;
                 line-height: 26px;
                 padding: 0 10px;
-                color: #FFFFFF;
-                background: url("@/assets/img/home/title-bg.png") no-repeat center center /100% 100%;
+                color: #ffffff;
+                background: url("@/assets/img/home/title-bg.png") no-repeat center center / 100% 100%;
             }
             .box-text {
                 padding: 22px 0 12px 0;
@@ -300,7 +470,25 @@ function downloadImage(dataUrl, filename) {
             margin-top: 20px;
         }
         .report-excute {
+            position: relative;
             margin-top: 12px;
+            .tag-label {
+                position: absolute;
+                top: 0;
+                left: 0;
+                padding: 4px 10px;
+                background: rgba(54, 52, 52, 0.8);
+                color: #fff;
+                font-size: 12px;
+                border-radius: 8px 0 8px 0;
+                z-index: 1;
+            }
+            ::v-deep {
+                .carousel-container .carousel-wrapper .carousel-img {
+                    min-width: calc(100vw - 32px);
+                    width: calc(100vw - 32px);
+                }
+            }
         }
     }
     .download-btn {
@@ -312,5 +500,103 @@ function downloadImage(dataUrl, filename) {
         // width: 100%;
         transform: translateX(-50%);
     }
+
+    .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;
+            // }
+            .review-image-item-img {
+                width: 100%;
+                height: 100%;
+                object-fit: cover;
+                object-position: center;
+            }
+            .left-img {
+                border-radius: 8px 0 0 8px;
+            }
+            .right-img {
+                border-radius: 0 8px 8px 0;
+            }
+        }
+    }
 }
 </style>

+ 147 - 45
src/views/old_mini/modify_work/reviewWork.vue

@@ -82,9 +82,9 @@
                 <div class="info-box bottom-box">
                     <div class="recheck-box">
                         <div class="recheck-ablum">
-                            <div class="img-list over-img-box">
+                            <!-- <div class="img-list over-img-box">
                                 <album-carousel :key="1" labelText="农事前" :images="triggerImg"></album-carousel>
-                            </div>
+                            </div> -->
                             <div class="img-list over-img-box">
                                 <album-carousel
                                     class="execute-img"
@@ -93,7 +93,7 @@
                                     :images="workItem.executeEvidence"
                                 ></album-carousel>
                             </div>
-                            <div
+                            <!-- <div
                                 class="img-list over-img-box"
                                 v-if="workItem.reviewImage && workItem.reviewImage.length"
                             >
@@ -102,11 +102,11 @@
                                     labelText="农事后"
                                     :images="workItem.reviewImage"
                                 ></album-carousel>
-                            </div>
+                            </div> -->
                             <div class="img-list over-img-box" v-if="combinedReviewImages.length">
-                                <album-carousel :key="3" :images="combinedReviewImages"></album-carousel>
+                                <album-carousel :key="3" :isAchievementImgs="true" :images="combinedReviewImages"></album-carousel>
                             </div>
-                            <div class="img-list" v-else>
+                            <div class="img-list" v-if="!workItem.reviewImage || !workItem.reviewImage.length">
                                 <div
                                     class="recheck-text-wrap active"
                                     :class="{
@@ -244,14 +244,9 @@
                 </template>
             </div>
 
-            <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>
-                </template>
-                <template v-else>
-                    <div class="fixed-btn second" @click="handleRemindUser">提醒农户拍照</div>
-                </template>
+            <div class="fixed-btn-wrap center" >
+                    <!-- <div class="fixed-btn more" @click="handleMore">查看更多农事</div> -->
+                    <div class="fixed-btn excute" @click="generateReport">生成成果报告</div>
             </div>
             <div
                 class="fixed-btn-wrap center"
@@ -273,20 +268,41 @@
                         <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
+                        <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=""
+                        /> -->
+                        <img
+                        class="review-image-item-img left-img"
+                        :src="leftCoverImg"
+                        style="
+                            width: 100%;
+                            height: 255px;
+                            display: block;
+                            image-rendering: auto;
+                        "
                         />
+
                     </div>
                     <div class="review-image-item" v-if="workItem?.reviewImage?.length">
-                        <div class="review-image-item-title">农事后</div>
-                        <img
+                        <!-- <img
                             class="review-image-item-img right-img"
                             :src="base_img_url2 + workItem.reviewImage[workItem.reviewImage.length - 1]"
                             alt=""
+                        /> -->
+                        <img
+                        class="review-image-item-img right-img"
+                        :src="rightCoverImg"
+                        style="
+                            width: 100%;
+                            height: 255px;
+                            display: block;
+                            image-rendering: auto;
+                        "
                         />
+
                     </div>
                 </div>
             </div>
@@ -370,23 +386,96 @@ 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);
+  try {
+    await prepareCoverImages()
+    await nextTick()
+
+    const canvas = await html2canvas(reviewComboRef.value, {
+      backgroundColor: null,
+      useCORS: true,
+      allowTaint: true,
+      scale: window.devicePixelRatio || 2,
+    })
+
+    combinedReviewImages.value = [canvas.toDataURL('image/png')]
+  } catch (e) {
+    console.error('生成组合照片失败', e)
+  }
+}
+const prepareCoverImages = async () => {
+  await nextTick()
+
+  const itemEl =
+    reviewComboRef.value.querySelector('.review-image-item')
+
+  const cssWidth = itemEl.offsetWidth
+  const cssHeight = 255
+
+  if (triggerImg.value?.length) {
+    leftCoverImg.value = await coverImageToBase64HD(
+      base_img_url2 + triggerImg.value.at(-1).cloudFilename,
+      cssWidth,
+      cssHeight
+    )
+  }
+
+  if (workItem.value?.reviewImage?.length) {
+    rightCoverImg.value = await coverImageToBase64HD(
+      base_img_url2 + workItem.value.reviewImage.at(-1),
+      cssWidth,
+      cssHeight
+    )
+  }
+}
+
+const leftCoverImg = ref('')
+const rightCoverImg = ref('')
+
+
+function coverImageToBase64HD(imgUrl, cssWidth, cssHeight) {
+  return new Promise((resolve, reject) => {
+    const dpr = window.devicePixelRatio || 2
+
+    const img = new Image()
+    img.crossOrigin = 'anonymous'
+    img.src = imgUrl
+
+    img.onload = () => {
+      // ⚠️ 用“物理像素”创建 canvas
+      const canvas = document.createElement('canvas')
+      canvas.width = cssWidth * dpr
+      canvas.height = cssHeight * dpr
+
+      const ctx = canvas.getContext('2d')
+      ctx.scale(dpr, dpr)
+
+      const imgRatio = img.width / img.height
+      const targetRatio = cssWidth / cssHeight
+
+      let sx = 0, sy = 0, sw = img.width, sh = img.height
+
+      if (imgRatio > targetRatio) {
+        sw = img.height * targetRatio
+        sx = (img.width - sw) / 2
+      } else {
+        sh = img.width / targetRatio
+        sy = (img.height - sh) / 2
+      }
+
+      ctx.drawImage(
+        img,
+        sx, sy, sw, sh,
+        0, 0, cssWidth, cssHeight
+      )
+
+      resolve(canvas.toDataURL('image/png'))
     }
-};
+
+    img.onerror = reject
+  })
+}
+
+
 
 watch(
     () => [triggerImg.value, workItem.value.reviewImage],
@@ -424,6 +513,13 @@ const handleShare = () => {
     reviewPopupRef.value.handleShowPopup(workItem.value.id, preImg, resImg);
 };
 
+const generateReport = () => {
+    router.push({
+        path: "/achievement_report",
+        query: { miniJson: JSON.stringify({ id: workItem.value.id }) },
+    });
+};
+
 const handleRemindUser = () => {
     uploadExecuteRef.value.showPopup({ ...workItem.value, type: "remindUser" });
 };
@@ -607,6 +703,9 @@ const handleUpload = ({ imgArr }) => {
                 }
             }
             .info-box {
+                // padding-top: 12px;
+                display: flex;
+                align-items: center;
                 &.subject-content {
                     border: none;
                 }
@@ -616,9 +715,6 @@ const handleUpload = ({ imgArr }) => {
                 &.bottom-box {
                     flex-direction: column;
                 }
-                padding-top: 12px;
-                display: flex;
-                align-items: center;
                 .info-l {
                     .farm-img {
                         width: 78px;
@@ -816,13 +912,13 @@ const handleUpload = ({ imgArr }) => {
             .upload-wrap {
                 margin-top: 12px;
             }
-            .over-img-box {
-                ::v-deep {
-                    img {
-                        border-radius: 8px;
-                    }
-                }
-            }
+            // .over-img-box {
+            //     ::v-deep {
+            //         img {
+            //             border-radius: 8px;
+            //         }
+            //     }
+            // }
             .img-list {
                 width: 100%;
             }
@@ -982,10 +1078,16 @@ const handleUpload = ({ imgArr }) => {
                 font-size: 12px;
                 color: #fff;
             }
+            // .review-image-item-img {
+            //     width: 100%;
+            //     height: 250px;
+            //     object-fit: cover;
+            // }
             .review-image-item-img {
                 width: 100%;
-                height: 250px;
+                height: 100%;
                 object-fit: cover;
+                object-position: center;
             }
             .left-img {
                 border-radius: 8px 0 0 8px;

+ 0 - 1
src/views/old_mini/task_condition/components/interact.vue

@@ -510,7 +510,6 @@ function handleTimelineAction(item) {
     //     farmId: item.farmId,
     //     workName: item.workName,
     // });
-
     eventBus.emit("activeUpload:show", {
         gardenIdVal: item.farmId,
         needExecutorVal: true,

+ 3 - 3
src/views/old_mini/task_condition/components/task.vue

@@ -8,15 +8,15 @@
         </div>
         <div class="task-list">
             <div class="list-filter">
-                <!-- <div class="filter-item" :class="{ active: activeIndex === 1 }" @click="handleActiveFilter(1)">
-                    已确认({{ taskCounts[1] || 0 }})
-                </div> -->
                 <div class="filter-item" :class="{ active: activeIndex === 2 }" @click="handleActiveFilter(2)">
                     待完成({{ taskCounts[2] || 0 }})
                 </div>
                 <div class="filter-item" :class="{ active: activeIndex === 3 }" @click="handleActiveFilter(3)">
                     已完成({{ taskCounts[3] || 0 }})
                 </div>
+                <div class="filter-item" :class="{ active: activeIndex === 1 }" @click="handleActiveFilter(1)">
+                    已复核({{ taskCounts[1] || 0 }})
+                </div>
             </div>
             <div class="select-group">
                 <el-select

+ 87 - 101
src/views/old_mini/user/farmDetails.vue

@@ -42,22 +42,21 @@
                                 <div
                                     class="farm-work-item"
                                     v-for="(item, index) in farmWorkData"
-                                    :key="item.time"
-                                    :class="{ 'is-estimated': index === farmWorkData.length - 1 }"
+                                    :key="index"
                                 >
                                     <div class="timeline-left">
                                         <div class="timeline-dot"></div>
                                         <div class="timeline-line"></div>
                                     </div>
                                     <div class="timeline-right">
-                                        <div class="farm-work-card" :class="['card-' + item.recordType, { 'is-future': item.isFuture }]">
-                                            <div class="type-box">{{ getRecordTypeText(item.recordType) }}</div>
-                                            <div class="farm-work-date">{{ item.time }}</div>
-                                            <div class="farm-work-desc">
+                                        <div class="farm-work-card" :class="['card-' + getRecordTypeText(item.speakTitleName)]">
+                                            <div class="type-box">{{ item.speakTitleName }}</div>
+                                            <div class="farm-work-date" v-html="item.content.renderedContent"></div>
+                                            <!-- <div class="farm-work-desc">
                                                 <span class="farm-work-action">{{ item.action }}</span>
                                                 <span class="farm-work-content">{{ item.content }}</span>
                                                 <span>{{ item.text }}</span>
-                                            </div>
+                                            </div> -->
                                         </div>
                                     </div>
                                 </div>
@@ -196,6 +195,9 @@ onMounted(() => {
     getFarmWorkList();
     getDetailList();
     getFarmPastServiceCost();
+
+    // 获取图片数据
+    getFarmPhoto();
 });
 
 const farmIdVal = ref(null);
@@ -210,105 +212,87 @@ const getFarmDetail = () => {
 
 const farmWorkData = ref([]);
 const typeObj = {
-    1: '物候进程',
-    2: '农事',
-    3: '异常',
+    物候: '1',
+    异常: '2',
+    病虫: '3',
+    农事: '4',
 }
 
 const getRecordTypeText = (recordType) => {
     return typeObj[recordType];
 }
 const getFarmWorkList = () => {
-    // VE_API.user.getFarmWorkList(paramsPage.value).then(({ data }) => {
-    //     if (data && data.length > 0) {
-    //         farmWorkData.value = data[0] || {};
-    //     }
-    // });
-    farmWorkData.value = [
-        {
-            time: "8/8",
-            action: "进入",
-            content: "花芽分化期",
-            recordType: 1,
-        },
-        {
-            time: "8/1",
-            action: "做了",
-            content: "杀虫",
-            text: "农事",
-            recordType: 2,
-        },
-        {
-            time: "7/30",
-            action: "出现",
-            content: "蒂蛀虫",
-            recordType: 3,
-        },
-        {
-            time: "7/25",
-            action: "预计进入",
-            content: "果实膨大期",
+    VE_API.container_phenology.getFarmSpeakInfo({ farmId: farmIdVal.value }).then(({ data }) => {
+        const list = data || [];
+        const res = list.filter(item => item.content.hasException !== 0);
+        getPhenologyBroadcast();
+        // 只展示前三条数据
+        farmWorkData.value = res.slice(0, 3);
+    });
+};
+
+const getPhenologyBroadcast = () => {
+    VE_API.container_phenology.phenologyBroadcast({ farmId: farmIdVal.value }).then(({ data }) => {
+        farmWorkData.value.push({
+            speakTitleName: "物候",
             isFuture: true,
-            recordType: 1,
-        },
-    ];
+            content: {
+                renderedContent: data.content,
+            },
+        });
+    });
 };
 
-const cropAlbum = ref([
-    {
-        district: "",
-        filename: "birdseye-look-mini/91429/1765361542408.png",
-        fosterCode: "",
-        gardenId: null,
-        gardenName: "",
-        growText: "",
-        id: "786656608116543488",
-        localPath: "",
-        localResPath: "",
-        location: "POINT(113.407189 23.032344)",
-        treeCode: "",
-        treeGeoHash: "ws0eh8utt204",
-    },
-    {
-        address: "",
-        angle: "",
-        baseMap: "",
-        blueZoneId: null,
-        district: "",
-        filename: "birdseye-look-mini/91429/1763371316207.jpg",
-        fosterCode: "",
-        gardenId: null,
-        gardenName: "",
-        growText: "",
-        id: "778308963891417088",
-        localPath: "",
-        localResPath: "",
-        location: "POINT(113.407189 23.032344)",
-        miniUserId: null,
-        miniUserName: "",
-        modelId: null,
-        phenologyId: "39",
-        treeCode: "",
-        treeGeoHash: "ws0eh8utt204",
-        treeId: 223920,
-        uploadDate: "2025-11-17",
-    },
-    {
-        filename: "birdseye-look-mini/91754/1763367875786.jpg",
-        fosterCode: "",
-        gardenId: null,
-        gardenName: "",
-        growText: "",
-        id: "778294553688936448",
-        localPath: "",
-        localResPath: "",
-        location: "POINT(113.407189 23.032344)",
-        treeCode: "",
-        treeGeoHash: "ws0eh8utt204",
-        treeId: 223920,
-        uploadDate: "2025-11-17",
-    },
-]);
+const cropAlbum = ref([]);
+// 获取作物相册的三张照片(参考 patrolPhoto 的获取逻辑)
+const getFarmPhoto = async () => {
+    if (!farmIdVal.value) return;
+    try {
+        // 先获取有图片的日期列表
+        const dateParams = {
+            farmId: farmIdVal.value,
+            pageIndex: 0,
+            limit: 10,
+        };
+        const { data: dateData } = await VE_API.farm.findHasImagesDate(dateParams);
+        const dateList = Array.isArray(dateData) ? dateData : [];
+        if (!dateList.length) {
+            cropAlbum.value = [];
+            return;
+        }
+
+        const result = [];
+        let dateIndex = 0;
+
+        // 按日期依次取图片,直到凑够 3 张或没有更多日期
+        while (result.length < 3 && dateIndex < dateList.length) {
+            const currentDate = dateList[dateIndex];
+            const imgParams = {
+                farmId: farmIdVal.value,
+                date: currentDate,
+            };
+            const { data } = await VE_API.farm.getImageInfo(imgParams);
+            const images = (data && Array.isArray(data.images)) ? data.images : [];
+
+            images.forEach((item) => {
+                if (result.length < 3) {
+                    // 统一存成 filename 字段,供模板使用
+                    result.push({
+                        ...item,
+                        filename: item.resFilename || item.filename,
+                    });
+                }
+            });
+
+            dateIndex += 1;
+        }
+
+        cropAlbum.value = result;
+    } catch (e) {
+        console.error("获取农场相册失败", e);
+        cropAlbum.value = [];
+    }
+};
 
 const detailList = ref([]);
 const getDetailList = () => {
@@ -341,9 +325,10 @@ const handleDetail = (path) => {
 
 const handleShareReport = () => {
     router.push({
-        path: "/farm_report",
+        path: "/farm_photo",
         query: {
-            miniJson: JSON.stringify({ farmId: farmIdVal.value }),
+            // miniJson: JSON.stringify({ farmId: farmIdVal.value }),
+            farmId: farmIdVal.value 
         },
     });
 };
@@ -618,7 +603,7 @@ const onSelect = () => {
                                 color: #2199F8;
                             }
                         }
-                        &.card-2 {
+                        &.card-4 {
                             border: 1px solid #FFBB83;
                             .type-box {
                                 background: #FF953D;
@@ -627,7 +612,7 @@ const onSelect = () => {
                                 color: #FF953D;
                             }
                         }
-                        &.card-3 {
+                        &.card-2, &.card-3 {
                             border: 1px solid #FF9C9C;
                             .type-box {
                                 background: #FD7676;
@@ -652,6 +637,7 @@ const onSelect = () => {
                             height: 20px;
                             line-height: 20px;
                             font-size: 12px;
+                            flex: none;
                             color: #fff;
                             border-radius: 0 4px 0 4px;
                         }

+ 141 - 161
src/views/old_mini/user/subPages/cropRecord.vue

@@ -2,216 +2,196 @@
     <div class="crop-record-page">
         <custom-header name="作物档案"></custom-header>
         <div class="farm-work-timeline">
-                                <div
-                                    class="farm-work-item"
-                                    v-for="(item, index) in farmWorkData"
-                                    :key="item.time"
-                                    :class="{ 'is-estimated': index === farmWorkData.length - 1 }"
-                                >
-                                    <div class="timeline-left">
-                                        <div class="timeline-dot"></div>
-                                        <div class="timeline-line"></div>
-                                    </div>
-                                    <div class="timeline-right">
-                                        <div class="farm-work-card" :class="['card-' + item.recordType, { 'is-future': item.isFuture }]">
-                                            <div class="type-box">{{ getRecordTypeText(item.recordType) }}</div>
-                                            <div class="farm-work-date">{{ item.time }}</div>
-                                            <div class="farm-work-desc">
-                                                <span class="farm-work-action">{{ item.action }}</span>
-                                                <span class="farm-work-content">{{ item.content }}</span>
-                                                <span>{{ item.text }}</span>
-                                            </div>
-                                        </div>
-                                    </div>
-                                </div>
-                            </div>
+            <div class="farm-work-item" v-for="(item, index) in farmWorkData" :key="index">
+                <div class="timeline-left">
+                    <div class="timeline-dot"></div>
+                    <div class="timeline-line"></div>
+                </div>
+                <div class="timeline-right">
+                    <div class="farm-work-card" :class="['card-' + getRecordTypeText(item.speakTitleName), {'is-future': item.isFuture}]">
+                        <div class="type-box">{{ item.speakTitleName }}</div>
+                        <div class="farm-work-date" v-html="item.content.renderedContent"></div>
+                    </div>
+                </div>
+            </div>
+        </div>
     </div>
 </template>
 
 <script setup>
-import CustomHeader from '@/components/customHeader.vue';
-import { ref, onMounted } from 'vue';
+import CustomHeader from "@/components/customHeader.vue";
+import { ref, onMounted } from "vue";
+import { useRoute } from "vue-router";
 
+const route = useRoute();
 const cropRecordList = ref([]);
 
 onMounted(() => {
     getFarmWorkList();
 });
 
-
 const farmWorkData = ref([]);
 const typeObj = {
-    1: '物候进程',
-    2: '农事',
-    3: '异常',
-}
+    物候: "1",
+    异常: "2",
+    病虫: "3",
+    农事: "4",
+};
 
 const getRecordTypeText = (recordType) => {
     return typeObj[recordType];
-}
+};
 const getFarmWorkList = () => {
-    // VE_API.user.getFarmWorkList(paramsPage.value).then(({ data }) => {
-    //     if (data && data.length > 0) {
-    //         farmWorkData.value = data[0] || {};
-    //     }
-    // });
-    farmWorkData.value = [
-        {
-            time: "8/8",
-            action: "进入",
-            content: "花芽分化期",
-            recordType: 1,
-        },
-        {
-            time: "8/1",
-            action: "做了",
-            content: "杀虫",
-            text: "农事",
-            recordType: 2,
-        },
-        {
-            time: "7/30",
-            action: "出现",
-            content: "蒂蛀虫",
-            recordType: 3,
-        },
-        {
-            time: "7/25",
-            action: "预计进入",
-            content: "果实膨大期",
-            isFuture: true,
-            recordType: 1,
-        },
-    ];
+    VE_API.container_phenology.getFarmSpeakInfo({ farmId: route.query.farmId }).then(({ data }) => {
+        const res = data.filter((item) => item.content.hasException !== 0);
+        farmWorkData.value = res || [];
+        getPhenologyBroadcast();
+    });
 };
 
+const getPhenologyBroadcast = () => {
+    VE_API.container_phenology.phenologyBroadcast({ farmId: 93954 }).then(({ data }) => {
+        farmWorkData.value.push({
+            speakTitleName: "物候",
+            isFuture: true,
+            content: {
+                renderedContent: data.content,
+            },
+        });
+    });
+};
 </script>
 
 <style lang="scss" scoped>
 .crop-record-page {
     width: 100%;
     min-height: 100vh;
-    background: #F5F7FB;
+    background: #f5f7fb;
 
-    
     .farm-work-timeline {
         padding: 12px;
         height: calc(100% - 40px);
-            .farm-work-item {
+        .farm-work-item {
+            display: flex;
+            align-items: flex-start;
+            position: relative;
+            &:not(:last-child) {
+                margin-bottom: 20px;
+            }
+            .timeline-left {
                 display: flex;
-                align-items: flex-start;
+                flex-direction: column;
+                align-items: center;
+                margin-right: 12px;
                 position: relative;
-                &:not(:last-child) {
-                    margin-bottom: 20px;
+                .timeline-dot {
+                    width: 6px;
+                    height: 6px;
+                    border-radius: 50%;
+                    border: 1px solid #2199f8;
+                    flex-shrink: 0;
+                    z-index: 1;
+                }
+                .timeline-line {
+                    width: 1px;
+                    height: 48px;
+                    background: #e5e6eb;
+                    position: absolute;
+                    top: 8px;
                 }
-                .timeline-left {
+            }
+            .timeline-right {
+                flex: 1;
+                .farm-work-card {
+                    border: 1px solid #8bccff;
+                    border-radius: 4px;
+                    background: #f8fcff;
+                    padding: 12px;
                     display: flex;
-                    flex-direction: column;
                     align-items: center;
-                    margin-right: 12px;
-                    position: relative;
-                    .timeline-dot {
-                        width: 6px;
-                        height: 6px;
-                        border-radius: 50%;
-                        border: 1px solid #2199f8;
-                        flex-shrink: 0;
-                        z-index: 1;
-                    }
-                    .timeline-line {
-                        width: 1px;
-                        height: 48px;
-                        background: #e5e6eb;
-                        position: absolute;
-                        top: 8px;
-                    }
-                }
-                .timeline-right {
-                    flex: 1;
-                    .farm-work-card {
+                    color: #1d2129;
+                    font-size: 14px;
+                    line-height: 22px;
+                    display: flex;
+                    align-items: center;
+                    &.card-1 {
                         border: 1px solid #8bccff;
-                        border-radius: 4px;
-                        background: #f8fcff;
-                        padding: 12px;
-                        display: flex;
-                        align-items: center;
-                        color: #1d2129;
-                        font-size: 14px;
-                        line-height: 22px;
-                        display: flex;
-                        align-items: center;
-                        &.card-1 {
-                            border: 1px solid #8BCCFF;
-                            .type-box {
-                                background: #2199F8;
-                            }
-                            .farm-work-content {
-                                color: #2199F8;
-                            }
+                        .type-box {
+                            background: #2199f8;
                         }
-                        &.card-2 {
-                            border: 1px solid #FFBB83;
-                            .type-box {
-                                background: #FF953D;
-                            }
-                            .farm-work-content {
-                                color: #FF953D;
-                            }
+                        .farm-work-content {
+                            color: #2199f8;
                         }
-                        &.card-3 {
-                            border: 1px solid #FF9C9C;
-                            .type-box {
-                                background: #FD7676;
-                            }
-                            .farm-work-content {
-                                color: #FD7676;
-                            }
+                    }
+                    &.card-4 {
+                        border: 1px solid #ffbb83;
+                        .type-box {
+                            background: #ff953d;
                         }
-                        &.is-future {
-                            // opacity: 0.5;
-                            background: rgba(246, 250, 255, 0.5);
-                            .type-box {
-                                background: rgba(33, 153, 248, 0.5);
-                            }
-                            .farm-work-content {
-                                color: rgba(29, 33, 41, 0.5);
-                            }
+                        .farm-work-content {
+                            color: #ff953d;
+                        }
+                    }
+                    &.card-2,
+                    &.card-3 {
+                        border: 1px solid #ff9c9c;
+                        .type-box {
+                            background: #fd7676;
                         }
+                        .farm-work-content {
+                            color: #fd7676;
+                        }
+                    }
+                    &.is-future {
+                        padding: 0 12px;
+                        // opacity: 0.5;
+                        background: rgba(246, 250, 255, 0.5);
                         .type-box {
-                            margin-right: 12px;
-                            padding: 0 5px;
-                            height: 20px;
-                            line-height: 20px;
-                            font-size: 12px;
-                            color: #fff;
-                            border-radius: 0 4px 0 4px;
+                            background: rgba(33, 153, 248, 0.5);
+                        }
+                        .farm-work-content {
+                            color: rgba(29, 33, 41, 0.5);
                         }
-                        .farm-work-date {
+                    }
+                    .type-box {
+                        margin-right: 12px;
+                        padding: 0 5px;
+                        height: 20px;
+                        line-height: 20px;
+                        flex: none;
+                        font-size: 12px;
+                        color: #fff;
+                        border-radius: 0 4px 0 4px;
+                    }
+                    .farm-work-date {
+                        padding-right: 4px;
+                        &.is-future {
+                            // color: rgba(29, 33, 41, 0.5);
+                        }
+                    }
+                    .farm-work-desc {
+                        .farm-work-action {
                             padding-right: 4px;
                         }
-                        .farm-work-desc {
-                            .farm-work-action {
-                                padding-right: 4px;
-                            }
-                            .farm-work-content {
-                                padding-right: 4px;
-                            }
+                        .farm-work-content {
+                            padding-right: 4px;
                         }
                     }
                 }
-                &.is-estimated {
-                    .timeline-right {
-                        .farm-work-card {
-                            background: rgba(246, 250, 255, 0.5);
-                            border: 1px solid rgba(190, 218, 255, 0.5);
-                            .farm-work-date,
-                            .farm-work-desc {
-                                color: rgba(29, 33, 41, 0.5);
-                            }
+            }
+            &.is-estimated {
+                .timeline-right {
+                    .farm-work-card {
+                        background: rgba(246, 250, 255, 0.5);
+                        border: 1px solid rgba(190, 218, 255, 0.5);
+                        .farm-work-date,
+                        .farm-work-desc {
+                            color: rgba(29, 33, 41, 0.5);
                         }
                     }
                 }
             }
         }
+    }
 }
-</style>
+</style>