|
|
@@ -17,7 +17,7 @@
|
|
|
<div class="edit-map-content">
|
|
|
<div class="edit-map-tip" v-if="!viewOnly">操作提示:拖动圆点,即可调整地块边界</div>
|
|
|
<div class="map-container" ref="mapContainer"></div>
|
|
|
- <div class="edit-map-footer" :style="{ 'bottom': varietyTabs.length > 0 ? '75px' : '35px' }">
|
|
|
+ <div class="edit-map-footer">
|
|
|
<div class="footer-back" @click="goBack">
|
|
|
<img class="back-icon" src="@/assets/img/home/go-back.png" alt="" />
|
|
|
</div>
|
|
|
@@ -39,7 +39,7 @@
|
|
|
|
|
|
<script setup>
|
|
|
import customHeader from "@/components/customHeader.vue";
|
|
|
-import { ref, computed, onActivated, onDeactivated, nextTick } from "vue";
|
|
|
+import { ref, computed, watch, onActivated, onDeactivated, nextTick } from "vue";
|
|
|
import DrawRegionMap from "../../interactionList/map/drawRegionMap.js";
|
|
|
import { Map as KMapMap } from "@/utils/ol-map/KMap";
|
|
|
import { useRouter, useRoute } from "vue-router";
|
|
|
@@ -49,7 +49,8 @@ import TipPopup from "@/components/popup/tipPopup.vue";
|
|
|
import ConfirmDrawTypePopup from "@/components/popup/confirmDrawTypePopup.vue";
|
|
|
import Style from "ol/style/Style";
|
|
|
import { Fill, Stroke, Circle, Text } from "ol/style.js";
|
|
|
-import { Point } from "ol/geom";
|
|
|
+import { Point, Polygon, MultiPolygon } from "ol/geom";
|
|
|
+import WKT from "ol/format/WKT.js";
|
|
|
import * as proj from "ol/proj";
|
|
|
import { getArea } from "ol/sphere.js";
|
|
|
|
|
|
@@ -58,6 +59,15 @@ const route = useRoute();
|
|
|
const mapContainer = ref(null);
|
|
|
const drawRegionMap = new DrawRegionMap();
|
|
|
|
|
|
+/** OpenLayers 地图在容器尺寸变化后不会自动重算 canvas,需手动 updateSize */
|
|
|
+const resizeMapToContainer = () => {
|
|
|
+ nextTick(() => {
|
|
|
+ requestAnimationFrame(() => {
|
|
|
+ drawRegionMap.kmap?.map?.updateSize?.();
|
|
|
+ });
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
// handleVarietyClick 内部有延迟渲染(setTimeout),切换查看/编辑或重建地图时必须取消,避免旧回调在新实例上再次 addFeature 造成叠加
|
|
|
const pendingGeomRenderTimer = ref(null);
|
|
|
const clearPendingGeomRenderTimer = () => {
|
|
|
@@ -115,15 +125,28 @@ const handleRegionTypeClick = (item) => {
|
|
|
}
|
|
|
};
|
|
|
|
|
|
+/** 取 Polygon / MultiPolygon 的外环坐标(地图投影),用于顶点样式 */
|
|
|
+const getOuterRingCoordinates = (geometry) => {
|
|
|
+ if (!geometry || typeof geometry.getType !== "function") return [];
|
|
|
+ const type = geometry.getType();
|
|
|
+ const c = geometry.getCoordinates();
|
|
|
+ if (!c || !c.length) return [];
|
|
|
+ if (type === "Polygon") return c[0] || [];
|
|
|
+ if (type === "MultiPolygon") return c[0]?.[0] || [];
|
|
|
+ return [];
|
|
|
+};
|
|
|
+
|
|
|
const createPolygonStyleFunc = (fillColor, strokeColor) => {
|
|
|
return (feature) => {
|
|
|
const styles = [];
|
|
|
- const coord = feature.getGeometry().getCoordinates()[0];
|
|
|
- for (let i = 0; i < coord[0].length - 1; i++) {
|
|
|
+ const ring = getOuterRingCoordinates(feature.getGeometry());
|
|
|
+ const ringLen = ring.length;
|
|
|
+ const vertexCount = ringLen > 1 ? ringLen - 1 : ringLen;
|
|
|
+ for (let i = 0; i < vertexCount; i++) {
|
|
|
if (i % 2) {
|
|
|
styles.push(
|
|
|
new Style({
|
|
|
- geometry: new Point(coord[0][i]),
|
|
|
+ geometry: new Point(ring[i]),
|
|
|
image: new Circle({
|
|
|
radius: 4,
|
|
|
fill: new Fill({
|
|
|
@@ -139,7 +162,7 @@ const createPolygonStyleFunc = (fillColor, strokeColor) => {
|
|
|
} else {
|
|
|
styles.push(
|
|
|
new Style({
|
|
|
- geometry: new Point(coord[0][i]),
|
|
|
+ geometry: new Point(ring[i]),
|
|
|
image: new Circle({
|
|
|
radius: 6,
|
|
|
fill: new Fill({
|
|
|
@@ -262,6 +285,75 @@ const isValidGeom = (geom) => {
|
|
|
return true;
|
|
|
};
|
|
|
|
|
|
+/** 与 drawRegionMap / setAreaGeometry 一致:WKT 按 WGS84 解析 */
|
|
|
+const WGS84_WKT_OPTS = Object.freeze({
|
|
|
+ dataProjection: "EPSG:4326",
|
|
|
+ featureProjection: "EPSG:4326",
|
|
|
+});
|
|
|
+
|
|
|
+const wktGeomFormat = new WKT();
|
|
|
+
|
|
|
+/** Polygon / MultiPolygon 各自对应一组环坐标(MultiPolygon 为多块) */
|
|
|
+const polygonCoordSetsFromGeometry = (geometry) => {
|
|
|
+ if (!geometry || typeof geometry.getType !== "function") return [];
|
|
|
+ const type = geometry.getType();
|
|
|
+ if (type === "Polygon") return [geometry.getCoordinates()];
|
|
|
+ if (type === "MultiPolygon") return geometry.getCoordinates();
|
|
|
+ return [];
|
|
|
+};
|
|
|
+
|
|
|
+const mergePolygonWktsForApi = (wktArr) => {
|
|
|
+ if (!Array.isArray(wktArr) || wktArr.length === 0) return "";
|
|
|
+ const trimmed = wktArr.map((x) => String(x).trim()).filter(isValidGeom);
|
|
|
+ if (trimmed.length === 0) return "";
|
|
|
+ if (trimmed.length === 1) return trimmed[0];
|
|
|
+
|
|
|
+ const coordSets = [];
|
|
|
+ for (const w of trimmed) {
|
|
|
+ try {
|
|
|
+ const g = wktGeomFormat.readGeometry(w, WGS84_WKT_OPTS);
|
|
|
+ coordSets.push(...polygonCoordSetsFromGeometry(g));
|
|
|
+ } catch (_) {
|
|
|
+ /* 单条解析失败则跳过 */
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (coordSets.length === 0) return trimmed[0];
|
|
|
+ if (coordSets.length === 1) {
|
|
|
+ try {
|
|
|
+ return wktGeomFormat.writeGeometry(new Polygon(coordSets[0]), WGS84_WKT_OPTS);
|
|
|
+ } catch (_) {
|
|
|
+ return trimmed[0];
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return wktGeomFormat.writeGeometry(new MultiPolygon(coordSets), WGS84_WKT_OPTS);
|
|
|
+};
|
|
|
+
|
|
|
+const flattenWktToPolygonWktArray = (wkt) => {
|
|
|
+ if (!isValidGeom(wkt)) return [];
|
|
|
+ const s = String(wkt).trim();
|
|
|
+ try {
|
|
|
+ const g = wktGeomFormat.readGeometry(s, WGS84_WKT_OPTS);
|
|
|
+ const sets = polygonCoordSetsFromGeometry(g);
|
|
|
+ if (sets.length === 0) return [s];
|
|
|
+ return sets.map((rings) => wktGeomFormat.writeGeometry(new Polygon(rings), WGS84_WKT_OPTS));
|
|
|
+ } catch (_) {
|
|
|
+ return [s];
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const flattenGeomWktList = (arr) => {
|
|
|
+ if (!Array.isArray(arr)) return [];
|
|
|
+ const out = [];
|
|
|
+ for (const item of arr) {
|
|
|
+ if (!isValidGeom(item)) continue;
|
|
|
+ out.push(...flattenWktToPolygonWktArray(String(item).trim()));
|
|
|
+ }
|
|
|
+ return out;
|
|
|
+};
|
|
|
+
|
|
|
+/** 地图回显:展开 MULTIPOLYGON 为多个 POLYGON WKT,并去掉无效项 */
|
|
|
+const flattenGeomWktListForMap = (arr) => flattenGeomWktList(arr).filter(isValidGeom);
|
|
|
+
|
|
|
// 从 tab.geomItems[{geomWkt}] 中提取 WKT 数组(兼容单块/多块)
|
|
|
const getGeomArrFromGeomItems = (tab) => {
|
|
|
const items = tab?.geomItems;
|
|
|
@@ -399,7 +491,7 @@ const handleVarietyClick = (tab, index) => {
|
|
|
// 2) 接口返回的 tab.geomItems[].geomWkt(你给的结构)
|
|
|
// 3) 兜底 tab.geom(兼容旧结构)
|
|
|
const draftGeom = getDraftGeomForTab(activeRegionType.value, tab, index);
|
|
|
- const geomArr =
|
|
|
+ const rawGeomArr =
|
|
|
isValidGeom(draftGeom)
|
|
|
? [String(draftGeom).trim()]
|
|
|
: (() => {
|
|
|
@@ -407,6 +499,7 @@ const handleVarietyClick = (tab, index) => {
|
|
|
if (fromItems.length) return fromItems;
|
|
|
return isValidGeom(tab?.geom) ? [String(tab.geom).trim()] : [];
|
|
|
})();
|
|
|
+ const geomArr = flattenGeomWktListForMap(rawGeomArr);
|
|
|
// 保留一份“当前选中地块”的原始字符串形态给其它逻辑使用
|
|
|
regionGeom.value = geomArr.length > 1 ? JSON.stringify(geomArr) : geomArr[0] || "";
|
|
|
// 地图尚未初始化时,仅更新状态,不做图层绘制,避免首次进入重复叠加
|
|
|
@@ -429,9 +522,25 @@ const handleVarietyClick = (tab, index) => {
|
|
|
pendingGeomRenderTimer.value = setTimeout(() => {
|
|
|
// 再兜底清一次,避免异步回调在 destroy/init 后仍执行导致重复追加
|
|
|
drawRegionMap.kmap?.polygonLayer?.source?.clear?.();
|
|
|
- // 切换小类仅负责“显示”,不主动缩放;缩放留到点击“编辑”时触发
|
|
|
- drawRegionMap.setAreaGeometry(geomArr, false, undefined, undefined, getAbnormalGrowthOverlayMeta());
|
|
|
- // 需要 fit 时,确保视野包含当前渲染的所有地块(只读层 + 当前层)
|
|
|
+ const viewOnlyVarietyLabel =
|
|
|
+ route.query.type === "viewOnly" && activeRegionType.value === "variety"
|
|
|
+ ? (tab.regionName || tab.problemZoneTypeName || "").toString().trim()
|
|
|
+ : "";
|
|
|
+ drawRegionMap.setAreaGeometry(
|
|
|
+ geomArr,
|
|
|
+ false,
|
|
|
+ undefined,
|
|
|
+ undefined,
|
|
|
+ getAbnormalGrowthOverlayMeta(),
|
|
|
+ viewOnlyVarietyLabel || undefined
|
|
|
+ );
|
|
|
+ // 有当前小类地块:仅 fit 当前可编辑层,避免被其它只读参考地块拉远
|
|
|
+ drawRegionMap.fitView?.();
|
|
|
+ }, 50);
|
|
|
+ } else {
|
|
|
+ // 当前小类无地块:视野包含品种区只读底图(与其它参考地块联合范围)
|
|
|
+ pendingGeomRenderTimer.value = setTimeout(() => {
|
|
|
+ drawRegionMap.kmap?.polygonLayer?.source?.clear?.();
|
|
|
drawRegionMap.fitAllRegions?.();
|
|
|
}, 50);
|
|
|
}
|
|
|
@@ -648,10 +757,20 @@ onActivated(async () => {
|
|
|
if (!viewOnly.value) {
|
|
|
drawRegionMap.setMapPosition(convertPointToArray(point.value));
|
|
|
}
|
|
|
+
|
|
|
+ resizeMapToContainer();
|
|
|
});
|
|
|
|
|
|
+watch(
|
|
|
+ () => [varietyTabs.value.length, activeRegionType.value, activeVariety.value],
|
|
|
+ () => {
|
|
|
+ if (drawRegionMap.kmap?.map) resizeMapToContainer();
|
|
|
+ }
|
|
|
+);
|
|
|
+
|
|
|
onDeactivated(() => {
|
|
|
activeVariety.value = 0;
|
|
|
+ activeRegionType.value = "variety";
|
|
|
clearPendingGeomRenderTimer();
|
|
|
// 离开页面时中止未完成绘制并销毁地图,确保下次进入是干净实例
|
|
|
drawRegionMap.abortOngoingDrawSketch?.();
|
|
|
@@ -689,8 +808,9 @@ const resetPolygon = () => {
|
|
|
try {
|
|
|
const parsed = JSON.parse(polygonDataStr);
|
|
|
if (parsed && Array.isArray(parsed.geometryArr) && parsed.geometryArr.length) {
|
|
|
+ const resetGeomArr = flattenGeomWktListForMap(parsed.geometryArr);
|
|
|
drawRegionMap.setAreaGeometry(
|
|
|
- parsed.geometryArr,
|
|
|
+ resetGeomArr.length ? resetGeomArr : parsed.geometryArr,
|
|
|
false,
|
|
|
undefined,
|
|
|
undefined,
|
|
|
@@ -766,13 +886,16 @@ const confirmArea = async (drawMeta) => {
|
|
|
return;
|
|
|
}
|
|
|
const polygonData = drawRegionMap.getAreaGeometry?.();
|
|
|
- console.log(polygonData, 'polygonData');
|
|
|
const geometryArr = polygonData?.geometryArr;
|
|
|
if (!Array.isArray(geometryArr) || geometryArr.length === 0) {
|
|
|
ElMessage.warning("请先勾画地块后再确认");
|
|
|
return;
|
|
|
}
|
|
|
- const geom = geometryArr.length === 1 ? geometryArr[0] : JSON.stringify(geometryArr);
|
|
|
+ const geom = mergePolygonWktsForApi(geometryArr);
|
|
|
+ if (!isValidGeom(geom)) {
|
|
|
+ ElMessage.warning("地块几何无效,请重新勾画");
|
|
|
+ return;
|
|
|
+ }
|
|
|
const key = draftKeyFromParts(meta.type, meta.category);
|
|
|
if (!key) {
|
|
|
ElMessage.warning("无法识别勾画类别,请重新选择");
|
|
|
@@ -895,9 +1018,10 @@ const handleEditRegion = async () => {
|
|
|
const currentGeomStr = tab
|
|
|
? (getDraftGeomForTab(activeRegionType.value, tab, activeVariety.value) || tab.geom)
|
|
|
: "";
|
|
|
- const currentGeomArr = isValidGeom(currentGeomStr)
|
|
|
+ const currentGeomArrRaw = isValidGeom(currentGeomStr)
|
|
|
? [String(currentGeomStr).trim()]
|
|
|
: getGeomArrFromGeomItems(tab);
|
|
|
+ const currentGeomArr = flattenGeomWktListForMap(currentGeomArrRaw);
|
|
|
regionGeom.value =
|
|
|
currentGeomArr.length > 1 ? JSON.stringify(currentGeomArr) : currentGeomArr[0] || "";
|
|
|
if (currentGeomArr.length > 0) {
|
|
|
@@ -925,7 +1049,7 @@ const handleEditRegion = async () => {
|
|
|
handleVarietyClick(varietyTabs.value[activeVariety.value], activeVariety.value);
|
|
|
const tab = varietyTabs.value[activeVariety.value];
|
|
|
const draftGeom = getDraftGeomForTab(activeRegionType.value, tab, activeVariety.value);
|
|
|
- const geomArr =
|
|
|
+ const rawGeomArr =
|
|
|
isValidGeom(draftGeom)
|
|
|
? [String(draftGeom).trim()]
|
|
|
: (() => {
|
|
|
@@ -933,6 +1057,7 @@ const handleEditRegion = async () => {
|
|
|
if (fromItems.length) return fromItems;
|
|
|
return isValidGeom(tab?.geom) ? [String(tab.geom).trim()] : [];
|
|
|
})();
|
|
|
+ const geomArr = flattenGeomWktListForMap(rawGeomArr);
|
|
|
drawRegionMap.kmap?.polygonLayer?.source?.clear?.();
|
|
|
if (geomArr.length > 0) {
|
|
|
drawRegionMap.setAreaGeometry(
|
|
|
@@ -978,8 +1103,16 @@ const handleTipConfirm = () => {
|
|
|
width: 100%;
|
|
|
height: 100vh;
|
|
|
overflow: hidden;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ box-sizing: border-box;
|
|
|
+
|
|
|
+ & > :first-child {
|
|
|
+ flex-shrink: 0;
|
|
|
+ }
|
|
|
|
|
|
.region-type-tabs {
|
|
|
+ flex-shrink: 0;
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
background: #f4f4f4;
|
|
|
@@ -1003,6 +1136,7 @@ const handleTipConfirm = () => {
|
|
|
}
|
|
|
|
|
|
.variety-tabs {
|
|
|
+ flex-shrink: 0;
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: 8px;
|
|
|
@@ -1031,9 +1165,12 @@ const handleTipConfirm = () => {
|
|
|
.edit-map-content {
|
|
|
width: 100%;
|
|
|
margin-top: 10px;
|
|
|
- height: calc(100% - 162px);
|
|
|
+ flex: 1;
|
|
|
+ min-height: 0;
|
|
|
position: relative;
|
|
|
-
|
|
|
+ box-sizing: border-box;
|
|
|
+ /* 底部「重置 / 确认」等为 position:fixed,占高约 68px,地图区域需预留避免被盖住 */
|
|
|
+ padding-bottom: 68px;
|
|
|
.edit-map-tip {
|
|
|
position: absolute;
|
|
|
top: 23px;
|
|
|
@@ -1053,7 +1190,7 @@ const handleTipConfirm = () => {
|
|
|
|
|
|
.edit-map-footer {
|
|
|
position: absolute;
|
|
|
- bottom: 35px;
|
|
|
+ bottom: 85px;
|
|
|
left: 12px;
|
|
|
width: calc(100% - 24px);
|
|
|
display: flex;
|