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)