feat: normalize E01 ecological redline screen

M先生 3 months ago
parent d97e9846df
commit d4523a9d12
  1. 2
      src/config/layers.js
  2. 42
      src/services/ecologicalProtectionApi.js
  3. 40
      src/views/ecologicalProtection/data.js
  4. 75
      src/views/ecologicalProtection/index.vue
  5. 19
      src/views/forestGrassWet/components/FeatureInfoPanel.vue

@ -399,7 +399,7 @@ export const ecologicalProtectionLayers = {
{ {
key: "ecological-redline-boundary", key: "ecological-redline-boundary",
name: "生态保护红线", name: "生态保护红线",
layerName: "ne:t_ecological_redline_hy", layerName: "ne:resource_ecological_redline_boundary",
sourceType: "wfs", sourceType: "wfs",
styles: "ecological_redline_hy", styles: "ecological_redline_hy",
opacity: 0.58, opacity: 0.58,

@ -2,12 +2,44 @@ import { normalizeResponseData, requestJson } from "./request"
const OPEN_API_BASE = import.meta.env.VITE_OPEN_API_BASE || "/openApi" const OPEN_API_BASE = import.meta.env.VITE_OPEN_API_BASE || "/openApi"
export async function getEcologicalScopeStats(scopeKey) { const REDLINE_PANEL_KEYS = [
const endpoint = scopeKey === "natural-protected-area" "grasslandDistribution",
? "naturalProtectedArea" "wetlandDistribution",
: "redline" "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( const response = await requestJson(
`${OPEN_API_BASE}/screen2/ecologicalProtection/${endpoint}`, `${OPEN_API_BASE}/screen2/ecologicalProtection/naturalProtectedArea`,
{ method: "GET", timeout: 30000 }, { method: "GET", timeout: 30000 },
) )
const data = normalizeResponseData(response) const data = normalizeResponseData(response)

@ -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 sumBy = (rows, field) => rows.reduce((sum, item) => sum + Number(item[field] || 0), 0)
const round = (value, digits = 2) => Number(Number(value || 0).toFixed(digits)) 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 = []) { function normalizeScopeRows(value = []) {
let rows = value let rows = value
@ -81,7 +111,7 @@ export function resolveEcologicalScopeStats(stats = {}) {
return stats && typeof stats === "object" ? 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) stats = resolveEcologicalScopeStats(stats, viewKey)
const grasslandDistributionRows = buildScopeTownRows(stats, "grasslandDistribution", "grasslandAreaWanMu", "万亩") const grasslandDistributionRows = buildScopeTownRows(stats, "grasslandDistribution", "grasslandAreaWanMu", "万亩")
const wetlandDistributionRows = buildScopeTownRows(stats, "wetlandDistribution", "wetlandAreaWanMu", "万亩") 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) stats = resolveEcologicalScopeStats(stats, viewKey)
const scopeName = viewKey === "natural-protected-area" ? "保护地" : "红线" const scopeName = "保护地"
return [ return [
{ zh: `${scopeName}面积`, value: round(stats.scopeAreaWanMu, 2), unit: "万亩", decimals: 2, icon: "park" }, { zh: `${scopeName}面积`, value: round(stats.scopeAreaWanMu, 2), unit: "万亩", decimals: 2, icon: "park" },
{ zh: "草地面积", value: round(stats.grasslandAreaWanMu, 2), unit: "万亩", decimals: 2, icon: "grass-basic" }, { zh: "草地面积", value: round(stats.grasslandAreaWanMu, 2), unit: "万亩", decimals: 2, icon: "grass-basic" },
@ -527,7 +557,7 @@ export const centerMapNotes = {
} }
export function getEcologicalMapRanking(viewKey, stats = {}) { export function getEcologicalMapRanking(viewKey, stats = {}) {
if (ecologicalScopeViewKeys.includes(viewKey)) { if (viewKey === "ecological-redline" || ecologicalScopeViewKeys.includes(viewKey)) {
return { return {
status: "success", status: "success",
title: viewKey === "natural-protected-area" ? "自然保护地" : "生态保护红线", title: viewKey === "natural-protected-area" ? "自然保护地" : "生态保护红线",

@ -14,6 +14,15 @@
/> />
<div class="forest-grass-wet-ui ecological-protection-ui"> <div class="forest-grass-wet-ui ecological-protection-ui">
<div
v-if="isEcologicalRedlineView && redlineDataState !== 'ready'"
class="fgw-data-state"
:class="{ 'is-error': redlineDataState === 'error' }"
role="status"
aria-live="polite"
>
{{ redlineDataStateMessage }}
</div>
<div v-if="!isEngineeringAnalysis" class="fgw-left"> <div v-if="!isEngineeringAnalysis" class="fgw-left">
<div class="fgw-side-stack is-left"> <div class="fgw-side-stack is-left">
<m-card <m-card
@ -170,8 +179,9 @@ import {
} from "@/config/layers" } from "@/config/layers"
import { import {
getEcologicalEngineeringAnalysisStats, getEcologicalEngineeringAnalysisStats,
getEcologicalRedlineStats,
getEcologicalRestorationStats, getEcologicalRestorationStats,
getEcologicalScopeStats, getNaturalProtectedAreaStats,
getNationalParkStats, getNationalParkStats,
} from "@/services/ecologicalProtectionApi" } from "@/services/ecologicalProtectionApi"
import { getProjectClickInfoByWkt } from "@/services/pastureApi" import { getProjectClickInfoByWkt } from "@/services/pastureApi"
@ -186,6 +196,7 @@ import TopicSwitcher from "@/views/forestGrassWet/components/TopicSwitcher.vue"
import EngineeringAnalysisPanel from "./components/EngineeringAnalysisPanel.vue" import EngineeringAnalysisPanel from "./components/EngineeringAnalysisPanel.vue"
import EngineeringStatisticsPanel from "./components/EngineeringStatisticsPanel.vue" import EngineeringStatisticsPanel from "./components/EngineeringStatisticsPanel.vue"
import { import {
buildEcologicalRedlinePanels,
buildEcologicalScopePanels, buildEcologicalScopePanels,
buildEcologicalRestorationPanels, buildEcologicalRestorationPanels,
buildNationalParkPanels, buildNationalParkPanels,
@ -216,7 +227,9 @@ const emit = defineEmits(["module-stats", "view-change"])
const mapSceneRef = ref(null) const mapSceneRef = ref(null)
const activeLayerKey = ref("") const activeLayerKey = ref("")
const selectedLegendKey = ref("") const selectedLegendKey = ref("")
const scopePanels = ref(buildEcologicalScopePanels({}, "ecological-redline")) const redlinePanels = ref(buildEcologicalRedlinePanels())
const redlineDataState = ref("loading")
const scopePanels = ref(buildEcologicalScopePanels({}, "natural-protected-area"))
const nationalParkPanels = ref(buildNationalParkPanels({})) const nationalParkPanels = ref(buildNationalParkPanels({}))
const restorationPanels = ref(buildEcologicalRestorationPanels({})) const restorationPanels = ref(buildEcologicalRestorationPanels({}))
const selectedFeature = ref(null) const selectedFeature = ref(null)
@ -242,11 +255,13 @@ let mapAssetsReady = false
let mapLoadStarted = false let mapLoadStarted = false
let mapLoadTimer = null let mapLoadTimer = null
let scopeStatsRequestId = 0 let scopeStatsRequestId = 0
let redlineStatsRequestId = 0
let nationalParkStatsRequestId = 0 let nationalParkStatsRequestId = 0
let restorationStatsRequestId = 0 let restorationStatsRequestId = 0
let engineeringAnalysisRequestId = 0 let engineeringAnalysisRequestId = 0
let featureQueryRequestId = 0 let featureQueryRequestId = 0
const scopeStatsCache = new Map() const scopeStatsCache = new Map()
let redlineStatsCache = null
let nationalParkStatsCache = null let nationalParkStatsCache = null
let restorationStatsCache = null let restorationStatsCache = null
let engineeringAnalysisCache = null let engineeringAnalysisCache = null
@ -281,7 +296,14 @@ const currentView = computed(() =>
) )
const isEngineeringAnalysis = computed(() => currentView.value === "engineering-analysis") const isEngineeringAnalysis = computed(() => currentView.value === "engineering-analysis")
const isProjectDetailView = computed(() => ["restoration", "engineering-analysis"].includes(currentView.value)) const isProjectDetailView = computed(() => ["restoration", "engineering-analysis"].includes(currentView.value))
const isEcologicalRedlineView = computed(() => currentView.value === "ecological-redline")
const isEcologicalScopeView = computed(() => ecologicalScopeViewKeys.includes(currentView.value)) 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 baseActiveLayer = computed(() => activeLayerKey.value ? getLayerByKey(currentView.value, activeLayerKey.value) : null)
const selectedLegend = computed(() => const selectedLegend = computed(() =>
(baseActiveLayer.value?.legends || []).find((item) => item.key === selectedLegendKey.value) || null (baseActiveLayer.value?.legends || []).find((item) => item.key === selectedLegendKey.value) || null
@ -314,6 +336,7 @@ const activeLayer = computed(() => {
: layer : layer
}) })
const currentPanels = computed(() => { const currentPanels = computed(() => {
if (isEcologicalRedlineView.value) return redlinePanels.value
if (isEcologicalScopeView.value) return scopePanels.value if (isEcologicalScopeView.value) return scopePanels.value
if (currentView.value === "restoration") return restorationPanels.value if (currentView.value === "restoration") return restorationPanels.value
if (currentView.value === "engineering-analysis") return engineeringAnalysisPanels if (currentView.value === "engineering-analysis") return engineeringAnalysisPanels
@ -398,12 +421,16 @@ function syncView(viewKey) {
const isNationalPark = viewKey === "national-park" const isNationalPark = viewKey === "national-park"
const isRestoration = viewKey === "restoration" const isRestoration = viewKey === "restoration"
const isEngineering = viewKey === "engineering-analysis" const isEngineering = viewKey === "engineering-analysis"
const isRedline = viewKey === "ecological-redline"
mapRanking.value = getEcologicalMapRanking(viewKey) mapRanking.value = getEcologicalMapRanking(viewKey)
mapSceneRef.value?.setBoundaryDrawing?.(false) mapSceneRef.value?.setBoundaryDrawing?.(false)
mapSceneRef.value?.clearAnalysisBoundary?.() mapSceneRef.value?.clearAnalysisBoundary?.()
if (viewKey !== "engineering-analysis") mapSceneRef.value?.clearEngineeringSuitability?.() if (viewKey !== "engineering-analysis") mapSceneRef.value?.clearEngineeringSuitability?.()
closeFeaturePanel() closeFeaturePanel()
if (ecologicalScopeViewKeys.includes(viewKey)) { if (isRedline) {
redlineDataState.value = "loading"
redlinePanels.value = buildEcologicalRedlinePanels()
} else if (ecologicalScopeViewKeys.includes(viewKey)) {
scopePanels.value = buildEcologicalScopePanels({}, viewKey) scopePanels.value = buildEcologicalScopePanels({}, viewKey)
} }
if (isNationalPark) { if (isNationalPark) {
@ -419,7 +446,9 @@ function syncView(viewKey) {
emit("module-stats", { emit("module-stats", {
moduleKey: "ecological-protection", moduleKey: "ecological-protection",
viewKey, viewKey,
items: ecologicalScopeViewKeys.includes(viewKey) items: isRedline
? []
: ecologicalScopeViewKeys.includes(viewKey)
? getEcologicalScopeTopStats({}, viewKey) ? getEcologicalScopeTopStats({}, viewKey)
: isNationalPark : isNationalPark
? getNationalParkTopStats({}) ? getNationalParkTopStats({})
@ -427,7 +456,9 @@ function syncView(viewKey) {
? getEcologicalRestorationTopStats({}) ? getEcologicalRestorationTopStats({})
: [], : [],
}) })
if (ecologicalScopeViewKeys.includes(viewKey)) { if (isRedline) {
loadEcologicalRedlineStats(viewKey)
} else if (ecologicalScopeViewKeys.includes(viewKey)) {
loadEcologicalScopeStats(viewKey) loadEcologicalScopeStats(viewKey)
} else if (isNationalPark) { } else if (isNationalPark) {
loadNationalParkStats(viewKey) loadNationalParkStats(viewKey)
@ -541,7 +572,7 @@ async function loadEcologicalScopeStats(viewKey) {
const requestId = ++scopeStatsRequestId const requestId = ++scopeStatsRequestId
try { try {
const cached = scopeStatsCache.get(viewKey) const cached = scopeStatsCache.get(viewKey)
const stats = cached || await getEcologicalScopeStats(viewKey) const stats = cached || await getNaturalProtectedAreaStats()
if (requestId !== scopeStatsRequestId || currentView.value !== viewKey) return if (requestId !== scopeStatsRequestId || currentView.value !== viewKey) return
scopeStatsCache.set(viewKey, stats) scopeStatsCache.set(viewKey, stats)
scopePanels.value = buildEcologicalScopePanels(stats, viewKey) 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() { function initAssets() {
assets = new Assets() assets = new Assets()
const params = { progress: 0 } const params = { progress: 0 }

@ -736,6 +736,12 @@ const isEcologicalScopeFeature = computed(() => {
|| signal.includes("natural_protected_area") || 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 isEngineeringSuitabilityFeature = computed(() => {
const layerKey = String(props.feature?.layer?.key || "").toLowerCase() const layerKey = String(props.feature?.layer?.key || "").toLowerCase()
return layerKey.includes("engineering-suitability") || Boolean(propsData.value.engineering_name) 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 useCompactGrassland = (isGrassland || activeTopicKey.value === "wetland") && props.compactGrassland
const businessFields = useCompactGrassland ? grasslandCoreBusinessFields : topicBusinessFields[activeTopicKey.value] || topicBusinessFields.woodland const businessFields = useCompactGrassland ? grasslandCoreBusinessFields : topicBusinessFields[activeTopicKey.value] || topicBusinessFields.woodland
businessFields.forEach(([keys, label]) => { 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)) { if (row && !rows.some((item) => item.label === row.label)) {
rows.push(row) rows.push(row)
used.add(row.sourceKey) used.add(row.sourceKey)
} }
}) })
if (isEcologicalRedlineFeature.value && !rows.some((row) => row.label === "图斑面积")) {
addStaticRow(rows, used, "图斑面积", "数据缺失", "area_mu-missing")
}
const areaLabel = getTopicAreaLabel(activeTopicKey.value) 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") addStaticRow(rows, used, areaLabel, formatGeometryArea(featureGeometry.value), "geometry-area")
} }
if (useCompactGrassland) return rows.slice(0, 10) if (useCompactGrassland) return rows.slice(0, 10)

Loading…
Cancel
Save