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

@ -42,31 +42,49 @@ async function getEcologicalDirectStats(path, featureName) {
return stats
}
const NATIONAL_PARK_JSON_FIELDS = [
const NATIONAL_PARK_ROW_SECTIONS = [
"topRows",
"coreRows",
"townRows",
"townResourceRows",
"townProductivityRows",
"townYakRows",
"townHouseholdRows",
"townJobRows",
"zoningRows",
"balanceGaugeRows",
"villageRows",
]
const NATIONAL_PARK_PANEL_KEYS = [
"townResource",
"townProductivity",
"townYak",
"townHousehold",
"townJob",
"zoning",
"balance",
]
export async function getNationalParkStats() {
const response = await requestJson(
`${OPEN_API_BASE}/screen2/ecologicalProtection/nationalPark`,
`${OPEN_API_BASE}/screen2/ecology/national-park`,
{ method: "GET", timeout: 30000 },
)
const data = normalizeResponseData(response)
const stats = Array.isArray(data) ? data[0] || {} : data || {}
return NATIONAL_PARK_JSON_FIELDS.reduce((result, key) => {
result[key] = parseJsonbRows(stats[key])
return result
}, { ...stats })
const payload = normalizeResponseData(response)
const stats = payload || {}
if (Array.isArray(stats) || !stats || typeof stats !== "object" || !["ready", "empty"].includes(stats.dataState)) {
throw new Error("国家公园接口返回的数据状态无效。")
}
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 = [

@ -1,9 +1,7 @@
import { NATIONAL_PARK_ZONING_COLORS } from "@/config/layers"
import { ecologicalProtectionMenu } from "@/config/ecologicalProtection"
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 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 = {}) {
return normalizeEcologicalRows(stats.topRows).map((item, index) => ({
icon: item.icon || (index % 2 === 0 ? "xiaoshoujine" : "zongxiaoliang"),
en: "",
...item,
}))
return getNationalParkRows(stats, "topRows")
}
export function getNationalParkCoreRows(stats = {}) {
return normalizeEcologicalRows(stats.coreRows)
return getNationalParkRows(stats, "coreRows")
}
export function getNationalParkTownRows(stats = {}) {
return normalizeEcologicalRows(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)}%`,
}))
return getNationalParkRows(stats, "townRows")
}
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 {
left: [
{
@ -125,7 +90,7 @@ export function buildNationalParkPanels(stats = {}) {
{ key: "grassland", name: "草地面积", color: "#24f6c4" },
{ key: "wetland", name: "湿地面积", color: "#8bf7ff" },
],
rows: areaRows,
rows: getNationalParkPanelRows(stats, "townResource"),
},
{
key: "park-town-productivity",
@ -135,21 +100,21 @@ export function buildNationalParkPanels(stats = {}) {
labelMode: "horizontal",
showValues: true,
unit: "万吨",
rows: productivityRows,
rows: getNationalParkPanelRows(stats, "townProductivity"),
},
{
key: "park-town-yak",
title: "乡镇牦牛数量",
type: "bar",
variant: "parkBar",
rows: yakRows,
rows: getNationalParkPanelRows(stats, "townYak"),
},
{
key: "park-town-balance",
title: "草畜平衡超载率",
type: "balanceGaugeGrid",
limit: 1,
rows: normalizeEcologicalRows(stats.balanceGaugeRows),
rows: getNationalParkPanelRows(stats, "balance"),
},
],
right: [
@ -157,20 +122,20 @@ export function buildNationalParkPanels(stats = {}) {
key: "park-zoning-structure",
title: "红原管控区结构",
type: "donut",
rows: zoningRows,
rows: getNationalParkPanelRows(stats, "zoning"),
},
{
key: "park-town-household",
title: "牧户分布",
type: "split",
rows: householdRows,
rows: getNationalParkPanelRows(stats, "townHousehold"),
},
{
key: "park-town-ecology-job",
title: "公益岗位分布",
type: "column",
variant: "plain",
rows: jobRows,
rows: getNationalParkPanelRows(stats, "townJob"),
},
],
}
@ -420,7 +385,7 @@ export function getEcologicalMapRanking(viewKey, stats = {}) {
unit: "万亩",
rows: getNationalParkTownRows(stats).map((item) => ({
name: item.name,
value: round(item.protectedAreaWanMu, 2),
value: item.value,
unit: "万亩",
metricLabel: "划定面积",
})),

@ -15,13 +15,13 @@
<div class="forest-grass-wet-ui ecological-protection-ui">
<div
v-if="isEcologicalDirectView && ecologicalDirectDataState !== 'ready'"
v-if="isEcologicalDataView && ecologicalDataState !== 'ready'"
class="fgw-data-state"
:class="{ 'is-error': ecologicalDirectDataState === 'error' }"
:class="{ 'is-error': ecologicalDataState === 'error' }"
role="status"
aria-live="polite"
>
{{ ecologicalDirectDataStateMessage }}
{{ ecologicalDataStateMessage }}
</div>
<div v-if="!isEngineeringAnalysis" class="fgw-left">
<div class="fgw-side-stack is-left">
@ -229,7 +229,8 @@ const redlinePanels = ref(buildEcologicalRedlinePanels())
const redlineDataState = ref("loading")
const naturalProtectedAreaPanels = ref(buildNaturalProtectedAreaPanels())
const naturalProtectedAreaDataState = ref("loading")
const nationalParkPanels = ref(buildNationalParkPanels({}))
const nationalParkPanels = ref(buildNationalParkPanels())
const nationalParkDataState = ref("loading")
const restorationPanels = ref(buildEcologicalRestorationPanels({}))
const selectedFeature = ref(null)
const featureEmpty = ref(false)
@ -297,15 +298,24 @@ const isEngineeringAnalysis = computed(() => currentView.value === "engineering-
const isProjectDetailView = computed(() => ["restoration", "engineering-analysis"].includes(currentView.value))
const isEcologicalRedlineView = computed(() => currentView.value === "ecological-redline")
const isNaturalProtectedAreaView = computed(() => currentView.value === "natural-protected-area")
const isEcologicalDirectView = computed(() => isEcologicalRedlineView.value || isNaturalProtectedAreaView.value)
const ecologicalDirectDataState = computed(() => (
isEcologicalRedlineView.value ? redlineDataState.value : naturalProtectedAreaDataState.value
const isNationalParkView = computed(() => currentView.value === "national-park")
const isEcologicalDataView = computed(() => (
isEcologicalRedlineView.value || isNaturalProtectedAreaView.value || isNationalParkView.value
))
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}数据加载失败,请稍后重试`
const ecologicalDataState = computed(() => {
if (isEcologicalRedlineView.value) return redlineDataState.value
if (isNaturalProtectedAreaView.value) return naturalProtectedAreaDataState.value
return nationalParkDataState.value
})
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 ""
})
const baseActiveLayer = computed(() => activeLayerKey.value ? getLayerByKey(currentView.value, activeLayerKey.value) : null)
@ -440,7 +450,8 @@ function syncView(viewKey) {
naturalProtectedAreaPanels.value = buildNaturalProtectedAreaPanels()
}
if (isNationalPark) {
nationalParkPanels.value = buildNationalParkPanels({})
nationalParkDataState.value = "loading"
nationalParkPanels.value = buildNationalParkPanels()
}
if (isRestoration) {
restorationPanels.value = buildEcologicalRestorationPanels({})
@ -455,7 +466,7 @@ function syncView(viewKey) {
items: isRedline || isNaturalProtectedArea
? []
: isNationalPark
? getNationalParkTopStats({})
? []
: isRestoration
? getEcologicalRestorationTopStats({})
: [],
@ -527,10 +538,13 @@ async function loadEcologicalRestorationStats(viewKey) {
async function loadNationalParkStats(viewKey) {
const requestId = ++nationalParkStatsRequestId
nationalParkDataState.value = "loading"
nationalParkPanels.value = buildNationalParkPanels()
try {
const stats = nationalParkStatsCache || await getNationalParkStats()
if (requestId !== nationalParkStatsRequestId || currentView.value !== viewKey) return
nationalParkStatsCache = stats
nationalParkDataState.value = stats.dataState
nationalParkPanels.value = buildNationalParkPanels(stats)
mapRanking.value = getEcologicalMapRanking(viewKey, stats)
emit("module-stats", {
@ -540,12 +554,13 @@ async function loadNationalParkStats(viewKey) {
})
} catch (error) {
if (requestId !== nationalParkStatsRequestId || currentView.value !== viewKey) return
nationalParkPanels.value = buildNationalParkPanels({})
nationalParkDataState.value = "error"
nationalParkPanels.value = buildNationalParkPanels()
mapRanking.value = getEcologicalMapRanking(viewKey)
emit("module-stats", {
moduleKey: "ecological-protection",
viewKey,
items: getNationalParkTopStats({}),
items: [],
})
layerStatus.value = {
type: "error",

@ -734,6 +734,8 @@ const isEcologicalScopeFeature = computed(() => {
|| signal.includes("ecological_redline")
|| signal.includes("natural-protected-area")
|| signal.includes("natural_protected_area")
|| signal.includes("national-park")
|| signal.includes("resource_national_park_boundary")
})
const isEcologicalRedlineFeature = computed(() => {
@ -748,8 +750,14 @@ const isNaturalProtectedAreaFeature = computed(() => {
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(() => (
isEcologicalRedlineFeature.value || isNaturalProtectedAreaFeature.value
isEcologicalRedlineFeature.value || isNaturalProtectedAreaFeature.value || isNationalParkFeature.value
))
const isEngineeringSuitabilityFeature = computed(() => {
@ -779,7 +787,11 @@ const displayRows = computed(() => {
const businessFields = useCompactGrassland ? grasslandCoreBusinessFields : topicBusinessFields[activeTopicKey.value] || topicBusinessFields.woodland
businessFields.forEach(([keys, 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(
propsData.value,
ecologicalArea ? areaKeys : keys,
@ -792,7 +804,11 @@ const displayRows = computed(() => {
}
})
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`)
}
const areaLabel = getTopicAreaLabel(activeTopicKey.value)
@ -996,6 +1012,7 @@ function formatValue(key, value) {
if (number === 0 && zeroHiddenKeys.has(key)) return ""
const formatted = Number.isInteger(number) ? `${number}` : `${Number(number.toFixed(2))}`
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_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)} 亩`

Loading…
Cancel
Save