Przeglądaj źródła

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

lxf 2 tygodni temu
rodzic
commit
8be92d2f1a

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

@@ -31,4 +31,14 @@ module.exports = {
         url: config.base_new_url + "report/county_phenophase",
         type: "get",
     },
+    // 标准农事列表
+    standardAgronomy: {
+        url: config.base_new_url + "report/standard_agronomy",
+        type: "get",
+    },
+    // 预警农事列表
+    warningAgronomy: {
+        url: config.base_new_url + "report/warning_agronomy",
+        type: "get",
+    },
 }

+ 103 - 22
src/views/old_mini/entry_information/components/baInformation.vue

@@ -68,20 +68,28 @@
         </div>
 
         <div class="custom-bottom-fixed-btns">
-            <div class="bottom-btn primary-btn" @click="handleNext">确认信息</div>
+            <div
+                class="bottom-btn primary-btn"
+                :class="{ disabled: submitting }"
+                @click="handleNext"
+            >
+                {{ submitting ? "提交中..." : "确认信息" }}
+            </div>
         </div>
     </div>
 </template>
 
 <script setup>
 import { nextTick, onActivated, onBeforeUnmount, onMounted, reactive, ref } from "vue";
-import { useRouter } from "vue-router";
+import { useRoute, useRouter } from "vue-router";
+import { useStore } from "vuex";
 import { ElMessage } from "element-plus";
 import SelectLocationMap from "../map/selectLocationMap.js";
 
 const LOCATION_KEY = "ENTRY_RESIDENT_LOCATION";
 const FORM_KEY = "ENTRY_BA_FORM";
 const STEP_KEY = "ENTRY_INFORMATION_STEP";
+const EQUIPMENT_KEY = "ENTRY_SELECTED_EQUIPMENT";
 /** 本地标记:用户已新增农场 */
 const HAS_FARM_KEY = "HAS_ENTRY_FARM";
 /** 新增农场信息(点位、品类等) */
@@ -93,12 +101,20 @@ const DEFAULT_COUNTY_CODE = "130528";
 const MAP_KEY = "CZLBZ-LJICQ-R4A5J-BN62X-YXCRJ-GNBUT";
 
 const router = useRouter();
+const route = useRoute();
+const store = useStore();
 
 const formRef = ref(null);
 const previewMapRef = ref(null);
 let previewMap = null;
 const cropOptions = ref([]);
 const cropLoading = ref(false);
+const submitting = ref(false);
+/** 区县接口附带的气象预警 / 胁迫,确认时写入 ENTRY_FARM_DATA */
+const weatherMeta = reactive({
+    weatherRisk: "",
+    weatherStress: "",
+});
 const form = reactive({
     name: "",
     phone: "",
@@ -215,9 +231,13 @@ async function fetchCropOptions() {
         });
         if (res?.code != null && res.code !== 200) {
             cropOptions.value = [];
+            weatherMeta.weatherRisk = "";
+            weatherMeta.weatherStress = "";
             ElMessage.error(res.msg || "获取种植品类失败");
             return;
         }
+        weatherMeta.weatherRisk = res?.data?.weather_risk || "";
+        weatherMeta.weatherStress = res?.data?.weather_stress || "";
         const list = Array.isArray(res?.data?.phenophases) ? res.data.phenophases : [];
         const options = [];
         const seen = new Set();
@@ -236,6 +256,8 @@ async function fetchCropOptions() {
         }
     } catch {
         cropOptions.value = [];
+        weatherMeta.weatherRisk = "";
+        weatherMeta.weatherStress = "";
         ElMessage.error("获取种植品类失败,请稍后再试");
     } finally {
         cropLoading.value = false;
@@ -282,8 +304,38 @@ async function resolveRegionName(point) {
     }
 }
 
+function toFiniteNumber(value, fallback) {
+    const n = Number(value);
+    return Number.isFinite(n) ? n : fallback;
+}
+
+/** 邀请管理员 id:优先路由参数 */
+function resolveAdminId() {
+    return toFiniteNumber(route.query.adminId ?? route.query.admin_id, 1);
+}
+
+function resolveUserId() {
+    return toFiniteNumber(store.state.home.miniUserId || localStorage.getItem("MINI_USER_ID"), 1);
+}
+
+/** 已选农机 id(当前简化流程可能为空) */
+function resolveMachineIds() {
+    try {
+        const cache = JSON.parse(sessionStorage.getItem(EQUIPMENT_KEY) || "null");
+        if (!cache) return [];
+        const list = Array.isArray(cache)
+            ? cache
+            : [...(cache.field || []), ...(cache.fruit || []), ...(cache.list || [])];
+        return list
+            .map((item) => Number(item?.id ?? item?.machine_id))
+            .filter((id) => Number.isFinite(id));
+    } catch {
+        return [];
+    }
+}
+
 const handleNext = async () => {
-    if (!formRef.value) return;
+    if (!formRef.value || submitting.value) return;
     try {
         await formRef.value.validate();
     } catch {
@@ -304,26 +356,50 @@ const handleNext = async () => {
     const selectedCrop = cropOptions.value.find((item) => item.name === crop);
     const cropCode = selectedCrop?.code || "";
     const point = form.location || saved?.point || "";
-    const farmData = {
-        name: form.name,
-        phone: form.phone,
-        crop,
-        cropCode,
-        point,
-        coordinate: saved?.coordinate || null,
-        region,
-        adcode: saved?.adcode || "",
-    };
-    localStorage.setItem(ENTRY_FARM_DATA_KEY, JSON.stringify(farmData));
-    if (point) {
-        localStorage.setItem("GROWTH_REPORT_MAP_POINT", point);
-        localStorage.setItem("selectedFarmPoint", point);
+    const countyCode = toFiniteNumber(saved?.adcode || resolveCountyCode(), Number(DEFAULT_COUNTY_CODE));
+
+    submitting.value = true;
+    try {
+        const res = await VE_API.questionnaire.addPlotInfo({
+            user_name: form.name,
+            tel: form.phone,
+            admin_id: resolveAdminId(),
+            user_id: resolveUserId(),
+            machine_id: resolveMachineIds(),
+            county_code: countyCode,
+        });
+        if (res?.code != null && res.code !== 200) {
+            ElMessage.error(res.msg || "提交失败,请稍后再试");
+            return;
+        }
+
+        const farmData = {
+            name: form.name,
+            phone: form.phone,
+            crop,
+            cropCode,
+            point,
+            coordinate: saved?.coordinate || null,
+            region,
+            adcode: saved?.adcode || String(countyCode),
+            weatherRisk: weatherMeta.weatherRisk,
+            weatherStress: weatherMeta.weatherStress,
+        };
+        localStorage.setItem(ENTRY_FARM_DATA_KEY, JSON.stringify(farmData));
+        if (point) {
+            localStorage.setItem("GROWTH_REPORT_MAP_POINT", point);
+            localStorage.setItem("selectedFarmPoint", point);
+        }
+        VE_API.questionnaire.generateReport({ region, crop, tel: form.phone }).catch(() => {});
+        localStorage.setItem(HAS_FARM_KEY, "1");
+        clearFormDraft();
+        router.replace("/growth_report");
+        ElMessage.success("上传成功,报告正在生成中...");
+    } catch {
+        ElMessage.error("提交失败,请稍后再试");
+    } finally {
+        submitting.value = false;
     }
-    VE_API.questionnaire.generateReport({ region, crop, tel: form.phone }).catch(() => {});
-    localStorage.setItem(HAS_FARM_KEY, "1");
-    clearFormDraft();
-    router.replace("/growth_report");
-    ElMessage.success("上传成功,报告正在生成中...");
 };
 
 onMounted(() => {
@@ -580,6 +656,11 @@ onBeforeUnmount(() => {
             line-height: 40px;
             font-size: 14px;
             border-radius: 25px;
+
+            &.disabled {
+                opacity: 0.6;
+                pointer-events: none;
+            }
         }
 
         .primary-btn {

+ 13 - 5
src/views/old_mini/growth_report/components/CropAlertPanel.vue

@@ -100,7 +100,7 @@
 <script setup>
 import { FloatingPanel } from "vant";
 import { CaretBottom, Link } from "@element-plus/icons-vue";
-import { nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue";
+import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue";
 import { useStore } from "vuex";
 import { useI18n } from "@/i18n";
 import { useRouter } from "vue-router";
@@ -124,6 +124,14 @@ const props = defineProps({
         type: Array,
         default: () => ["水稻", "荔枝", "香蕉"],
     },
+    weatherRisk: {
+        type: String,
+        default: "具体预警",
+    },
+    weatherStress: {
+        type: String,
+        default: "某某胁迫",
+    },
 });
 
 const emit = defineEmits(["switchCategory", "viewDetail", "heightChange"]);
@@ -249,10 +257,10 @@ watch(
     }
 );
 
-const alertCards = [
+const alertCards = computed(() => [
     {
         id: "warning",
-        title: "具体预警",
+        title: props.weatherRisk || "具体预警",
         icon: weatherIcon,
         content: "详情体预警体预警体预警体预警体预警体预详情详情详情详情详情详情详情详情详情详情",
         patrolTip: "",
@@ -260,12 +268,12 @@ const alertCards = [
     {
         id: 2,
         type: "abnormal",
-        title: "某某胁迫",
+        title: props.weatherStress || "某某胁迫",
         icon: weatherIcon,
         content: "详情详情详情具体预警具体预警情详情详详情详情详情详情详情详情",
         patrolTip: "异常态势巡园要点",
     },
-];
+]);
 
 const handleViewDetail = (item) => {
     emit("viewDetail", item);

+ 6 - 0
src/views/old_mini/growth_report/index.vue

@@ -51,6 +51,8 @@
 
         <crop-alert-panel
             :crop-name="currentCropName"
+            :weather-risk="weatherRisk"
+            :weather-stress="weatherStress"
             @switch-category="handleSwitchCategory"
             @view-detail="handleViewDetail"
             @patrol-tip="handlePatrolTip"
@@ -118,6 +120,8 @@ const currentMapPoint = ref(
         DEFAULT_MAP_POINT
 );
 const currentCropName = ref(entryFarmData?.crop || "水稻");
+const weatherRisk = ref(entryFarmData?.weatherRisk || "具体预警");
+const weatherStress = ref(entryFarmData?.weatherStress || "某某胁迫");
 const activeMenuKey = ref("hot-drought-2");
 const panelHeight = ref(280);
 const inviteBottom = computed(() => panelHeight.value);
@@ -170,6 +174,8 @@ function syncEntryFarmData() {
     const data = readEntryFarmData();
     if (!data) return;
     if (data.crop) currentCropName.value = data.crop;
+    if (data.weatherRisk) weatherRisk.value = data.weatherRisk;
+    if (data.weatherStress) weatherStress.value = data.weatherStress;
     if (data.point) saveCurrentMapPoint(data.point);
 }
 

+ 142 - 124
src/views/old_mini/work_execute/index.vue

@@ -74,7 +74,7 @@
 </template>
 
 <script setup>
-import { computed, nextTick, onActivated, onMounted, ref } from "vue";
+import { computed, nextTick, onActivated, ref } from "vue";
 import { useStore } from "vuex";
 import { useRouter } from "vue-router";
 import { ElMessage } from "element-plus";
@@ -103,58 +103,16 @@ const formatDateYMD = (date = new Date()) =>
 
 const filterDate = ref(formatDateYMD());
 
+const ENTRY_FARM_DATA_KEY = "ENTRY_FARM_DATA";
+const MAP_POINT_STORAGE_KEY = "GROWTH_REPORT_MAP_POINT";
+const DEFAULT_MAP_POINT = "POINT(113.6142086995688 23.585836479509055)";
+
 /**
- * 假数据模板:wkt 相对中心点偏移(经度、纬度)
- * 后续可替换为接口,接口返回绝对坐标时可去掉 offset
+ * 假数据模板(已完成 tab 兜底):wkt 相对中心点偏移
  */
-const TASK_MOCK_TEMPLATE = [
-    {
-        id: 1,
-        status: "pending",
-        title: "喷施杀菌剂",
-        isHighRisk: false,
-        isNormal: true,
-        isAbnormal: false,
-        isWeather: false,
-        cornerTip: "待执行",
-        analysisSummary: "近期高温高湿,蒂蛀虫风险上升",
-        areaCode: "A-01 / 分区一",
-        farmName: "喷施杀菌剂",
-        executeDate: "08/06",
-        offset: [-0.0018, 0.0012],
-    },
-    {
-        id: 2,
-        status: "pending",
-        title: "叶面追肥",
-        isHighRisk: true,
-        isNormal: false,
-        isAbnormal: true,
-        isWeather: false,
-        cornerTip: "发现异常后机动执行",
-        analysisSummary: "转色期养分需求增加,需补充钾肥",
-        areaCode: "B-03 / 分区二",
-        farmName: "叶面追肥",
-        executeDate: "08/06",
-        offset: [0.0016, 0.0008],
-    },
+const COMPLETED_MOCK_TEMPLATE = [
     {
-        id: 3,
-        status: "pending",
-        title: "灌溉补水",
-        isHighRisk: true,
-        isNormal: false,
-        isAbnormal: false,
-        isWeather: true,
-        cornerTip: "建议执行",
-        analysisSummary: "土壤墒情偏低,局部出现轻度干旱胁迫",
-        areaCode: "C-02 / 分区三",
-        farmName: "灌溉补水",
-        executeDate: "08/06",
-        offset: [-0.0006, -0.0014],
-    },
-    {
-        id: 4,
+        id: "mock-c-4",
         status: "completed",
         title: "病虫害巡查",
         isHighRisk: false,
@@ -164,13 +122,13 @@ const TASK_MOCK_TEMPLATE = [
         isByMyself: true,
         cornerTip: "已完成",
         analysisSummary: "新梢与果实表面未见明显异常斑点",
-        areaCode: "D-05 / 全园",
+        areaCode: "分区一",
         farmName: "病虫害巡查",
         executeDate: "08/05",
         offset: [0.0004, -0.0003],
     },
     {
-        id: 5,
+        id: "mock-c-5",
         status: "completed",
         title: "修剪整枝",
         isHighRisk: false,
@@ -179,53 +137,35 @@ const TASK_MOCK_TEMPLATE = [
         isWeather: false,
         cornerTip: "已完成",
         analysisSummary: "已清理过密枝条,改善通风透光",
-        areaCode: "A-02 / 分区一",
+        areaCode: "分区一",
         farmName: "修剪整枝",
         executeDate: "08/04",
         offset: [-0.0024, 0.0004],
     },
-    {
-        id: 6,
-        status: "completed",
-        title: "除草松土",
-        isHighRisk: false,
-        isNormal: false,
-        isAbnormal: true,
-        isWeather: false,
-        cornerTip: "已完成",
-        analysisSummary: "行间杂草已清除,土壤表层已疏松",
-        areaCode: "B-01 / 分区二",
-        farmName: "除草松土",
-        executeDate: "08/03",
-        offset: [0.002, -0.0009],
-    },
-    {
-        id: 7,
-        status: "completed",
-        title: "防风加固",
-        isHighRisk: false,
-        isNormal: false,
-        isAbnormal: false,
-        isWeather: true,
-        cornerTip: "已完成",
-        analysisSummary: "台风过境前已完成支架加固与果袋检查",
-        areaCode: "C-01 / 分区三",
-        farmName: "防风加固",
-        executeDate: "08/02",
-        offset: [-0.0011, 0.0016],
-    },
 ];
 
-/** 假数据:后续可替换为接口;status: pending 待执行 / completed 已完成 */
+/** status: pending 待执行 / completed 已完成 */
 const taskList = ref([]);
 
-/** 根据中心点生成附近任务点位 */
-const buildTaskListNearCenter = (centerWkt) => {
+function readEntryFarmData() {
+    try {
+        return JSON.parse(localStorage.getItem(ENTRY_FARM_DATA_KEY) || "null");
+    } catch {
+        return null;
+    }
+}
+
+function formatExecuteDate(date = new Date()) {
+    return `${String(date.getMonth() + 1).padStart(2, "0")}/${String(date.getDate()).padStart(2, "0")}`;
+}
+
+/** 根据中心点生成附近任务点位(已完成 mock) */
+const buildCompletedNearCenter = (centerWkt) => {
     const coord = convertPointToArray(centerWkt);
     const lng = Number(coord?.[0]);
     const lat = Number(coord?.[1]);
     if (!Number.isFinite(lng) || !Number.isFinite(lat)) return [];
-    return TASK_MOCK_TEMPLATE.map((item) => {
+    return COMPLETED_MOCK_TEMPLATE.map((item) => {
         const [dx = 0, dy = 0] = item.offset || [];
         const { offset, ...rest } = item;
         return {
@@ -234,6 +174,98 @@ const buildTaskListNearCenter = (centerWkt) => {
         };
     });
 };
+
+/**
+ * 接口农事 → 列表卡片
+ * @param {'standard' | 'warning'} type 标准 / 预警
+ */
+function mapAgronomyToTask(item, index, centerWkt, type = "standard") {
+    const coord = convertPointToArray(centerWkt);
+    const lng = Number(coord?.[0]);
+    const lat = Number(coord?.[1]);
+    const dx = ((index % 3) - 1) * 0.0012;
+    const dy = (Math.floor(index / 3) - 0.5) * 0.001;
+    const hasCoord = Number.isFinite(lng) && Number.isFinite(lat);
+    const title = item.fw_name || "";
+    const isWarning = type === "warning";
+    return {
+        id: item.id ?? `${type}-${index}`,
+        status: "pending",
+        title,
+        farmName: title,
+        isHighRisk: isWarning,
+        isNormal: !isWarning,
+        isAbnormal: false,
+        isWeather: isWarning,
+        cornerTip: isWarning ? "建议执行" : "待执行",
+        analysisSummary: item.fw_explain_simple || item.fw_explain || "",
+        areaCode: item.crop_category_name || "分区一",
+        executeDate: formatExecuteDate(),
+        phenophaseCode: item.phenophase_code,
+        phenophaseName: item.phenophase_name,
+        fwExplain: item.fw_explain,
+        fwAtten: item.fw_atten,
+        fwParams: item.fw_params,
+        fwPrecrip: item.fw_precrip,
+        agronomyType: type,
+        wkt: hasCoord ? `POINT(${lng + dx} ${lat + dy})` : "",
+    };
+}
+
+/** 拉取单类农事,失败返回空数组(不阻断另一类) */
+async function requestAgronomyList(apiFn, params, errorMsg) {
+    try {
+        const res = await apiFn(params);
+        if (res?.code != null && res.code !== 200) {
+            ElMessage.error(res.msg || errorMsg);
+            return [];
+        }
+        return Array.isArray(res?.data) ? res.data : [];
+    } catch {
+        ElMessage.error(errorMsg);
+        return [];
+    }
+}
+
+/** 并行拉取标准农事 + 预警农事 */
+async function fetchAgronomyTasks(centerWkt) {
+    const entry = readEntryFarmData();
+    const cropType = entry?.crop;
+    const phenophaseCode = entry?.cropCode;
+    const riskName = entry?.weatherRisk;
+    if (!cropType || !phenophaseCode) {
+        taskList.value = buildCompletedNearCenter(centerWkt);
+        return;
+    }
+
+    const baseParams = {
+        crop_type: cropType,
+        phenophase_code: phenophaseCode,
+    };
+
+    const standardPromise = requestAgronomyList(
+        VE_API.questionnaire.standardAgronomy,
+        baseParams,
+        "获取标准农事失败"
+    );
+
+    const warningPromise = riskName
+        ? requestAgronomyList(
+              VE_API.questionnaire.warningAgronomy,
+              { ...baseParams, risk_name: riskName },
+              "获取预警农事失败"
+          )
+        : Promise.resolve([]);
+
+    const [standardList, warningList] = await Promise.all([standardPromise, warningPromise]);
+    const pending = [
+        ...standardList.map((item, index) => mapAgronomyToTask(item, index, centerWkt, "standard")),
+        ...warningList.map((item, index) =>
+            mapAgronomyToTask(item, standardList.length + index, centerWkt, "warning")
+        ),
+    ];
+    taskList.value = [...pending, ...buildCompletedNearCenter(centerWkt)];
+}
 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);
@@ -297,10 +329,6 @@ const handleSurveyConfirm = () => {
 
 const mapPoint = ref(null);
 
-const DEFAULT_MAP_POINT = "POINT(113.6142086995688 23.585836479509055)";
-const ENTRY_FARM_DATA_KEY = "ENTRY_FARM_DATA";
-const MAP_POINT_STORAGE_KEY = "GROWTH_REPORT_MAP_POINT";
-
 const isPointWkt = (value) => typeof value === "string" && /^POINT\s*\(/i.test(value.trim());
 
 /** 与长势报告 / 农情档案一致:优先用户选中点位 */
@@ -337,11 +365,8 @@ const resolveMapPoint = () => {
 
 const getMapMarkerList = () => displayTaskList.value.filter((item) => item.wkt);
 
-const initWorkMap = () => {
+const refreshMapMarkers = () => {
     if (!mapContainer.value) return;
-    mapPoint.value = resolveMapPoint();
-    // 假数据点位落在用户选中中心附近
-    taskList.value = buildTaskListNearCenter(mapPoint.value);
     if (!indexMap.kmap) {
         indexMap.initMap(mapPoint.value, mapContainer.value, true);
     } else {
@@ -352,39 +377,32 @@ const initWorkMap = () => {
     indexMap.setMapPosition(mapPoint.value);
 };
 
-onMounted(() => {
-    nextTick(() => {
-        initWorkMap();
-    });
-});
+/** keep-alive 下仅在 onActivated 拉取,避免与 onMounted 重复请求 */
+const loadWorkPage = async () => {
+    if (!mapContainer.value) return;
+    mapPoint.value = resolveMapPoint();
+    await fetchAgronomyTasks(mapPoint.value);
+    refreshMapMarkers();
+    if (mapContainer.value && indexMap.kmap?.map) {
+        const checkAndUpdateSize = () => {
+            const container = mapContainer.value;
+            if (!container) return;
+            const rect = container.getBoundingClientRect();
+            if (rect.width > 0 && rect.height > 0) {
+                indexMap.kmap.map.updateSize();
+            } else {
+                setTimeout(checkAndUpdateSize, 100);
+            }
+        };
+        setTimeout(checkAndUpdateSize, 200);
+    }
+};
 
 onActivated(() => {
     nextTick(() => {
-        if (!indexMap.kmap) {
-            initWorkMap();
-            return;
-        }
-        mapPoint.value = resolveMapPoint();
-        taskList.value = buildTaskListNearCenter(mapPoint.value);
-        indexMap.setMapPosition(mapPoint.value);
-        indexMap.initData(getMapMarkerList(), "farmName", "wkt");
-        indexMap.setMapPosition(mapPoint.value);
-        if (mapContainer.value && indexMap.kmap.map) {
-            const checkAndUpdateSize = () => {
-                const container = mapContainer.value;
-                if (!container) return;
-                const rect = container.getBoundingClientRect();
-                if (rect.width > 0 && rect.height > 0) {
-                    indexMap.kmap.map.updateSize();
-                } else {
-                    setTimeout(checkAndUpdateSize, 100);
-                }
-            };
-            setTimeout(checkAndUpdateSize, 200);
-        }
+        loadWorkPage();
     });
-});
-</script>
+});</script>
 
 <style lang="scss" scoped>
 .task-page {