From 1ef2fec9c9cb1db986812f963acb8ee2efe7ed3f Mon Sep 17 00:00:00 2001 From: wzy-warehouse <18135009705@163.com> Date: Sun, 14 Jun 2026 19:50:28 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=8E=A8=E6=BC=94=E4=BB=A5?= =?UTF-8?q?=E5=8F=8A=E8=84=89=E5=86=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/api.ts | 8 +- src/api/rainfall.ts | 11 +- .../detail-panels/RainfallGridComponent.vue | 13 +- src/types/common/WarningList.ts | 8 + src/types/rainstorm/RainPredictRequest.ts | 19 +++ src/types/rainstorm/RainPredictResponse.ts | 8 +- src/utils/cesium/CesiumUtils.ts | 148 ++++++++++++++++++ 7 files changed, 197 insertions(+), 18 deletions(-) create mode 100644 src/types/common/WarningList.ts create mode 100644 src/types/rainstorm/RainPredictRequest.ts diff --git a/src/api/api.ts b/src/api/api.ts index da6d729..a98af06 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -57,6 +57,7 @@ import type { XianReservoirList } from '@/types/base/XianReservoirList'; import type { XianSubwayStations } from '@/types/base/XianSubwayStations'; import { modelDeduction as rainfallModelDeduction } from './rainfall'; import type { RainPredictResponse } from '@/types/rainstorm/RainPredictResponse'; +import type { RainPredictRequest } from '@/types/rainstorm/RainPredictRequest'; /** * API接口统一导出对象 @@ -284,12 +285,11 @@ export const $api = { rainfall: { /** * 进行模型推演 - * @param disasterName 灾害名称 + * @param req 请求体 * @returns 推演点的概率 */ modelDeduction: ( - disasterName: string - ): Promise> => - rainfallModelDeduction(disasterName), + req: RainPredictRequest + ): Promise> => rainfallModelDeduction(req), }, }; diff --git a/src/api/rainfall.ts b/src/api/rainfall.ts index b7360ce..60462c2 100644 --- a/src/api/rainfall.ts +++ b/src/api/rainfall.ts @@ -1,16 +1,15 @@ import type { ApiResponse } from '@/types/ApiResponse'; +import type { RainPredictRequest } from '@/types/rainstorm/RainPredictRequest'; import type { RainPredictResponse } from '@/types/rainstorm/RainPredictResponse'; import httpInstance from '@/utils/request/http'; /** * 进行模型推演 - * @param disasterName 灾害名称 + * @param req 请求体 * @returns 推演点的概率 */ export const modelDeduction = ( - disasterName: string -): Promise> => { - return httpInstance.post('/algorithm-api/rainfall/predict', { - disaster_name: disasterName, - }); + req: RainPredictRequest +): Promise> => { + return httpInstance.post('/algorithm-api/rainfall/predict', req); }; diff --git a/src/component/rain-earthquake/detail-panels/RainfallGridComponent.vue b/src/component/rain-earthquake/detail-panels/RainfallGridComponent.vue index e9f0384..974e1cd 100644 --- a/src/component/rain-earthquake/detail-panels/RainfallGridComponent.vue +++ b/src/component/rain-earthquake/detail-panels/RainfallGridComponent.vue @@ -10,6 +10,7 @@ import { useStepStore } from '@/stores/useStepStore'; import type { ApiResponse } from '@/types/ApiResponse'; import type { RainfallGridResponse } from '@/types/rainstorm/RainfallGridResponse'; + import { CesiumUtilsSingleton } from '@/utils/cesium/CesiumUtils'; import { WebSocketService } from '@/utils/request/websocket'; import { Utils } from '@/utils/utils'; import { onMounted, onUnmounted, watch } from 'vue'; @@ -51,14 +52,18 @@ // 进行模型计算 $api.rainfall - .modelDeduction( - `${Utils.formatDate('YYYYMMDDHHmmss')}暴雨自动推演` - ) + .modelDeduction({ + disaster_name: `${Utils.formatDate('YYYYMMDDHHmmss', new Date('2025-09-16 20:00:00'))}暴雨自动推演`, + occurred_time: '2025-09-16 20:00:00', + operation_type: '暴雨灾害链自动推演', + }) .then((res) => { // 推进到下一步 stepStore.nextStep(); - // 报告产出 + // 进行预警 + CesiumUtilsSingleton.addPulseEffect(res.data.list); + console.log(res); }); } else { console.warn('响应错误:', response.message); diff --git a/src/types/common/WarningList.ts b/src/types/common/WarningList.ts new file mode 100644 index 0000000..d083b79 --- /dev/null +++ b/src/types/common/WarningList.ts @@ -0,0 +1,8 @@ +/** + * 预警列表 + */ +export interface WarningList { + probability: number; + lon: number; + lat: number; +} diff --git a/src/types/rainstorm/RainPredictRequest.ts b/src/types/rainstorm/RainPredictRequest.ts new file mode 100644 index 0000000..7276bdf --- /dev/null +++ b/src/types/rainstorm/RainPredictRequest.ts @@ -0,0 +1,19 @@ +/** + * 降雨预测请求值 + */ +export interface RainPredictRequest { + /** 灾害名称 */ + disaster_name: string; + /** id列表 */ + point_ids?: number[]; + /** 行政区划代码 */ + region_code?: string; + /** 累计降雨量 */ + rainfall?: number; + /** 持续时间 */ + duration?: number; + /** 发生时间 */ + occurred_time?: string; + /** 操作类型 */ + operation_type?: string; +} diff --git a/src/types/rainstorm/RainPredictResponse.ts b/src/types/rainstorm/RainPredictResponse.ts index dcb29fd..e1e7fdf 100644 --- a/src/types/rainstorm/RainPredictResponse.ts +++ b/src/types/rainstorm/RainPredictResponse.ts @@ -1,6 +1,6 @@ +import type { WarningList } from '../common/WarningList'; + export interface RainPredictResponse { - id: number; - type: string; - probability: number; - level: string; + record_id: number; + list: Record; } diff --git a/src/utils/cesium/CesiumUtils.ts b/src/utils/cesium/CesiumUtils.ts index b9e78ba..582ab12 100644 --- a/src/utils/cesium/CesiumUtils.ts +++ b/src/utils/cesium/CesiumUtils.ts @@ -20,6 +20,11 @@ import { SceneTransforms, Rectangle, Color, + JulianDate, + CallbackProperty, + HeightReference, + VerticalOrigin, + HorizontalOrigin, } from 'cesium'; import { CesiumViewerManager } from './CesiumViewerManager'; import { EntityManager } from './EntityManager'; @@ -29,6 +34,7 @@ import { GeoJsonManager, type ClearType } from './GeoJsonManager'; import { CameraController } from './CameraController'; import config from '@/config/config.json'; import type { ClickObject } from '@/types/cesium/ClickObject'; +import type { WarningList } from '@/types/common/WarningList'; // 导出 ClearType 类型 export type { ClearType }; @@ -48,6 +54,12 @@ export class CesiumUtils { // 颜色缓存 #colorCache = new Map(); + // 脉冲相关状态 + #pulseMap: Record = {}; + #maxPulseRadius = 30; + #pulseDuration = 5; + #pulseCircleImage: string | null = null; + constructor() { this.#viewerManager = new CesiumViewerManager(); } @@ -613,6 +625,44 @@ export class CesiumUtils { return color; } + /** + * 添加脉冲效果 + * @param list 点列表 + * @returns + */ + addPulseEffect(list: Record) { + const viewer = this.getViewer(); + if (!viewer) return; + + // 移除已有脉冲 + this.#removeAllPulses(); + + // 生成圆形贴图 + if (!this.#pulseCircleImage) { + this.#pulseCircleImage = this.#createCircleImage(this.#maxPulseRadius); + } + + for (const [disasterType, warning] of Object.entries(list)) { + const { lon, lat, probability } = warning; + + // 根据概率确定颜色 + let color: Color; + if (probability >= 0.7) { + color = Color.RED; + } else if (probability >= 0.5) { + color = Color.YELLOW; + } else { + continue; + } + + const key = `${disasterType}_${lon}_${lat}`; + const pulseId = `PULSE_${key}_${Date.now()}`; + + this.#createPulseCircle(pulseId, lon, lat, color); + this.#pulseMap[key] = { pulseId, probability }; + } + } + // ===================== 私有方法 ===================== /** @@ -625,6 +675,104 @@ export class CesiumUtils { throw new Error(`${managerName} 未初始化,请先调用 initCesiumViewer()`); } } + + /** + * 生成脉冲圆形贴图 + */ + #createCircleImage(maxRadius: number): string { + const canvas = document.createElement('canvas'); + canvas.width = maxRadius * 2; + canvas.height = maxRadius * 2; + const ctx = canvas.getContext('2d')!; + + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.beginPath(); + ctx.arc(maxRadius, maxRadius, maxRadius, 0, Math.PI * 2, false); + ctx.closePath(); + ctx.fillStyle = 'rgba(255,255,255,0.7)'; + ctx.fill(); + return canvas.toDataURL('image/png'); + } + + /** + * 创建脉冲实体 + * @param pulseId - 脉冲实体 ID + * @param lon - 经度 + * @param lat - 纬度 + * @param color - 颜色 + */ + #createPulseCircle( + pulseId: string, + lon: number, + lat: number, + color: Color + ): void { + const viewer = this.getViewer(); + if (!viewer || !this.#pulseCircleImage) return; + + const startTime = JulianDate.now(); + const maxRadius = this.#maxPulseRadius; + const duration = this.#pulseDuration; + + viewer.entities.add({ + id: pulseId, + position: Cartesian3.fromDegrees(lon, lat), + billboard: { + image: this.#pulseCircleImage, + width: new CallbackProperty((time) => { + const elapsed = + JulianDate.secondsDifference(time, startTime) % duration; + const progress = elapsed / duration; + return maxRadius * 2 * Math.abs(Math.sin(progress * Math.PI)); + }, false), + height: new CallbackProperty((time) => { + const elapsed = + JulianDate.secondsDifference(time, startTime) % duration; + const progress = elapsed / duration; + return maxRadius * 2 * Math.abs(Math.sin(progress * Math.PI)); + }, false), + color: new CallbackProperty((time) => { + const elapsed = + JulianDate.secondsDifference(time, startTime) % duration; + const progress = elapsed / duration; + const alpha = 0.7 * (1 - progress); + return color.withAlpha(alpha); + }, false), + heightReference: HeightReference.CLAMP_TO_GROUND, + verticalOrigin: VerticalOrigin.CENTER, + horizontalOrigin: HorizontalOrigin.CENTER, + eyeOffset: new Cartesian3(0.0, 0.0, -10.0), + disableDepthTestDistance: Number.POSITIVE_INFINITY, + }, + }); + } + + /** + * 移除所有脉冲实体 + */ + #removeAllPulses(): void { + for (const key of Object.keys(this.#pulseMap)) { + this.#deletePulseEntity(key); + } + } + + /** + * 删除单个脉冲实体 + * @param key - 脉冲映射 key + */ + #deletePulseEntity(key: string): void { + const viewer = this.getViewer(); + if (!viewer) return; + + const entry = this.#pulseMap[key]; + if (!entry) return; + + const entity = viewer.entities.getById(entry.pulseId); + if (entity) { + viewer.entities.remove(entity); + } + delete this.#pulseMap[key]; + } } /**