| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567 |
- <template>
- <div class="add-zone-page">
- <custom-header :name="isFarmDrawMode ? t('agriFile.drawFarmArea') : t('agriFile.addZone')" />
- <div class="add-zone-content">
- <div class="map-container" ref="mapContainer"></div>
- <div class="search-bar">
- <location-search class="search-bar__search" :user-location="userLocation"
- @change="handleLocationChange" />
- </div>
- <div class="map-tip normal-text" v-show="isFarmDrawMode">请勾画 <span class="map-tip__highlight">{{ getFarmType }}</span>
- 的种植区域</div>
- <div class="map-tip" v-show="!isFarmDrawMode">{{ t("agriFile.selectZoneHint") }}</div>
- <div class="locate-btn" @click="handleLocate">
- <img class="locate-btn__icon" src="@/assets/img/map/map-icon.png" alt="" />
- </div>
- </div>
- <div class="custom-bottom-fixed-btns">
- <div class="bottom-btn secondary-btn" @click="handleClearDraw">{{ t("agriFile.cancelSelect") }}</div>
- <div class="bottom-btn primary-btn" @click="handleConfirmDraw">{{ t("agriFile.confirmArea") }}</div>
- </div>
- <RegionNamePopup v-model:show="showRegionNamePopup" @confirm="handleFarmNameConfirm" />
- <add-zone-info-popup
- v-model:show="showZoneInfoPopup"
- :polygon="drawnPolygon"
- @confirm="handleZoneInfoConfirm"
- />
- <tip-popup v-model:show="showExecuteSuccessPopup" type="executeSuccess" text="分区/相册已创建成功" buttonText="完成" @confirm="handleComplete">
- <!-- <template #footer>
- <div class="bottom-btn-container">
- <div class="bottom-btn secondary-btn" @click="handleComplete">完成</div>
- </div>
- </template> -->
- </tip-popup>
- <tip-popup
- v-model:show="showDrawSuccessPopup"
- type="success"
- text="区域已勾画完成"
- hideBtn
- @closedPopup="handleDrawSuccessClosed"
- />
- </div>
- </template>
- <script setup>
- import { computed, nextTick, onActivated, onDeactivated, ref } from "vue";
- import { useRoute, useRouter } from "vue-router";
- import { useStore } from "vuex";
- import { ElMessage } from "element-plus";
- import customHeader from "@/components/customHeader.vue";
- import locationSearch from "@/components/pageComponents/locationSearch.vue";
- import RegionNamePopup from "@/views/old_mini/recordDetails/components/RegionNamePopup.vue";
- import addZoneInfoPopup from "../components/addZoneInfoPopup.vue";
- import MapManage from "@/views/old_mini/recordDetails/map/mapManage.js";
- import { POINT_ICON } from "@/views/old_mini/entry_information/map/selectLocationMap.js";
- import * as util from "@/common/ol_common.js";
- import { useI18n } from "@/i18n";
- import tipPopup from "@/components/popup/tipPopup.vue";
- const { t } = useI18n();
- const route = useRoute();
- const router = useRouter();
- const isFarmDrawMode = computed(() => route.query.type === "farm");
- const isRegionDrawMode = computed(() => route.query.type === "region");
- const isSuccessPopupParam = computed(() => route.query.isSuccessPopup === "1");
- const showDrawSuccessPopup = ref(false);
- const targetUrl = computed(() => route.query.targetUrl);
- const isAbnormal = computed(() => {
- const val = route.query.isAbnormal;
- return val === true || val === "true";
- });
- const store = useStore();
- const mapContainer = ref(null);
- const mapManage = new MapManage();
- const showRegionNamePopup = ref(false);
- const showZoneInfoPopup = ref(false);
- /** 确认勾画后传给信息弹窗提交的范围 WKT */
- const drawnPolygon = ref("");
- const DEFAULT_MAP_LOCATION = "POINT(113.6142086995688 23.585836479509055)";
- const ZONE_STYLE = {
- fill: "rgba(33, 153, 248, 0.35)",
- fillSelected: "rgba(33, 153, 248, 0.45)",
- stroke: "#2199F8",
- };
- /** 已有分区种植范围样式 */
- const EXISTING_ZONE_STYLE = {
- fill: "rgba(0, 0, 0, 0.38)",
- stroke: "rgba(200, 200, 200, 0.55)",
- };
- const existingVarietyZones = ref([]);
- function mapExistingZoneItem(zone) {
- const polygon = typeof zone?.polygon === "string" ? zone.polygon.trim() : "";
- if (!polygon) return null;
- return {
- id: zone.zone_id,
- name: zone.zone_name || "",
- fill: EXISTING_ZONE_STYLE.fill,
- stroke: EXISTING_ZONE_STYLE.stroke,
- polygon,
- };
- }
- async function fetchExistingVarietyZones() {
- const farm = getFarmData();
- const farmId = farm.farm_id ?? farm.id;
- if (farmId == null || farmId === "") {
- existingVarietyZones.value = [];
- return;
- }
- try {
- const res = await VE_API.questionnaire.getZones({ farm_id: farmId });
- const zones = Array.isArray(res?.data?.zones)
- ? res.data.zones
- : Array.isArray(res?.data)
- ? res.data
- : [];
- existingVarietyZones.value = zones.map(mapExistingZoneItem).filter(Boolean);
- } catch (e) {
- console.warn("[addZone] getZones failed", e);
- existingVarietyZones.value = [];
- }
- }
- const userLocation = ref(
- store.state.home.miniUserLocation ||
- localStorage.getItem("MINI_USER_LOCATION") ||
- "113.61702297075017,23.584863449735067"
- );
- function getFarmData() {
- try {
- return JSON.parse(localStorage.getItem("selectedFarmData") || "{}");
- } catch {
- return {};
- }
- }
- function isPointWkt(value) {
- return typeof value === "string" && /^POINT\s*\(/i.test(value.trim());
- }
- function wkbHexToPointWkt(hex) {
- const text = String(hex || "").trim();
- if (!/^[0-9a-fA-F]+$/.test(text) || text.length < 42) return null;
- const bytes = new Uint8Array(text.length / 2);
- for (let i = 0; i < bytes.length; i++) {
- bytes[i] = parseInt(text.slice(i * 2, i * 2 + 2), 16);
- }
- const view = new DataView(bytes.buffer);
- const little = view.getUint8(0) === 1;
- let type = view.getUint32(1, little);
- const hasSrid = (type & 0x20000000) !== 0;
- type &= 0xff;
- if (type !== 1) return null;
- let offset = 5;
- if (hasSrid) offset += 4;
- if (offset + 16 > bytes.length) return null;
- const lng = view.getFloat64(offset, little);
- const lat = view.getFloat64(offset + 8, little);
- if (!Number.isFinite(lng) || !Number.isFinite(lat)) return null;
- return `POINT(${lng} ${lat})`;
- }
- function toPointWkt(value) {
- if (!value || typeof value !== "string") return null;
- if (isPointWkt(value)) return value.trim();
- return wkbHexToPointWkt(value);
- }
- function getFarmMapLocation() {
- const farmData = getFarmData();
- const candidates = [farmData.wkt, farmData.geom_wkt, farmData.farm_location];
- for (const item of candidates) {
- const pointWkt = toPointWkt(item);
- if (pointWkt) return pointWkt;
- }
- return DEFAULT_MAP_LOCATION;
- }
- const getFarmType = computed(() => getFarmData().variety_name);
- function getDefaultMapLocation() {
- return (
- toPointWkt(localStorage.getItem("MINI_USER_LOCATION_POINT")) ||
- toPointWkt(store.state.home.miniUserLocationPoint) ||
- getFarmMapLocation()
- );
- }
- function getDefaultMapCoordinate() {
- return util.wktCastGeom(getDefaultMapLocation()).getFirstCoordinate();
- }
- function getFarmDrawCoordinate() {
- return util.wktCastGeom(getFarmMapLocation()).getFirstCoordinate();
- }
- function getMapLocation() {
- if (isFarmDrawMode.value || isRegionDrawMode.value) return getFarmMapLocation();
- return getDefaultMapLocation();
- }
- async function initMapView() {
- if (!mapContainer.value) return;
- mapManage.destroyMap();
- mapManage.initMap(getMapLocation(), mapContainer.value, {
- editable: true,
- zoneStyle: ZONE_STYLE,
- singleDraw: true,
- preventOverlapExisting: isRegionDrawMode.value,
- onDrawOverlap: () => {
- ElMessage.warning("新绘制范围不能与已有品种种植范围重叠");
- },
- onDrawLimit: () => {
- ElMessage.warning("只能勾画一个范围,请先取消后再重新勾画");
- },
- });
- mapManage.enableRegionDrawing();
- if (isFarmDrawMode.value) {
- mapManage.showCenterMarker(getFarmDrawCoordinate(), POINT_ICON.plant);
- }
- if (isRegionDrawMode.value) {
- await fetchExistingVarietyZones();
- mapManage.setExistingVarietyZones(existingVarietyZones.value);
- mapManage.setMapPosition(getFarmDrawCoordinate());
- }
- }
- const handleLocationChange = (payload) => {
- if (!payload?.coordinateArray) return;
- mapManage.setMapPosition(payload.coordinateArray);
- };
- const handleLocate = () => {
- if (isFarmDrawMode.value) {
- const coordinate = getFarmDrawCoordinate();
- mapManage.setMapPosition(coordinate);
- mapManage.showCenterMarker(coordinate, POINT_ICON.plant);
- return;
- }
- if (isRegionDrawMode.value) {
- mapManage.setMapPosition(getFarmDrawCoordinate());
- return;
- }
- mapManage.setMapPosition(getDefaultMapCoordinate());
- };
- const handleClearDraw = () => {
- // 只清当前勾画,保留已有品种种植范围
- mapManage.clearLayer();
- if (isRegionDrawMode.value) {
- mapManage.setExistingVarietyZones(existingVarietyZones.value);
- }
- };
- const handleConfirmDraw = () => {
- const payload = mapManage.getAreaGeometry();
- if (!payload.geometryArr.length) {
- ElMessage.warning(t("agriFile.pleaseDrawZone"));
- return;
- }
- if (isRegionDrawMode.value && mapManage.hasDrawnOverlapWithExisting()) {
- ElMessage.warning("新绘制范围不能与已有品种种植范围重叠");
- return;
- }
- drawnPolygon.value = toMultiPolygonWkt(mapManage.mergeGeometryWkts(payload.geometryArr));
- if (isFarmDrawMode.value) {
- showRegionNamePopup.value = true;
- return;
- }
- showZoneInfoPopup.value = true;
- };
- const leaveAfterConfirm = () => {
- if (targetUrl.value) {
- setTimeout(() => {
- router.push(targetUrl.value);
- }, 800);
- return;
- }
- setTimeout(() => {
- router.back();
- }, 800);
- };
- /** POLYGON → MULTIPOLYGON,与 update_zone_polygon 入参一致 */
- function toMultiPolygonWkt(wkt) {
- const text = String(wkt || "").trim();
- if (!text) return "";
- if (/^MULTIPOLYGON/i.test(text)) return text;
- if (/^POLYGON\s*\(/i.test(text)) {
- const body = text.replace(/^POLYGON\s*/i, "").trim();
- return `MULTIPOLYGON (${body})`;
- }
- return text;
- }
- function resolveSubmitFarmId() {
- const farm = getFarmData();
- return farm.farm_id ?? farm.id ?? "";
- }
- function resolveSubmitZoneId() {
- const farm = getFarmData();
- return route.query.trackId || farm.zone_id || farm.zoneId || "";
- }
- const submittingZoneRange = ref(false);
- /** 巡园要点 needDraw 进入:填名称后提交分区范围 */
- const handleFarmNameConfirm = async (regionName) => {
- if (submittingZoneRange.value) return;
- const payload = mapManage.getAreaGeometry();
- if (!payload.geometryArr.length) {
- ElMessage.warning(t("agriFile.pleaseDrawZone"));
- return;
- }
- const farmId = resolveSubmitFarmId();
- const zoneId = resolveSubmitZoneId();
- const polygon = toMultiPolygonWkt(mapManage.mergeGeometryWkts(payload.geometryArr));
- if (!farmId || !zoneId || !polygon) {
- ElMessage.warning("缺少农场或分区信息,请返回重试");
- return;
- }
- submittingZoneRange.value = true;
- try {
- const res = await VE_API.questionnaire.drawZoneRange({
- farm_id: farmId,
- zone_id: zoneId,
- zone_name: regionName,
- polygon,
- });
- if (res?.code !== 200) {
- ElMessage.error(res?.msg || "提交失败,请稍后再试");
- return;
- }
- } catch (e) {
- console.warn("[addZone] drawZoneRange failed", e);
- ElMessage.error("提交失败,请稍后再试");
- return;
- } finally {
- submittingZoneRange.value = false;
- }
- const geometryData = {
- zone_name: regionName,
- geometryArr: payload.geometryArr,
- mianji: payload.mianji,
- polygon,
- };
- sessionStorage.setItem("FARM_RANGE_GEOMETRY", JSON.stringify(geometryData));
- // console.log("isSuccessPopupParam", isSuccessPopupParam.value);
- console.log("isSuccessPopupParam", isSuccessPopupParam.value, route.query);
- if (isSuccessPopupParam.value) {
- showDrawSuccessPopup.value = true;
- return;
- }
- ElMessage.success(t("agriFile.drawFarmAreaSuccess"));
- if (!isAbnormal.value && targetUrl.value) {
- setTimeout(() => {
- const query = { showUpload: "1" };
- if (route.query.trackId) query.id = route.query.trackId;
- if (route.query.trackType) query.type = route.query.trackType;
- router.replace({ path: "/growth_track", query });
- }, 800);
- return;
- }
- leaveAfterConfirm();
- };
- const showExecuteSuccessPopup = ref(false);
- const handleZoneInfoConfirm = (formData) => {
- const payload = mapManage.getAreaGeometry();
- const geometryData = {
- ...formData,
- geometryArr: payload.geometryArr,
- mianji: payload.mianji,
- };
- sessionStorage.setItem("ADD_ZONE_GEOMETRY", JSON.stringify(geometryData));
- showExecuteSuccessPopup.value = true;
- };
- const handleDrawSuccessClosed = () => {
- showDrawSuccessPopup.value = false;
- leaveAfterConfirm();
- }
- const handleComplete = () => {
- showExecuteSuccessPopup.value = false;
- // 农事档案上传入口 needDraw 勾画:返回并继续打开上传弹窗
- if (isRegionDrawMode.value && route.query.continueUpload === "1") {
- sessionStorage.setItem(
- "AGRI_FILE_CONTINUE_UPLOAD",
- JSON.stringify({ zoneId: route.query.zoneId || "" })
- );
- router.back();
- return;
- }
- leaveAfterConfirm();
- }
- onActivated(() => {
- nextTick(() => initMapView());
- });
- onDeactivated(() => {
- mapManage.destroyMap();
- });
- </script>
- <style lang="scss" scoped>
- .add-zone-page {
- width: 100%;
- height: 100vh;
- overflow: hidden;
- background: #fff;
- display: flex;
- flex-direction: column;
- }
- .add-zone-content {
- position: relative;
- flex: 1;
- min-height: 0;
- overflow: hidden;
- .map-container {
- width: 100%;
- height: 100%;
- }
- .search-bar {
- position: absolute;
- top: 12px;
- left: 12px;
- right: 12px;
- z-index: 2;
- display: flex;
- align-items: center;
- height: 40px;
- padding: 0 4px 0 12px;
- border-radius: 20px;
- background: rgba(0, 0, 0, 0.45);
- box-sizing: border-box;
- &__search {
- flex: 1;
- min-width: 0;
- :deep(.el-select__wrapper) {
- background: transparent;
- box-shadow: none;
- border: none;
- min-height: 40px;
- padding-left: 0;
- }
- :deep(.el-select__placeholder),
- :deep(.el-select__input) {
- color: rgba(255, 255, 255, 0.7);
- }
- :deep(.el-icon) {
- color: rgba(255, 255, 255, 0.85);
- }
- }
- }
- .map-tip {
- position: absolute;
- top: 64px;
- left: 50%;
- z-index: 2;
- transform: translateX(-50%);
- padding: 8px 30px;
- border-radius: 25px;
- background: rgba(0, 0, 0, 0.5);
- color: #7CC5FF;
- font-size: 12px;
- &.normal-text {
- color: #fff;
- }
- &__highlight {
- color: #2199F8;
- }
- }
- .locate-btn {
- position: absolute;
- right: 12px;
- bottom: 24px;
- z-index: 2;
- display: flex;
- align-items: center;
- justify-content: center;
- width: 36px;
- height: 36px;
- border-radius: 8px;
- background: #fff;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
- &__icon {
- width: 16px;
- height: 18px;
- }
- }
- }
- .custom-bottom-fixed-btns {
- position: relative;
- justify-content: space-between;
- .bottom-btn {
- padding: 10px 20px;
- border-radius: 25px;
- }
- .secondary-btn {
- color: #666;
- border: 1px solid rgba(153, 153, 153, 0.5);
- }
- .primary-btn {
- background: #2199f8;
- }
- }
- .bottom-btn-container {
- display: flex;
- justify-content: space-between;
- gap: 12px;
- width: 100%;
- .bottom-btn {
- padding: 0 25px;
- border-radius: 4px;
- font-size: 16px;
- height: 40px;
- line-height: 40px;
- box-sizing: border-box;
- font-weight: 500;
- text-align: center;
- &.secondary-btn {
- color: #B1B1B1;
- border: 1px solid #EAEAEA;
- }
- &.primary-btn {
- flex: 1;
- background: #2199F8;
- color: #fff;
- }
- }
- }
- </style>
|