diff --git a/src/config/layers.js b/src/config/layers.js index db5cea6..d72ad29 100644 --- a/src/config/layers.js +++ b/src/config/layers.js @@ -15,11 +15,11 @@ export const SATELLITE_TEXTURE_ZOOM = 9 export const NATIONAL_PARK_ZONING_COLORS = { core: { fillColor: "#24F6C4", - strokeColor: "#B7FFF2", + strokeColor: "#FFB15E", }, general: { fillColor: "#FFE06B", - strokeColor: "#FFF2A0", + strokeColor: "#FF8F3D", }, } diff --git a/src/services/homeVideoApi.js b/src/services/homeVideoApi.js index c1c2fb6..8b3410b 100644 --- a/src/services/homeVideoApi.js +++ b/src/services/homeVideoApi.js @@ -14,6 +14,10 @@ const HONGYUAN_TOWN_NAMES = [ "麦洼乡", "色地镇", ] +const MONITORING_FALLBACK_ROWS = [ + { id: "fallback-qiongxi-pasture", name: "邛溪镇家庭生态牧场监控", number: "CAM-001", statusName: "预览点位" }, + { id: "fallback-wache-grassland", name: "瓦切镇草场监控", number: "CAM-002", statusName: "预览点位" }, +] function joinBase(path) { if (/^https?:\/\//.test(path)) return path @@ -139,11 +143,18 @@ function normalizeMonitoring(row, index, total, validTotal) { } export async function getHomeMonitorings(params = {}) { - const data = await requestJson(joinBase("/api/home/monitorings"), { - params, - timeout: 18000, - }).then(unwrapResponse) - const rows = Array.isArray(data) ? data : data?.rows || data?.list || [] - const playableRows = rows.filter(isPlayableMonitoring) - return playableRows.map((row, index) => normalizeMonitoring(row, index, rows.length, playableRows.length)) + try { + const data = await requestJson(joinBase("/api/home/monitorings"), { + params, + timeout: 18000, + }).then(unwrapResponse) + const rows = Array.isArray(data) ? data : data?.rows || data?.list || [] + const playableRows = rows.filter(isPlayableMonitoring) + if (playableRows.length) { + return playableRows.map((row, index) => normalizeMonitoring(row, index, rows.length, playableRows.length)) + } + } catch (error) { + console.warn("[homeVideo] fallback monitorings used", error) + } + return MONITORING_FALLBACK_ROWS.map((row, index) => normalizeMonitoring(row, index, MONITORING_FALLBACK_ROWS.length, 0)) } diff --git a/src/views/forestGrassWet/map.js b/src/views/forestGrassWet/map.js index b8b9a98..66b8292 100644 --- a/src/views/forestGrassWet/map.js +++ b/src/views/forestGrassWet/map.js @@ -96,6 +96,11 @@ function toFiniteNumber(value, fallback = 0) { const number = Number(value) return Number.isFinite(number) ? number : fallback } +function clampNumber(value, min, max) { + const number = Number(value) + if (!Number.isFinite(number)) return min + return Math.max(min, Math.min(max, number)) +} function escapeHtml(value) { return String(value ?? "").replace(/[&<>"']/g, (char) => ({ "&": "&", @@ -1592,20 +1597,29 @@ export class World extends Mini3d { const baseStyle = this.getVectorFeatureStyle(layer, feature) this.drawVectorFeature(context, feature.geometry, { ...baseStyle, - fill: "#001D2C", - stroke: "#001D2C", + fill: "#351A05", + stroke: "#351A05", polygonAlpha: 0, - lineAlpha: 0.58, - lineWidth: 12, + lineAlpha: 0.52, + lineWidth: 9, glow: true, }) this.drawVectorFeature(context, feature.geometry, { ...baseStyle, - fill: "#F8FFFF", - stroke: "#F8FFFF", + fill: "#FFE3AA", + stroke: "#FFE3AA", polygonAlpha: 0, - lineAlpha: 0.96, - lineWidth: 3.2, + lineAlpha: 0.98, + lineWidth: 2.8, + glow: false, + }) + this.drawVectorFeature(context, feature.geometry, { + ...baseStyle, + fill: "#FF8F3D", + stroke: "#FF8F3D", + polygonAlpha: 0, + lineAlpha: 0.9, + lineWidth: 1.45, glow: false, }) }) @@ -2332,11 +2346,11 @@ export class World extends Mini3d { return { fill: legend.fillColor || "#30DCFF", stroke: legend.strokeColor || "#F2FFFF", - polygonAlpha: 0.36, - lineAlpha: 1, - lineWidth: 5.8, + polygonAlpha: 0.28, + lineAlpha: 0.95, + lineWidth: 3.2, pointRadius: 4.5, - glow: true, + glow: false, } } if (layer?.boundaryOnly) { @@ -3916,6 +3930,8 @@ export class World extends Mini3d { label.setParent(labelGroup) label.userData = { ...province, + labelType: "town-label", + baseScale: 0.018, townName: province.name, } return label @@ -5361,10 +5377,26 @@ export class World extends Mini3d { geoProjection(args) { return geoMercator().center(this.geoProjectionCenter).scale(this.geoProjectionScale).translate([0, 0])(args) - } + } + updateTownLabelScale() { + const labels = this.townNameLabels || this.otherLabel || [] + const camera = this.camera?.instance + const target = this.camera?.controls?.target + if (!labels.length || !camera || !target) return + const distance = camera.position.distanceTo(target) + const scale = clampNumber(distance * 0.00072, 0.011, 0.026) + const opacity = distance > 45 ? "0.76" : distance < 22 ? "0.94" : "1" + labels.forEach((label) => { + if (label?.userData?.labelType !== "town-label") return + label.scale.set(scale, scale, scale) + const element = label.element?.querySelector?.(".other-label") + if (element) element.style.opacity = opacity + }) + } update(delta) { super.update(delta) this.updateDynamicSatelliteTiles() + this.updateTownLabelScale() this.updateTradeFlowPulses(delta) this.interactionManager && this.interactionManager.update() } diff --git a/src/views/gdMap/map.js b/src/views/gdMap/map.js index cdf906d..8900d22 100644 --- a/src/views/gdMap/map.js +++ b/src/views/gdMap/map.js @@ -71,6 +71,11 @@ function normalizeMapGeoJsonData(data) { }) return parsed } +function clampNumber(value, min, max) { + const number = Number(value) + if (!Number.isFinite(number)) return min + return Math.max(min, Math.min(max, number)) +} export class World extends Mini3d { constructor(canvas, assets, options = {}) { super(canvas) @@ -965,9 +970,14 @@ export class World extends Mini3d { new Vector3(x, -y, self.depth + 0.85) ) label3d.setLabelStyle(label, 0.018, "x") - label.setParent(labelGroup) - return label - } + label.setParent(labelGroup) + label.userData = { + ...province, + labelType: "town-label", + baseScale: 0.018, + } + return label + } function labelStyle03(data, label3d, labelGroup) { let label = label3d.create("", `decoration-label ${data.reflect ? " reflect" : ""}`, false) const [x, y] = self.geoProjection(data.center) @@ -1322,11 +1332,27 @@ export class World extends Mini3d { }) } - geoProjection(args) { - return geoMercator().center(this.geoProjectionCenter).scale(this.geoProjectionScale).translate([0, 0])(args) - } + geoProjection(args) { + return geoMercator().center(this.geoProjectionCenter).scale(this.geoProjectionScale).translate([0, 0])(args) + } + updateTownLabelScale() { + const labels = this.otherLabel || [] + const camera = this.camera?.instance + const target = this.camera?.controls?.target + if (!labels.length || !camera || !target) return + const distance = camera.position.distanceTo(target) + const scale = clampNumber(distance * 0.00072, 0.011, 0.026) + const opacity = distance > 45 ? "0.76" : distance < 22 ? "0.94" : "1" + labels.forEach((label) => { + if (label?.userData?.labelType !== "town-label") return + label.scale.set(scale, scale, scale) + const element = label.element?.querySelector?.(".other-label") + if (element) element.style.opacity = opacity + }) + } update() { super.update() + this.updateTownLabelScale() this.interactionManager && this.interactionManager.update() } pauseScene() { diff --git a/src/views/grassLivestockBalance/index.vue b/src/views/grassLivestockBalance/index.vue index ab49b32..22eda81 100644 --- a/src/views/grassLivestockBalance/index.vue +++ b/src/views/grassLivestockBalance/index.vue @@ -204,6 +204,27 @@ + +
+
+ 超载率排行 + 乡镇 Top10 +
+
+
+ {{ item.rank }} + {{ item.name }} +
+ {{ formatPercent(item.overloadRate) }} +
+
+
@@ -437,6 +458,36 @@ const workflowRows = computed(() => [ ]) const activeAdviceRows = computed(() => analysisDone.value ? splitAdvice(activeBalance.value, activeDerived.value.risk) : []) +const overloadRankSourceRows = computed(() => + townRows.value + .filter((item) => Number.isFinite(Number(item.overloadRate))) + .slice() + .sort((a, b) => Number(b.overloadRate) - Number(a.overloadRate)) + .slice(0, 10) +) +const overloadRankMax = computed(() => Math.max( + 1, + ...overloadRankSourceRows.value.map((item) => Math.max(0, Number(item.overloadRate) || 0)), +)) +const overloadRankRows = computed(() => + overloadRankSourceRows.value.map((item, index) => ({ + ...item, + rank: index + 1, + })) +) + +function getOverloadRankProgress(item) { + const rate = Math.max(0, Number(item?.overloadRate) || 0) + return Math.max(6, Math.min(100, (rate / overloadRankMax.value) * 100)) +} + +function getOverloadRankClass(item) { + const rate = Number(item?.overloadRate) + if (!Number.isFinite(rate)) return "is-empty" + if (rate >= 2.5) return "is-alert" + if (rate >= 2) return "is-warning" + return "is-stable" +} function updateScreenViewportHeight() { screenViewportHeight.value = typeof window === "undefined" ? 1080 : window.innerHeight @@ -509,8 +560,9 @@ function startAnalysis() { let requestFailed = false const analysisRequest = refreshActiveBalance().catch((error) => { requestFailed = true - analysisError.value = error?.message || "未查询到该对象的草畜平衡结果" - return null + console.warn("[grassLivestockBalance] fallback balance row used", error) + analysisError.value = "接口暂不可用,已使用本地兜底口径" + return activeBalance.value }) activeAnalysisRequest = analysisRequest analysisTimer = window.setInterval(() => { @@ -530,12 +582,15 @@ function startAnalysis() { if (destroyed || analysisState.value !== "running" || activeAnalysisRequest !== analysisRequest) return activeAnalysisRequest = null clearAnalysisTimer() - if (requestFailed || !apiResults.value[activeBalance.value?.key]) { + if (!activeBalance.value) { analysisProgress.value = 0 processStageIndex.value = 0 analysisState.value = "idle" return } + if (!requestFailed && !apiResults.value[activeBalance.value?.key]) { + analysisError.value = "未查询到接口结果,已使用本地兜底口径" + } analysisProgress.value = 100 processStageIndex.value = analysisProcessRows.value.length - 1 analysisState.value = "done" @@ -1997,13 +2052,14 @@ function getProcessStepState(index) { .balance-detail { display: grid; - grid-template-columns: minmax(0, 1.26fr) minmax(0, 0.74fr); - gap: 15px; + grid-template-columns: minmax(0, 1.04fr) minmax(0, 0.82fr) 248px; + gap: 12px; min-height: 0; } .balance-process, -.balance-decision { +.balance-decision, +.balance-overload-ranking { position: relative; min-width: 0; min-height: 0; @@ -2183,6 +2239,120 @@ function getProcessStepState(index) { gap: 12px; } +.balance-overload-ranking { + display: grid; + grid-template-rows: 28px minmax(0, 1fr); + gap: 10px; +} + +.balance-rank-list { + position: relative; + z-index: 1; + display: grid; + align-content: start; + gap: 5px; + min-height: 0; + overflow: hidden; +} + +.balance-rank-row { + position: relative; + display: grid; + grid-template-columns: 24px 58px minmax(0, 1fr) 50px; + align-items: center; + gap: 7px; + height: 25px; + min-width: 0; + padding: 0 8px 0 6px; + background: + linear-gradient(90deg, rgba(48, 220, 255, 0.08), transparent 62%), + rgba(4, 33, 46, 0.62); + clip-path: polygon(0 0, calc(100% - 8px) 0, 100% 50%, calc(100% - 8px) 100%, 0 100%); + box-sizing: border-box; + + &::before { + content: ""; + position: absolute; + inset: 0; + border: 1px solid rgba(48, 220, 255, 0.08); + clip-path: inherit; + pointer-events: none; + } + + > span { + position: relative; + z-index: 1; + width: 20px; + height: 18px; + color: rgba(126, 251, 246, 0.9); + font-family: D-DIN, Arial, sans-serif; + font-size: 12px; + line-height: 18px; + font-weight: 800; + text-align: center; + background: rgba(126, 251, 246, 0.08); + } + + strong { + position: relative; + z-index: 1; + min-width: 0; + overflow: hidden; + color: rgba(237, 253, 255, 0.9); + font-size: 12px; + line-height: 18px; + font-weight: 800; + text-overflow: ellipsis; + white-space: nowrap; + } + + div { + position: relative; + z-index: 1; + height: 5px; + overflow: hidden; + background: rgba(126, 251, 246, 0.08); + + i { + display: block; + width: var(--rank-progress); + height: 100%; + background: linear-gradient(90deg, rgba(126, 251, 246, 0.35), #7efbf6); + box-shadow: 0 0 10px rgba(126, 251, 246, 0.22); + } + } + + em { + position: relative; + z-index: 1; + color: #7efbf6; + font-family: D-DIN, Arial, sans-serif; + font-size: 12px; + font-style: normal; + font-weight: 800; + text-align: right; + white-space: nowrap; + } + + &.is-warning div i { + background: linear-gradient(90deg, rgba(255, 215, 108, 0.32), #ffd76c); + box-shadow: 0 0 10px rgba(255, 215, 108, 0.2); + } + + &.is-warning em { + color: #ffd76c; + } + + &.is-alert div i { + background: linear-gradient(90deg, rgba(255, 143, 107, 0.3), #ff8f6b); + box-shadow: 0 0 10px rgba(255, 143, 107, 0.24); + } + + &.is-alert em { + color: #ffb15e; + } +} + .balance-conclusion { position: relative; z-index: 1; @@ -2542,7 +2712,8 @@ function getProcessStepState(index) { .balance-dashboard, .balance-compare, .balance-process, -.balance-decision { +.balance-decision, +.balance-overload-ranking { background: linear-gradient(180deg, rgba(7, 48, 62, 0.9), rgba(3, 28, 42, 0.9)); clip-path: polygon(0 0, calc(100% - 16px) 0, 100% 16px, 100% 100%, 0 100%); @@ -2607,7 +2778,8 @@ function getProcessStepState(index) { } .balance-process, -.balance-decision { +.balance-decision, +.balance-overload-ranking { padding: 14px 16px; } @@ -2712,12 +2884,13 @@ function getProcessStepState(index) { } .balance-sheet.is-ready .balance-detail { - grid-template-columns: minmax(0, 1.18fr) minmax(360px, 0.82fr); - gap: 14px; + grid-template-columns: minmax(0, 1fr) minmax(318px, 0.78fr) 238px; + gap: 12px; } .balance-sheet.is-ready .balance-process, -.balance-sheet.is-ready .balance-decision { +.balance-sheet.is-ready .balance-decision, +.balance-sheet.is-ready .balance-overload-ranking { overflow: hidden; padding: 13px 15px; } @@ -2732,6 +2905,11 @@ function getProcessStepState(index) { gap: 10px; } +.balance-sheet.is-ready .balance-overload-ranking { + grid-template-rows: 26px minmax(0, 1fr); + gap: 8px; +} + .balance-sheet.is-ready .balance-section-title { height: 26px; } @@ -2807,4 +2985,15 @@ function getProcessStepState(index) { line-height: 18px; -webkit-line-clamp: 2; } + +.balance-sheet.is-ready .balance-rank-list { + gap: 4px; +} + +.balance-sheet.is-ready .balance-rank-row { + grid-template-columns: 22px 54px minmax(0, 1fr) 48px; + gap: 6px; + height: 23px; + padding-right: 7px; +} diff --git a/src/views/home/index.vue b/src/views/home/index.vue index 534e51e..50fdd21 100644 --- a/src/views/home/index.vue +++ b/src/views/home/index.vue @@ -138,20 +138,27 @@ :disabled="homeVideoRows.length <= 1" @click="showPrevVideo" >‹ -
-
-
-
-
加载中
-
{{ videoError }}
-
{{ panel.live.title }}
+
+
+
+
+
+
加载中
+
{{ videoError }}
+
{{ item.live.title }}
+
-
-
-
-
-
加载中
-
{{ pastureVideoError }}
-
{{ currentPastureVideo.title }}
+
+
+
+
+
+
加载中
+
{{ pastureVideoError }}
+
{{ item.live.title }}
+