From d4523a9d1220911197bac9d19361270fd7f56e96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=E5=85=88=E7=94=9F?= <1211120248@qq.com> Date: Wed, 12 Aug 2026 22:25:05 +0800 Subject: [PATCH] feat: normalize E01 ecological redline screen --- src/config/layers.js | 2 +- src/services/ecologicalProtectionApi.js | 42 ++++++++-- src/views/ecologicalProtection/data.js | 40 ++++++++-- src/views/ecologicalProtection/index.vue | 77 +++++++++++++++++-- .../components/FeatureInfoPanel.vue | 19 ++++- 5 files changed, 160 insertions(+), 20 deletions(-) diff --git a/src/config/layers.js b/src/config/layers.js index 9a8ea6e..72a8c43 100644 --- a/src/config/layers.js +++ b/src/config/layers.js @@ -399,7 +399,7 @@ export const ecologicalProtectionLayers = { { key: "ecological-redline-boundary", name: "生态保护红线", - layerName: "ne:t_ecological_redline_hy", + layerName: "ne:resource_ecological_redline_boundary", sourceType: "wfs", styles: "ecological_redline_hy", opacity: 0.58, diff --git a/src/services/ecologicalProtectionApi.js b/src/services/ecologicalProtectionApi.js index 2b3cbde..c99a09a 100644 --- a/src/services/ecologicalProtectionApi.js +++ b/src/services/ecologicalProtectionApi.js @@ -2,12 +2,44 @@ import { normalizeResponseData, requestJson } from "./request" const OPEN_API_BASE = import.meta.env.VITE_OPEN_API_BASE || "/openApi" -export async function getEcologicalScopeStats(scopeKey) { - const endpoint = scopeKey === "natural-protected-area" - ? "naturalProtectedArea" - : "redline" +const REDLINE_PANEL_KEYS = [ + "grasslandDistribution", + "wetlandDistribution", + "woodlandDistribution", + "grasslandProductivity", + "grasslandTypes", + "grasslandLevels", + "woodlandQuality", + "resourceStructure", +] + +export async function getEcologicalRedlineStats() { + const response = await requestJson( + `${OPEN_API_BASE}/screen2/ecology/redline`, + // The initial spatial overlay can populate PostGIS caches after a service + // restart; retain the page's explicit loading/error state while allowing + // the complete normalized result to arrive. + { method: "GET", timeout: 30000 }, + ) + const payload = normalizeResponseData(response) + const stats = Array.isArray(payload) ? payload[0] || {} : payload || {} + + if (!stats || typeof stats !== "object" || !["ready", "empty"].includes(stats.dataState)) { + throw new Error("生态保护红线接口返回的数据状态无效。") + } + if (!Array.isArray(stats.topRows?.rows) || !stats.panels || typeof stats.panels !== "object") { + throw new Error("生态保护红线接口未返回完整展示模型。") + } + const invalidPanelKey = REDLINE_PANEL_KEYS.find((key) => !Array.isArray(stats.panels[key]?.rows)) + if (invalidPanelKey) { + throw new Error(`生态保护红线接口缺少 ${invalidPanelKey} 面板展示数据。`) + } + return stats +} + +export async function getNaturalProtectedAreaStats() { const response = await requestJson( - `${OPEN_API_BASE}/screen2/ecologicalProtection/${endpoint}`, + `${OPEN_API_BASE}/screen2/ecologicalProtection/naturalProtectedArea`, { method: "GET", timeout: 30000 }, ) const data = normalizeResponseData(response) diff --git a/src/views/ecologicalProtection/data.js b/src/views/ecologicalProtection/data.js index dad23fc..d1e9c65 100644 --- a/src/views/ecologicalProtection/data.js +++ b/src/views/ecologicalProtection/data.js @@ -8,7 +8,37 @@ const toWanMu = (mu) => Number((Number(mu || 0) / 10000).toFixed(2)) const sumBy = (rows, field) => rows.reduce((sum, item) => sum + Number(item[field] || 0), 0) const round = (value, digits = 2) => Number(Number(value || 0).toFixed(digits)) -export const ecologicalScopeViewKeys = ["ecological-redline", "natural-protected-area"] +// The legacy scope builder is retained solely for the natural-protected-area view. +// Ecological redline data is an API-owned ready-to-render model below. +export const ecologicalScopeViewKeys = ["natural-protected-area"] + +const ecologicalRedlinePanelDefinitions = { + left: [ + { key: "grasslandDistribution", title: "草地分布", type: "column", variant: "plain", labelMode: "horizontal", scrollable: false, span: 1 }, + { key: "wetlandDistribution", title: "湿地面积", type: "rank", labelMode: "horizontal", scrollable: false, span: 1 }, + { key: "woodlandDistribution", title: "林地分布", type: "split", variant: "compact", span: 1 }, + { key: "grasslandProductivity", title: "草地生产力", type: "line", fixedType: true, labelMode: "horizontal", span: 1 }, + ], + right: [ + { key: "grasslandTypes", title: "草原类型", type: "donut", variant: "ring", span: 1 }, + { key: "grasslandLevels", title: "草原级占比", type: "rose", span: 1 }, + { key: "woodlandQuality", title: "林地质量", type: "heat", limit: 6, span: 1 }, + { key: "resourceStructure", title: "林草湿面积构成", type: "structure", span: 1 }, + ], +} + +export function buildEcologicalRedlinePanels(panels = {}) { + return Object.fromEntries( + Object.entries(ecologicalRedlinePanelDefinitions).map(([side, definitions]) => [ + side, + definitions.map((definition) => ({ + ...definition, + rows: Array.isArray(panels?.[definition.key]?.rows) ? panels[definition.key].rows : [], + count: panels?.[definition.key]?.count, + })), + ]), + ) +} function normalizeScopeRows(value = []) { let rows = value @@ -81,7 +111,7 @@ export function resolveEcologicalScopeStats(stats = {}) { return stats && typeof stats === "object" ? stats : {} } -export function buildEcologicalScopePanels(stats = {}, viewKey = "ecological-redline") { +export function buildEcologicalScopePanels(stats = {}, viewKey = "natural-protected-area") { stats = resolveEcologicalScopeStats(stats, viewKey) const grasslandDistributionRows = buildScopeTownRows(stats, "grasslandDistribution", "grasslandAreaWanMu", "万亩") const wetlandDistributionRows = buildScopeTownRows(stats, "wetlandDistribution", "wetlandAreaWanMu", "万亩") @@ -174,9 +204,9 @@ export function buildEcologicalScopePanels(stats = {}, viewKey = "ecological-red } } -export function getEcologicalScopeTopStats(stats = {}, viewKey = "ecological-redline") { +export function getEcologicalScopeTopStats(stats = {}, viewKey = "natural-protected-area") { stats = resolveEcologicalScopeStats(stats, viewKey) - const scopeName = viewKey === "natural-protected-area" ? "保护地" : "红线" + const scopeName = "保护地" return [ { zh: `${scopeName}面积`, value: round(stats.scopeAreaWanMu, 2), unit: "万亩", decimals: 2, icon: "park" }, { zh: "草地面积", value: round(stats.grasslandAreaWanMu, 2), unit: "万亩", decimals: 2, icon: "grass-basic" }, @@ -527,7 +557,7 @@ export const centerMapNotes = { } export function getEcologicalMapRanking(viewKey, stats = {}) { - if (ecologicalScopeViewKeys.includes(viewKey)) { + if (viewKey === "ecological-redline" || ecologicalScopeViewKeys.includes(viewKey)) { return { status: "success", title: viewKey === "natural-protected-area" ? "自然保护地" : "生态保护红线", diff --git a/src/views/ecologicalProtection/index.vue b/src/views/ecologicalProtection/index.vue index ee2fcc9..610bb8b 100644 --- a/src/views/ecologicalProtection/index.vue +++ b/src/views/ecologicalProtection/index.vue @@ -14,6 +14,15 @@ />
+
+ {{ redlineDataStateMessage }} +
) const isEngineeringAnalysis = computed(() => currentView.value === "engineering-analysis") const isProjectDetailView = computed(() => ["restoration", "engineering-analysis"].includes(currentView.value)) +const isEcologicalRedlineView = computed(() => currentView.value === "ecological-redline") const isEcologicalScopeView = computed(() => ecologicalScopeViewKeys.includes(currentView.value)) +const redlineDataStateMessage = computed(() => { + if (redlineDataState.value === "loading") return "生态保护红线数据加载中" + if (redlineDataState.value === "empty") return "生态保护红线暂无数据" + if (redlineDataState.value === "error") return "生态保护红线数据加载失败,请稍后重试" + return "" +}) const baseActiveLayer = computed(() => activeLayerKey.value ? getLayerByKey(currentView.value, activeLayerKey.value) : null) const selectedLegend = computed(() => (baseActiveLayer.value?.legends || []).find((item) => item.key === selectedLegendKey.value) || null @@ -314,6 +336,7 @@ const activeLayer = computed(() => { : layer }) const currentPanels = computed(() => { + if (isEcologicalRedlineView.value) return redlinePanels.value if (isEcologicalScopeView.value) return scopePanels.value if (currentView.value === "restoration") return restorationPanels.value if (currentView.value === "engineering-analysis") return engineeringAnalysisPanels @@ -398,12 +421,16 @@ function syncView(viewKey) { const isNationalPark = viewKey === "national-park" const isRestoration = viewKey === "restoration" const isEngineering = viewKey === "engineering-analysis" + const isRedline = viewKey === "ecological-redline" mapRanking.value = getEcologicalMapRanking(viewKey) mapSceneRef.value?.setBoundaryDrawing?.(false) mapSceneRef.value?.clearAnalysisBoundary?.() if (viewKey !== "engineering-analysis") mapSceneRef.value?.clearEngineeringSuitability?.() closeFeaturePanel() - if (ecologicalScopeViewKeys.includes(viewKey)) { + if (isRedline) { + redlineDataState.value = "loading" + redlinePanels.value = buildEcologicalRedlinePanels() + } else if (ecologicalScopeViewKeys.includes(viewKey)) { scopePanels.value = buildEcologicalScopePanels({}, viewKey) } if (isNationalPark) { @@ -419,15 +446,19 @@ function syncView(viewKey) { emit("module-stats", { moduleKey: "ecological-protection", viewKey, - items: ecologicalScopeViewKeys.includes(viewKey) - ? getEcologicalScopeTopStats({}, viewKey) + items: isRedline + ? [] + : ecologicalScopeViewKeys.includes(viewKey) + ? getEcologicalScopeTopStats({}, viewKey) : isNationalPark ? getNationalParkTopStats({}) : isRestoration ? getEcologicalRestorationTopStats({}) : [], }) - if (ecologicalScopeViewKeys.includes(viewKey)) { + if (isRedline) { + loadEcologicalRedlineStats(viewKey) + } else if (ecologicalScopeViewKeys.includes(viewKey)) { loadEcologicalScopeStats(viewKey) } else if (isNationalPark) { loadNationalParkStats(viewKey) @@ -541,7 +572,7 @@ async function loadEcologicalScopeStats(viewKey) { const requestId = ++scopeStatsRequestId try { const cached = scopeStatsCache.get(viewKey) - const stats = cached || await getEcologicalScopeStats(viewKey) + const stats = cached || await getNaturalProtectedAreaStats() if (requestId !== scopeStatsRequestId || currentView.value !== viewKey) return scopeStatsCache.set(viewKey, stats) scopePanels.value = buildEcologicalScopePanels(stats, viewKey) @@ -566,6 +597,38 @@ async function loadEcologicalScopeStats(viewKey) { } } +async function loadEcologicalRedlineStats(viewKey) { + const requestId = ++redlineStatsRequestId + redlineDataState.value = "loading" + redlinePanels.value = buildEcologicalRedlinePanels() + try { + const stats = redlineStatsCache || await getEcologicalRedlineStats() + if (requestId !== redlineStatsRequestId || currentView.value !== viewKey) return + redlineStatsCache = stats + redlineDataState.value = stats.dataState + redlinePanels.value = buildEcologicalRedlinePanels(stats.panels) + emit("module-stats", { + moduleKey: "ecological-protection", + viewKey, + items: stats.topRows.rows, + }) + } catch (error) { + if (requestId !== redlineStatsRequestId || currentView.value !== viewKey) return + redlineDataState.value = "error" + redlinePanels.value = buildEcologicalRedlinePanels() + emit("module-stats", { + moduleKey: "ecological-protection", + viewKey, + items: [], + }) + layerStatus.value = { + type: "error", + text: "生态保护红线统计加载失败", + } + console.warn("Failed to load ecological-redline statistics", error) + } +} + function initAssets() { assets = new Assets() const params = { progress: 0 } diff --git a/src/views/forestGrassWet/components/FeatureInfoPanel.vue b/src/views/forestGrassWet/components/FeatureInfoPanel.vue index b12b36b..2208ad1 100644 --- a/src/views/forestGrassWet/components/FeatureInfoPanel.vue +++ b/src/views/forestGrassWet/components/FeatureInfoPanel.vue @@ -736,6 +736,12 @@ const isEcologicalScopeFeature = computed(() => { || signal.includes("natural_protected_area") }) +const isEcologicalRedlineFeature = computed(() => { + const layerKey = String(props.feature?.layer?.key || "").toLowerCase() + const layerName = String(props.feature?.layer?.layerName || "").toLowerCase() + return layerKey.includes("ecological-redline") || layerName.includes("resource_ecological_redline_boundary") +}) + const isEngineeringSuitabilityFeature = computed(() => { const layerKey = String(props.feature?.layer?.key || "").toLowerCase() return layerKey.includes("engineering-suitability") || Boolean(propsData.value.engineering_name) @@ -762,14 +768,23 @@ const displayRows = computed(() => { const useCompactGrassland = (isGrassland || activeTopicKey.value === "wetland") && props.compactGrassland const businessFields = useCompactGrassland ? grasslandCoreBusinessFields : topicBusinessFields[activeTopicKey.value] || topicBusinessFields.woodland businessFields.forEach(([keys, label]) => { - const row = buildRow(propsData.value, keys, resolveDisplayLabel(label, keys), used) + const redlineArea = isEcologicalRedlineFeature.value && isEcologicalAreaLabel(label, keys) + const row = buildRow( + propsData.value, + redlineArea ? ["area_mu"] : keys, + resolveDisplayLabel(label, keys), + used, + ) if (row && !rows.some((item) => item.label === row.label)) { rows.push(row) used.add(row.sourceKey) } }) + if (isEcologicalRedlineFeature.value && !rows.some((row) => row.label === "图斑面积")) { + addStaticRow(rows, used, "图斑面积", "数据缺失", "area_mu-missing") + } const areaLabel = getTopicAreaLabel(activeTopicKey.value) - if (!isArtificialGrassland.value && !isEngineeringSuitabilityFeature.value && !rows.some((row) => row.label === areaLabel || isGenericPatchAreaLabel(row.label))) { + if (!isEcologicalRedlineFeature.value && !isArtificialGrassland.value && !isEngineeringSuitabilityFeature.value && !rows.some((row) => row.label === areaLabel || isGenericPatchAreaLabel(row.label))) { addStaticRow(rows, used, areaLabel, formatGeometryArea(featureGeometry.value), "geometry-area") } if (useCompactGrassland) return rows.slice(0, 10)