feat(ecology): normalize national park data flow

M先生 3 months ago
parent 497f9e3399
commit 3f488c59f2
  1. 10
      src/config/layers.js
  2. 48
      src/services/ecologicalProtectionApi.js
  3. 73
      src/views/ecologicalProtection/data.js
  4. 47
      src/views/ecologicalProtection/index.vue
  5. 23
      src/views/forestGrassWet/components/FeatureInfoPanel.vue

@ -475,10 +475,8 @@ export const ecologicalProtectionLayers = {
{ {
key: "national-park-zoning", key: "national-park-zoning",
name: "国家公园分区", name: "国家公园分区",
sourceType: "wkt-api", layerName: "ne:resource_national_park_boundary_render",
apiUrl: "/openApi/gjgy/gjgy_area", sourceType: "wfs",
clipToHongyuanBoundary: true,
clipToLayerBounds: false,
opacity: 0.9, opacity: 0.9,
mapView: { mapView: {
targetLngLat: [102.88, 33.15], targetLngLat: [102.88, 33.15],
@ -492,7 +490,7 @@ export const ecologicalProtectionLayers = {
name: "核心保护区", name: "核心保护区",
fillColor: NATIONAL_PARK_ZONING_COLORS.core.fillColor, fillColor: NATIONAL_PARK_ZONING_COLORS.core.fillColor,
strokeColor: NATIONAL_PARK_ZONING_COLORS.core.strokeColor, strokeColor: NATIONAL_PARK_ZONING_COLORS.core.strokeColor,
match: { field: "type", value: "核心保护区" }, match: { field: "zone_type", value: "核心保护区" },
selectable: false, selectable: false,
}, },
{ {
@ -500,7 +498,7 @@ export const ecologicalProtectionLayers = {
name: "一般控制区", name: "一般控制区",
fillColor: NATIONAL_PARK_ZONING_COLORS.general.fillColor, fillColor: NATIONAL_PARK_ZONING_COLORS.general.fillColor,
strokeColor: NATIONAL_PARK_ZONING_COLORS.general.strokeColor, strokeColor: NATIONAL_PARK_ZONING_COLORS.general.strokeColor,
match: { field: "type", value: "一般控制区" }, match: { field: "zone_type", value: "一般控制区" },
selectable: false, selectable: false,
}, },
], ],

@ -42,31 +42,49 @@ async function getEcologicalDirectStats(path, featureName) {
return stats return stats
} }
const NATIONAL_PARK_JSON_FIELDS = [ const NATIONAL_PARK_ROW_SECTIONS = [
"topRows", "topRows",
"coreRows", "coreRows",
"townRows", "townRows",
"townResourceRows",
"townProductivityRows",
"townYakRows",
"townHouseholdRows",
"townJobRows",
"zoningRows",
"balanceGaugeRows",
"villageRows", "villageRows",
] ]
const NATIONAL_PARK_PANEL_KEYS = [
"townResource",
"townProductivity",
"townYak",
"townHousehold",
"townJob",
"zoning",
"balance",
]
export async function getNationalParkStats() { export async function getNationalParkStats() {
const response = await requestJson( const response = await requestJson(
`${OPEN_API_BASE}/screen2/ecologicalProtection/nationalPark`, `${OPEN_API_BASE}/screen2/ecology/national-park`,
{ method: "GET", timeout: 30000 }, { method: "GET", timeout: 30000 },
) )
const data = normalizeResponseData(response) const payload = normalizeResponseData(response)
const stats = Array.isArray(data) ? data[0] || {} : data || {} const stats = payload || {}
return NATIONAL_PARK_JSON_FIELDS.reduce((result, key) => {
result[key] = parseJsonbRows(stats[key]) if (Array.isArray(stats) || !stats || typeof stats !== "object" || !["ready", "empty"].includes(stats.dataState)) {
return result throw new Error("国家公园接口返回的数据状态无效。")
}, { ...stats }) }
NATIONAL_PARK_ROW_SECTIONS.forEach((key) => assertDirectRows(stats[key], `国家公园 ${key}`))
if (!stats.panels || typeof stats.panels !== "object") {
throw new Error("国家公园接口未返回完整展示面板。")
}
NATIONAL_PARK_PANEL_KEYS.forEach((key) => assertDirectRows(stats.panels[key], `国家公园 ${key} 面板`))
return stats
}
function assertDirectRows(section, label) {
if (!section || typeof section !== "object" || !Array.isArray(section.rows) || !Number.isInteger(section.count)) {
throw new Error(`${label}未返回合法的 rows/count 展示模型。`)
}
if (section.count !== section.rows.length) {
throw new Error(`${label}的 count 与 rows 数量不一致。`)
}
} }
const RESTORATION_JSON_FIELDS = [ const RESTORATION_JSON_FIELDS = [

@ -1,9 +1,7 @@
import { NATIONAL_PARK_ZONING_COLORS } from "@/config/layers"
import { ecologicalProtectionMenu } from "@/config/ecologicalProtection" import { ecologicalProtectionMenu } from "@/config/ecologicalProtection"
export { ecologicalProtectionMenu } export { ecologicalProtectionMenu }
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))
const ecologicalDirectPanelDefinitions = { const ecologicalDirectPanelDefinitions = {
@ -57,60 +55,27 @@ function normalizeEcologicalRows(value = []) {
})) }))
} }
function getNationalParkRows(stats, key) {
return stats?.[key]?.rows || []
}
function getNationalParkPanelRows(stats, key) {
return stats?.panels?.[key]?.rows || []
}
export function getNationalParkTopStats(stats = {}) { export function getNationalParkTopStats(stats = {}) {
return normalizeEcologicalRows(stats.topRows).map((item, index) => ({ return getNationalParkRows(stats, "topRows")
icon: item.icon || (index % 2 === 0 ? "xiaoshoujine" : "zongxiaoliang"),
en: "",
...item,
}))
} }
export function getNationalParkCoreRows(stats = {}) { export function getNationalParkCoreRows(stats = {}) {
return normalizeEcologicalRows(stats.coreRows) return getNationalParkRows(stats, "coreRows")
} }
export function getNationalParkTownRows(stats = {}) { export function getNationalParkTownRows(stats = {}) {
return normalizeEcologicalRows(stats.townRows) return getNationalParkRows(stats, "townRows")
}
function withNationalParkPercentExtra(rows = []) {
const total = sumBy(rows, "value")
return rows.map((item) => ({
...item,
extra: item.extra || `占比 ${round((Number(item.value || 0) / Math.max(total, 1)) * 100, 1)}%`,
}))
} }
export function buildNationalParkPanels(stats = {}) { export function buildNationalParkPanels(stats = {}) {
const townRows = getNationalParkTownRows(stats)
const townByName = new Map(townRows.map((item) => [item.name, item]))
const areaRows = normalizeEcologicalRows(stats.townResourceRows)
const productivityRows = normalizeEcologicalRows(stats.townProductivityRows)
const yakRows = withNationalParkPercentExtra(normalizeEcologicalRows(stats.townYakRows))
const zoningRows = withNationalParkPercentExtra(
normalizeEcologicalRows(stats.zoningRows).map((item) => ({
...item,
color: item.name === "核心保护区"
? NATIONAL_PARK_ZONING_COLORS.core.fillColor
: item.name === "一般控制区"
? NATIONAL_PARK_ZONING_COLORS.general.fillColor
: item.color,
})),
)
const householdRows = normalizeEcologicalRows(stats.townHouseholdRows).map((item) => ({
...item,
extra: `公益岗位 ${Number(item.ecologyJobs || 0)}户`,
}))
const jobColors = {
麦洼乡: "#24f6c4",
瓦切镇: "#32dcff",
色地镇: "#8bf7ff",
}
const jobRows = normalizeEcologicalRows(stats.townJobRows).map((item) => ({
...item,
color: jobColors[item.name],
extra: `牧户 ${Number(townByName.get(item.name)?.householdCount || 0)}户`,
}))
return { return {
left: [ left: [
{ {
@ -125,7 +90,7 @@ export function buildNationalParkPanels(stats = {}) {
{ key: "grassland", name: "草地面积", color: "#24f6c4" }, { key: "grassland", name: "草地面积", color: "#24f6c4" },
{ key: "wetland", name: "湿地面积", color: "#8bf7ff" }, { key: "wetland", name: "湿地面积", color: "#8bf7ff" },
], ],
rows: areaRows, rows: getNationalParkPanelRows(stats, "townResource"),
}, },
{ {
key: "park-town-productivity", key: "park-town-productivity",
@ -135,21 +100,21 @@ export function buildNationalParkPanels(stats = {}) {
labelMode: "horizontal", labelMode: "horizontal",
showValues: true, showValues: true,
unit: "万吨", unit: "万吨",
rows: productivityRows, rows: getNationalParkPanelRows(stats, "townProductivity"),
}, },
{ {
key: "park-town-yak", key: "park-town-yak",
title: "乡镇牦牛数量", title: "乡镇牦牛数量",
type: "bar", type: "bar",
variant: "parkBar", variant: "parkBar",
rows: yakRows, rows: getNationalParkPanelRows(stats, "townYak"),
}, },
{ {
key: "park-town-balance", key: "park-town-balance",
title: "草畜平衡超载率", title: "草畜平衡超载率",
type: "balanceGaugeGrid", type: "balanceGaugeGrid",
limit: 1, limit: 1,
rows: normalizeEcologicalRows(stats.balanceGaugeRows), rows: getNationalParkPanelRows(stats, "balance"),
}, },
], ],
right: [ right: [
@ -157,20 +122,20 @@ export function buildNationalParkPanels(stats = {}) {
key: "park-zoning-structure", key: "park-zoning-structure",
title: "红原管控区结构", title: "红原管控区结构",
type: "donut", type: "donut",
rows: zoningRows, rows: getNationalParkPanelRows(stats, "zoning"),
}, },
{ {
key: "park-town-household", key: "park-town-household",
title: "牧户分布", title: "牧户分布",
type: "split", type: "split",
rows: householdRows, rows: getNationalParkPanelRows(stats, "townHousehold"),
}, },
{ {
key: "park-town-ecology-job", key: "park-town-ecology-job",
title: "公益岗位分布", title: "公益岗位分布",
type: "column", type: "column",
variant: "plain", variant: "plain",
rows: jobRows, rows: getNationalParkPanelRows(stats, "townJob"),
}, },
], ],
} }
@ -420,7 +385,7 @@ export function getEcologicalMapRanking(viewKey, stats = {}) {
unit: "万亩", unit: "万亩",
rows: getNationalParkTownRows(stats).map((item) => ({ rows: getNationalParkTownRows(stats).map((item) => ({
name: item.name, name: item.name,
value: round(item.protectedAreaWanMu, 2), value: item.value,
unit: "万亩", unit: "万亩",
metricLabel: "划定面积", metricLabel: "划定面积",
})), })),

@ -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="isEcologicalDirectView && ecologicalDirectDataState !== 'ready'" v-if="isEcologicalDataView && ecologicalDataState !== 'ready'"
class="fgw-data-state" class="fgw-data-state"
:class="{ 'is-error': ecologicalDirectDataState === 'error' }" :class="{ 'is-error': ecologicalDataState === 'error' }"
role="status" role="status"
aria-live="polite" aria-live="polite"
> >
{{ ecologicalDirectDataStateMessage }} {{ ecologicalDataStateMessage }}
</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">
@ -229,7 +229,8 @@ const redlinePanels = ref(buildEcologicalRedlinePanels())
const redlineDataState = ref("loading") const redlineDataState = ref("loading")
const naturalProtectedAreaPanels = ref(buildNaturalProtectedAreaPanels()) const naturalProtectedAreaPanels = ref(buildNaturalProtectedAreaPanels())
const naturalProtectedAreaDataState = ref("loading") const naturalProtectedAreaDataState = ref("loading")
const nationalParkPanels = ref(buildNationalParkPanels({})) const nationalParkPanels = ref(buildNationalParkPanels())
const nationalParkDataState = ref("loading")
const restorationPanels = ref(buildEcologicalRestorationPanels({})) const restorationPanels = ref(buildEcologicalRestorationPanels({}))
const selectedFeature = ref(null) const selectedFeature = ref(null)
const featureEmpty = ref(false) const featureEmpty = ref(false)
@ -297,15 +298,24 @@ const isEngineeringAnalysis = computed(() => currentView.value === "engineering-
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 isNaturalProtectedAreaView = computed(() => currentView.value === "natural-protected-area") const isNaturalProtectedAreaView = computed(() => currentView.value === "natural-protected-area")
const isEcologicalDirectView = computed(() => isEcologicalRedlineView.value || isNaturalProtectedAreaView.value) const isNationalParkView = computed(() => currentView.value === "national-park")
const ecologicalDirectDataState = computed(() => ( const isEcologicalDataView = computed(() => (
isEcologicalRedlineView.value ? redlineDataState.value : naturalProtectedAreaDataState.value isEcologicalRedlineView.value || isNaturalProtectedAreaView.value || isNationalParkView.value
)) ))
const ecologicalDirectDataStateMessage = computed(() => { const ecologicalDataState = computed(() => {
const featureName = isEcologicalRedlineView.value ? "生态保护红线" : "自然保护地" if (isEcologicalRedlineView.value) return redlineDataState.value
if (ecologicalDirectDataState.value === "loading") return `${featureName}数据加载中` if (isNaturalProtectedAreaView.value) return naturalProtectedAreaDataState.value
if (ecologicalDirectDataState.value === "empty") return `${featureName}暂无数据` return nationalParkDataState.value
if (ecologicalDirectDataState.value === "error") return `${featureName}数据加载失败,请稍后重试` })
const ecologicalDataStateMessage = computed(() => {
const featureName = isEcologicalRedlineView.value
? "生态保护红线"
: isNaturalProtectedAreaView.value
? "自然保护地"
: "国家公园"
if (ecologicalDataState.value === "loading") return `${featureName}数据加载中`
if (ecologicalDataState.value === "empty") return `${featureName}暂无数据`
if (ecologicalDataState.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)
@ -440,7 +450,8 @@ function syncView(viewKey) {
naturalProtectedAreaPanels.value = buildNaturalProtectedAreaPanels() naturalProtectedAreaPanels.value = buildNaturalProtectedAreaPanels()
} }
if (isNationalPark) { if (isNationalPark) {
nationalParkPanels.value = buildNationalParkPanels({}) nationalParkDataState.value = "loading"
nationalParkPanels.value = buildNationalParkPanels()
} }
if (isRestoration) { if (isRestoration) {
restorationPanels.value = buildEcologicalRestorationPanels({}) restorationPanels.value = buildEcologicalRestorationPanels({})
@ -455,7 +466,7 @@ function syncView(viewKey) {
items: isRedline || isNaturalProtectedArea items: isRedline || isNaturalProtectedArea
? [] ? []
: isNationalPark : isNationalPark
? getNationalParkTopStats({}) ? []
: isRestoration : isRestoration
? getEcologicalRestorationTopStats({}) ? getEcologicalRestorationTopStats({})
: [], : [],
@ -527,10 +538,13 @@ async function loadEcologicalRestorationStats(viewKey) {
async function loadNationalParkStats(viewKey) { async function loadNationalParkStats(viewKey) {
const requestId = ++nationalParkStatsRequestId const requestId = ++nationalParkStatsRequestId
nationalParkDataState.value = "loading"
nationalParkPanels.value = buildNationalParkPanels()
try { try {
const stats = nationalParkStatsCache || await getNationalParkStats() const stats = nationalParkStatsCache || await getNationalParkStats()
if (requestId !== nationalParkStatsRequestId || currentView.value !== viewKey) return if (requestId !== nationalParkStatsRequestId || currentView.value !== viewKey) return
nationalParkStatsCache = stats nationalParkStatsCache = stats
nationalParkDataState.value = stats.dataState
nationalParkPanels.value = buildNationalParkPanels(stats) nationalParkPanels.value = buildNationalParkPanels(stats)
mapRanking.value = getEcologicalMapRanking(viewKey, stats) mapRanking.value = getEcologicalMapRanking(viewKey, stats)
emit("module-stats", { emit("module-stats", {
@ -540,12 +554,13 @@ async function loadNationalParkStats(viewKey) {
}) })
} catch (error) { } catch (error) {
if (requestId !== nationalParkStatsRequestId || currentView.value !== viewKey) return if (requestId !== nationalParkStatsRequestId || currentView.value !== viewKey) return
nationalParkPanels.value = buildNationalParkPanels({}) nationalParkDataState.value = "error"
nationalParkPanels.value = buildNationalParkPanels()
mapRanking.value = getEcologicalMapRanking(viewKey) mapRanking.value = getEcologicalMapRanking(viewKey)
emit("module-stats", { emit("module-stats", {
moduleKey: "ecological-protection", moduleKey: "ecological-protection",
viewKey, viewKey,
items: getNationalParkTopStats({}), items: [],
}) })
layerStatus.value = { layerStatus.value = {
type: "error", type: "error",

@ -734,6 +734,8 @@ const isEcologicalScopeFeature = computed(() => {
|| signal.includes("ecological_redline") || signal.includes("ecological_redline")
|| signal.includes("natural-protected-area") || signal.includes("natural-protected-area")
|| signal.includes("natural_protected_area") || signal.includes("natural_protected_area")
|| signal.includes("national-park")
|| signal.includes("resource_national_park_boundary")
}) })
const isEcologicalRedlineFeature = computed(() => { const isEcologicalRedlineFeature = computed(() => {
@ -748,8 +750,14 @@ const isNaturalProtectedAreaFeature = computed(() => {
return layerKey.includes("natural-protected-area") || layerName.includes("resource_natural_protected_area_boundary") return layerKey.includes("natural-protected-area") || layerName.includes("resource_natural_protected_area_boundary")
}) })
const isNationalParkFeature = computed(() => {
const layerKey = String(props.feature?.layer?.key || "").toLowerCase()
const layerName = String(props.feature?.layer?.layerName || "").toLowerCase()
return layerKey.includes("national-park") || layerName.includes("resource_national_park_boundary")
})
const isServerOwnedEcologicalAreaFeature = computed(() => ( const isServerOwnedEcologicalAreaFeature = computed(() => (
isEcologicalRedlineFeature.value || isNaturalProtectedAreaFeature.value isEcologicalRedlineFeature.value || isNaturalProtectedAreaFeature.value || isNationalParkFeature.value
)) ))
const isEngineeringSuitabilityFeature = computed(() => { const isEngineeringSuitabilityFeature = computed(() => {
@ -779,7 +787,11 @@ const displayRows = computed(() => {
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 ecologicalArea = isServerOwnedEcologicalAreaFeature.value && String(label) === "图斑面积" const ecologicalArea = isServerOwnedEcologicalAreaFeature.value && String(label) === "图斑面积"
const areaKeys = isEcologicalRedlineFeature.value ? ["area_mu"] : ["area_source_mj"] const areaKeys = isEcologicalRedlineFeature.value
? ["area_mu"]
: isNationalParkFeature.value
? ["total_acreage"]
: ["area_source_mj"]
const row = buildRow( const row = buildRow(
propsData.value, propsData.value,
ecologicalArea ? areaKeys : keys, ecologicalArea ? areaKeys : keys,
@ -792,7 +804,11 @@ const displayRows = computed(() => {
} }
}) })
if (isServerOwnedEcologicalAreaFeature.value && !rows.some((row) => row.label === "图斑面积")) { if (isServerOwnedEcologicalAreaFeature.value && !rows.some((row) => row.label === "图斑面积")) {
const sourceKey = isEcologicalRedlineFeature.value ? "area_mu" : "area_source_mj" const sourceKey = isEcologicalRedlineFeature.value
? "area_mu"
: isNationalParkFeature.value
? "total_acreage"
: "area_source_mj"
addStaticRow(rows, used, "图斑面积", "数据缺失", `${sourceKey}-missing`) addStaticRow(rows, used, "图斑面积", "数据缺失", `${sourceKey}-missing`)
} }
const areaLabel = getTopicAreaLabel(activeTopicKey.value) const areaLabel = getTopicAreaLabel(activeTopicKey.value)
@ -996,6 +1012,7 @@ function formatValue(key, value) {
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 formatted if (key === "area_source_mj") return formatted
if (key === "total_acreage") 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_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