| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670 |
- <template>
- <div class="base-container no-events">
- <fnHeader showDate :autoGo="true" hideSwitch></fnHeader>
- <div class="content">
- <div class="warning-l left">
- <div class="warning-top yes-events">
- <div class="btn-common">四川省-简阳市-平泉街道</div>
- </div>
- </div>
- <!-- 顶部基础 tabs -->
- <div class="base-tabs yes-events">
- <div v-for="tab in baseTabs" :key="tab" class="tab-item" :class="{ active: activeBaseTab === tab }"
- @click="handleBaseTabClick(tab)">
- {{ tab }}
- </div>
- </div>
- <div class="btn-common smart-farm-btn yes-events" @click="handleSmartFarmClick">智慧农场</div>
- <!-- 地图图例 -->
- <!-- <map-legend></map-legend> -->
- <land-use-legend @change="handleLegendChange"></land-use-legend>
- <div class="show-point yes-events" v-show="showPoint" @click="handleShowPointClick"></div>
- </div>
- </div>
- <div ref="mapRef" class="bottom-map"></div>
- <track-dialog></track-dialog>
- </template>
- <script setup>
- import "./map/mockFarmLayer";
- import StaticMapLayers from "@/components/static_map_change/Layers.js";
- import StaticMapPointLayers from "@/components/static_map_change/pointLayer.js"
- import { onMounted, onUnmounted, ref, nextTick } from "vue";
- import fnHeader from "@/components/fnHeader.vue";
- import landUseLegend from "./components/landUseLegend.vue";
- import WarningMap from "./warningMap";
- import trackDialog from "./components/trackDialog.vue";
- import AlarmLayer from "./map/alarmLayer";
- import DistributionLayer from "./map/distributionLayer";
- import BoundaryLayer from "./map/boundaryLayer";
- import WaterLayer from "./map/waterLayer";
- import eventBus from "@/api/eventBus";
- import { useStore } from "vuex";
- import { useRouter, useRoute } from "vue-router";
- let store = useStore();
- const router = useRouter();
- const route = useRoute();
- let warningMap = new WarningMap();
- let alarmLayer = null;
- let staticMapLayers = null;
- let distributionLayer = null;
- let staticMapPointLayers = null;
- let boundaryLayer = null;
- let waterLayer = null;
- const mapRef = ref(null);
- const treeRef = ref(null);
- const treeActionData = ref([]);
- // 保存原始数据,用于恢复
- const originalTreeData = ref([]);
- // 物候期分布下,当前激活的"二级"节点(只允许一个)
- const activePhenologySecondId = ref(null);
- // 当前选中的年份和季度
- const currentYear = ref(2025);
- const currentQuarter = ref(1);
- const isLandRecognition = ref(false);
- // 顶部基础 tabs
- const baseTabs = ["物候期分布", "长势等级", "水利", "灌渠与泵站", "资源", "导出报告"];
- const activeBaseTab = ref("物候期分布");
- const warningLayers = ref({});
- const showPoint = ref(false)
- const handleBaseTabClick = (tab) => {
- activeBaseTab.value = tab;
- showPoint.value = false
- staticMapPointLayers.hidePoint()
- staticMapLayers.hideAll()
- // 水利图层隐藏
- waterLayer && waterLayer.toggleLayer(false)
- waterLayer && waterLayer.toggleCanalLayer(false)
- if (tab === "资源") {
- staticMapPointLayers.showPoint()
- }else if (tab === "灌渠与泵站") {
- showPoint.value = true
- waterLayer && waterLayer.toggleCanalLayer(true)
- }else if (tab === "长势等级") {
- staticMapLayers.showSingle("Dongguan长势", false);
- }else if (tab === "物候期分布") {
- staticMapLayers.showSingle("Dongguan物候期", false);
- }else if (tab === "水利") {
- waterLayer && waterLayer.toggleLayer(true)
- }
- };
- const handleSmartFarmClick = () => {
- window.open("https://feiniao-pc-gly.feiniaotech.com/#/login");
- };
- const handleShowPointClick = () => {
- eventBus.emit("chat:showTrackDialog")
- }
- onMounted(async () => {
- // 使用 nextTick 确保 DOM 已经渲染完成,地图容器有正确的尺寸
- await nextTick();
- warningMap.initMap(store.getters.userinfo.location, mapRef.value);
- // 地图初始化后,更新地图尺寸以确保正确渲染
- if (warningMap.kmap && warningMap.kmap.map) {
- // 使用 setTimeout 确保地图容器尺寸已计算
- setTimeout(() => {
- warningMap.kmap.map.updateSize();
- }, 0);
- }
- alarmLayer = new AlarmLayer(warningMap.kmap);
- staticMapLayers = new StaticMapLayers(warningMap.kmap);
- staticMapPointLayers = new StaticMapPointLayers(warningMap.kmap);
- distributionLayer = new DistributionLayer(warningMap.kmap);
- boundaryLayer = new BoundaryLayer(warningMap.kmap);
- waterLayer = new WaterLayer(warningMap.kmap);
- await getSpeciesListData();
- getWaterData();
- getWaterCanalData();
- getDistributionData();
- // 数据加载完成后,再次更新地图尺寸以确保正确渲染
- if (warningMap.kmap && warningMap.kmap.map) {
- setTimeout(() => {
- warningMap.kmap.map.updateSize();
- }, 100);
- }
- // 胜华村的村界
- getVillageBoundary()
- eventBus.emit("warningMap:init", warningMap.kmap);
- handleBaseTabClick("物候期分布")
- // 图例数据
- eventBus.on("alarmList:warningLayers", (data) => {
- warningLayers.value = data;
- });
- // 窗口大小改变时更新地图尺寸
- const handleResize = () => {
- if (warningMap.kmap && warningMap.kmap.map) {
- warningMap.kmap.map.updateSize();
- }
- };
- window.addEventListener("resize", handleResize);
- // 在组件卸载时清理
- onUnmounted(() => {
- window.removeEventListener("resize", handleResize);
- });
- });
- // 加载东莞市行政区边界(使用远程 GeoJSON 数据)
- const getVillageBoundary = async () => {
- const url =
- "https://birdseye-img.sysuimars.com/geojson/mlxy/%E4%B8%9C%E8%8E%9E%E8%A1%8C%E6%94%BF%E5%8C%BA.geojson";
- try {
- const response = await fetch(url);
- if (!response.ok) {
- console.error("获取行政区 GeoJSON 失败:", response.statusText);
- return;
- }
- const geojson = await response.json();
- if (!geojson || !Array.isArray(geojson.features)) {
- console.error("行政区 GeoJSON 格式不正确:", geojson);
- return;
- }
- // 将 GeoJSON MultiPolygon 转成 boundaryLayer 需要的 WKT 格式数据
- const geoJsonMultiPolygonToWkt = (geometry) => {
- if (!geometry || geometry.type !== "MultiPolygon" || !Array.isArray(geometry.coordinates)) {
- return "";
- }
- const polygons = geometry.coordinates
- .map((polygon) => {
- // polygon: [ [ [x, y], ... ] ] => 每个 polygon 可能包含多个 ring
- const rings = polygon
- .map((ring) => {
- const coords = ring.map((coord) => `${coord[0]} ${coord[1]}`).join(", ");
- return `(${coords})`;
- })
- .join(", ");
- return `(${rings})`;
- })
- .join(", ");
- return `MULTIPOLYGON (${polygons})`;
- };
- const villageBoundary = geojson.features
- .map((f, index) => {
- const wkt = geoJsonMultiPolygonToWkt(f.geometry);
- if (!wkt) return null;
- const props = f.properties || {};
- return {
- id: props.code || props["区划码"] || index,
- name: props["地名"] || props.ENG_NAME || "行政区边界",
- geom: wkt,
- };
- })
- .filter((item) => item);
- if (!villageBoundary.length) {
- console.warn("未能从 GeoJSON 中解析出有效的行政区边界");
- return;
- }
- boundaryLayer && boundaryLayer.initData(villageBoundary);
- } catch (error) {
- console.error("加载行政区 GeoJSON 出错:", error);
- }
- };
- const getWaterData = async () => {
- const { data } = await VE_API.layer.waterList();
- waterLayer.initData(data);
- const { data: riverData } = await VE_API.layer.riverList();
- waterLayer.initRiver(riverData);
- };
- // 获取水渠数据
- const getWaterCanalData = async () => {
- const { data } = await VE_API.warning.fetchLandCanalList();
- waterLayer.initCanal(data);
- };
- sessionStorage.removeItem("farmId");
- onUnmounted(() => {
- eventBus.off("alarmList:changeMapLayer");
- eventBus.off("chartList:updateMap");
- // 时间轴
- eventBus.off("weatherTime:changeTime");
- });
- const getSpeciesListData = async () => {
- const res = await VE_API.species.speciesList();
- treeActionData.value = res.data;
- // 保存原始数据副本(深拷贝)
- originalTreeData.value = JSON.parse(JSON.stringify(res.data));
- };
- const getDistributionData = async (statType = null) => {
- const { data } = await VE_API.agri_land_crop.queryDistribution({ statType: statType || 0 });
- // 把点位图层去掉:不返回 centerPoint,只保留地块相关信息
- const list = Array.isArray(data)
- ? data.map((item) => {
- const { centerPoint, ...rest } = item || {};
- return rest;
- })
- : [];
- distributionLayer.initData(list);
- };
- const fetchFarmList = (phenologyIds) => {
- const params = {
- year: currentYear.value,
- quarter: currentQuarter.value,
- phenologyIds: phenologyIds || [],
- };
- return new Promise((resolve, reject) => {
- VE_API.warning
- .fetchFarmList(params)
- .then((res) => {
- if (res.code === 0 && res.data && res.data.length > 0) {
- // 将接口数据转换为地图需要的格式
- const cropData = res.data.map((item) => {
- // 组合作物名称和物候期名称作为 label
- const label = item.phenologyName
- ? `${item.speciesName}-${item.phenologyName}`
- : item.speciesName;
- return {
- ...item,
- label: label,
- color: item.speciesColor || "#2199F8",
- centerPoint: item.point, // 使用 point 作为 centerPoint
- };
- });
- // 渲染作物数据到地图
- distributionLayer.initData(cropData, "label");
- resolve(cropData);
- } else {
- // 接口返回空数据时,清空地图
- distributionLayer.initData([]);
- resolve([]);
- }
- })
- .catch((error) => {
- // 错误时也清空地图
- distributionLayer.initData([]);
- reject(error);
- });
- });
- };
- // 根据节点 id 在当前树数据中计算其层级(1/2/3)及所属的二级节点 id
- const getNodeLevelInfo = (id) => {
- const roots = treeActionData.value || [];
- for (const root of roots) {
- if (root.id === id) {
- return { level: 1, secondId: null };
- }
- const rootChildren = root.items || root.children || [];
- for (const second of rootChildren) {
- if (second.id === id) {
- return { level: 2, secondId: second.id };
- }
- const secondChildren = second.items || second.children || [];
- for (const third of secondChildren) {
- if (third.id === id) {
- return { level: 3, secondId: second.id };
- }
- }
- }
- }
- return { level: 0, secondId: null };
- };
- const getTreeChecks = async (nodeData, data) => {
- const { checkedNodes } = data;
- let finalCheckedNodes = checkedNodes;
- // 物候期分布:限制"二级只能选一个,三级不限个数"
- if (
- (activeBaseTab.value === "物候期分布" ||
- activeBaseTab.value === "预警分布" ||
- activeBaseTab.value === "农场分布") &&
- treeRef.value
- ) {
- const tree = treeRef.value;
- const { level, secondId } = getNodeLevelInfo(nodeData.id);
- if (level === 2 || level === 3) {
- const currentSecondId = secondId;
- // 判断当前这个二级分支下,是否还有被选中的节点(包含二级自己或其子级)
- const hasAnyCheckedInCurrentSecond = checkedNodes.some((n) => {
- const info = getNodeLevelInfo(n.id);
- return info.secondId === currentSecondId || (info.level === 2 && n.id === currentSecondId);
- });
- if (hasAnyCheckedInCurrentSecond) {
- // 仍有节点被选中 → 保证只有当前这个二级分支被选中,其它分支全部取消
- activePhenologySecondId.value = currentSecondId;
- const roots = treeActionData.value || [];
- roots.forEach((root) => {
- const rootChildren = root.items || root.children || [];
- rootChildren.forEach((second) => {
- if (second.id !== currentSecondId) {
- // 取消其它二级及其所有子级勾选
- tree.setChecked(second.id, false, true);
- }
- // 对于当前二级节点,不手动设置其选中状态
- // 让 Element Plus 根据子节点的选中状态自动计算半选中状态
- // 这样当部分三级节点被选中时,二级节点会自动显示半选中状态
- });
- });
- } else {
- // 当前二级分支已经被全部取消勾选 → 清空激活记录,允许"全部不选"
- activePhenologySecondId.value = null;
- }
- }
- // 对树进行了 setChecked 操作后,重新从树组件拿一次最新的选中节点列表
- // 这里只需要最后一层(叶子节点 / 有 wktArr 的节点),不用父级节点
- const allCheckedNodes = treeRef.value.getCheckedNodes(false, true);
- finalCheckedNodes = allCheckedNodes.filter((n) => !n.children || n.children.length === 0 || n.wktArr);
- }
- // 任意 tab 下,最终都用当前选中的节点驱动地图渲染
- // 提取最后一级节点的 id 到数组(没有子节点的叶子节点)
- const field = activeBaseTab.value === "物候期分布" || activeBaseTab.value === "农场分布" ? "originalId" : "id";
- const lastLevelIds = finalCheckedNodes
- .filter((n) => (!n.items || n.items.length === 0) && (!n.children || n.children.length === 0))
- .map((n) => n[field]);
- if (lastLevelIds && lastLevelIds.length === 0) {
- distributionLayer.initData([]);
- return;
- }
- if (activeBaseTab.value === "物候期分布") {
- const phenologyData = await getDistributionData(null, lastLevelIds);
- distributionLayer.initData(phenologyData, "phenologyName");
- return;
- }
- if (activeBaseTab.value === "农场分布") {
- await fetchFarmList(lastLevelIds);
- return;
- }
- // 并发请求所有数据,等待所有 Promise 完成
- const promises = lastLevelIds.map((id) => {
- const node = finalCheckedNodes.find((n) => n.id === id);
- if (node) {
- return getDistributionData(node.id);
- }
- return Promise.resolve([]);
- });
- // 等待所有请求完成,并将结果扁平化
- const results = await Promise.all(promises);
- const finalMapData = results.flat();
- distributionLayer.initData(finalMapData);
- };
- // 处理图例变化
- const handleLegendChange = (data) => {
- console.log("图例变化:", data);
- // 通过 eventBus 将选中的类别信息传递给饼图组件
- if (data.checked) {
- eventBus.emit("landUseLegend:change", {
- category: data.category,
- nonGrain: data.nonGrain,
- children: data.children,
- });
- }
- };
- </script>
- <style lang="scss" scoped>
- .base-container {
- width: 100%;
- height: 100vh;
- color: #fff;
- position: absolute;
- box-sizing: border-box;
- z-index: 1;
- overflow: hidden;
- .content {
- width: 100%;
- height: calc(100% - 74px - 48px);
- padding: 16px 20px 0 27px;
- display: flex;
- justify-content: space-between;
- box-sizing: border-box;
- position: relative;
- .show-point{
- position: absolute;
- top: calc(50% - 250px);
- left: calc(50% - 250px);
- width: 500px;
- height: 500px;
- // background: red;
- z-index: 2;
- }
- .left,
- .right {
- width: calc(376px + 54px);
- height: 100%;
- box-sizing: border-box;
- // display: flex;
- }
- .right {
- // width: 395px;
- width: 376px;
- overflow: auto;
- position: relative;
- .list {
- width: 100%;
- height: 100%;
- }
- }
- .chart-wrap {
- padding: 8px;
- background: #101010;
- border: 1px solid #444444;
- }
- .btn-common {
- color: #f7be5a;
- font-size: 20px;
- font-family: "PangMenZhengDao";
- margin-right: 15px;
- border: 2px solid rgba(255, 212, 137, 0.3);
- border-radius: 4px;
- padding: 8px 14px 11px;
- background: rgba(29, 29, 29, 0.54);
- cursor: pointer;
- }
- .smart-farm-btn{
- position: fixed;
- top: 35px;
- right: 200px;
- padding: 10px 45px 13px;
- }
- .warning-top {
- display: flex;
- align-items: center;
- width: max-content;
- }
- .base-tabs {
- display: flex;
- position: fixed;
- top: 35px;
- left: 390px;
- .tab-item {
- border-radius: 6px;
- padding: 6px 15px 8px;
- margin-right: 12px;
- text-align: center;
- font-size: 20px;
- font-family: "PangMenZhengDao";
- color: #ffffff;
- background: rgba(37, 50, 57, 0.6);
- cursor: pointer;
- border: 2px solid transparent;
- &.active {
- background: rgba(115, 74, 2, 0.16);
- color: #FFD489;
- border-color: #FFD489;
- }
- }
- }
- }
- }
- .bottom-map {
- width: 100%;
- height: 100vh;
- position: absolute;
- z-index: 0;
- }
- </style>
- <style lang="less">
- .ol-scale-line {
- left: auto;
- right: 435px;
- bottom: 13px;
- .ol-scale-line-inner {
- max-width: 80px;
- width: 80px !important;
- color: #fff;
- border-color: #fff;
- }
- }
- .focus-farm-select {
- &.el-popper.is-light {
- background: #232323;
- border-color: rgba(255, 212, 137, 0.3);
- box-shadow: 0px 0px 12px rgba(255, 212, 137, 0.3);
- .el-select-dropdown__item {
- background: none;
- color: rgba(255, 212, 137, 0.6);
- }
- .el-select-dropdown__item.is-selected {
- background: rgba(255, 212, 137, 0.2);
- color: #ffd489;
- }
- }
- &.el-popper.is-light .el-popper__arrow:before {
- background: #232323;
- border-color: rgba(255, 212, 137, 0.3);
- }
- }
- .ol-popup-warning {
- position: relative;
- width: 295px;
- background: rgb(35, 35, 35, 0.86);
- color: #fff;
- font-size: 16px;
- border-radius: 4px;
- .warning-info-title {
- display: flex;
- padding: 6px 10px;
- background: rgba(255, 255, 255, 0.05);
- font-size: 18px;
- border-radius: 4px 4px 0 0;
- .icon {
- padding-right: 6px;
- }
- .close {
- position: absolute;
- right: 12px;
- top: 4px;
- }
- }
- .info-content {
- padding: 16px 20px 40px 20px;
- line-height: 26px;
- text-indent: 2em;
- }
- }
- .area-cascader {
- &.el-popper.is-light {
- background: #232323;
- border-color: rgba(255, 212, 137, 0.3);
- box-shadow: 0px 0px 12px rgba(255, 212, 137, 0.3);
- .el-cascader-menu {
- color: rgba(255, 212, 137, 0.6);
- border-color: rgba(255, 212, 137, 0.3);
- }
- .el-cascader-node.in-active-path,
- .el-cascader-node.is-active,
- .el-cascader-node.is-selectable.in-checked-path {
- color: #f7be5a;
- background: transparent;
- }
- .el-radio__input.is-checked .el-radio__inner {
- background: #f7be5a;
- border-color: #f7be5a;
- }
- .el-cascader-node:not(.is-disabled):hover,
- .el-cascader-node:not(.is-disabled):focus,
- .el-cascader-node:not(.is-disabled):hover {
- background: rgba(255, 212, 137, 0.2);
- }
- }
- .el-radio__inner {
- background-color: rgba(255, 212, 137, 0.3);
- border-color: rgba(255, 212, 137, 0.6);
- }
- .el-radio__inner::after {
- background: #000;
- }
- &.el-popper.is-light .el-popper__arrow:before {
- background: #232323;
- border-color: rgba(255, 212, 137, 0.3);
- }
- }
- </style>
|