ソースを参照

Merge branch 'agriculture' of http://www.sysuimars.cn:3000/feiniao/feiniao-farm-h5 into agriculture

lxf 1 週間 前
親
コミット
25f7a77214

+ 1 - 1
src/api/modules/entry.js

@@ -13,7 +13,7 @@ module.exports = {
     },
     // 录入用户权属信息
     addPlotInfo: {
-        url: config.base_new_url + "add_plot_info",
+        url: config.base_new_url + "questionnaire/add_plot_info",
         type: "post",
     },
     // 品类列表

+ 10 - 0
src/api/modules/questionnaire.js

@@ -26,4 +26,14 @@ module.exports = {
         url: config.base_new_url + "pheno/inner_interact_tasks",
         type: "get",
     },
+    // 获取农事任务(zone_id + date,返回当日扁平 fw_tasks)
+    getFwTasks: {
+        url: config.base_new_url + "pheno/fw_tasks",
+        type: "get",
+    },
+    // 获取农事详细内容(人工 / 机械方案)
+    getFwTaskDetails: {
+        url: config.base_new_url + "pheno/fw_task_details",
+        type: "get",
+    },
 }

+ 11 - 2
src/views/old_mini/entry_information/components/inviteEntryPopup.vue

@@ -30,13 +30,16 @@ import { Popup } from "vant";
 
 const INVITE_TYPE_KEY = "ENTRY_INVITE_TYPE";
 const STEP_KEY = "ENTRY_INFORMATION_STEP";
+const ADMIN_ID_KEY = "ENTRY_ADMIN_ID";
 
 const router = useRouter();
 const show = ref(false);
 const inviteType = ref("farmer");
+const adminId = ref("");
 
-const open = (type = "farmer") => {
+const open = (type = "farmer", inviterAdminId = "") => {
     inviteType.value = type === "service" ? "service" : "farmer";
+    adminId.value = inviterAdminId ? String(inviterAdminId) : "";
     show.value = true;
 };
 
@@ -48,9 +51,15 @@ const handleConfirm = () => {
     close();
     sessionStorage.setItem(INVITE_TYPE_KEY, inviteType.value);
     sessionStorage.removeItem(STEP_KEY);
+    if (adminId.value) {
+        sessionStorage.setItem(ADMIN_ID_KEY, adminId.value);
+    }
     router.push({
         path: "/entry_information",
-        query: { inviteType: inviteType.value },
+        query: {
+            inviteType: inviteType.value,
+            ...(adminId.value ? { adminId: adminId.value } : {}),
+        },
     });
 };
 

+ 36 - 16
src/views/old_mini/entry_information/components/manualFarmingService.vue

@@ -306,35 +306,50 @@ function getPhenophaseLabel(item) {
     return item.phenophase || item.phenologyName || String(item.phenologyId || "");
 }
 
+function getUserId() {
+    const id = localStorage.getItem("MINI_USER_ID");
+    return id != null && id !== "" ? Number(id) : 0;
+}
+
+/** 邀请录入时为邀请人 id,自主录入时与 user_id 相同 */
+function getAdminId() {
+    const fromSession = sessionStorage.getItem("ENTRY_ADMIN_ID");
+    if (fromSession != null && fromSession !== "") return Number(fromSession);
+    return getUserId();
+}
+
+/** 第一步常驻点位选点时写入的区县 adcode */
+function getCountyCode() {
+    const location = readJson(LOCATION_KEY);
+    return location?.adcode ? String(location.adcode) : "";
+}
+
 function buildPlotPayload() {
     const baForm = readJson("ENTRY_BA_FORM") || {};
-    const varietyList = getVarietyDraftList();
+    const varietyList = getVarietyDraftList().filter((item) => item?.id != null && item.id !== "");
     const equipmentCache = readJson(EQUIPMENT_KEY);
     const equipmentList = Array.isArray(equipmentCache)
         ? equipmentCache
         : [...(equipmentCache?.field || []), ...(equipmentCache?.fruit || [])];
-    const fieldSelected = fieldTaskList.value.filter((item) => selectedIds.has(String(item.id)));
-    const fruitSelected = allFruitTasks.value.filter((item) => selectedIds.has(String(item.id)));
-    const taskSelected = [...fieldSelected, ...fruitSelected];
+
     return {
         user_name: baForm.name,
         tel: baForm.phone,
-        team_name: baForm.teamName || "",
-        team_type: baForm.teamType || "",
-        invite_type: baForm.inviteType || "service",
+        admin_id: getAdminId(),
+        user_id: getUserId(),
+        county_code: getCountyCode(),
+        machine_id: equipmentList
+            .map((item) => Number(item.id))
+            .filter((id) => Number.isFinite(id)),
         crops: varietyList.map((item) => ({
             crop_type: item.categoryName,
             crop_id: Number(item.categoryId),
-            variety_list: [Number(item.id)],
+            variety_list: [String(item.id)],
             phenophase: getPhenophaseLabel(item),
             start_time: item.startTime,
             plant_area: Number(item.area),
             point: item.location,
         })),
-        farm_machines: equipmentList.map((item) => ({ id: item.id, name: item.name })),
-        farm_tasks: taskSelected.map((item) => ({ id: item.id, name: item.name })),
-        field_tasks: fieldSelected.map((item) => ({ id: item.id, name: item.name })),
-        fruit_tasks: fruitSelected.map((item) => ({ id: item.id, name: item.name })),
     };
 }
 
@@ -349,9 +364,10 @@ function clearEntrySession() {
     sessionStorage.removeItem(TASK_VIEW_KEY);
     sessionStorage.removeItem(STEP_KEY);
     sessionStorage.removeItem("ENTRY_INVITE_TYPE");
+    sessionStorage.removeItem("ENTRY_ADMIN_ID");
 }
 
-const handleSubmit = () => {
+const handleSubmit = async () => {
     if (submitting.value) return;
     const baForm = readJson("ENTRY_BA_FORM");
     if (!baForm?.name || !baForm?.phone) {
@@ -361,9 +377,13 @@ const handleSubmit = () => {
     submitting.value = true;
     try {
         const params = buildPlotPayload();
-        console.log("entry submit params", params);
-        showSuccessPopup.value = true;
-        clearEntrySession();
+        const res = await VE_API.entry.addPlotInfo(params);
+        if (res.code === 200) {
+            clearEntrySession();
+            showSuccessPopup.value = true;
+        } else {
+            ElMessage.error(res.msg || "提交失败,请稍后再试");
+        }
     } catch (error) {
         console.error("entry submit failed", error);
         ElMessage.error("提交失败");

+ 32 - 39
src/views/old_mini/entry_information/components/selectEquipment.vue

@@ -116,6 +116,7 @@ const SESSION = {
     BA_FORM: "ENTRY_BA_FORM",
     MANUAL_VIEW: "ENTRY_MANUAL_VIEW",
     INVITE_TYPE: "ENTRY_INVITE_TYPE",
+    ADMIN_ID: "ENTRY_ADMIN_ID",
 };
 
 /** 用户手动添加的农机归入此分组,始终置顶 */
@@ -461,52 +462,49 @@ function getPhenophaseLabel(item) {
     return item.phenophase || item.phenologyName || String(item.phenologyId || "");
 }
 
-function flattenTasks(taskCache) {
-    if (!taskCache) return [];
-    if (Array.isArray(taskCache)) return taskCache;
-    return [...(taskCache.field || []), ...(taskCache.fruit || [])];
+function getUserId() {
+    const id = localStorage.getItem("MINI_USER_ID");
+    return id != null && id !== "" ? Number(id) : 0;
+}
+
+/** 邀请录入时为邀请人 id,自主录入时与 user_id 相同 */
+function getAdminId() {
+    const fromSession = sessionStorage.getItem(SESSION.ADMIN_ID);
+    if (fromSession != null && fromSession !== "") return Number(fromSession);
+    return getUserId();
+}
+
+/** 第一步常驻点位选点时写入的区县 adcode */
+function getCountyCode() {
+    const location = readJson(SESSION.LOCATION);
+    return location?.adcode ? String(location.adcode) : "";
 }
 
 /** 汇总各步骤 session 数据,供 addPlotInfo 提交 */
 function buildPlotPayload(includeEquipment = true) {
     const baForm = readJson(SESSION.BA_FORM) || {};
-    const varietyList = getVarietyDraftList();
+    const varietyList = getVarietyDraftList().filter((item) => item?.id != null && item.id !== "");
     const equipmentCache = readJson(SESSION.EQUIPMENT);
     const equipmentList = includeEquipment ? flattenEquipment(equipmentCache) : [];
-    const taskCache = readJson(SESSION.TASK);
-    const taskList = flattenTasks(taskCache);
 
     return {
         user_name: baForm.name,
         tel: baForm.phone,
-        team_name: baForm.teamName || "",
-        team_type: baForm.teamType || "",
-        invite_type: baForm.inviteType || (props.isService ? "service" : "farmer"),
+        admin_id: getAdminId(),
+        user_id: getUserId(),
+        county_code: getCountyCode(),
+        machine_id: equipmentList
+            .map((item) => Number(item.id))
+            .filter((id) => Number.isFinite(id)),
         crops: varietyList.map((item) => ({
             crop_type: item.categoryName,
             crop_id: Number(item.categoryId),
-            variety_list: [Number(item.id)],
+            variety_list: [String(item.id)],
             phenophase: getPhenophaseLabel(item),
             start_time: item?.startTime,
             plant_area: Number(item.area),
             point: item.location,
         })),
-        farm_machines: equipmentList.map((item) => ({
-            id: item.id,
-            name: item.name,
-        })),
-        farm_tasks: taskList.map((item) => ({
-            id: item.id,
-            name: item.name,
-        })),
-        field_tasks: (taskCache?.field || []).map((item) => ({
-            id: item.id,
-            name: item.name,
-        })),
-        fruit_tasks: (taskCache?.fruit || []).map((item) => ({
-            id: item.id,
-            name: item.name,
-        })),
     };
 }
 
@@ -531,18 +529,13 @@ async function submitEntry(includeEquipment = true) {
     submitting.value = true;
     try {
         const params = buildPlotPayload(includeEquipment);
-        console.log("entry submit params", params);
-        // const res = await VE_API.entry.addPlotInfo(params);
-        // if (res.code === 200) {
-        //     ElMessage.success(res.msg || "提交成功");
-        //     clearEntrySession();
-        //     emit("confirm", params);
-        // } else {
-        //     ElMessage.error(res.msg || "提交失败,请稍后再试");
-        // }
-
-        showSuccessPopup.value = true;
-        clearEntrySession();
+        const res = await VE_API.entry.addPlotInfo(params);
+        if (res.code === 200) {
+            clearEntrySession();
+            showSuccessPopup.value = true;
+        } else {
+            ElMessage.error(res.msg || "提交失败,请稍后再试");
+        }
     } catch (error) {
         console.error("entry submit failed", error);
         ElMessage.error("提交失败");

+ 9 - 0
src/views/old_mini/entry_information/index.vue

@@ -48,6 +48,7 @@ import manualFarmingService from "./components/manualFarmingService.vue";
 const STEP_KEY = "ENTRY_INFORMATION_STEP";
 const INVITE_TYPE_KEY = "ENTRY_INVITE_TYPE";
 const CATEGORY_SESSION_KEY = "ENTRY_SELECTED_CATEGORY";
+const ADMIN_ID_KEY = "ENTRY_ADMIN_ID";
 
 const router = useRouter();
 const route = useRoute();
@@ -66,6 +67,13 @@ function readInviteType() {
     return sessionStorage.getItem(INVITE_TYPE_KEY) || "farmer";
 }
 
+function syncAdminIdFromQuery() {
+    const fromQuery = route.query.adminId;
+    if (fromQuery != null && fromQuery !== "") {
+        sessionStorage.setItem(ADMIN_ID_KEY, String(fromQuery));
+    }
+}
+
 const currentStep = ref(readStep());
 const inviteType = ref(readInviteType());
 const isServiceEntry = computed(() => inviteType.value === "service");
@@ -127,6 +135,7 @@ function clampStep() {
 
 function syncEntryState() {
     inviteType.value = readInviteType();
+    syncAdminIdFromQuery();
     currentStep.value = readStep();
     refreshHasField();
     clampStep();

+ 15 - 2
src/views/old_mini/growth_report/index.vue

@@ -263,11 +263,12 @@ const handleInvite = (type) => {
         // ignore
     }
     const inviteType = isService ? "service" : "farmer";
+    const adminId = localStorage.getItem("MINI_USER_ID") || "";
     const query = {
         askInfo: { title: "邀请完善信息", content: "是否分享该邀请给好友" },
         shareText: isService ? "邀请您完善农服信息" : "邀请您完善地块种植信息",
         targetUrl: `growth_report`,
-        paramsPage: JSON.stringify({ inviteName, showInviteEntry: 1, inviteType }),
+        paramsPage: JSON.stringify({ inviteName, showInviteEntry: 1, inviteType, adminId }),
         imageUrl: 'https://birdseye-img.sysuimars.com/temp/field.png',
     };
     wx.miniProgram.navigateTo({
@@ -299,6 +300,16 @@ function getInviteTypeFromRoute() {
     return candidates.find((item) => item === "service" || item === "farmer") || "farmer";
 }
 
+function getAdminIdFromRoute() {
+    const candidates = [
+        route.query.adminId,
+        parseMaybeJson(route.query.paramsPage)?.adminId,
+        parseMaybeJson(route.query.miniJson)?.adminId,
+        parseMaybeJson(parseMaybeJson(route.query.miniJson)?.paramsPage)?.adminId,
+    ];
+    return candidates.find((item) => item != null && item !== "") || "";
+}
+
 function shouldShowInviteEntryPopup() {
     if (isTruthyFlag(route.query.showInviteEntry)) return true;
 
@@ -319,6 +330,7 @@ function clearInviteEntryQuery() {
     if (!shouldShowInviteEntryPopup()) return;
     const newQuery = { ...(route.query || {}) };
     delete newQuery.showInviteEntry;
+    delete newQuery.adminId;
     if (newQuery.paramsPage) {
         const paramsPage = parseMaybeJson(newQuery.paramsPage);
         if (paramsPage && typeof paramsPage === "object") {
@@ -346,8 +358,9 @@ function clearInviteEntryQuery() {
 function tryOpenInviteEntryPopup() {
     if (!shouldShowInviteEntryPopup()) return;
     const inviteType = getInviteTypeFromRoute();
+    const adminId = getAdminIdFromRoute();
     nextTick(() => {
-        invitePopupRef.value?.open(inviteType);
+        invitePopupRef.value?.open(inviteType, adminId);
         clearInviteEntryQuery();
     });
 }

+ 135 - 20
src/views/old_mini/work_detail/index.vue

@@ -13,13 +13,18 @@
             <div class="content-status" :class="{'done-status': statusVal === 'completed'}">
                 <div class="status-l">
                     <div class="status-title">
-                        <span>08.18 {{ farmData.work_name }}</span>
-                        <!-- <span class="status-sub small-tag mt-4" :style="statusTagStyle[farmData?.farm_work_type]">标准类</span> -->
-                        <!-- <span class="status-sub small-tag mt-4 isAbnormal" :style="statusTagStyle[farmData?.farm_work_type]">异常类</span> -->
-                        <span class="status-sub small-tag mt-4 isWeather" :style="statusTagStyle[farmData?.farm_work_type]">气象类</span>
-                        <span class="status-sub small-tag mt-4 isHighRisk" :style="statusTagStyle[farmData?.farm_work_type]">高风险</span>
+                        <span>{{ displayDate }} {{ displayName }}</span>
+                        <template v-if="isFwTaskMode">
+                            <span v-if="taskDetail?.fw_status" class="status-sub small-tag mt-4">{{ taskDetail.fw_status }}</span>
+                        </template>
+                        <template v-else>
+                            <!-- <span class="status-sub small-tag mt-4" :style="statusTagStyle[farmData?.farm_work_type]">标准类</span> -->
+                            <!-- <span class="status-sub small-tag mt-4 isAbnormal" :style="statusTagStyle[farmData?.farm_work_type]">异常类</span> -->
+                            <span class="status-sub small-tag mt-4 isWeather" :style="statusTagStyle[farmData?.farm_work_type]">气象类</span>
+                            <span class="status-sub small-tag mt-4 isHighRisk" :style="statusTagStyle[farmData?.farm_work_type]">高风险</span>
+                        </template>
                     </div>
-                    <div class="status-hint-card" v-if="farmData?.work_status === 0">
+                    <div class="status-hint-card" v-if="!isFwTaskMode && farmData?.work_status === 0">
                         <div class="status-hint-text">{{ farmData?.best_time }}</div>
                         <div class="status-hint-question">
                             <el-icon class="status-hint-link-icon">
@@ -28,7 +33,7 @@
                             <span class="status-hint-question-text">{{ farmData.interaction_issue }}</span>
                         </div>
                     </div>
-                    <div class="status-sub-wrap" :style="getFarmWorkTypeColorStyle(farmData)" v-else>
+                    <div class="status-sub-wrap" :style="getFarmWorkTypeColorStyle(farmData)" v-else-if="!isFwTaskMode">
                         <div v-if="statusVal !== 'completed'" class="status-sub" :style="statusTagStyle[farmData?.farm_work_type]">{{ farmWorkTypeObj[farmData?.farm_work_type] }}</div>
                         <div v-if="statusVal === 'completed'" class="status-sub done-status__text">
                             <el-icon size="14" class="done-status__icon"><SuccessFilled /></el-icon>
@@ -48,10 +53,10 @@
 
             <div class="work-wrap" :class="{ 'from-share': isFromShare }">
                 <!-- 农事组信息 -->
-                <div class="group-info group-box" v-if="farmData.interaction_reason">
+                <div class="group-info group-box" v-if="displayExplain">
                     <div class="group-name">
                         <div class="group-title">农情研判:</div>
-                        {{ farmData.interaction_reason }}
+                        {{ displayExplain }}
                     </div>
                 </div>
 
@@ -60,7 +65,7 @@
                         <div class="execution-area-card" @click="handleViewArea">
                             <div class="execution-area-card__header">
                                 <span class="title">{{ $t('agriRecordDetail.executionArea') }}</span>
-                                <span class="code">ws0gefwg9tdn</span>
+                                <span class="code">{{ displayAreaCode }}</span>
                             </div>
                             <div class="map-container" ref="mapContainer"></div>
                         </div>
@@ -109,16 +114,16 @@
                         <div class="info-item">
                             <div class="info-title"><span class="title-block"></span>{{ $t('workDetail.prescription') }}
                             </div>
-                            <div class="info-value">{{ farmData.drug_prescription }}</div>
+                            <div class="info-value">{{ displayPrescription }}</div>
                         </div>
                         <div class="info-item">
                             <div class="info-title"><span class="title-block"></span>{{ $t('workDetail.executeParameters')
                                 }}</div>
-                            <div class="info-value">{{ farmData.execution_method }}</div>
+                            <div class="info-value">{{ displayParams }}</div>
                         </div>
                         <div class="info-item">
                             <div class="info-title"><span class="title-block"></span>{{ $t('workDetail.notes') }}</div>
-                            <div class="info-value">{{ farmData.precautions }}</div>
+                            <div class="info-value">{{ displayNotes }}</div>
                         </div>
                     </div>
                 </div>
@@ -143,7 +148,7 @@
             </div>
         </div>
 
-        <div v-if="statusVal !== 'completed'" class="warning-info" :class="{ 'from-share': isFromShare }">
+        <div v-if="isAbnormalWork && statusVal !== 'completed'" class="warning-info" :class="{ 'from-share': isFromShare }">
             <div class="warning-l">
                 <div class="warning-title">
                     <el-icon size="16" color="#FF953D"><WarningFilled /></el-icon>
@@ -221,6 +226,29 @@ function resolvePageParams() {
 const pageParams = computed(() => resolvePageParams());
 const workId = computed(() => pageParams.value?.id ?? null);
 
+const hasQueryValue = (v) => v != null && v !== "";
+const toQueryNum = (v) => {
+    if (!hasQueryValue(v)) return undefined;
+    const n = Number(v);
+    return Number.isNaN(n) ? undefined : n;
+};
+
+/** 列表进入:带 zone_id + date + 人工/机械 id,走 fw_task_details */
+const isFwTaskMode = computed(() => {
+    const p = pageParams.value || {};
+    return hasQueryValue(p.zone_id) && hasQueryValue(p.date) && (hasQueryValue(p.manual_id) || hasQueryValue(p.mechanical_id));
+});
+
+const fwTaskQuery = computed(() => {
+    const p = pageParams.value || {};
+    return {
+        zone_id: toQueryNum(p.zone_id),
+        date: p.date,
+        manual_id: toQueryNum(p.manual_id),
+        mechanical_id: toQueryNum(p.mechanical_id),
+    };
+});
+
 /** 通过分享进入:无返回箭头、无底部转发 */
 const isFromShare = computed(() => isTruthyFlag(pageParams.value?.fromShare));
 
@@ -282,6 +310,7 @@ onMounted(() => {
 
 const miniJson = ref(null);
 const farmData = ref({});
+const taskDetail = ref(null); // fw_task_details 返回
 
 const wktFmt = new WKT();
 const MAP_CENTER_FALLBACK = "POINT(113.1093017627431 22.57454083668)";
@@ -317,8 +346,18 @@ function destroyWorkDetailMap() {
 function initWorkDetailMap() {
     nextTick(() => {
         if (!mapContainer.value) return;
-        const zones = farmZones.value;
         areaMap.destroyMap();
+        if (isFwTaskMode.value) {
+            const lat = Number(pageParams.value?.lat);
+            const lng = Number(pageParams.value?.lng);
+            const point =
+                Number.isFinite(lat) && Number.isFinite(lng)
+                    ? `POINT(${lng} ${lat})`
+                    : MAP_CENTER_FALLBACK;
+            areaMap.initMap(point, mapContainer.value);
+            return;
+        }
+        const zones = farmZones.value;
         areaMap.initMap(wktCenterPointFromZones(zones), mapContainer.value);
         areaMap.initZones(zones);
     });
@@ -374,7 +413,27 @@ const backgroundFarmWorkStatus = (status) => {
     }
 }
 
+const fetchFwTaskDetails = () => {
+    const query = fwTaskQuery.value;
+    if (query.zone_id == null || !query.date) return;
+    farmData.value = {};
+    const params = { zone_id: query.zone_id, date: query.date };
+    if (query.manual_id != null) params.manual_id = query.manual_id;
+    if (query.mechanical_id != null) params.mechanical_id = query.mechanical_id;
+    VE_API.questionnaire.getFwTaskDetails(params).then((res) => {
+        if (res?.code === 200 && res.data) {
+            taskDetail.value = res.data;
+            initWorkDetailMap();
+        }
+    });
+};
+
 const getDetail = () => {
+    if (isFwTaskMode.value) {
+        fetchFwTaskDetails();
+        return;
+    }
+    taskDetail.value = null;
     const id = workId.value;
     if (id == null || id === "") return;
     VE_API.monitor.getWorkDetail({ id }).then(res => {
@@ -405,6 +464,50 @@ const methodTabs = [
 ];
 const executeMethod = ref("machine");
 
+const currentMethodDetail = computed(() => {
+    const data = taskDetail.value;
+    if (!data) return null;
+    return executeMethod.value === "manual" ? data.manual : data.mechanical;
+});
+
+const formatTitleDate = (dateStr) => {
+    if (!dateStr) return "";
+    return String(dateStr).replace(/^\d{4}-(\d{2})-(\d{2})$/, "$1.$2");
+};
+
+const displayDate = computed(() => {
+    if (pageParams.value?.date) return formatTitleDate(pageParams.value.date);
+    return "08.18";
+});
+const displayName = computed(
+    () => taskDetail.value?.fw_name || pageParams.value?.fw_name || farmData.value?.work_name || ""
+);
+const displayExplain = computed(
+    () => taskDetail.value?.fw_explain || farmData.value?.interaction_reason || ""
+);
+const displayPrescription = computed(
+    () => currentMethodDetail.value?.fw_precrip || farmData.value?.drug_prescription || ""
+);
+const displayParams = computed(
+    () => currentMethodDetail.value?.fw_params || farmData.value?.execution_method || ""
+);
+const displayNotes = computed(
+    () => currentMethodDetail.value?.fw_atten || farmData.value?.precautions || ""
+);
+const displayAreaCode = computed(() => {
+    if (isFwTaskMode.value) {
+        const zoneId = fwTaskQuery.value.zone_id;
+        return zoneId != null ? `分区 ${zoneId}` : "";
+    }
+    return "ws0gefwg9tdn";
+});
+
+/** 仅异常类农事展示底部「发现异常后机动执行」 */
+const isAbnormalWork = computed(() => {
+    if (Number(farmData.value?.farm_work_type) === 5) return true;
+    return isTruthyFlag(pageParams.value?.isAbnormal);
+});
+
 // 执行方式 Tab 配置
 const executionTabs = [
     { label: "植保机", value: 1 },
@@ -523,14 +626,26 @@ const handleShare = () => {
     const query = {
         askInfo: { title: "转发农事", content: "是否分享给好友" },
         shareText:
-            ((farmData.value?.work_name ||
+            ((displayName.value ||
+            farmData.value?.work_name ||
             detail.value?.farmWorkName) + '  已触发,请在最佳农时内及时执行') ||
             "农事详情",
         targetUrl: "work_detail",
-        paramsPage: JSON.stringify({
-            id: farmData.value?.id || workId.value || route.query.id,
-            fromShare: 1,
-        }),
+        paramsPage: JSON.stringify(
+            isFwTaskMode.value
+                ? {
+                    ...fwTaskQuery.value,
+                    statusVal: statusVal.value,
+                    lat: pageParams.value?.lat,
+                    lng: pageParams.value?.lng,
+                    fw_name: displayName.value,
+                    fromShare: 1,
+                }
+                : {
+                    id: farmData.value?.id || workId.value || route.query.id,
+                    fromShare: 1,
+                }
+        ),
         imageUrl: "https://birdseye-img.sysuimars.com/birdseye-look-mini/work_img.png",
     };
     wx.miniProgram.navigateTo({

+ 49 - 32
src/views/old_mini/work_execute/components/calendar.vue

@@ -32,11 +32,7 @@
                     <span class="days-one">{{ day.dayLabel }}</span>
                 </div>
                 <div v-if="day.solarTerm" class="solar-term">{{ day.solarTerm }}</div>
-                <div v-if="day.typeName" class="type-num">{{ day.typeName.lengthVal }}</div>
-                <!-- <div v-if="day.isHeatWarning" class="heat-warning"></div>
-                <div v-if="day.typeName" class="type-name">
-                    <div class="type-text">{{ day.typeName.farmWorkName }}</div>
-                </div> -->
+                <div v-if="day.typeName?.lengthVal" class="type-num">{{ day.typeName.lengthVal }}</div>
             </div>
 
             <!-- <div class="days-fixed">
@@ -105,15 +101,14 @@ const calendarDays = computed(() => {
     return daysList;
 });
 
-// 根据日期获取农事数据
+// 根据日期获取农事数据(优先用 executeDateFull,兼容旧字段)
 function getTaskByDate(dateStr) {
     if (!taskListData.value || taskListData.value.length === 0) {
         return null;
     }
-    // 查找匹配日期的农事
-    const matchedTasks = taskListData.value.filter(task => {
+    const matchedTasks = taskListData.value.filter((task) => {
+        if (task.executeDateFull) return task.executeDateFull === dateStr;
         if (!task.expectedExecuteDate && !task.executeDeadlineDate) return false;
-        // 格式化日期字符串进行比较(确保格式一致)
         const taskDate = formatDate(new Date(task.expectedExecuteDate || task.executeDeadlineDate));
         return taskDate === dateStr;
     });
@@ -122,24 +117,45 @@ function getTaskByDate(dateStr) {
         return null;
     }
 
-    // const farmWorkNames = matchedTasks.map(task => task.farmWorkName || '').filter(Boolean);
+    // 日历外侧默认展示人工农事名称
+    const farmWorkName = matchedTasks[0]?.farmName || matchedTasks[0]?.title || "";
     return {
         lengthVal: matchedTasks.length,
-        // farmWorkName: farmWorkNames.length > 1 ? farmWorkNames.join('、') : farmWorkNames[0] || ''
+        farmWorkName,
     };
 }
 
 function setSolarTerm(taskList) {
-    // 存储任务列表数据
     taskListData.value = taskList || [];
-    // 为每个任务计算节气
     for (let task of taskListData.value) {
-        if (task.expectedExecuteDate || task.executeDeadlineDate) {
-            task.solarTerm = getSolarTerm(new Date(task.expectedExecuteDate || task.executeDeadlineDate));
+        const dateVal = task.executeDateFull || task.expectedExecuteDate || task.executeDeadlineDate;
+        if (dateVal) {
+            task.solarTerm = getSolarTerm(new Date(dateVal));
         }
     }
 }
 
+const activeDay = ref(null);
+const selectedDate = ref(null);
+const emit = defineEmits(["dateSelect"]);
+
+/** 日历起始日对齐接口返回的 start_date */
+function setStartDate(dateStr) {
+    if (!dateStr) return;
+    const next = new Date(`${dateStr}T00:00:00`);
+    if (Number.isNaN(next.getTime())) return;
+    startDate.value = next;
+}
+
+/** 选中指定日期(不触发取消逻辑) */
+function selectDateByValue(dateStr) {
+    if (!dateStr) return;
+    const day = calendarDays.value.find((item) => item.date === dateStr);
+    activeDay.value = dateStr;
+    selectedDate.value = day ? `${dateStr} (${day.dayOfWeek})` : dateStr;
+    emit("dateSelect", dateStr);
+}
+
 const expiredCounts = ref(0);
 const completedCounts = ref(0);
 function setCounts(index, counts) {
@@ -155,13 +171,6 @@ const clearSelection = () => {
     activeDay.value = null;
     selectedDate.value = null;
 };
-
-defineExpose({
-    setSolarTerm,
-    setCounts,
-    clearSelection
-});
-
 const dateRange = computed(() => {
     let start = calendarDays.value[0].date;
     start = start.replace(/-/g, ".");
@@ -210,23 +219,20 @@ function nextPeriod() {
     startDate.value = new Date(startDate.value);
 }
 
-const activeDay = ref(null);
-const emit = defineEmits(['dateSelect']);
-
 const selectDate = (date, day) => {
     // 如果点击的是已选中的日期,取消选中
     if (activeDay.value === date) {
         activeDay.value = null;
         selectedDate.value = null;
-        emit('dateSelect', null); // 传递 null 表示取消筛选
+        emit("dateSelect", null);
     } else {
         activeDay.value = date;
         selectedDate.value = `${date} (${day.dayOfWeek})`;
-        emit('dateSelect', date); // 传递选中的日期
+        emit("dateSelect", date);
     }
 };
 
-// 初始化默认选中今天
+// 初始化默认选中今天(接口回来后会被 selectDateByValue 覆盖)
 onMounted(() => {
     const todayDay = calendarDays.value.find((day) => day.isToday);
     if (todayDay) {
@@ -238,7 +244,14 @@ onMounted(() => {
 function closeDialog() {
     activeDay.value = null;
 }
-const selectedDate = ref(null);
+
+defineExpose({
+    setSolarTerm,
+    setStartDate,
+    selectDateByValue,
+    setCounts,
+    clearSelection,
+});
 </script>
 
 <style lang="scss" scoped>
@@ -402,15 +415,19 @@ const selectedDate = ref(null);
         }
         .type-name {
             font-size: 10px;
-            position: relative;
-            top: -4px;
+            margin-top: 2px;
             border-radius: 12px;
-            position: relative;
             background: #fff;
             padding-top: 2px;
             .type-text {
                 border-radius: 12px;
                 padding: 2px;
+                overflow: hidden;
+                text-overflow: ellipsis;
+                white-space: nowrap;
+                max-width: 44px;
+                margin: 0 auto;
+                color: #666;
             }
         }
     }

+ 1 - 0
src/views/old_mini/work_execute/index.js

@@ -111,6 +111,7 @@ class IndexMap {
 
   initData(taskList, label, pointType = "point") {
     this.labelStyleCache = {};
+    this.gardenPointLayer.source.clear();
     if (taskList.length > 0) {
       for (let item of taskList) {
         item.mapInfo = label

+ 162 - 144
src/views/old_mini/work_execute/index.vue

@@ -76,7 +76,6 @@
 import { computed, nextTick, onActivated, onMounted, ref } from "vue";
 import { useStore } from "vuex";
 import { useRouter } from "vue-router";
-import { ElMessage } from "element-plus";
 import IndexMap from "./index";
 import customCalendar from "./components/calendar.vue";
 import farmHeader from "@/components/farmHeader.vue";
@@ -85,6 +84,14 @@ import surveyAskPopup from "@/components/popup/surveyAskPopup.vue";
 import { useI18n } from "@/i18n";
 import wx from "weixin-js-sdk";
 
+// ---------- 常量 ----------
+/** zoneId / date 临时固定,后续改为当前分区 / 当天 */
+const QUERY = {
+    zoneId: 41,
+    date: "2025-09-11",
+};
+const COMPLETED_STATUS_RE = /已完成|已执行|已认证|本人完成/;
+
 const { t } = useI18n();
 const store = useStore();
 const router = useRouter();
@@ -96,138 +103,140 @@ const showFarmCalendarPopup = ref(false);
 const showSurveyAskPopup = ref(false);
 const tabBarHeight = computed(() => store.state.home.tabBarHeight);
 
-const formatDateYMD = (date = new Date()) =>
-    `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
-
-const filterDate = ref(formatDateYMD());
-
-/** 假数据:后续可替换为接口;status: pending 待执行 / completed 已完成 */
-const taskList = ref([
-    {
-        id: 1,
-        status: "pending",
-        title: "喷施杀菌剂",
-        isHighRisk: false,
-        isNormal: true,
-        isAbnormal: false,
-        isWeather: false,
-        cornerTip: "待执行",
-        analysisSummary: "近期高温高湿,蒂蛀虫风险上升",
-        areaCode: "A-01 / 分区一",
-        farmName: "喷施杀菌剂",
-        executeDate: "08/06",
-        wkt: "POINT(113.612409 23.587036)",
-    },
-    {
-        id: 2,
-        status: "pending",
-        title: "叶面追肥",
-        isHighRisk: true,
-        isNormal: false,
-        isAbnormal: true,
-        isWeather: false,
-        cornerTip: "发现异常后机动执行",
-        analysisSummary: "转色期养分需求增加,需补充钾肥",
-        areaCode: "B-03 / 分区二",
-        farmName: "叶面追肥",
-        executeDate: "08/06",
-        wkt: "POINT(113.615809 23.586636)",
-    },
-    {
-        id: 3,
-        status: "pending",
-        title: "灌溉补水",
-        isHighRisk: true,
-        isNormal: false,
-        isAbnormal: false,
-        isWeather: true,
-        cornerTip: "建议执行",
-        analysisSummary: "土壤墒情偏低,局部出现轻度干旱胁迫",
-        areaCode: "C-02 / 分区三",
-        farmName: "灌溉补水",
-        executeDate: "08/06",
-        wkt: "POINT(113.613609 23.584436)",
-    },
-    {
-        id: 4,
-        status: "completed",
-        title: "病虫害巡查",
-        isHighRisk: false,
-        isNormal: true,
-        isAbnormal: false,
-        isWeather: false,
-        isByMyself: true,
-        cornerTip: "本人完成",
-        analysisSummary: "新梢与果实表面未见明显异常斑点",
-        areaCode: "D-05 / 全园",
-        farmName: "病虫害巡查",
-        executeDate: "08/05",
-        wkt: "POINT(113.614209 23.585836)",
-    },
-    {
-        id: 5,
-        status: "completed",
-        title: "修剪整枝",
-        isHighRisk: false,
-        isNormal: true,
-        isAbnormal: false,
-        isWeather: false,
-        cornerTip: "已完成",
-        analysisSummary: "已清理过密枝条,改善通风透光",
-        areaCode: "A-02 / 分区一",
-        farmName: "修剪整枝",
-        executeDate: "08/04",
-        wkt: "POINT(113.611809 23.586236)",
-    },
-    {
-        id: 6,
-        status: "completed",
-        title: "除草松土",
-        isHighRisk: false,
-        isNormal: false,
-        isAbnormal: true,
-        isWeather: false,
-        cornerTip: "已完成",
-        analysisSummary: "行间杂草已清除,土壤表层已疏松",
-        areaCode: "B-01 / 分区二",
-        farmName: "除草松土",
-        executeDate: "08/03",
-        wkt: "POINT(113.616209 23.584936)",
-    },
-    {
-        id: 7,
-        status: "completed",
-        title: "防风加固",
-        isHighRisk: false,
-        isNormal: false,
-        isAbnormal: false,
-        isWeather: true,
-        cornerTip: "已完成",
-        analysisSummary: "台风过境前已完成支架加固与果袋检查",
-        areaCode: "C-01 / 分区三",
-        farmName: "防风加固",
-        executeDate: "08/02",
-        wkt: "POINT(113.613109 23.587436)",
-    },
-]);
-
+// ---------- 页面状态 ----------
+const filterDate = ref(QUERY.date); // 当前选中日期
+const taskList = ref([]); // 已映射的农事卡片(归属接口 date)
+const mapCenterPoint = ref(null); // 接口返回的分区中心点 WKT
 const activeTab = ref("pending");
-const pendingCount = computed(() => taskList.value.filter((item) => item.status === "pending").length);
-const completedCount = computed(() => taskList.value.filter((item) => item.status === "completed").length);
-const displayTaskList = computed(() => taskList.value.filter((item) => item.status === activeTab.value));
 
+// ---------- 计算属性 ----------
+const dayTaskList = computed(() =>
+    taskList.value.filter((item) => item.executeDateFull === filterDate.value)
+);
+const pendingCount = computed(() => dayTaskList.value.filter((item) => item.status === "pending").length);
+const completedCount = computed(() => dayTaskList.value.filter((item) => item.status === "completed").length);
+const displayTaskList = computed(() => dayTaskList.value.filter((item) => item.status === activeTab.value));
+
+// ---------- 工具函数 ----------
+const isCompletedStatus = (status) => COMPLETED_STATUS_RE.test(String(status || ""));
+
+const formatExecuteDate = (dateStr) => {
+    if (!dateStr) return "";
+    return String(dateStr).replace(/^\d{4}-(\d{2})-(\d{2})$/, "$1/$2");
+};
+
+const buildPointWkt = (lng, lat, index = 0) => {
+    if (lng == null || lat == null || Number.isNaN(Number(lng)) || Number.isNaN(Number(lat))) return "";
+    const offset = (index % 6) * 0.00025;
+    return `POINT(${Number(lng) + offset} ${Number(lat) + offset * 0.6})`;
+};
+
+/** 将扁平 fw_tasks 映射为卡片;日期优先用任务自身 date,否则用接口 data.date */
+const mapFwTasks = (fwTasks, dateStr, lat, lng, zoneId) => {
+    const list = Array.isArray(fwTasks) ? fwTasks : [];
+    return list
+        .filter((task) => task)
+        .map((task, index) => {
+            const executeDateFull = task.date || dateStr;
+            const completed = isCompletedStatus(task.fw_status);
+            return {
+                id: `${task.manual_id || task.mechanical_id || "fw"}_${index}_${executeDateFull}`,
+                status: completed ? "completed" : "pending",
+                title: task.fw_name || "",
+                isHighRisk: false,
+                isNormal: true,
+                isAbnormal: false,
+                isWeather: false,
+                cornerTip: task.fw_status || (completed ? "已完成" : "待执行"),
+                analysisSummary: task.fw_explain || "",
+                areaCode: zoneId ? `分区 ${zoneId}` : "",
+                farmName: task.fw_name || "",
+                executeDate: formatExecuteDate(executeDateFull),
+                executeDateFull,
+                zoneId,
+                manualId: task.manual_id,
+                mechanicalId: task.mechanical_id,
+                latitude: lat,
+                longitude: lng,
+                wkt: buildPointWkt(lng, lat, index),
+            };
+        });
+};
+
+const resolveRangeStart = (data) => {
+    if (data?.start_date) return data.start_date;
+    if (data?.date) return data.date;
+    return QUERY.date;
+};
+
+// ---------- 业务逻辑 ----------
+const refreshMapMarkers = () => {
+    if (!indexMap.kmap) return;
+    indexMap.initData(getMapMarkerList(), "farmName", "wkt");
+};
+
+/** 地图展示全部日期的待执行农事 */
+const getMapMarkerList = () =>
+    taskList.value.filter((item) => item.status === "pending" && item.wkt);
+
+const syncCalendar = (data) => {
+    const start = resolveRangeStart(data);
+    calendarRef.value?.setStartDate?.(start);
+    nextTick(() => {
+        const preferDate =
+            (taskList.value.some((item) => item.executeDateFull === filterDate.value) && filterDate.value) ||
+            start;
+        calendarRef.value?.selectDateByValue?.(preferDate);
+        calendarRef.value?.setSolarTerm?.(taskList.value);
+    });
+};
+
+const fetchFwTasks = async () => {
+    try {
+        const res = await VE_API.questionnaire.getFwTasks({
+            zone_id: QUERY.zoneId,
+            date: QUERY.date,
+        });
+        const data = res?.data;
+        if (!data) {
+            taskList.value = [];
+            mapCenterPoint.value = null;
+            calendarRef.value?.setStartDate?.(QUERY.date);
+            calendarRef.value?.selectDateByValue?.(QUERY.date);
+            calendarRef.value?.setSolarTerm?.([]);
+            refreshMapMarkers();
+            return;
+        }
+
+        const lat = data.latitude ?? 22.970229;
+        const lng = data.longitude ?? 113.746646;
+        const dateStr = resolveRangeStart(data);
+        mapCenterPoint.value = `POINT(${lng} ${lat})`;
+
+        taskList.value = mapFwTasks(
+            data.fw_tasks,
+            dateStr,
+            lat,
+            lng,
+            data.zone_id ?? QUERY.zoneId
+        );
+        filterDate.value = dateStr;
+        syncCalendar(data);
+        nextTick(() => refreshMapMarkers());
+    } catch (e) {
+        console.error("获取农事任务失败", e);
+        taskList.value = [];
+    }
+};
+
+// ---------- 事件处理 ----------
 const handleTabChange = (key) => {
     if (activeTab.value === key) return;
     activeTab.value = key;
-    nextTick(() => {
-        if (indexMap.kmap) {
-            indexMap.initData(getMapMarkerList(), "farmName", "wkt");
-        }
-    });
 };
 
 const handleDateSelect = (date) => {
-    filterDate.value = date ?? formatDateYMD();
+    filterDate.value = date ?? QUERY.date;
 };
 
 const handleForward = (item) => {
@@ -237,7 +246,7 @@ const handleForward = (item) => {
         shareText: `${workName}  已触发,请在最佳农时内及时执行`,
         targetUrl: "work_detail",
         paramsPage: JSON.stringify({
-            id: item?.id,
+            ...buildFwTaskQuery(item),
             fromShare: 1,
         }),
         imageUrl: "https://birdseye-img.sysuimars.com/birdseye-look-mini/work_img.png",
@@ -247,13 +256,22 @@ const handleForward = (item) => {
     });
 };
 
+const buildFwTaskQuery = (item) => ({
+    zone_id: item?.zoneId ?? QUERY.zoneId,
+    date: item?.executeDateFull || QUERY.date,
+    manual_id: item?.manualId,
+    mechanical_id: item?.mechanicalId,
+    statusVal: item?.status,
+    lat: item?.latitude,
+    lng: item?.longitude,
+    fw_name: item?.title || item?.farmName || "",
+    isAbnormal: item?.isAbnormal ? 1 : 0,
+});
+
 const handleViewDetail = (item) => {
     router.push({
         path: "/work_detail",
-        query: {
-            id: item.id,
-            statusVal: item.status,
-        },
+        query: buildFwTaskQuery(item),
     });
 };
 
@@ -266,28 +284,27 @@ const handleSurveyConfirm = () => {
     router.push("/survey_entry");
 };
 
-const mapPoint = ref(null);
-
-const getMapMarkerList = () => displayTaskList.value.filter((item) => item.wkt);
+const initMap = () => {
+    if (!mapContainer.value) return;
+    const fallback = store.state.home.miniUserLocationPoint || "POINT(113.746646 22.970229)";
+    const point = mapCenterPoint.value || fallback;
+    if (!indexMap.kmap) {
+        indexMap.initMap(point, mapContainer.value, true);
+    }
+    refreshMapMarkers();
+};
 
 onMounted(() => {
-    mapPoint.value = store.state.home.miniUserLocationPoint || "POINT(113.614209 23.585836)";
-    nextTick(() => {
-        if (mapContainer.value) {
-            indexMap.initMap(mapPoint.value, mapContainer.value, true);
-            indexMap.initData(getMapMarkerList(), "farmName", "wkt");
-        }
+    nextTick(async () => {
+        await fetchFwTasks();
+        initMap();
     });
 });
 
 onActivated(() => {
     nextTick(() => {
         if (!indexMap.kmap) {
-            if (mapContainer.value) {
-                mapPoint.value = store.state.home.miniUserLocationPoint || "POINT(113.614209 23.585836)";
-                indexMap.initMap(mapPoint.value, mapContainer.value, true);
-                indexMap.initData(getMapMarkerList(), "farmName", "wkt");
-            }
+            initMap();
             return;
         }
         if (mapContainer.value && indexMap.kmap.map) {
@@ -297,6 +314,7 @@ onActivated(() => {
                 const rect = container.getBoundingClientRect();
                 if (rect.width > 0 && rect.height > 0) {
                     indexMap.kmap.map.updateSize();
+                    refreshMapMarkers();
                 } else {
                     setTimeout(checkAndUpdateSize, 100);
                 }