drawRegion2.vue 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612
  1. <template>
  2. <div class="edit-map">
  3. <custom-header :name="viewOnly ? '查看区域' : '勾画区域'"></custom-header>
  4. <div class="region-type-tabs">
  5. <div v-for="item in regionTypeTabs" :key="item.code" class="region-type-tab"
  6. :class="{ 'region-type-tab--active': activeRegionType === item.code }">
  7. {{ item.name }}
  8. </div>
  9. </div>
  10. <div class="variety-tabs" v-if="varietyTabs.length > 0 && activeRegionType !== 'DORMANCY'">
  11. <div v-for="(v, index) in varietyTabs" :key="index" class="variety-tab"
  12. :class="{ 'variety-tab--active': activeVariety === index }">
  13. {{ v.regionName || v.problemZoneTypeName }}
  14. </div>
  15. </div>
  16. <div class="edit-map-content">
  17. <div class="edit-map-tip" v-if="!viewOnly">操作提示:拖动圆点,即可调整地块边界</div>
  18. <div class="map-container" ref="mapContainer"></div>
  19. <div class="edit-map-footer" :style="{ 'bottom': activeRegionType !== 'DORMANCY' ? '85px' : '59px' }">
  20. <div class="footer-back" @click="goBack">
  21. <img class="back-icon" src="@/assets/img/home/go-back.png" alt="" />
  22. </div>
  23. <div class="edit-map-footer-btn" v-if="!viewOnly">
  24. <div class="btn-delete" @click="deletePolygon">删除地块</div>
  25. <!-- <div class="btn-cancel" @click="goBack">取消</div> -->
  26. <div class="btn-confirm" @click="confirm">确认</div>
  27. </div>
  28. </div>
  29. </div>
  30. <popup v-model:show="showAbnormalTypePopup" round closeable class="abnormal-popup"
  31. :close-on-click-overlay="true">
  32. <div class="abnormal-popup-content">
  33. <div class="abnormal-popup-title">请选择异常类型</div>
  34. <el-select v-model="selectedAbnormalType" class="abnormal-type-select" placeholder="请选择类型" size="large">
  35. <el-option v-for="item in abnormalTypeOptions" :key="item.value" :label="item.name"
  36. :value="item.value" />
  37. </el-select>
  38. <div class="abnormal-popup-confirm" @click="handleConfirmUpload">确认上传</div>
  39. </div>
  40. </popup>
  41. </div>
  42. </template>
  43. <script setup>
  44. import customHeader from "@/components/customHeader.vue";
  45. import { ref, computed, onMounted, onActivated, onDeactivated } from "vue";
  46. import { Popup } from "vant";
  47. import DrawRegionMap from "./map/drawRegionMap.js";
  48. import { Map as KMapMap } from "@/utils/ol-map/KMap";
  49. import { useRouter, useRoute } from "vue-router";
  50. import { convertPointToArray } from "@/utils/index";
  51. import { ElMessage, ElMessageBox } from "element-plus";
  52. import Style from "ol/style/Style";
  53. import { Fill, Stroke, Circle, Text } from "ol/style.js";
  54. import { Point } from "ol/geom";
  55. import * as proj from "ol/proj";
  56. import { getArea } from "ol/sphere.js";
  57. const router = useRouter();
  58. const route = useRoute();
  59. const mapContainer = ref(null);
  60. const drawRegionMap = new DrawRegionMap();
  61. const type = ref(null);
  62. const viewOnly = computed(() => route.query.viewOnly === "1" || route.query.viewOnly === "true");
  63. const showAbnormalTypePopup = ref(false);
  64. const selectedAbnormalType = ref("");
  65. const abnormalTypeOptions = [{
  66. name: "病害",
  67. value: "DISEASE"
  68. }, {
  69. name: "虫害",
  70. value: "PEST"
  71. }, {
  72. name: "不确定",
  73. value: "UNCERTAIN"
  74. }];
  75. onMounted(() => {
  76. type.value = route.query.type;
  77. const point = route.query.mapCenter || "POINT (113.6142086995688 23.585836479509055)";
  78. const editable = !viewOnly.value;
  79. const showPoint = !viewOnly.value;
  80. drawRegionMap.initMap(point, mapContainer.value, editable, true, showPoint);
  81. applyRegionStyles();
  82. // 地图初始化之后(比如 initPreviewMap 里)
  83. // const regions = [
  84. // {
  85. // geometry:
  86. // "MULTIPOLYGON(((113.61674040430906 23.586573370597367,113.61586610436014 23.585922976493354,113.61710291900188 23.58486741952544,113.61770000158238 23.585651090473736,113.61674040430906 23.586573370597367)))",
  87. // status: "unresolved", // 未解决(蓝色)
  88. // },
  89. // {
  90. // geometry:
  91. // "MULTIPOLYGON(((113.61516640298626 23.588441931082958,113.61445736699218 23.58799411906573,113.61572616841707 23.586954554834552,113.61642987338976 23.588180707433526,113.61516640298626 23.588441931082958)))",
  92. // status: "resolved", // 已解决(灰色)
  93. // },
  94. // ];
  95. // drawRegionMap.setStatusRegions(regions);
  96. });
  97. onActivated(async () => {
  98. const point = route.query.mapCenter || "POINT (113.6142086995688 23.585836479509055)";
  99. await fetchRegionInfo();
  100. // 从编辑态进入仅查看时,需重新初始化为不可编辑
  101. if (viewOnly.value && drawRegionMap.kmap && drawRegionMap.editable) {
  102. drawRegionMap.destroyMap();
  103. drawRegionMap.initMap(point, mapContainer.value, false, true, false);
  104. }
  105. // 从仅查看进入勾画(编辑)时,需重新初始化为可编辑
  106. if (!viewOnly.value && drawRegionMap.kmap && !drawRegionMap.editable) {
  107. drawRegionMap.destroyMap();
  108. drawRegionMap.initMap(point, mapContainer.value, true, true, true);
  109. }
  110. applyRegionStyles();
  111. // 先绘制地块
  112. const polygonData = route.query.polygonData;
  113. const rawRangeWkt = route.query.rangeWkt;
  114. const rangeWkt = rawRangeWkt ? decodeURIComponent(rawRangeWkt) : null;
  115. if (rangeWkt) {
  116. let regions = [];
  117. try {
  118. const parsed = JSON.parse(rangeWkt);
  119. if (parsed && Array.isArray(parsed.geometryArr)) {
  120. regions = parsed.geometryArr.map((item) => ({
  121. geometry: item,
  122. status: "unresolved",
  123. reproductiveName: route.query.reproductiveName,
  124. updatedTime: route.query.updatedTime,
  125. }));
  126. } else if (typeof rangeWkt === "string" && rangeWkt.trim().length > 10) {
  127. regions = [{ geometry: rangeWkt.trim(), status: "unresolved", reproductiveName: route.query.reproductiveName, updatedTime: route.query.updatedTime }];
  128. }
  129. } catch (_) {
  130. if (typeof rangeWkt === "string" && rangeWkt.trim().length > 10) {
  131. regions = [{ geometry: rangeWkt.trim(), status: "unresolved", reproductiveName: route.query.reproductiveName, updatedTime: route.query.updatedTime }];
  132. }
  133. }
  134. if (regions.length) {
  135. drawRegionMap.setStatusRegions(regions);
  136. if (viewOnly.value && drawRegionMap.fitAllRegions) {
  137. drawRegionMap.fitAllRegions();
  138. }
  139. }
  140. }
  141. if (!viewOnly.value && polygonData) {
  142. drawRegionMap.setAreaGeometry(
  143. JSON.parse(polygonData)?.geometryArr,
  144. false,
  145. undefined,
  146. undefined,
  147. getAbnormalGrowthOverlayMeta()
  148. );
  149. }
  150. // 查看模式下已通过 fitAllRegions 适配;编辑模式再设置地图中心
  151. if (!viewOnly.value) {
  152. drawRegionMap.setMapPosition(convertPointToArray(point));
  153. }
  154. applyRegionStyles();
  155. });
  156. const regionTypeTabs = ref([]);
  157. const activeRegionType = ref("variety");
  158. const regionInfo = ref([]);
  159. async function fetchRegionInfo() {
  160. const { data } = await VE_API.basic_farm.fetchRegionInfo({ subjectId: localStorage.getItem('selectedFarmId') });
  161. if (data && data.length > 0) {
  162. regionInfo.value = data[0] || [];
  163. regionTypeTabs.value = regionInfo.value.problemZoneList || [];
  164. regionTypeTabs.value.unshift({ name: "品种区", code: "variety" });
  165. // if (data[0]?.regionList?.length) {
  166. // // if (!hasAppliedInitialVariety.value && route.query?.varietyId) {
  167. // // activeVariety.value = resolveInitialVarietyIndex(data[0]?.regionList);
  168. // // hasAppliedInitialVariety.value = true;
  169. // // }
  170. // // point.value = data[0].point;
  171. // varietyTabs.value = regionInfo.value.regionList || [];
  172. // }
  173. if (route.query.firstAct) {
  174. activeRegionType.value = route.query.firstAct;
  175. const index = regionTypeTabs.value.findIndex(item => item.code === route.query.firstAct);
  176. if (index !== -1) {
  177. varietyTabs.value = data[0].problemZoneList[index].children || [];
  178. activeVariety.value = 0;
  179. }
  180. }
  181. }
  182. }
  183. const varietyTabs = ref([]);
  184. const activeVariety = ref(0);
  185. /** 样式用的大类:与接口 tab.code 解耦(避免 ABNORMAL / 数字 code 等导致勾画色落到默认灰) */
  186. const getCanonicalRegionTypeForStyles = () => {
  187. const raw = activeRegionType.value;
  188. if (raw === "variety") return "variety";
  189. const tabs = regionTypeTabs.value || [];
  190. const item = tabs.find((t) => String(t?.code) === String(raw));
  191. if (item) {
  192. const kind = item.code;
  193. if (kind) return kind;
  194. }
  195. return "SLEEP";
  196. };
  197. const ABNORMAL_BADGE_BG_DISEASE_PEST = "#E32A28";
  198. const ABNORMAL_BADGE_BG_GROWTH = "#F76F00";
  199. /** 异常区小类(长势/病害/虫害等)闭合地块后在多边形内展示标签与发现日期 */
  200. const getAbnormalGrowthOverlayMeta = () => {
  201. if (getCanonicalRegionTypeForStyles() !== "ABNORMAL") return null;
  202. const tab = varietyTabs.value?.[activeVariety.value];
  203. if (!tab) return null;
  204. const name = (tab.problemZoneTypeName || tab.regionName || "").toString();
  205. let badgeText = "";
  206. let badgeBackground = ABNORMAL_BADGE_BG_GROWTH;
  207. if (name.includes("病害")) {
  208. badgeText = "新增病害";
  209. badgeBackground = ABNORMAL_BADGE_BG_DISEASE_PEST;
  210. } else if (name.includes("虫害")) {
  211. badgeText = "新增虫害";
  212. badgeBackground = ABNORMAL_BADGE_BG_DISEASE_PEST;
  213. } else if (name.includes("过慢")) {
  214. badgeText = "新增长势过慢";
  215. } else if (name.includes("过快")) {
  216. badgeText = "新增长势过快";
  217. } else {
  218. return null;
  219. }
  220. const now = new Date();
  221. const y = now.getFullYear();
  222. const m = String(now.getMonth() + 1).padStart(2, "0");
  223. const d = String(now.getDate()).padStart(2, "0");
  224. return { badgeText, discoveryDate: `${y}.${m}.${d}`, badgeBackground };
  225. };
  226. const createPolygonStyleFunc = (fillColor, strokeColor) => {
  227. return (feature) => {
  228. const styles = [];
  229. const coord = feature.getGeometry().getCoordinates()[0];
  230. for (let i = 0; i < coord[0].length - 1; i++) {
  231. if (i % 2) {
  232. styles.push(
  233. new Style({
  234. geometry: new Point(coord[0][i]),
  235. image: new Circle({
  236. radius: 4,
  237. fill: new Fill({
  238. color: strokeColor,
  239. }),
  240. stroke: new Stroke({
  241. color: "#fff",
  242. width: 1,
  243. }),
  244. }),
  245. })
  246. );
  247. } else {
  248. styles.push(
  249. new Style({
  250. geometry: new Point(coord[0][i]),
  251. image: new Circle({
  252. radius: 6,
  253. fill: new Fill({
  254. color: "#fff",
  255. }),
  256. }),
  257. })
  258. );
  259. }
  260. }
  261. const fillStyle = new Style({
  262. fill: new Fill({
  263. color: fillColor,
  264. }),
  265. stroke: new Stroke({
  266. color: strokeColor,
  267. width: 2,
  268. }),
  269. });
  270. let geom = feature.getGeometry().clone();
  271. geom.transform(proj.get("EPSG:4326"), proj.get("EPSG:38572"));
  272. let area = getArea(geom);
  273. area = (area + area / 2) / 1000;
  274. const growth = getAbnormalGrowthOverlayMeta();
  275. if (growth) {
  276. styles.push(
  277. new Style({
  278. text: new Text({
  279. text: growth.badgeText,
  280. font: "bold 13px sans-serif",
  281. fill: new Fill({ color: "#ffffff" }),
  282. backgroundFill: new Fill({ color: growth.badgeBackground || ABNORMAL_BADGE_BG_GROWTH }),
  283. padding: [4, 10, 4, 10],
  284. offsetY: -40,
  285. }),
  286. }),
  287. new Style({
  288. text: new Text({
  289. text: `发现时间:${growth.discoveryDate}`,
  290. font: "12px sans-serif",
  291. fill: new Fill({ color: "#ffffff" }),
  292. offsetY: -16,
  293. }),
  294. })
  295. );
  296. }
  297. const areaValStyle = new Style({
  298. text: new Text({
  299. font: "16px sans-serif",
  300. text: area.toFixed(2) + "亩",
  301. fill: new Fill({ color: "#fff" }),
  302. offsetY: growth ? 14 : 0,
  303. }),
  304. });
  305. styles.push(fillStyle, areaValStyle);
  306. return styles;
  307. };
  308. };
  309. const applyRegionStyles = () => {
  310. const kmap = drawRegionMap.kmap;
  311. if (!kmap) return;
  312. let lineColor = "#2199F8";
  313. let vertexColor = "#2199F8";
  314. let fillColor = [0, 0, 0, 0.5];
  315. let strokeColor = "#2199F8";
  316. const styleKind = getCanonicalRegionTypeForStyles();
  317. if (styleKind === "variety") {
  318. lineColor = "#18AA8B";
  319. vertexColor = "#18AA8B";
  320. fillColor = [0, 57, 44, 0.5];
  321. strokeColor = "#18AA8B";
  322. } else if (styleKind === "ABNORMAL") {
  323. if (activeVariety.value < 2) {
  324. lineColor = "#E03131";
  325. vertexColor = "#E03131";
  326. fillColor = [100, 0, 0, 0.5];
  327. strokeColor = "#E03131";
  328. } else {
  329. lineColor = "#FF7300";
  330. vertexColor = "#FF7300";
  331. fillColor = [124, 46, 0, 0.5];
  332. strokeColor = "#FF7300";
  333. }
  334. } else if (styleKind === "ENVIRONMENT") {
  335. lineColor = "#FDCF7F";
  336. vertexColor = "#FDCF7F";
  337. fillColor = [151, 96, 0, 0.5];
  338. strokeColor = "#FDCF7F";
  339. } else {
  340. lineColor = "#A6A6A6";
  341. vertexColor = "#A6A6A6";
  342. fillColor = [166, 166, 166, 0.25];
  343. strokeColor = "#A6A6A6";
  344. }
  345. KMapMap.drawStyleColors = {
  346. line: lineColor,
  347. vertex: vertexColor,
  348. fill: fillColor,
  349. stroke: strokeColor,
  350. };
  351. kmap.polygonStyle = createPolygonStyleFunc(fillColor, strokeColor);
  352. if (kmap.polygonLayer?.layer && typeof kmap.polygonLayer.layer.setStyle === "function") {
  353. kmap.polygonLayer.layer.setStyle(kmap.polygonStyle);
  354. }
  355. };
  356. onDeactivated(() => {
  357. drawRegionMap.clearLayer()
  358. })
  359. const goBack = () => {
  360. // drawRegionMap.clearLayer()
  361. router.back()
  362. };
  363. const deletePolygon = () => {
  364. ElMessageBox.confirm(
  365. '确认要删除当前地块吗?删除后可以重新勾画。',
  366. '删除确认',
  367. {
  368. confirmButtonText: '确认删除',
  369. cancelButtonText: '取消',
  370. type: 'warning',
  371. }
  372. ).then(() => {
  373. drawRegionMap.abortOngoingDrawSketch();
  374. if (drawRegionMap.kmap && drawRegionMap.kmap.polygonLayer && drawRegionMap.kmap.polygonLayer.source) {
  375. drawRegionMap.kmap.polygonLayer.source.clear();
  376. }
  377. ElMessage.success("地块已删除");
  378. }).catch(() => {
  379. // 用户取消删除,不做任何操作
  380. });
  381. };
  382. const saveAndBack = () => {
  383. const polygonData = drawRegionMap.getAreaGeometry();
  384. sessionStorage.setItem("drawRegionPolygonData", JSON.stringify(polygonData));
  385. if (selectedAbnormalType.value) {
  386. sessionStorage.setItem("drawRegionAbnormalType", selectedAbnormalType.value);
  387. } else {
  388. sessionStorage.removeItem("drawRegionAbnormalType");
  389. }
  390. router.back();
  391. };
  392. const handleConfirmUpload = () => {
  393. if (!selectedAbnormalType.value) {
  394. ElMessage.warning("请选择异常类型");
  395. return;
  396. }
  397. showAbnormalTypePopup.value = false;
  398. saveAndBack();
  399. };
  400. const confirm = () => {
  401. if (getCanonicalRegionTypeForStyles() === "ABNORMAL") {
  402. showAbnormalTypePopup.value = true;
  403. return;
  404. }
  405. saveAndBack();
  406. };
  407. </script>
  408. <style lang="scss" scoped>
  409. .edit-map {
  410. width: 100%;
  411. height: 100vh;
  412. overflow: hidden;
  413. .region-type-tabs {
  414. display: flex;
  415. align-items: center;
  416. background: #f4f4f4;
  417. margin: 10px 10px 0;
  418. padding: 3px;
  419. border-radius: 4px;
  420. box-sizing: border-box;
  421. .region-type-tab {
  422. flex: 1;
  423. text-align: center;
  424. padding: 5px 0;
  425. color: #767676;
  426. }
  427. .region-type-tab--active {
  428. background: #ffffff;
  429. border-radius: 4px;
  430. color: #0D0D0D;
  431. }
  432. }
  433. .variety-tabs {
  434. display: flex;
  435. align-items: center;
  436. gap: 8px;
  437. padding: 10px 12px 0;
  438. overflow-x: auto;
  439. overflow-y: hidden;
  440. flex-wrap: nowrap;
  441. -webkit-overflow-scrolling: touch;
  442. .variety-tab {
  443. padding: 4px 12px;
  444. border-radius: 2px;
  445. color: #575757;
  446. background: #F4F4F4;
  447. border: 1px solid transparent;
  448. white-space: nowrap;
  449. }
  450. .variety-tab--active {
  451. background: rgba(33, 153, 248, 0.1);
  452. color: #2199F8;
  453. border: 1px solid #2199F8;
  454. }
  455. }
  456. .edit-map-content {
  457. width: 100%;
  458. height: calc(100% - 96px);
  459. margin-top: 10px;
  460. position: relative;
  461. .edit-map-tip {
  462. position: absolute;
  463. top: 23px;
  464. left: calc(50% - 256px / 2);
  465. z-index: 1;
  466. font-size: 12px;
  467. color: #fff;
  468. padding: 9px 20px;
  469. background: rgba(0, 0, 0, 0.5);
  470. border-radius: 20px;
  471. }
  472. .map-container {
  473. width: 100%;
  474. height: 100%;
  475. }
  476. .edit-map-footer {
  477. position: absolute;
  478. bottom: 85px;
  479. left: 12px;
  480. width: calc(100% - 24px);
  481. display: flex;
  482. flex-direction: column;
  483. align-items: flex-end;
  484. .footer-back {
  485. padding: 6px 7px 9px;
  486. background: #fff;
  487. border-radius: 8px;
  488. margin-bottom: 30px;
  489. .back-icon {
  490. width: 20px;
  491. height: 18px;
  492. }
  493. }
  494. .edit-map-footer-btn {
  495. display: flex;
  496. justify-content: center;
  497. align-items: center;
  498. width: 100%;
  499. gap: 8px;
  500. div {
  501. flex: 1;
  502. max-width: 100px;
  503. text-align: center;
  504. color: #666666;
  505. font-size: 14px;
  506. padding: 8px 0;
  507. border-radius: 25px;
  508. background: #fff;
  509. }
  510. .btn-delete {
  511. background: #ff4d4f;
  512. color: #fff;
  513. }
  514. .btn-confirm {
  515. background: #000;
  516. background-image: linear-gradient(180deg, #76c3ff 0%, #2199f8 100%);
  517. color: #fff;
  518. }
  519. }
  520. }
  521. }
  522. }
  523. .abnormal-popup {
  524. width: 100%;
  525. ::v-deep {
  526. .van-popup__close-icon {
  527. color: #000;
  528. }
  529. }
  530. .abnormal-popup-content {
  531. background: linear-gradient(180deg, #d9ecff 0%, #ffffff 60%);
  532. border-radius: 18px;
  533. padding: 24px 16px;
  534. box-sizing: border-box;
  535. position: relative;
  536. .abnormal-popup-title {
  537. font-size: 16px;
  538. margin-bottom: 12px;
  539. }
  540. .abnormal-type-select {
  541. width: 100%;
  542. }
  543. .abnormal-popup-confirm {
  544. margin-top: 24px;
  545. border-radius: 25px;
  546. padding: 8px;
  547. background: #2199F8;
  548. color: #fff;
  549. font-size: 16px;
  550. display: flex;
  551. align-items: center;
  552. justify-content: center;
  553. }
  554. }
  555. }
  556. </style>