feat: normalize E02 natural protected area screen

M先生 3 months ago
parent d4523a9d12
commit 497f9e3399
  1. 2
      src/config/layers.js
  2. 75
      src/services/ecologicalProtectionApi.js
  3. 190
      src/views/ecologicalProtection/data.js
  4. 77
      src/views/ecologicalProtection/index.vue
  5. 33
      src/views/forestGrassWet/components/FeatureInfoPanel.vue

@ -429,7 +429,7 @@ export const ecologicalProtectionLayers = {
{ {
key: "natural-protected-area-boundary", key: "natural-protected-area-boundary",
name: "自然保护地", name: "自然保护地",
layerName: "ne:t_natural_protected_area_hy", layerName: "ne:resource_natural_protected_area_boundary",
sourceType: "wfs", sourceType: "wfs",
styles: "natural_protected_area_hy", styles: "natural_protected_area_hy",
opacity: 0.74, opacity: 0.74,

@ -2,7 +2,7 @@ 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"
const REDLINE_PANEL_KEYS = [ const ECOLOGICAL_DIRECT_PANEL_KEYS = [
"grasslandDistribution", "grasslandDistribution",
"wetlandDistribution", "wetlandDistribution",
"woodlandDistribution", "woodlandDistribution",
@ -14,38 +14,34 @@ const REDLINE_PANEL_KEYS = [
] ]
export async function getEcologicalRedlineStats() { export async function getEcologicalRedlineStats() {
return getEcologicalDirectStats("redline", "生态保护红线")
}
export async function getNaturalProtectedAreaStats() {
return getEcologicalDirectStats("protected-area", "自然保护地")
}
async function getEcologicalDirectStats(path, featureName) {
const response = await requestJson( const response = await requestJson(
`${OPEN_API_BASE}/screen2/ecology/redline`, `${OPEN_API_BASE}/screen2/ecology/${path}`,
// 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 }, { method: "GET", timeout: 30000 },
) )
const payload = normalizeResponseData(response) const payload = normalizeResponseData(response)
const stats = Array.isArray(payload) ? payload[0] || {} : payload || {} const stats = Array.isArray(payload) ? payload[0] || {} : payload || {}
if (!stats || typeof stats !== "object" || !["ready", "empty"].includes(stats.dataState)) { if (!stats || typeof stats !== "object" || !["ready", "empty"].includes(stats.dataState)) {
throw new Error("生态保护红线接口返回的数据状态无效。") throw new Error(`${featureName}接口返回的数据状态无效。`)
} }
if (!Array.isArray(stats.topRows?.rows) || !stats.panels || typeof stats.panels !== "object") { if (!Array.isArray(stats.topRows?.rows) || !stats.panels || typeof stats.panels !== "object") {
throw new Error("生态保护红线接口未返回完整展示模型。") throw new Error(`${featureName}接口未返回完整展示模型。`)
} }
const invalidPanelKey = REDLINE_PANEL_KEYS.find((key) => !Array.isArray(stats.panels[key]?.rows)) const invalidPanelKey = ECOLOGICAL_DIRECT_PANEL_KEYS.find((key) => !Array.isArray(stats.panels[key]?.rows))
if (invalidPanelKey) { if (invalidPanelKey) {
throw new Error(`生态保护红线接口缺少 ${invalidPanelKey} 面板展示数据。`) throw new Error(`${featureName}接口缺少 ${invalidPanelKey} 面板展示数据。`)
} }
return stats return stats
} }
export async function getNaturalProtectedAreaStats() {
const response = await requestJson(
`${OPEN_API_BASE}/screen2/ecologicalProtection/naturalProtectedArea`,
{ method: "GET", timeout: 30000 },
)
const data = normalizeResponseData(response)
return normalizeEcologicalScopeStats(Array.isArray(data) ? data[0] || {} : data || {})
}
const NATIONAL_PARK_JSON_FIELDS = [ const NATIONAL_PARK_JSON_FIELDS = [
"topRows", "topRows",
"coreRows", "coreRows",
@ -127,46 +123,3 @@ function parseJsonbRows(value) {
return [] return []
} }
} }
const ECOLOGICAL_SCOPE_FIELD_MAP = {
scope_key: "scopeKey",
scope_name: "scopeName",
scope_area_wan_mu: "scopeAreaWanMu",
source_feature_count: "sourceFeatureCount",
grassland_area_wan_mu: "grasslandAreaWanMu",
wetland_area_wan_mu: "wetlandAreaWanMu",
woodland_area_wan_mu: "woodlandAreaWanMu",
grassland_fresh_yield_wan_ton: "grasslandFreshYieldWanTon",
wetland_fresh_yield_wan_ton: "wetlandFreshYieldWanTon",
grassland_mu_yield_jin: "grasslandMuYieldJin",
wetland_mu_yield_jin: "wetlandMuYieldJin",
grassland_vegetation_cover_percent: "grasslandVegetationCoverPercent",
grassland_patch_count: "grasslandPatchCount",
wetland_patch_count: "wetlandPatchCount",
woodland_patch_count: "woodlandPatchCount",
grassland_distribution: "grasslandDistribution",
wetland_distribution: "wetlandDistribution",
woodland_distribution: "woodlandDistribution",
grassland_productivity: "grasslandProductivity",
grassland_types: "grasslandTypes",
zoning_structure: "zoningStructure",
grassland_levels: "grasslandLevels",
vegetation_cover_bands: "vegetationCoverBands",
woodland_types: "woodlandTypes",
woodland_quality: "woodlandQuality",
area_method: "areaMethod",
area_unit: "areaUnit",
productivity_method: "productivityMethod",
}
function normalizeEcologicalScopeStats(raw = {}) {
if (!raw || typeof raw !== "object") return {}
return Object.entries(raw).reduce((result, [key, value]) => {
result[key] = value
const normalizedKey = ECOLOGICAL_SCOPE_FIELD_MAP[key]
if (normalizedKey && result[normalizedKey] === undefined) {
result[normalizedKey] = value
}
return result
}, {})
}

@ -1,18 +1,12 @@
import { hongyuanTownshipDisplayOrder } from "@/config/townshipBoundaries"
import { NATIONAL_PARK_ZONING_COLORS } from "@/config/layers" import { NATIONAL_PARK_ZONING_COLORS } from "@/config/layers"
import { ecologicalProtectionMenu } from "@/config/ecologicalProtection" import { ecologicalProtectionMenu } from "@/config/ecologicalProtection"
export { ecologicalProtectionMenu } export { ecologicalProtectionMenu }
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))
// The legacy scope builder is retained solely for the natural-protected-area view. const ecologicalDirectPanelDefinitions = {
// Ecological redline data is an API-owned ready-to-render model below.
export const ecologicalScopeViewKeys = ["natural-protected-area"]
const ecologicalRedlinePanelDefinitions = {
left: [ left: [
{ key: "grasslandDistribution", title: "草地分布", type: "column", variant: "plain", labelMode: "horizontal", scrollable: false, span: 1 }, { 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: "wetlandDistribution", title: "湿地面积", type: "rank", labelMode: "horizontal", scrollable: false, span: 1 },
@ -27,9 +21,9 @@ const ecologicalRedlinePanelDefinitions = {
], ],
} }
export function buildEcologicalRedlinePanels(panels = {}) { function buildEcologicalDirectPanels(panels = {}) {
return Object.fromEntries( return Object.fromEntries(
Object.entries(ecologicalRedlinePanelDefinitions).map(([side, definitions]) => [ Object.entries(ecologicalDirectPanelDefinitions).map(([side, definitions]) => [
side, side,
definitions.map((definition) => ({ definitions.map((definition) => ({
...definition, ...definition,
@ -40,180 +34,8 @@ export function buildEcologicalRedlinePanels(panels = {}) {
) )
} }
function normalizeScopeRows(value = []) { export const buildEcologicalRedlinePanels = buildEcologicalDirectPanels
let rows = value export const buildNaturalProtectedAreaPanels = buildEcologicalDirectPanels
if (rows && typeof rows === "object" && !Array.isArray(rows) && "value" in rows) {
rows = rows.value
}
if (typeof rows === "string") {
try {
rows = JSON.parse(rows)
} catch {
rows = []
}
}
return (Array.isArray(rows) ? rows : []).map((item, index) => ({
key: item.key || `${item.name || "统计项"}-${index}`,
name: item.name || `统计项${index + 1}`,
value: round(item.value, 2),
unit: item.unit || "万亩",
color: item.color,
extra: item.extra || "",
icon: item.icon || "",
}))
}
function scopeMetric(key, name, value, unit, extra = "") {
return { key, name, value: round(value, unit === "%" ? 1 : 2), unit, extra }
}
function scopePercentExtra(rows = [], total = sumBy(rows, "value")) {
return rows.map((item) => ({
...item,
extra: item.extra || `占比 ${round((Number(item.value || 0) / Math.max(total, 1)) * 100, 1)}%`,
}))
}
function sortScopeTownRows(rows = [], sortByValue = false) {
const order = new Map(hongyuanTownshipDisplayOrder.map((name, index) => [name, index]))
return [...rows].sort((a, b) => {
if (sortByValue) return Number(b.value || 0) - Number(a.value || 0)
const left = order.has(a.name) ? order.get(a.name) : 999
const right = order.has(b.name) ? order.get(b.name) : 999
return left - right || Number(b.value || 0) - Number(a.value || 0)
})
}
function buildScopeTownRows(stats = {}, key, fallbackMetricKey, unit = "万亩", options = {}) {
const rows = sortScopeTownRows(normalizeScopeRows(stats[key]), options.sortByValue === true)
.filter((item) => Number(item.value) > 0)
.map((item) => ({ ...item, unit: item.unit || unit }))
if (rows.length) return options.withPercent === false ? rows : scopePercentExtra(rows)
const fallbackValue = Number(stats[fallbackMetricKey] || 0)
return fallbackValue > 0 ? [scopeMetric(`${key}-total`, "全县合计", fallbackValue, unit)] : []
}
function buildScopeValueRows(stats = {}, key, fallbackMetricKey, fallbackName, unit = "万亩", limit = 6) {
const rows = normalizeScopeRows(stats[key])
.filter((item) => Number(item.value) > 0)
.slice(0, limit)
.map((item) => ({ ...item, unit: item.unit || unit }))
if (rows.length) return scopePercentExtra(rows)
const fallbackValue = Number(stats[fallbackMetricKey] || 0)
return fallbackValue > 0 ? [scopeMetric(`${key}-total`, fallbackName, fallbackValue, unit)] : []
}
function hasMeaningfulRows(panel) {
return (panel?.rows || []).some((item) => Number(item.value) > 0)
}
export function resolveEcologicalScopeStats(stats = {}) {
return stats && typeof stats === "object" ? stats : {}
}
export function buildEcologicalScopePanels(stats = {}, viewKey = "natural-protected-area") {
stats = resolveEcologicalScopeStats(stats, viewKey)
const grasslandDistributionRows = buildScopeTownRows(stats, "grasslandDistribution", "grasslandAreaWanMu", "万亩")
const wetlandDistributionRows = buildScopeTownRows(stats, "wetlandDistribution", "wetlandAreaWanMu", "万亩")
const woodlandDistributionRows = buildScopeTownRows(stats, "woodlandDistribution", "woodlandAreaWanMu", "万亩", {
sortByValue: true,
})
const grasslandProductivityRows = buildScopeTownRows(stats, "grasslandProductivity", "grasslandMuYieldJin", "斤/亩", {
withPercent: false,
})
const grasslandTypeRows = buildScopeValueRows(stats, "grasslandTypes", "grasslandAreaWanMu", "草地面积")
const grasslandLevelRows = buildScopeValueRows(stats, "grasslandLevels", "grasslandAreaWanMu", "草地面积")
const woodlandQualityRows = buildScopeValueRows(stats, "woodlandQuality", "woodlandAreaWanMu", "林地面积")
const resourceStructureRows = scopePercentExtra([
{ key: `${viewKey}-scope-grassland`, name: "草地资源", value: Number(stats.grasslandAreaWanMu || 0), unit: "万亩" },
{ key: `${viewKey}-scope-wetland`, name: "湿地资源", value: Number(stats.wetlandAreaWanMu || 0), unit: "万亩" },
{ key: `${viewKey}-scope-woodland`, name: "林地资源", value: Number(stats.woodlandAreaWanMu || 0), unit: "万亩" },
].filter((item) => Number(item.value) > 0))
return {
left: [
{
key: `${viewKey}-grassland-distribution`,
title: "草地分布",
type: "column",
variant: "plain",
labelMode: "horizontal",
scrollable: false,
span: 1,
rows: grasslandDistributionRows,
},
{
key: `${viewKey}-wetland-distribution`,
title: "湿地面积",
type: "rank",
labelMode: "horizontal",
scrollable: false,
span: 1,
rows: wetlandDistributionRows,
},
{
key: `${viewKey}-woodland-distribution`,
title: "林地分布",
type: "split",
variant: "compact",
span: 1,
rows: woodlandDistributionRows,
},
{
key: `${viewKey}-grassland-productivity`,
title: "草地生产力",
type: "line",
fixedType: true,
labelMode: "horizontal",
span: 1,
rows: grasslandProductivityRows,
},
].filter(hasMeaningfulRows),
right: [
{
key: `${viewKey}-grassland-type`,
title: "草原类型",
type: "donut",
variant: "ring",
span: 1,
rows: grasslandTypeRows,
},
{
key: `${viewKey}-grassland-level`,
title: "草原级占比",
type: "rose",
span: 1,
rows: grasslandLevelRows,
},
{
key: `${viewKey}-woodland-quality`,
title: "林地质量",
type: "heat",
limit: 6,
span: 1,
rows: woodlandQualityRows,
},
{
key: `${viewKey}-resource-structure`,
title: "林草湿面积构成",
type: "structure",
span: 1,
rows: resourceStructureRows,
},
].filter(hasMeaningfulRows),
}
}
export function getEcologicalScopeTopStats(stats = {}, viewKey = "natural-protected-area") {
stats = resolveEcologicalScopeStats(stats, viewKey)
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" },
{ zh: "湿地面积", value: round(stats.wetlandAreaWanMu, 2), unit: "万亩", decimals: 2, icon: "wetland" },
{ zh: "林地面积", value: round(stats.woodlandAreaWanMu, 2), unit: "万亩", decimals: 2, icon: "forest" },
]
}
function normalizeEcologicalRows(value = []) { function normalizeEcologicalRows(value = []) {
let rows = value let rows = value
@ -557,7 +379,7 @@ export const centerMapNotes = {
} }
export function getEcologicalMapRanking(viewKey, stats = {}) { export function getEcologicalMapRanking(viewKey, stats = {}) {
if (viewKey === "ecological-redline" || ecologicalScopeViewKeys.includes(viewKey)) { if (["ecological-redline", "natural-protected-area"].includes(viewKey)) {
return { return {
status: "success", status: "success",
title: viewKey === "natural-protected-area" ? "自然保护地" : "生态保护红线", title: viewKey === "natural-protected-area" ? "自然保护地" : "生态保护红线",

@ -15,13 +15,13 @@
<div class="forest-grass-wet-ui ecological-protection-ui"> <div class="forest-grass-wet-ui ecological-protection-ui">
<div <div
v-if="isEcologicalRedlineView && redlineDataState !== 'ready'" v-if="isEcologicalDirectView && ecologicalDirectDataState !== 'ready'"
class="fgw-data-state" class="fgw-data-state"
:class="{ 'is-error': redlineDataState === 'error' }" :class="{ 'is-error': ecologicalDirectDataState === 'error' }"
role="status" role="status"
aria-live="polite" aria-live="polite"
> >
{{ redlineDataStateMessage }} {{ ecologicalDirectDataStateMessage }}
</div> </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">
@ -197,12 +197,10 @@ import EngineeringAnalysisPanel from "./components/EngineeringAnalysisPanel.vue"
import EngineeringStatisticsPanel from "./components/EngineeringStatisticsPanel.vue" import EngineeringStatisticsPanel from "./components/EngineeringStatisticsPanel.vue"
import { import {
buildEcologicalRedlinePanels, buildEcologicalRedlinePanels,
buildEcologicalScopePanels, buildNaturalProtectedAreaPanels,
buildEcologicalRestorationPanels, buildEcologicalRestorationPanels,
buildNationalParkPanels, buildNationalParkPanels,
ecologicalScopeViewKeys,
engineeringAnalysisPanels, engineeringAnalysisPanels,
getEcologicalScopeTopStats,
getEcologicalMapRanking, getEcologicalMapRanking,
getEcologicalRestorationTopStats, getEcologicalRestorationTopStats,
getNationalParkTopStats, getNationalParkTopStats,
@ -229,7 +227,8 @@ const activeLayerKey = ref("")
const selectedLegendKey = ref("") const selectedLegendKey = ref("")
const redlinePanels = ref(buildEcologicalRedlinePanels()) const redlinePanels = ref(buildEcologicalRedlinePanels())
const redlineDataState = ref("loading") const redlineDataState = ref("loading")
const scopePanels = ref(buildEcologicalScopePanels({}, "natural-protected-area")) const naturalProtectedAreaPanels = ref(buildNaturalProtectedAreaPanels())
const naturalProtectedAreaDataState = ref("loading")
const nationalParkPanels = ref(buildNationalParkPanels({})) const nationalParkPanels = ref(buildNationalParkPanels({}))
const restorationPanels = ref(buildEcologicalRestorationPanels({})) const restorationPanels = ref(buildEcologicalRestorationPanels({}))
const selectedFeature = ref(null) const selectedFeature = ref(null)
@ -254,14 +253,14 @@ let destroyed = false
let mapAssetsReady = false let mapAssetsReady = false
let mapLoadStarted = false let mapLoadStarted = false
let mapLoadTimer = null let mapLoadTimer = null
let scopeStatsRequestId = 0
let redlineStatsRequestId = 0 let redlineStatsRequestId = 0
let naturalProtectedAreaStatsRequestId = 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()
let redlineStatsCache = null let redlineStatsCache = null
let naturalProtectedAreaStatsCache = null
let nationalParkStatsCache = null let nationalParkStatsCache = null
let restorationStatsCache = null let restorationStatsCache = null
let engineeringAnalysisCache = null let engineeringAnalysisCache = null
@ -297,11 +296,16 @@ 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 isEcologicalRedlineView = computed(() => currentView.value === "ecological-redline")
const isEcologicalScopeView = computed(() => ecologicalScopeViewKeys.includes(currentView.value)) const isNaturalProtectedAreaView = computed(() => currentView.value === "natural-protected-area")
const redlineDataStateMessage = computed(() => { const isEcologicalDirectView = computed(() => isEcologicalRedlineView.value || isNaturalProtectedAreaView.value)
if (redlineDataState.value === "loading") return "生态保护红线数据加载中" const ecologicalDirectDataState = computed(() => (
if (redlineDataState.value === "empty") return "生态保护红线暂无数据" isEcologicalRedlineView.value ? redlineDataState.value : naturalProtectedAreaDataState.value
if (redlineDataState.value === "error") return "生态保护红线数据加载失败,请稍后重试" ))
const ecologicalDirectDataStateMessage = computed(() => {
const featureName = isEcologicalRedlineView.value ? "生态保护红线" : "自然保护地"
if (ecologicalDirectDataState.value === "loading") return `${featureName}数据加载中`
if (ecologicalDirectDataState.value === "empty") return `${featureName}暂无数据`
if (ecologicalDirectDataState.value === "error") return `${featureName}数据加载失败,请稍后重试`
return "" return ""
}) })
const baseActiveLayer = computed(() => activeLayerKey.value ? getLayerByKey(currentView.value, activeLayerKey.value) : null) const baseActiveLayer = computed(() => activeLayerKey.value ? getLayerByKey(currentView.value, activeLayerKey.value) : null)
@ -337,7 +341,7 @@ const activeLayer = computed(() => {
}) })
const currentPanels = computed(() => { const currentPanels = computed(() => {
if (isEcologicalRedlineView.value) return redlinePanels.value if (isEcologicalRedlineView.value) return redlinePanels.value
if (isEcologicalScopeView.value) return scopePanels.value if (isNaturalProtectedAreaView.value) return naturalProtectedAreaPanels.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
return nationalParkPanels.value return nationalParkPanels.value
@ -422,6 +426,7 @@ function syncView(viewKey) {
const isRestoration = viewKey === "restoration" const isRestoration = viewKey === "restoration"
const isEngineering = viewKey === "engineering-analysis" const isEngineering = viewKey === "engineering-analysis"
const isRedline = viewKey === "ecological-redline" const isRedline = viewKey === "ecological-redline"
const isNaturalProtectedArea = viewKey === "natural-protected-area"
mapRanking.value = getEcologicalMapRanking(viewKey) mapRanking.value = getEcologicalMapRanking(viewKey)
mapSceneRef.value?.setBoundaryDrawing?.(false) mapSceneRef.value?.setBoundaryDrawing?.(false)
mapSceneRef.value?.clearAnalysisBoundary?.() mapSceneRef.value?.clearAnalysisBoundary?.()
@ -430,8 +435,9 @@ function syncView(viewKey) {
if (isRedline) { if (isRedline) {
redlineDataState.value = "loading" redlineDataState.value = "loading"
redlinePanels.value = buildEcologicalRedlinePanels() redlinePanels.value = buildEcologicalRedlinePanels()
} else if (ecologicalScopeViewKeys.includes(viewKey)) { } else if (isNaturalProtectedArea) {
scopePanels.value = buildEcologicalScopePanels({}, viewKey) naturalProtectedAreaDataState.value = "loading"
naturalProtectedAreaPanels.value = buildNaturalProtectedAreaPanels()
} }
if (isNationalPark) { if (isNationalPark) {
nationalParkPanels.value = buildNationalParkPanels({}) nationalParkPanels.value = buildNationalParkPanels({})
@ -446,10 +452,8 @@ function syncView(viewKey) {
emit("module-stats", { emit("module-stats", {
moduleKey: "ecological-protection", moduleKey: "ecological-protection",
viewKey, viewKey,
items: isRedline items: isRedline || isNaturalProtectedArea
? [] ? []
: ecologicalScopeViewKeys.includes(viewKey)
? getEcologicalScopeTopStats({}, viewKey)
: isNationalPark : isNationalPark
? getNationalParkTopStats({}) ? getNationalParkTopStats({})
: isRestoration : isRestoration
@ -458,8 +462,8 @@ function syncView(viewKey) {
}) })
if (isRedline) { if (isRedline) {
loadEcologicalRedlineStats(viewKey) loadEcologicalRedlineStats(viewKey)
} else if (ecologicalScopeViewKeys.includes(viewKey)) { } else if (isNaturalProtectedArea) {
loadEcologicalScopeStats(viewKey) loadNaturalProtectedAreaStats(viewKey)
} else if (isNationalPark) { } else if (isNationalPark) {
loadNationalParkStats(viewKey) loadNationalParkStats(viewKey)
} else if (isRestoration) { } else if (isRestoration) {
@ -568,32 +572,35 @@ function handleViewSelect(viewKey) {
emit("view-change", nextKey) emit("view-change", nextKey)
} }
async function loadEcologicalScopeStats(viewKey) { async function loadNaturalProtectedAreaStats(viewKey) {
const requestId = ++scopeStatsRequestId const requestId = ++naturalProtectedAreaStatsRequestId
naturalProtectedAreaDataState.value = "loading"
naturalProtectedAreaPanels.value = buildNaturalProtectedAreaPanels()
try { try {
const cached = scopeStatsCache.get(viewKey) const stats = naturalProtectedAreaStatsCache || await getNaturalProtectedAreaStats()
const stats = cached || await getNaturalProtectedAreaStats() if (requestId !== naturalProtectedAreaStatsRequestId || currentView.value !== viewKey) return
if (requestId !== scopeStatsRequestId || currentView.value !== viewKey) return naturalProtectedAreaStatsCache = stats
scopeStatsCache.set(viewKey, stats) naturalProtectedAreaDataState.value = stats.dataState
scopePanels.value = buildEcologicalScopePanels(stats, viewKey) naturalProtectedAreaPanels.value = buildNaturalProtectedAreaPanels(stats.panels)
emit("module-stats", { emit("module-stats", {
moduleKey: "ecological-protection", moduleKey: "ecological-protection",
viewKey, viewKey,
items: getEcologicalScopeTopStats(stats, viewKey), items: stats.topRows.rows,
}) })
} catch (error) { } catch (error) {
if (requestId !== scopeStatsRequestId || currentView.value !== viewKey) return if (requestId !== naturalProtectedAreaStatsRequestId || currentView.value !== viewKey) return
scopePanels.value = buildEcologicalScopePanels({}, viewKey) naturalProtectedAreaDataState.value = "error"
naturalProtectedAreaPanels.value = buildNaturalProtectedAreaPanels()
emit("module-stats", { emit("module-stats", {
moduleKey: "ecological-protection", moduleKey: "ecological-protection",
viewKey, viewKey,
items: getEcologicalScopeTopStats({}, viewKey), items: [],
}) })
layerStatus.value = { layerStatus.value = {
type: "error", type: "error",
text: "保护范围统计加载失败", text: "自然保护地统计加载失败",
} }
console.warn("Failed to load ecological scope statistics", error) console.warn("Failed to load natural-protected-area statistics", error)
} }
} }

@ -742,6 +742,16 @@ const isEcologicalRedlineFeature = computed(() => {
return layerKey.includes("ecological-redline") || layerName.includes("resource_ecological_redline_boundary") return layerKey.includes("ecological-redline") || layerName.includes("resource_ecological_redline_boundary")
}) })
const isNaturalProtectedAreaFeature = computed(() => {
const layerKey = String(props.feature?.layer?.key || "").toLowerCase()
const layerName = String(props.feature?.layer?.layerName || "").toLowerCase()
return layerKey.includes("natural-protected-area") || layerName.includes("resource_natural_protected_area_boundary")
})
const isServerOwnedEcologicalAreaFeature = computed(() => (
isEcologicalRedlineFeature.value || isNaturalProtectedAreaFeature.value
))
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)
@ -768,10 +778,11 @@ 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 redlineArea = isEcologicalRedlineFeature.value && isEcologicalAreaLabel(label, keys) const ecologicalArea = isServerOwnedEcologicalAreaFeature.value && String(label) === "图斑面积"
const areaKeys = isEcologicalRedlineFeature.value ? ["area_mu"] : ["area_source_mj"]
const row = buildRow( const row = buildRow(
propsData.value, propsData.value,
redlineArea ? ["area_mu"] : keys, ecologicalArea ? areaKeys : keys,
resolveDisplayLabel(label, keys), resolveDisplayLabel(label, keys),
used, used,
) )
@ -780,11 +791,12 @@ const displayRows = computed(() => {
used.add(row.sourceKey) used.add(row.sourceKey)
} }
}) })
if (isEcologicalRedlineFeature.value && !rows.some((row) => row.label === "图斑面积")) { if (isServerOwnedEcologicalAreaFeature.value && !rows.some((row) => row.label === "图斑面积")) {
addStaticRow(rows, used, "图斑面积", "数据缺失", "area_mu-missing") const sourceKey = isEcologicalRedlineFeature.value ? "area_mu" : "area_source_mj"
addStaticRow(rows, used, "图斑面积", "数据缺失", `${sourceKey}-missing`)
} }
const areaLabel = getTopicAreaLabel(activeTopicKey.value) const areaLabel = getTopicAreaLabel(activeTopicKey.value)
if (!isEcologicalRedlineFeature.value && !isArtificialGrassland.value && !isEngineeringSuitabilityFeature.value && !rows.some((row) => row.label === areaLabel || isGenericPatchAreaLabel(row.label))) { if (!isServerOwnedEcologicalAreaFeature.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)
@ -914,8 +926,8 @@ function addStaticRow(rows, used, label, value, sourceKey) {
used.add(sourceKey) used.add(sourceKey)
} }
function resolveDisplayLabel(label, keys = []) { function resolveDisplayLabel(label) {
if (activeTopicKey.value === "ecological" && isEcologicalScopeFeature.value && isEcologicalAreaLabel(label, keys)) { if (activeTopicKey.value === "ecological" && isServerOwnedEcologicalAreaFeature.value && String(label) === "图斑面积") {
return "图斑面积" return "图斑面积"
} }
if (isGenericPatchAreaLabel(label)) return getTopicAreaLabel(activeTopicKey.value) if (isGenericPatchAreaLabel(label)) return getTopicAreaLabel(activeTopicKey.value)
@ -926,11 +938,6 @@ function isGenericPatchAreaLabel(label) {
return ["图斑面积", "图斑地类面积", "小班面积"].includes(String(label || "")) return ["图斑面积", "图斑地类面积", "小班面积"].includes(String(label || ""))
} }
function isEcologicalAreaLabel(label, keys = []) {
if (String(label || "") === "治理面积" || isGenericPatchAreaLabel(label)) return true
return keys.some((key) => patchAreaFieldKeys.has(key))
}
function getTopicAreaLabel(topicKey) { function getTopicAreaLabel(topicKey) {
if (topicKey === "grassland" || topicKey === "artificial-grassland") return "草地面积" if (topicKey === "grassland" || topicKey === "artificial-grassland") return "草地面积"
if (topicKey === "wetland") return "湿地面积" if (topicKey === "wetland") return "湿地面积"
@ -988,7 +995,7 @@ function formatValue(key, value) {
if (patchAreaFieldKeys.has(key) && number === 0) return "" if (patchAreaFieldKeys.has(key) && number === 0) return ""
if (number === 0 && zeroHiddenKeys.has(key)) return "" if (number === 0 && zeroHiddenKeys.has(key)) return ""
const formatted = Number.isInteger(number) ? `${number}` : `${Number(number.toFixed(2))}` const formatted = Number.isInteger(number) ? `${number}` : `${Number(number.toFixed(2))}`
if (key === "area_source_mj") return `${formatAreaWanMu(number * 15)} 万亩` if (key === "area_source_mj") return formatted
if (activeTopicKey.value === "ecological" && isEcologicalScopeFeature.value && ["area_mu", "acreage", "mj"].includes(key)) return `${formatAreaWanMu(number)} 万亩` if (activeTopicKey.value === "ecological" && isEcologicalScopeFeature.value && ["area_mu", "acreage", "mj"].includes(key)) return `${formatAreaWanMu(number)} 万亩`
if (activeTopicKey.value === "ecological" && isEcologicalScopeFeature.value && ["area_m2", "original_area_m2", "adjusted_area_m2"].includes(key)) return `${formatAreaWanMu(number / 666.6667)} 万亩` if (activeTopicKey.value === "ecological" && isEcologicalScopeFeature.value && ["area_m2", "original_area_m2", "adjusted_area_m2"].includes(key)) return `${formatAreaWanMu(number / 666.6667)} 万亩`
if (["xbmj", "tbdlmj", "geomj", "area_square_meter"].includes(key)) return `${formatAreaMu(number / 666.6667)} 亩` if (["xbmj", "tbdlmj", "geomj", "area_square_meter"].includes(key)) return `${formatAreaMu(number / 666.6667)} 亩`

Loading…
Cancel
Save