WQQ 22 tuntia sitten
vanhempi
commit
a55e05bce5
100 muutettua tiedostoa jossa 998 lisäystä ja 463 poistoa
  1. BIN
      RuoYi-Vue3/public/style/images/location4.png
  2. 2 3
      RuoYi-Vue3/public/supermap.config.js
  3. BIN
      RuoYi-Vue3/public/texture/sky.jpg
  4. 2 57
      RuoYi-Vue3/src/permission.js
  5. 1 1
      RuoYi-Vue3/src/supermap-cesium-module/components/analysis_3d/profile/profile.js
  6. 1 1
      RuoYi-Vue3/src/supermap-cesium-module/components/analysis_3d/shadow-query/shadow-query.js
  7. 1 1
      RuoYi-Vue3/src/supermap-cesium-module/components/analysis_3d/viewshed/viewshed.js
  8. 1 1
      RuoYi-Vue3/src/supermap-cesium-module/components/analysis_3d/viewshed/viewshed1.js
  9. 25 1
      RuoYi-Vue3/src/supermap-cesium-module/components/clip/clip-box-editor/clip-box-editor.js
  10. 22 2
      RuoYi-Vue3/src/supermap-cesium-module/components/clip/clip-box/clip-box.js
  11. 15 0
      RuoYi-Vue3/src/supermap-cesium-module/components/clip/clip-cross/clip-cross.js
  12. 8 1
      RuoYi-Vue3/src/supermap-cesium-module/components/clip/clip-plane-new/clip-plane-new.js
  13. 8 0
      RuoYi-Vue3/src/supermap-cesium-module/components/clip/clip-plane/clip-plane.js
  14. 8 0
      RuoYi-Vue3/src/supermap-cesium-module/components/clip/clip-polygon/clip-polygon.js
  15. 2 1
      RuoYi-Vue3/src/supermap-cesium-module/components/cloud-map-visualization/cloud-map-visualization.vue
  16. 34 34
      RuoYi-Vue3/src/supermap-cesium-module/components/draw/add-point-symbol/models.js
  17. 1 1
      RuoYi-Vue3/src/supermap-cesium-module/components/layer/pbr-material/pbr-material.js
  18. 2 1
      RuoYi-Vue3/src/supermap-cesium-module/components/radar-visualization/radar-visualization.vue
  19. 2 1
      RuoYi-Vue3/src/supermap-cesium-module/components/rainfall-visualization/rainfall-visualization.vue
  20. 5 4
      RuoYi-Vue3/src/supermap-cesium-module/components/realtime-typhoon/realtime-typhoon.vue
  21. 1 1
      RuoYi-Vue3/src/supermap-cesium-module/components/scene/light/light.js
  22. 4 3
      RuoYi-Vue3/src/supermap-cesium-module/components/scene/overview-map/overview-map.vue
  23. 3 3
      RuoYi-Vue3/src/supermap-cesium-module/components/search/search.vue
  24. 1 1
      RuoYi-Vue3/src/supermap-cesium-module/components/space-search/spatial-query3d/spatial-query3d.js
  25. 8 8
      RuoYi-Vue3/src/supermap-cesium-module/components/special-effects/particle-system/config.js
  26. 16 46
      RuoYi-Vue3/src/supermap-cesium-module/components/special-effects/particle-system/creat-particle-system.js
  27. 58 81
      RuoYi-Vue3/src/supermap-cesium-module/components/special-effects/particle-system/particle-system.js
  28. 1 1
      RuoYi-Vue3/src/supermap-cesium-module/components/special-effects/particle-system/particle-system.vue
  29. 5 5
      RuoYi-Vue3/src/supermap-cesium-module/components/special-effects/scan-effect/textures.js
  30. 6 5
      RuoYi-Vue3/src/supermap-cesium-module/components/viewer/viewer.js
  31. 23 7
      RuoYi-Vue3/src/supermap-cesium-module/components/wind-field-visualization/wind-field-visualization.vue
  32. 192 165
      RuoYi-Vue3/src/supermap-cesium-module/components/wind-field-visualization/windField.js
  33. 16 0
      RuoYi-Vue3/src/supermap-cesium-module/config/server_config.js
  34. 444 0
      RuoYi-Vue3/src/supermap-cesium-module/js/common/clip-model.js
  35. 1 1
      RuoYi-Vue3/src/supermap-cesium-module/js/common/waterLayer.js
  36. 14 10
      RuoYi-Vue3/src/supermap-cesium-module/views/layout/aside.vue
  37. 4 16
      RuoYi-Vue3/src/utils/request.js
  38. 20 0
      RuoYi-Vue3/vite.config.js
  39. 41 0
      nginx-production.conf
  40. BIN
      ruoyi-admin/target/classes/com/ruoyi/web/controller/system/SysProfileController.class
  41. BIN
      ruoyi-common/target/classes/com/ruoyi/common/annotation/Excel$ColumnType.class
  42. BIN
      ruoyi-common/target/classes/com/ruoyi/common/annotation/Excel$Type.class
  43. BIN
      ruoyi-common/target/classes/com/ruoyi/common/annotation/Excel.class
  44. BIN
      ruoyi-common/target/classes/com/ruoyi/common/config/RuoYiConfig.class
  45. BIN
      ruoyi-common/target/classes/com/ruoyi/common/config/serializer/SensitiveJsonSerializer.class
  46. BIN
      ruoyi-common/target/classes/com/ruoyi/common/constant/CacheConstants.class
  47. BIN
      ruoyi-common/target/classes/com/ruoyi/common/constant/Constants.class
  48. BIN
      ruoyi-common/target/classes/com/ruoyi/common/constant/GenConstants.class
  49. BIN
      ruoyi-common/target/classes/com/ruoyi/common/constant/HttpStatus.class
  50. BIN
      ruoyi-common/target/classes/com/ruoyi/common/constant/ScheduleConstants$Status.class
  51. BIN
      ruoyi-common/target/classes/com/ruoyi/common/constant/ScheduleConstants.class
  52. BIN
      ruoyi-common/target/classes/com/ruoyi/common/constant/UserConstants.class
  53. BIN
      ruoyi-common/target/classes/com/ruoyi/common/core/controller/BaseController$1.class
  54. BIN
      ruoyi-common/target/classes/com/ruoyi/common/core/controller/BaseController.class
  55. BIN
      ruoyi-common/target/classes/com/ruoyi/common/core/domain/AjaxResult.class
  56. BIN
      ruoyi-common/target/classes/com/ruoyi/common/core/domain/BaseEntity.class
  57. BIN
      ruoyi-common/target/classes/com/ruoyi/common/core/domain/R.class
  58. BIN
      ruoyi-common/target/classes/com/ruoyi/common/core/domain/TreeEntity.class
  59. BIN
      ruoyi-common/target/classes/com/ruoyi/common/core/domain/TreeSelect.class
  60. BIN
      ruoyi-common/target/classes/com/ruoyi/common/core/domain/entity/SysDept.class
  61. BIN
      ruoyi-common/target/classes/com/ruoyi/common/core/domain/entity/SysDictData.class
  62. BIN
      ruoyi-common/target/classes/com/ruoyi/common/core/domain/entity/SysDictType.class
  63. BIN
      ruoyi-common/target/classes/com/ruoyi/common/core/domain/entity/SysMenu.class
  64. BIN
      ruoyi-common/target/classes/com/ruoyi/common/core/domain/entity/SysRole.class
  65. BIN
      ruoyi-common/target/classes/com/ruoyi/common/core/domain/entity/SysUser.class
  66. BIN
      ruoyi-common/target/classes/com/ruoyi/common/core/domain/model/LoginBody.class
  67. BIN
      ruoyi-common/target/classes/com/ruoyi/common/core/domain/model/LoginUser.class
  68. BIN
      ruoyi-common/target/classes/com/ruoyi/common/core/domain/model/RegisterBody.class
  69. BIN
      ruoyi-common/target/classes/com/ruoyi/common/core/page/PageDomain.class
  70. BIN
      ruoyi-common/target/classes/com/ruoyi/common/core/page/TableDataInfo.class
  71. BIN
      ruoyi-common/target/classes/com/ruoyi/common/core/page/TableSupport.class
  72. BIN
      ruoyi-common/target/classes/com/ruoyi/common/core/redis/RedisCache.class
  73. BIN
      ruoyi-common/target/classes/com/ruoyi/common/core/text/CharsetKit.class
  74. BIN
      ruoyi-common/target/classes/com/ruoyi/common/core/text/Convert.class
  75. BIN
      ruoyi-common/target/classes/com/ruoyi/common/core/text/StrFormatter.class
  76. BIN
      ruoyi-common/target/classes/com/ruoyi/common/enums/BusinessStatus.class
  77. BIN
      ruoyi-common/target/classes/com/ruoyi/common/enums/BusinessType.class
  78. BIN
      ruoyi-common/target/classes/com/ruoyi/common/enums/DataSourceType.class
  79. BIN
      ruoyi-common/target/classes/com/ruoyi/common/enums/DesensitizedType.class
  80. BIN
      ruoyi-common/target/classes/com/ruoyi/common/enums/HttpMethod.class
  81. BIN
      ruoyi-common/target/classes/com/ruoyi/common/enums/LimitType.class
  82. BIN
      ruoyi-common/target/classes/com/ruoyi/common/enums/OperatorType.class
  83. BIN
      ruoyi-common/target/classes/com/ruoyi/common/enums/UserStatus.class
  84. BIN
      ruoyi-common/target/classes/com/ruoyi/common/exception/DemoModeException.class
  85. BIN
      ruoyi-common/target/classes/com/ruoyi/common/exception/GlobalException.class
  86. BIN
      ruoyi-common/target/classes/com/ruoyi/common/exception/ServiceException.class
  87. BIN
      ruoyi-common/target/classes/com/ruoyi/common/exception/UtilException.class
  88. BIN
      ruoyi-common/target/classes/com/ruoyi/common/exception/base/BaseException.class
  89. BIN
      ruoyi-common/target/classes/com/ruoyi/common/exception/file/FileException.class
  90. BIN
      ruoyi-common/target/classes/com/ruoyi/common/exception/file/FileNameLengthLimitExceededException.class
  91. BIN
      ruoyi-common/target/classes/com/ruoyi/common/exception/file/FileSizeLimitExceededException.class
  92. BIN
      ruoyi-common/target/classes/com/ruoyi/common/exception/file/FileUploadException.class
  93. BIN
      ruoyi-common/target/classes/com/ruoyi/common/exception/file/InvalidExtensionException$InvalidFlashExtensionException.class
  94. BIN
      ruoyi-common/target/classes/com/ruoyi/common/exception/file/InvalidExtensionException$InvalidImageExtensionException.class
  95. BIN
      ruoyi-common/target/classes/com/ruoyi/common/exception/file/InvalidExtensionException$InvalidMediaExtensionException.class
  96. BIN
      ruoyi-common/target/classes/com/ruoyi/common/exception/file/InvalidExtensionException$InvalidVideoExtensionException.class
  97. BIN
      ruoyi-common/target/classes/com/ruoyi/common/exception/file/InvalidExtensionException.class
  98. BIN
      ruoyi-common/target/classes/com/ruoyi/common/exception/job/TaskException$Code.class
  99. BIN
      ruoyi-common/target/classes/com/ruoyi/common/exception/job/TaskException.class
  100. BIN
      ruoyi-common/target/classes/com/ruoyi/common/exception/user/BlackListException.class

BIN
RuoYi-Vue3/public/style/images/location4.png


+ 2 - 3
RuoYi-Vue3/public/supermap.config.js

@@ -8,7 +8,6 @@ window.SUPERMAP_CONFIG = {
   // 完整地址(可选,如果设置了则使用此地址)
   // baseUrl: 'http://106.15.124.118:8090',
 
-  // 其他配置
-  // 天地图Key
-  tiandituKey: 'your_tianditu_key_here'
+  // 天地图Key(如需更换 token,直接改这里,刷新浏览器即可,无需重新打包)
+  tiandituKey: '3fb1e9fda20ee995dc815c8243553ce8'
 }

BIN
RuoYi-Vue3/public/texture/sky.jpg


+ 2 - 57
RuoYi-Vue3/src/permission.js

@@ -1,68 +1,13 @@
 import router from './router'
-import { ElMessage } from 'element-plus'
 import NProgress from 'nprogress'
 import 'nprogress/nprogress.css'
-import { getToken } from '@/utils/auth'
-import { isHttp, isPathMatch } from '@/utils/validate'
-import { isRelogin } from '@/utils/request'
-import useUserStore from '@/store/modules/user'
-import useSettingsStore from '@/store/modules/settings'
-import usePermissionStore from '@/store/modules/permission'
 
 NProgress.configure({ showSpinner: false })
 
-// 白名单 - 包含不需要登录的页面
-const whiteList = ['/login', '/register', '', '/model-management']
-
-const isWhiteList = (path) => {
-  return whiteList.some(pattern => isPathMatch(pattern, path))
-}
-
+// 登录验证已禁用,所有路由直接放行
 router.beforeEach((to, from, next) => {
   NProgress.start()
-  if (getToken()) {
-    to.meta.title && useSettingsStore().setTitle(to.meta.title)
-    /* has token*/
-    if (to.path === '/login') {
-      next({ path: '/' })
-      NProgress.done()
-    } else if (isWhiteList(to.path)) {
-      next()
-    } else {
-      if (useUserStore().roles.length === 0) {
-        isRelogin.show = true
-        // 判断当前用户是否已拉取完user_info信息
-        useUserStore().getInfo().then(() => {
-          isRelogin.show = false
-          usePermissionStore().generateRoutes().then(accessRoutes => {
-            // 根据roles权限生成可访问的路由表
-            accessRoutes.forEach(route => {
-              if (!isHttp(route.path)) {
-                router.addRoute(route) // 动态添加可访问路由表
-              }
-            })
-            next({ ...to, replace: true }) // hack方法 确保addRoutes已完成
-          })
-        }).catch(err => {
-          useUserStore().logOut().then(() => {
-            ElMessage.error(err)
-            next({ path: '/' })
-          })
-        })
-      } else {
-        next()
-      }
-    }
-  } else {
-    // 没有token
-    if (isWhiteList(to.path)) {
-      // 在免登录白名单,直接进入
-      next()
-    } else {
-      next(`/login?redirect=${to.fullPath}`) // 否则全部重定向到登录页
-      NProgress.done()
-    }
-  }
+  next()
 })
 
 router.afterEach(() => {

+ 1 - 1
RuoYi-Vue3/src/supermap-cesium-module/components/analysis_3d/profile/profile.js

@@ -267,7 +267,7 @@ function profile(props) {
                 return Entypositions;
             }, false),
             billboard: {
-                image: "src/style/images/location4.png",
+                image: "/style/images/location4.png",
                 width: 30,
                 height: 40,
                 scaleByDistance: new Cesium.NearFarScalar(10, 1.0, 2000, 0.6),

+ 1 - 1
RuoYi-Vue3/src/supermap-cesium-module/components/analysis_3d/shadow-query/shadow-query.js

@@ -90,7 +90,7 @@ function shadowquery(props) {
   console.log('Initial currentTimeArr:', currentTimeArr);
   const bubble = ref(null);
 
-  let box_s3m = "public/data/s3m/box.s3m";
+  let box_s3m = "/data/s3m/box.s3m";
   // 核心:校验S3M模型路径是否合理,增加日志提示
   if (!box_s3m || !box_s3m.endsWith(".s3m")) {
     console.error("S3M模型路径错误,必须是.s3m文件:", box_s3m);

+ 1 - 1
RuoYi-Vue3/src/supermap-cesium-module/components/analysis_3d/viewshed/viewshed.js

@@ -49,7 +49,7 @@ function viewshed(props) {
     // 初始化数据
     let viewshed3D, handler, s3mInstanceColc, scene, startPosition;
     let  tipFlag = true;
-    let Carurls = ['public/data/s3m/car1.s3m'], timers, dynamicLayer3D;
+    let Carurls = ['/data/s3m/car1.s3m'], timers, dynamicLayer3D;
 
     if (storeState.isViewer) {
         if (!window.tooltip) {

+ 1 - 1
RuoYi-Vue3/src/supermap-cesium-module/components/analysis_3d/viewshed/viewshed1.js

@@ -394,7 +394,7 @@ function viewshed(props) {
             }, false),
             // orientation: orientation,
             model: {
-                uri: "public/SampleData/models/Cesium_Ground.gltf",
+                uri: "/SampleData/models/Cesium_Ground.gltf",
                 scale: 1
             },
         });

+ 25 - 1
RuoYi-Vue3/src/supermap-cesium-module/components/clip/clip-box-editor/clip-box-editor.js

@@ -5,6 +5,7 @@ import tool from '../../../js/tool/tool.js'        //提示等工具
 import resource from '../../../js/local/lang.js'  //语言资源
 import createTooltip from '../../../js/tool/tooltip2.js';
 import { storeState, storeDate } from '../../../js/store/store.js'   //简单局部状态管理
+import { setAllModelClipBox, clearAllModelClip } from '../../../js/common/clip-model.js'
 
 
 function clipBoxByeditor(props) {
@@ -52,13 +53,16 @@ function clipBoxByeditor(props) {
 
     // 分析
     function BoxClipByEitor() {
+        console.log('[clip-box-editor] 🎯 BoxClipByEitor 按钮被点击! clipMode=', clipMode)
         clearBoxClipByEitor();
         if (editorBox) {
+            console.log('[clip-box-editor] editorBox 已存在, 直接激活 handler')
             handlerBox.activate();
             return;
         }
         // 设置裁剪线颜色
         setAllLayersClipColor();
+        console.log('[clip-box-editor] 开始激活 handlerBox, 等待用户绘制...')
         //交互绘制box
         handlerBox.movingEvt.addEventListener(windowPosition => {
             if (handlerBox.isDrawing) {
@@ -68,6 +72,7 @@ function clipBoxByeditor(props) {
             }
         });
         handlerBox.drawEvt.addEventListener(e => {
+            console.log('[clip-box-editor] ✅ drawEvt 触发! Box 绘制完成, e.object=', e.object)
             boxEntity = e.object;
             // let boxEntity = this.boxEntity;
             let newDim = boxEntity.box.dimensions.getValue();
@@ -78,6 +83,7 @@ function clipBoxByeditor(props) {
                 clipMode: clipMode,
                 heading: 0
             };
+            console.log('[clip-box-editor] 准备调用 setAllLayersClipOptions, boxOption=', boxOption)
 
             //box编辑
             editorBox = new Cesium.BoxEditor(viewer, boxEntity);
@@ -94,6 +100,7 @@ function clipBoxByeditor(props) {
             handlerBox.deactivate();
         });
         handlerBox.activate();
+        console.log('[clip-box-editor] handlerBox 已激活, 现在请在地球上拖动绘制 Box')
     };
     // 更新
     function setClipBox() {
@@ -139,9 +146,21 @@ function clipBoxByeditor(props) {
     };
     //设置图层裁剪
     function setAllLayersClipOptions(boxOptions) {
+        console.log('[clip-box-editor] setAllLayersClipOptions 调用, boxOptions:', boxOptions, 'clipMode:', clipMode)
         for (let i = 0, j = layers.length; i < j; i++) {
             layers[i].setCustomClipBox(boxOptions);
         }
+        // 同时裁剪场景中的 glTF Model(Entity 加载的 .glb)
+        // clipMode: 'clip_behind_all_plane'=裁剪 Box 内部(保留外部), cutInside=true
+        //           'clip_behind_any_plane'=裁剪 Box 外部(保留内部), cutInside=false
+        try {
+            const appliedCount = setAllModelClipBox(viewer, boxOptions, {
+                cutInside: clipMode === 'clip_behind_all_plane'
+            })
+            console.log('[clip-box-editor] setAllModelClipBox 返回, 应用到 Model 数量:', appliedCount)
+        } catch (e) {
+            console.error('[clip-box-editor] setAllModelClipBox 调用失败:', e.message, e.stack)
+        }
     };
     //设置线颜色
     function setAllLayersClipColor() {
@@ -162,9 +181,14 @@ function clipBoxByeditor(props) {
         for (let layer of layers) {
             layer.clearCustomClipBox();
         }
+        // 同时清除 glTF Model 的裁剪
+        clearAllModelClip(viewer)
+        // 仅移除 boxEntity(不能用 removeAll,会把场景中其他 entity 如水闸模型也清掉)
+        if (boxEntity) {
+            viewer.entities.remove(boxEntity);
+        }
         boxEntity = undefined;
         editorBox.deactivate();
-        viewer.entities.removeAll();
         handlerBox.clear();
     };
     // 监听

+ 22 - 2
RuoYi-Vue3/src/supermap-cesium-module/components/clip/clip-box/clip-box.js

@@ -5,6 +5,7 @@ import tool from '../../../js/tool/tool.js'        //提示等工具
 import createTooltip from '../../../js/tool/tooltip.js';
 import resource from '../../../js/local/lang.js'  //语言资源
 import { storeState, storeDate } from '../../../js/store/store.js'   //简单局部状态管理
+import { setAllModelClipBox, clearAllModelClip } from '../../../js/common/clip-model.js'
 
 
 function clipBoxAnalysis(props) {
@@ -60,7 +61,11 @@ function clipBoxAnalysis(props) {
     // 分析
     function BoxClip(e) {
         e.preventDefault();
-        viewer.entities.removeAll();
+        // 仅移除 Box Entity(不能用 removeAll,会把场景中其他 entity 如水闸模型也清掉)
+        if (boxEntity) {
+            viewer.entities.remove(boxEntity);
+            boxEntity = null;
+        }
         setAllLayersClipColor();
         //  提示
         screenSpaceEventHandler.setInputAction(evt => {
@@ -111,12 +116,24 @@ function clipBoxAnalysis(props) {
 
     // 设置是否带线框裁剪
     function setAllLayersClipOptions(boxOptions) {
+        console.log('[clip-box] setAllLayersClipOptions 调用, boxOptions:', boxOptions, 'clipModeOption:', clipModeOption)
         for (let layer of layers) {
             hasClipLine
                 ? (layer.clipLineColor = new Cesium.Color(1, 1, 1, 1))
                 : (layer.clipLineColor = new Cesium.Color(1, 1, 1, 0));
             layer.setCustomClipBox(boxOptions);
         }
+        // 同时裁剪场景中的 glTF Model(Entity 加载的 .glb)
+        // clipModeOption: 'clip_behind_all_plane'=裁剪 Box 内部(保留外部), cutInside=true
+        //                 'clip_behind_any_plane'=裁剪 Box 外部(保留内部), cutInside=false
+        try {
+            const appliedCount = setAllModelClipBox(viewer, boxOptions, {
+                cutInside: clipModeOption === 'clip_behind_all_plane'
+            })
+            console.log('[clip-box] setAllModelClipBox 返回, 应用到 Model 数量:', appliedCount)
+        } catch (e) {
+            console.error('[clip-box] setAllModelClipBox 调用失败:', e.message, e.stack)
+        }
     };
     // 设置裁剪线颜色
     function setAllLayersClipColor() {
@@ -147,11 +164,14 @@ function clipBoxAnalysis(props) {
             return;
         }
         tooltip.setVisible(false);
-        viewer.entities.removeAll();
+        // 仅移除 Box Entity(不能用 removeAll,会把场景中其他 entity 如水闸模型也清掉)
+        viewer.entities.remove(boxEntity);
         boxEntity = null;
         for (let layer of layers) {
             layer.clearCustomClipBox();
         }
+        // 同时清除 glTF Model 的裁剪
+        clearAllModelClip(viewer)
         screenSpaceEventHandler.removeInputAction(
             Cesium.ScreenSpaceEventType.MOUSE_MOVE
         );

+ 15 - 0
RuoYi-Vue3/src/supermap-cesium-module/components/clip/clip-cross/clip-cross.js

@@ -4,6 +4,7 @@ import { watch, reactive, toRefs, onBeforeUnmount } from "vue";
 import tool from '../../../js/tool/tool.js'        //提示等工具
 import resource from '../../../js/local/lang.js'  //语言资源
 import { storeState, storeDate } from '../../../js/store/store.js'   //简单局部状态管理
+import { setAllModelClipBox, clearAllModelClip, isClippingPlaneSupported } from '../../../js/common/clip-model.js'
 
 
 function clipCrossAnalysis(props) {
@@ -149,6 +150,18 @@ function clipCrossAnalysis(props) {
                 extrudeDistance: Number(state.extrude)
             });
         }
+        // 同时裁剪场景中的 glTF Model(Entity 加载的 .glb)
+        // cross 裁剪本质是两个交叉平面+一个 extrude 距离形成的十字体
+        // cross 默认效果是裁剪十字体内部(保留外部),对应 Box cutInside=true
+        if (isClippingPlaneSupported() && position && dim) {
+            setAllModelClipBox(viewer, {
+                position: position,
+                dimensions: new Cesium.Cartesian3(dim.x, dim.y, Number(state.extrude) || 0.1),
+                heading: Cesium.Math.toRadians(state.heading)
+            }, {
+                cutInside: true  // cross 默认裁剪十字体内部
+            })
+        }
     };
 
     // 清除
@@ -157,6 +170,8 @@ function clipCrossAnalysis(props) {
         for (let layer of layers) {
             layer.clearCustomClipBox();
         }
+        // 同时清除 glTF Model 的裁剪
+        clearAllModelClip(viewer)
         startClip = false;
         box = undefined;
         screenSpaceEventHandler.removeInputAction(

+ 8 - 1
RuoYi-Vue3/src/supermap-cesium-module/components/clip/clip-plane-new/clip-plane-new.js

@@ -6,6 +6,7 @@ import tool from '../../../js/tool/tool.js'        //提示等工具
 import resource from '../../../js/local/lang.js'  //语言资源
 import createTooltip from '../../../js/tool/tooltip2.js';
 import { storeState } from '../../../js/store/store.js'   //简单局部状态管理
+import { setAllModelClipPlane, clearAllModelClip, isClippingPlaneSupported } from '../../../js/common/clip-model.js'
 
 function clipPlane(props) {
     // 设置默认值数据
@@ -29,7 +30,7 @@ function clipPlane(props) {
     let layers;
     let planePositionObj;
     let editEntity, s3mInstanceColc;
-    let modelUrl = 'public/data/s3m/box.s3m';
+    let modelUrl = '/data/s3m/box.s3m';
     let modelEditor;
 
     /*
@@ -215,6 +216,10 @@ function clipPlane(props) {
                 planePositionObj.cartPositions[2]
             );
         }
+        // 同时裁剪场景中的 glTF Model(Entity 加载的 .glb)
+        if (isClippingPlaneSupported()) {
+            setAllModelClipPlane(viewer, planePositionObj.cartPositions)
+        }
     };
 
     // 清除
@@ -232,6 +237,8 @@ function clipPlane(props) {
         for (let layer of layers) {
             layer.clearCustomClipBox();
         }
+        // 同时清除 glTF Model 的裁剪
+        clearAllModelClip(viewer)
     };
 
     // 监听

+ 8 - 0
RuoYi-Vue3/src/supermap-cesium-module/components/clip/clip-plane/clip-plane.js

@@ -7,6 +7,7 @@ import tool from '../../../js/tool/tool.js'        //提示等工具
 import resource from '../../../js/local/lang.js'  //语言资源
 import createTooltip from '../../../js/tool/tooltip2.js';
 import { storeState } from '../../../js/store/store.js'   //简单局部状态管理
+import { setAllModelClipPlane, clearAllModelClip, isClippingPlaneSupported } from '../../../js/common/clip-model.js'
 
 function clipPlaneAnalysis(props) {
     // 设置默认值数据
@@ -104,6 +105,11 @@ function clipPlaneAnalysis(props) {
                 p[2]
             );
         }
+        // 同时裁剪场景中的 glTF Model(Entity 加载的 .glb)
+        // ClippingPlaneCollection 用三点确定一个平面进行裁剪
+        if (isClippingPlaneSupported()) {
+            setAllModelClipPlane(viewer, p)
+        }
     };
 
     // 清除
@@ -116,6 +122,8 @@ function clipPlaneAnalysis(props) {
         for (let layer of layers) {
             layer.clearCustomClipBox();
         }
+        // 同时清除 glTF Model 的裁剪
+        clearAllModelClip(viewer)
     };
 
     // 监听

+ 8 - 0
RuoYi-Vue3/src/supermap-cesium-module/components/clip/clip-polygon/clip-polygon.js

@@ -7,6 +7,7 @@ import tool from '../../../js/tool/tool.js'        //提示等工具
 import resource from '../../../js/local/lang.js'  //语言资源
 import createTooltip from '../../../js/tool/tooltip2.js';
 import { storeState } from '../../../js/store/store.js'   //简单局部状态管理
+import { clearAllModelClip, hasClippableModel } from '../../../js/common/clip-model.js'
 
 function clipPolygonAnalysis(props) {
     // 设置默认值数据
@@ -73,6 +74,11 @@ function clipPolygonAnalysis(props) {
             // 设置被裁剪对象的颜色
             layer.clipLineColor = new Cesium.Color(1, 1, 1, 0);
         }
+        // 多边形裁剪只能用于 S3M 图层,Cesium 原生 glTF Model 不支持任意多边形裁剪
+        // 若场景中只有 glTF Model 没有 S3M 图层,提示用户改用平面裁剪/Box 裁剪
+        if (layers.length === 0 && hasClippableModel(viewer)) {
+            tool.Message && tool.Message.errorMsg('多边形裁剪不支持 Entity 加载的 glTF 模型,请改用平面裁剪或 Box 裁剪');
+        }
         if (!window.handlerPolygon) {
             initHandler("Polygon");
         }
@@ -114,6 +120,8 @@ function clipPolygonAnalysis(props) {
         for (let layer of layers) {
             layer.clearModifyRegions();
         }
+        // 同时清除 glTF Model 上残留的其他类型裁剪(多边形本身不支持 Model,但兜底防止遗留)
+        clearAllModelClip(viewer)
     };
     // 监听
     watch(() => state.clipModelPolygon, val => {

+ 2 - 1
RuoYi-Vue3/src/supermap-cesium-module/components/cloud-map-visualization/cloud-map-visualization.vue

@@ -17,6 +17,7 @@
 <script setup>
 import { ref, watch, onMounted, onUnmounted } from 'vue'
 import axios from 'axios'
+import { PROXY_SLT } from '../../config/server_config.js'
 
 const props = defineProps({
   viewer: { type: Object, default: null },
@@ -76,7 +77,7 @@ const fetchCloud = async (type) => {
   error.value = ''
   clearLayer()
   try {
-    const { data } = await axios.get(`https://typhoon.slt.zj.gov.cn/Api/LeastCloud/?type=${type}`)
+    const { data } = await axios.get(`${PROXY_SLT}/Api/LeastCloud/?type=${type}`)
     const field = fieldMap[type] || 'cloudname'
     const img = data[field]
     const minLat = Number(data.minLat), maxLat = Number(data.maxLat)

+ 34 - 34
RuoYi-Vue3/src/supermap-cesium-module/components/draw/add-point-symbol/models.js

@@ -7,44 +7,44 @@ export default
             data: [{
                 id: 0,
                 name: "树1",
-                thumbnail: "public/data/s3m/S3MData/tree.png",
-                path: "public/data/s3m/S3MData/tree1.s3m"
+                thumbnail: "/data/s3m/S3MData/tree.png",
+                path: "/data/s3m/S3MData/tree1.s3m"
             },
             {
                 id: 1,
                 name: "树2",
-                thumbnail: "public/data/s3m/S3MData/pine.png",
-                path: "public/data/s3m/S3MData/tree2.s3m"
+                thumbnail: "/data/s3m/S3MData/pine.png",
+                path: "/data/s3m/S3MData/tree2.s3m"
             },
             {
                 id: 2,
                 name: "树3",
-                thumbnail: "public/data/s3m/S3MData/tree3.png",
-                path: "public/data/s3m/S3MData/tree3.s3m"
+                thumbnail: "/data/s3m/S3MData/tree3.png",
+                path: "/data/s3m/S3MData/tree3.s3m"
             },
             {
                 id: 3,
                 name: "树4",
-                thumbnail: "public/data/s3m/S3MData/tree2.png",
-                path: "public/data/s3m/S3MData/tree4.s3m"
+                thumbnail: "/data/s3m/S3MData/tree2.png",
+                path: "/data/s3m/S3MData/tree4.s3m"
             },
             {
                 id: 4,
                 name: "springTree",
-                thumbnail: "public/data/s3m/S3MData/springTree.png",
-                path: "public/data/s3m/S3MData/springTree.s3m"
+                thumbnail: "/data/s3m/S3MData/springTree.png",
+                path: "/data/s3m/S3MData/springTree.s3m"
             },
             {
                 id: 5,
                 name: "autumnTree",
-                thumbnail: "public/data/s3m/S3MData/autumnTree.png",
-                path: "public/data/s3m/S3MData/autumnTree.s3m"
+                thumbnail: "/data/s3m/S3MData/autumnTree.png",
+                path: "/data/s3m/S3MData/autumnTree.s3m"
             },
             {
                 id: 6,
                 name: "winterTree",
-                thumbnail: "public/data/s3m/S3MData/winterTree.png",
-                path: "public/data/s3m/S3MData/winterTree.s3m"
+                thumbnail: "/data/s3m/S3MData/winterTree.png",
+                path: "/data/s3m/S3MData/winterTree.s3m"
             },
             {}, //个数凑5的倍数便于排版
             {},
@@ -58,44 +58,44 @@ export default
             data: [{
                 id: 0,
                 name: "椅子",
-                thumbnail: "public/data/s3m/S3MData/bench.png",
-                path: "public/data/s3m/S3MData/Chair.s3m"
+                thumbnail: "/data/s3m/S3MData/bench.png",
+                path: "/data/s3m/S3MData/Chair.s3m"
             },
             {
                 id: 1,
                 name: "垃圾箱",
-                thumbnail: "public/data/s3m/S3MData/Dustbin.png",
-                path: "public/data/s3m/S3MData/Dustbin.s3m"
+                thumbnail: "/data/s3m/S3MData/Dustbin.png",
+                path: "/data/s3m/S3MData/Dustbin.s3m"
             },
             {
                 id: 2,
                 name: "垃圾箱2",
-                thumbnail: "public/data/s3m/S3MData/paper-bucket.png",
-                path: "public/data/s3m/S3MData/Dustbin2.s3m"
+                thumbnail: "/data/s3m/S3MData/paper-bucket.png",
+                path: "/data/s3m/S3MData/Dustbin2.s3m"
             },
             {
                 id: 3,
                 name: "垃圾桶",
-                thumbnail: "public/data/s3m/S3MData/dustbin1.png",
-                path: "public/data/s3m/S3MData/GarbageCan.s3m"
+                thumbnail: "/data/s3m/S3MData/dustbin1.png",
+                path: "/data/s3m/S3MData/GarbageCan.s3m"
             },
             {
                 id: 4,
                 name: "报亭",
-                thumbnail: "public/data/s3m/S3MData/newspaper.png",
-                path: "public/data/s3m/S3MData/NewsStand.s3m"
+                thumbnail: "/data/s3m/S3MData/newspaper.png",
+                path: "/data/s3m/S3MData/NewsStand.s3m"
             },
             {
                 id: 5,
                 name: "邮政信箱",
-                thumbnail: "public/data/s3m/S3MData/mailbox.png",
-                path: "public/data/s3m/S3MData/PO-Box.s3m"
+                thumbnail: "/data/s3m/S3MData/mailbox.png",
+                path: "/data/s3m/S3MData/PO-Box.s3m"
             },
             {
                 id: 6,
                 name: "电信电话亭",
-                thumbnail: "public/data/s3m/S3MData/phone-booth.png",
-                path: "public/data/s3m/S3MData/T-TelBooth.s3m"
+                thumbnail: "/data/s3m/S3MData/phone-booth.png",
+                path: "/data/s3m/S3MData/T-TelBooth.s3m"
             },
            {},
            {},
@@ -109,20 +109,20 @@ export default
             data: [{
                 id: 0,
                 name: "限速牌",
-                thumbnail: "public/data/s3m/S3MData/speed-limit.png",
-                path: "public/data/s3m/S3MData/RestrictionBoard.s3m"
+                thumbnail: "/data/s3m/S3MData/speed-limit.png",
+                path: "/data/s3m/S3MData/RestrictionBoard.s3m"
             },
             {
                 id: 1,
                 name: "路灯",
-                thumbnail: "public/data/s3m/S3MData/lamp-post.png",
-                path: "public/data/s3m/S3MData/StreetLight.s3m"
+                thumbnail: "/data/s3m/S3MData/lamp-post.png",
+                path: "/data/s3m/S3MData/StreetLight.s3m"
             },
             {
                 id: 2,
                 name: "红绿灯",
-                thumbnail: "public/data/s3m/S3MData/traffic-light.png",
-                path: "public/data/s3m/S3MData/TrafficLight.s3m"
+                thumbnail: "/data/s3m/S3MData/traffic-light.png",
+                path: "/data/s3m/S3MData/TrafficLight.s3m"
             },
             {},
             {},

+ 1 - 1
RuoYi-Vue3/src/supermap-cesium-module/components/layer/pbr-material/pbr-material.js

@@ -74,7 +74,7 @@ function s3mlayerAttribute(props) {
 
 
     function addPBR() {
-        let url = "public/data/pbr/jsons/" + state.selectedPbr + ".json"
+        let url = "/data/pbr/jsons/" + state.selectedPbr + ".json"
         if (state.selectedLayerName === '地形') {
             viewer.scene.globe.setPBRMaterialFromJSON(url);
             return;

+ 2 - 1
RuoYi-Vue3/src/supermap-cesium-module/components/radar-visualization/radar-visualization.vue

@@ -7,6 +7,7 @@
 <script setup>
 import { ref, watch, onMounted, onUnmounted } from 'vue'
 import axios from 'axios'
+import { PROXY_SLT } from '../../config/server_config.js'
 
 const props = defineProps({
   viewer: { type: Object, default: null },
@@ -106,7 +107,7 @@ const fetchRadar = async () => {
   error.value = ''
   clearLayer()
   try {
-    const { data } = await axios.get('https://typhoon.slt.zj.gov.cn/Api/LastRadar')
+    const { data } = await axios.get(`${PROXY_SLT}/Api/LastRadar`)
     const radarType = +data.radarType
     if (radarType === 2) {
       // 单张图

+ 2 - 1
RuoYi-Vue3/src/supermap-cesium-module/components/rainfall-visualization/rainfall-visualization.vue

@@ -33,6 +33,7 @@
 import { ref, watch, onMounted, onUnmounted } from 'vue'
 import axios from 'axios'
 import { ElMessage } from 'element-plus'
+import { PROXY_SLT } from '../../config/server_config.js'
 
 const props = defineProps({
   viewer: { type: Object, default: null },
@@ -263,7 +264,7 @@ const fetchRainfall = async (hours) => {
   clearLayer()
 
   try {
-    const url = `https://typhoon.slt.zj.gov.cn/Api/LeastRain/${hours}`
+    const url = `${PROXY_SLT}/Api/LeastRain/${hours}`
     const { data } = await axios.get(url)
     let contours = data?.contours
     if (typeof contours === 'string') {

+ 5 - 4
RuoYi-Vue3/src/supermap-cesium-module/components/realtime-typhoon/realtime-typhoon.vue

@@ -47,7 +47,7 @@
     :key="t.tfid"
     :viewer="viewer"
     :visible="visible"
-    :data-url="`https://typhoon.slt.zj.gov.cn/Api/TyphoonInfo/${t.tfid}`"
+    :data-url="`${PROXY_SLT}/Api/TyphoonInfo/${t.tfid}`"
     :show-warning-lines="i === 0"
   ></TyphoonVisualization>
 
@@ -57,7 +57,7 @@
     :key="`history-${t.tfid}`"
     :viewer="viewer"
     :visible="visible"
-    :data-url="`https://typhoon.slt.zj.gov.cn/Api/TyphoonInfo/${t.tfid}`"
+    :data-url="`${PROXY_SLT}/Api/TyphoonInfo/${t.tfid}`"
   ></TyphoonVisualization>
 </template>
 
@@ -67,6 +67,7 @@ import axios from 'axios'
 import { ElMessage } from 'element-plus'
 import { CircleClose } from '@element-plus/icons-vue'
 import TyphoonVisualization from '../typhoon-visualization/typhoon-visualization.vue'
+import { PROXY_SLT } from '../../config/server_config.js'
 
 const props = defineProps({
   viewer: { type: Object, default: null },
@@ -83,7 +84,7 @@ let searchTimer = null
 // 获取所有活动台风
 const fetchRealtimeTyphoon = async () => {
   try {
-    const { data: activityList } = await axios.get('https://typhoon.slt.zj.gov.cn/Api/TyhoonActivity')
+    const { data: activityList } = await axios.get(`${PROXY_SLT}/Api/TyhoonActivity`)
     const list = Array.isArray(activityList) ? activityList : []
     if (!isMounted) return
     typhoons.value = list
@@ -104,7 +105,7 @@ const querySearch = (queryString, cb) => {
   if (!q) { cb([]); return }
   searchTimer = setTimeout(async () => {
     try {
-      const { data } = await axios.get(`https://typhoon.slt.zj.gov.cn/Api/TyphoonSearch/${q}`)
+      const { data } = await axios.get(`${PROXY_SLT}/Api/TyphoonSearch/${q}`)
       cb(Array.isArray(data) ? data : [])
     } catch (e) {
       console.error('搜索台风失败:', e)

+ 1 - 1
RuoYi-Vue3/src/supermap-cesium-module/components/scene/light/light.js

@@ -42,7 +42,7 @@ function addPointSymbol(props) {
     let currentSelectedEntity, selectdeLightSource, s3mInstanceColc;
     let isAddPointLight = false;
     let entityLightPairs = new Map(); // Entity和点光源对象的键值对
-    let modelUrl = 'public/data/s3m/light.s3m';
+    let modelUrl = '/data/s3m/light.s3m';
     let modelEditor;
     const bubble = ref(null);
 

+ 4 - 3
RuoYi-Vue3/src/supermap-cesium-module/components/scene/overview-map/overview-map.vue

@@ -24,6 +24,7 @@
 
 <script>
 import { ref, reactive, onMounted, onUnmounted, watch, nextTick } from "vue";
+import { PROXY_TIANDITU_TILE, TIANDITU_KEY } from "../../../config/server_config.js";
 
 export default {
   name: "Sm3dOverviewMap",
@@ -328,13 +329,13 @@ export default {
     function createMiniViewer() {
       if (!cesiumContainer.value) return;
       
-      const tiandituKey = '3fb1e9fda20ee995dc815c8243553ce8';
+      // tk 来自外部配置 supermap.config.js
       
       // 使用地理坐标系(EPSG:4326)的瓦片方案
       const geographicTilingScheme = new Cesium.GeographicTilingScheme();
       
       const imageryProvider = new Cesium.WebMapTileServiceImageryProvider({
-        url: `https://t0.tianditu.gov.cn/img_c/wmts?service=WMTS&request=GetTile&version=1.0.0&LAYER=img&tileMatrixSet=c&TileMatrix={TileMatrix}&TileRow={TileRow}&TileCol={TileCol}&style=default&format=tiles&tk=${tiandituKey}`,
+        url: `${PROXY_TIANDITU_TILE}/img_c/wmts?service=WMTS&request=GetTile&version=1.0.0&LAYER=img&tileMatrixSet=c&TileMatrix={TileMatrix}&TileRow={TileRow}&TileCol={TileCol}&style=default&format=tiles&tk=${TIANDITU_KEY}`,
         layer: 'img',
         style: 'default',
         format: 'image/jpeg',
@@ -380,7 +381,7 @@ export default {
       
       // 添加注记图层
       const labelProvider = new Cesium.WebMapTileServiceImageryProvider({
-        url: `https://t0.tianditu.gov.cn/cia_c/wmts?service=WMTS&request=GetTile&version=1.0.0&LAYER=cia&tileMatrixSet=c&TileMatrix={TileMatrix}&TileRow={TileRow}&TileCol={TileCol}&style=default&format=tiles&tk=${tiandituKey}`,
+        url: `${PROXY_TIANDITU_TILE}/cia_c/wmts?service=WMTS&request=GetTile&version=1.0.0&LAYER=cia&tileMatrixSet=c&TileMatrix={TileMatrix}&TileRow={TileRow}&TileCol={TileCol}&style=default&format=tiles&tk=${TIANDITU_KEY}`,
         layer: 'cia',
         style: 'default',
         format: 'image/png',

+ 3 - 3
RuoYi-Vue3/src/supermap-cesium-module/components/search/search.vue

@@ -25,6 +25,7 @@
 
 <script setup>
 import { ref, onUnmounted } from 'vue';
+import { PROXY_TIANDITU_API, TIANDITU_KEY } from "../../config/server_config.js";
 
 const props = defineProps({
   viewer: {
@@ -38,8 +39,7 @@ const showResults = ref(false);
 const searchResults = ref([]);
 const isSearching = ref(false);
 
-// 天地图API密钥
-const tiandituKey = '3fb1e9fda20ee995dc815c8243553ce8';
+// 天地图 API 密钥来自外部配置 supermap.config.js
 
 // 处理输入
 const handleInput = async () => {
@@ -117,7 +117,7 @@ const selectResult = (result) => {
 
 // 天地图地理编码 API
 const getLonLatByTianditu = async (keyword) => {
-  const url = `https://api.tianditu.gov.cn/geocoder?ds={"keyWord":"${encodeURIComponent(keyword)}"}&tk=${tiandituKey}`;
+  const url = `${PROXY_TIANDITU_API}/geocoder?ds={"keyWord":"${encodeURIComponent(keyword)}"}&tk=${TIANDITU_KEY}`;
 
   const res = await fetch(url);
   const data = await res.json();

+ 1 - 1
RuoYi-Vue3/src/supermap-cesium-module/components/space-search/spatial-query3d/spatial-query3d.js

@@ -55,7 +55,7 @@ function spatialQuery3d(props) {
     let geometry, GeometryBodys = [];
 
     let editEntity, s3mInstanceColc;
-    let modelUrl = 'public/data/s3m/box.s3m';
+    let modelUrl = '/data/s3m/box.s3m';
     let modelEditor;
 
     if (storeState.isViewer) {

+ 8 - 8
RuoYi-Vue3/src/supermap-cesium-module/components/special-effects/particle-system/config.js

@@ -4,7 +4,7 @@ export default [
         particleType: 'flame',
         particleList: [
             {
-                image: 'public/img/particle/基础火焰.png',
+                image: '/img/particle/基础火焰.png',
                 startColor: 'rgba(255, 255, 255, 0.3)',
                 endColor: 'rgba(0, 0, 0, 0)',
                 lifetime: 6.0,
@@ -20,7 +20,7 @@ export default [
                 emitter:['ConeEmitter',[60]]
             },
             {
-                image: 'public/img/particle/环状火焰-火焰.png',
+                image: '/img/particle/环状火焰-火焰.png',
                 startColor: 'rgba(255, 255, 255, 0.3)',
                 endColor: 'rgba(102, 76.5, 0, 0)',
                 lifetime: 6.0,
@@ -36,7 +36,7 @@ export default [
                 emitter:['CircleEmitter',[30,25]]
             },
             {
-                image: 'public/img/particle/基础火焰.png',
+                image: '/img/particle/基础火焰.png',
                 startColor: 'rgba(255, 255, 255, 0.3)',
                 endColor: 'rgba(0, 0, 0, 0)',
                 lifetime: 16.0,
@@ -52,7 +52,7 @@ export default [
                 emitter:['CircleEmitter',[0.2]]
             },
             {
-                image: 'public/img/particle/流淌火-火焰.png',
+                image: '/img/particle/流淌火-火焰.png',
                 startColor: 'rgba(255, 255, 255, 0.8)',
                 endColor: 'rgba(0, 0, 0, 0)',
                 lifetime: 16.0,
@@ -68,7 +68,7 @@ export default [
                 emitter:['PolygonEmitter',[]]
             },
             {
-                image: 'public/img/particle/爆炸起火.png',
+                image: '/img/particle/爆炸起火.png',
                 startColor: 'rgba(255, 255, 255, 1)',
                 endColor: 'rgba(51,51, 51, 0)',
                 lifetime: 5,
@@ -89,7 +89,7 @@ export default [
         particleType: 'fountain',
         particleList: [
             {
-                image: 'public/img/particle/水柱.png',
+                image: '/img/particle/水柱.png',
                 startColor: 'rgba(255, 255, 255, 0.6)',
                 endColor: 'rgba(204, 219, 255, 0.4)',
                 lifetime: 16.0,
@@ -105,7 +105,7 @@ export default [
                 emitter:['CircleEmitter',[0.2]]
             },
             {
-                image: 'public/img/particle/水柱.png',
+                image: '/img/particle/水柱.png',
                 startColor: 'rgba(146, 170, 193, 1)',
                 endColor: 'rgba(146, 170, 193, 0)',
                 lifetime: 16.0,
@@ -126,7 +126,7 @@ export default [
     //     particleType: 'fireWork',
     //     particleList: [
     //         {
-    //             image: 'public/img/particle/flaretrail6.png',
+    //             image: '/img/particle/flaretrail6.png',
     //             startColor: 'rgba(255, 255, 255, 0.6)',
     //             endColor: 'rgba(204, 219, 255, 0)',
     //             lifetime: 10.0,

+ 16 - 46
RuoYi-Vue3/src/supermap-cesium-module/components/special-effects/particle-system/creat-particle-system.js

@@ -10,10 +10,6 @@ class ParticleSystem {
         this.state = null;
         this.particleSystem = null;
         this.gravity = 1;
-        // 烟花
-        this.fireWokes = [];
-        this.emitterInitialLocation = new Cesium.Cartesian3(0.0, 0.0, 200.0);
-        this.emitterModelMatrixScratch = new Cesium.Matrix4();
         this.applyGravity;
     }
     create(state, position, positions) {//模型位置/区域火位置
@@ -70,14 +66,27 @@ class ParticleSystem {
 
   
 
+    // 校验 position 是否为合法的地球表面笛卡尔坐标(非空、无 NaN、非地心原点)
+    // 地心 (0,0,0) 会让 eastNorthUpToFixedFrame 归一化零向量产生 NaN 矩阵,
+    // 进而污染 ParticleSystem.modelMatrix,触发 createPotentiallyVisibleSet 崩溃。
+    _isValidPosition(position) {
+        return position
+            && isFinite(position.x) && isFinite(position.y) && isFinite(position.z)
+            && !(position.x === 0 && position.y === 0 && position.z === 0);
+    }
+
     computeModelMatrix(position) {
-        if (!position) return Cesium.Matrix4.IDENTITY;
-        return Cesium.Transforms.eastNorthUpToFixedFrame(position, undefined, new Cesium.Matrix4())
+        if (!this._isValidPosition(position)) return Cesium.Matrix4.IDENTITY;
+        try {
+            return Cesium.Transforms.eastNorthUpToFixedFrame(position, undefined, new Cesium.Matrix4());
+        } catch (e) {
+            return Cesium.Matrix4.IDENTITY;
+        }
         // return Cesium.Transforms.eastNorthUpToFixedFrame(Cesium.Cartesian3.fromDegrees(116.381828897766, 39.981967807373));
     }
 
     computeEmitterModelMatrix(HeadingPitchRoll, translation, position) {
-        if (!position) return Cesium.Matrix4.IDENTITY;
+        if (!this._isValidPosition(position)) return Cesium.Matrix4.IDENTITY;
         this.hpr = Cesium.HeadingPitchRoll.fromDegrees(0, 0, 0, this.hpr);
         this.trs.translation = Cesium.Cartesian3.fromElements(0, 0, 4, this.translation);
         if (HeadingPitchRoll)
@@ -150,45 +159,6 @@ class ParticleSystem {
         if (!Cesium.defaultValue(this.particleSystem)) return;
         viewer.scene._primitives.remove(this.particleSystem);
     }
-
-    createFirework(offset, color, bursts, centerPosition) {
-        let position = Cesium.Cartesian3.add(this.emitterInitialLocation, offset, new Cesium.Cartesian3());
-        let emitterModelMatrix = Cesium.Matrix4.fromTranslation(position, this.emitterModelMatrixScratch);
-        function applyGravity(particle, dt) {
-            let position = particle.position;
-            let gravityVector = Cesium.Cartesian3.normalize(position, new Cesium.Cartesian3());
-            Cesium.Cartesian3.multiplyByScalar(gravityVector, 1 * dt, gravityVector);
-            particle.velocity = Cesium.Cartesian3.add(particle.velocity, gravityVector, particle.velocity);
-        }
-        let fireWoke = viewer.scene.primitives.add(new Cesium.ParticleSystem({
-            // image: getImage(),
-            image: 'public/img/particle/flaretrail6.png',
-            startColor: color,
-            endColor: color.withAlpha(0.0),
-            // particleLife: 0.5,
-            minimumParticleLife: 1,
-            maximumParticleLife: 2,
-            speed: 80.0,
-            // minimumSpeed: 80,
-            // maximumSpeed: 120,
-            imageSize: new Cesium.Cartesian2(15, 15),
-            emissionRate: 0,
-            emitter: new Cesium.SphereEmitter(0.1),
-            bursts: bursts,
-            lifetime: 12,
-            updateCallback: applyGravity,
-            modelMatrix: this.computeModelMatrix(centerPosition),
-            emitterModelMatrix: emitterModelMatrix,
-            performance:false
-        }));
-        this.fireWokes.push(fireWoke);
-    }
-
-
-    clearFireWoke() {
-        this.fireWokes.forEach((fw) => { viewer.scene._primitives.remove(fw) });
-        this.fireWokes.length = 0;
-    }
 }
 
 

+ 58 - 81
RuoYi-Vue3/src/supermap-cesium-module/components/special-effects/particle-system/particle-system.js

@@ -23,12 +23,6 @@ function particleSystem(props) {
                 iconfont: "iconshui",
                 particleName: "水"
             },
-            {
-                id: 2,
-                iconfont: "iconyanhua",
-                particleName: "烟花"
-            },
-          
         ],
         particleSelectedId: 0,
         flameTyle: '0',
@@ -43,7 +37,7 @@ function particleSystem(props) {
         gravity: 0,
         lifetime: 6,
         particleSystemType: "conical",
-        image: 'public/img/particle/基础火焰.png',
+        image: '/img/particle/基础火焰.png',
         startColor: 'rgba(255, 255, 255, 0.3)',
         endColor: 'rgba(0, 0, 0, 0)',
         visibleModel: true,
@@ -70,39 +64,28 @@ function particleSystem(props) {
     let currentSelectedEntity, selectParticleSystem, s3mInstanceColc,clickCount;
     let isAddParticleSystem = false;
     let entityParticlePairs = new Map(); // Entity和点光源对象的键值对
-    let modelUrl = 'public/data/s3m/box.s3m';
+    let modelUrl = '/data/s3m/box.s3m';
     let modelEditor;
     let flowingFires = [];
-    //烟花默认参数设置
-    let fireWorkSystem;
-    let numberOfFireworks = 18;
-    let xMin = -600.0;
-    let xMax = 600.0;
-    let yMin = -600.0;
-    let yMax = 600.0;
-    let zMin = 0.0;
-    let zMax = 200.0;
-    let colorOptions = [{
-        minimumRed: 0.95,
-        green: 0.0,
-        minimumBlue: 0.8,
-        alpha: 1.0
-    }, {
-        red: 0.0,
-        minimumGreen: 0.75,
-        minimumBlue: 0.8,
-        alpha: 1.0
-    }, {
-        red: 0.0,
-        green: 0.0,
-        minimumBlue: 0.8,
-        alpha: 1.0
-    }, {
-        minimumRed: 0.9,
-        minimumGreen: 0.9,
-        blue: 0.0,
-        alpha: 1.0
-    }];
+    // Cesium 原生 ParticleSystem 依赖 clock 前进计算 dt 推进粒子。
+    // 但 scene-attribute/time-slider 默认设置 shouldAnimate=false 暂停时钟,
+    // 导致粒子既不发射也不移动(dt 恒为 0)。此处保存原值并在粒子激活期间
+    // 临时启用时钟,清除粒子后恢复原值,避免影响时间动画面板的设置。
+    let prevShouldAnimate = null; // null=未修改; true/false=修改前的原值
+    const ensureClockRunning = () => {
+        if (prevShouldAnimate === null && viewer.clock && !viewer.clock.shouldAnimate) {
+            prevShouldAnimate = false;
+            viewer.clock.shouldAnimate = true;
+        } else if (prevShouldAnimate === null) {
+            prevShouldAnimate = viewer.clock ? viewer.clock.shouldAnimate : false;
+        }
+    };
+    const restoreClock = () => {
+        if (prevShouldAnimate !== null && viewer.clock) {
+            viewer.clock.shouldAnimate = prevShouldAnimate;
+            prevShouldAnimate = null;
+        }
+    };
 
     if (storeState.isViewer) {
         if (!window.tooltip) {
@@ -129,46 +112,13 @@ function particleSystem(props) {
 
     // 火焰
     function addFlame(position) {
+        ensureClockRunning();
         let particleSystem = new ParticleSystem();
         particleSystem.create(state, position);
         addModel(position, particleSystem);
         selectParticleSystem = particleSystem;
         console.log(particleSystem)
     }
-    //烟花
-    function addFireWork(position) {
-        // 参数场景默认
-        setSceneForFireWork(true);
-        fireWorkSystem = new ParticleSystem();
-        for (let i = 0; i < numberOfFireworks; ++i) {
-            let x = Cesium.Math.randomBetween(xMin, xMax);
-            let y = Cesium.Math.randomBetween(yMin, yMax);
-            let z = Cesium.Math.randomBetween(zMin, zMax);
-            let offset = new Cesium.Cartesian3(x, y, z);
-            let bursts = [];
-            for (let j = 0; j < 3; ++j) {
-                bursts.push(new Cesium.ParticleBurst({
-                    time: Cesium.Math.nextRandomNumber() * 12,
-                    minimum: 400,
-                    maximum: 400
-                }));
-            }
-            let color = Cesium.Color.fromRandom(colorOptions[i % colorOptions.length]);
-            fireWorkSystem.createFirework(offset, color, bursts, position);
-        }
-        right_click_removeEvent()
-    }
-
-    function setSceneForFireWork(isFlage) {
-        viewer.scene.globe.show = isFlage ? false : true;
-        viewer.scene.sun.show = isFlage ? false : true;
-        viewer.scene.globe.enableLighting = isFlage ? false : true;
-        viewer.scene.particlePostRender.quality = isFlage ? 1.0 : 0.25;
-        viewer.scene.bloomEffect.show = !isFlage ? false : true;
-        viewer.scene.bloomEffect.threshold = isFlage ? 0.8 : 0;
-        viewer.scene.bloomEffect.bloomIntensity = isFlage ? 3.6 : 1.34;
-        viewer.scene.skyAtmosphere.show = isFlage ? false : true;
-      }
 
     // 喷射火
     function addFJetFire() {
@@ -184,6 +134,7 @@ function particleSystem(props) {
                 handlerPolyline.deactivate();
                 tooltip.setVisible(false);
                 let positions = res.result.object.positions;
+                ensureClockRunning();
                 particleSystem.create(state, positions[0]);
                 selectParticleSystem = particleSystem;
                 // let heading0 = tool.getAngleAndRadian(positions[0], positions[1]).radian;
@@ -222,6 +173,7 @@ function particleSystem(props) {
                 handlerPolygon.polyline.show = false;
                 handlerPolygon.deactivate();
                 let particleSystem = new ParticleSystem();
+                ensureClockRunning();
                 particleSystem.create(state, undefined, res.result.object.positions);
                 selectParticleSystem = particleSystem;
                 flowingFires.push(particleSystem);
@@ -234,16 +186,39 @@ function particleSystem(props) {
     }
 
 
+    function pickValidPosition(windowPosition) {
+        // 优先 scene.pickPosition(基于深度缓冲,精确到模型表面/地形)
+        let pos = viewer.scene.pickPosition(windowPosition);
+        // 校验有效性:非 undefined、非 (0,0,0)、无 NaN
+        const isValid = (p) => p && isFinite(p.x) && isFinite(p.y) && isFinite(p.z)
+            && !(p.x === 0 && p.y === 0 && p.z === 0);
+        if (isValid(pos)) return pos;
+        // 回退1:globe.pick(ray, scene) —— 取地形交点(SuperMap 定制 Cesium 必须传 scene)
+        const ray = viewer.camera.getPickRay(windowPosition);
+        if (ray) {
+            try { pos = viewer.scene.globe.pick(ray, viewer.scene); } catch (e) { pos = undefined; }
+            if (isValid(pos)) return pos;
+        }
+        // 回退2:camera.pickEllipsoid —— 椭球面交点(地形未加载时也能取点)
+        try { pos = viewer.camera.pickEllipsoid(windowPosition); } catch (e) { pos = undefined; }
+        return isValid(pos) ? pos : undefined;
+    }
+
     function click_addParticle(e) {
         clickCount++
 
         if (isAddParticleSystem) {
-            let centerPosition = viewer.scene.pickPosition(e.message.position);
+            let centerPosition = pickValidPosition(e.message.position);
+            // 位置无效时提示用户并取消,避免 (0,0,0)/NaN 矩阵污染场景导致渲染崩溃
+            if (!centerPosition) {
+                tooltip.setVisible(false);
+                window.tooltip && window.tooltip.showAt(' <p>无法获取该位置的坐标,请在地形或模型上点击</p>', '400px');
+                return;
+            }
             if (state.particleSelectedId === 0)
                 addFlame(centerPosition);
             else if (state.particleSelectedId === 1)
                 addFountain(centerPosition);
-            else { addFireWork(centerPosition) }
         } else {
             let symbol = viewer.scene.pick(e.message.position) || viewer.selectedEntity;
             if (symbol && symbol.id && typeof (symbol.id) === 'string' && symbol.id.indexOf("particle-model-") != -1) {
@@ -271,6 +246,9 @@ function particleSystem(props) {
 
 
     function addParticle() {
+        // requestRenderMode 下 Cesium 原生 ParticleSystem 不会逐帧更新,
+        // 此处注册持续渲染需求,确保粒子动画(火焰/水)正常喷发显示。
+        window.__requestContinuousRender && window.__requestContinuousRender('particle-system');
         viewer.eventManager.addEventListener("CLICK", click_addParticle, true);
         viewer.eventManager.addEventListener("RIGHT_CLICK", right_click_removeEvent);
         if (state.particleSelectedId == 0 && state.flameTyle == '2') {
@@ -295,10 +273,6 @@ function particleSystem(props) {
         viewer._element.style.cursor = "";
         document.body.classList.add("measureCur");
         isAddParticleSystem = true;
-        if(state.particleSelectedId == 2){
-            window.tooltip.showAt(' <p>点击鼠标左键确认烟花中心位置</p>', '400px');
-            return;
-        }
         window.tooltip.showAt(' <p>点击鼠标左键确认位置</p><p>右键单击结束添加</p><p>选中模型进行删除和编辑</p> ', '400px');
 
         // if(state.particleSelectedId === 0)
@@ -363,9 +337,7 @@ function particleSystem(props) {
         // viewer.scene._primitives.remove(s3mInstanceColc);
         // console.log("entityParticlePairs:",entityParticlePairs)
         // console.log("currentSelectedEntity:",currentSelectedEntity)
-        
-        if (fireWorkSystem) { fireWorkSystem.clearFireWoke(); setSceneForFireWork(false);fireWorkSystem = null };
-        
+
         // if (selectParticleSystem) selectParticleSystem.clear();
         // if (currentSelectedEntity) {
         //     entityParticlePairs.delete(currentSelectedEntity.id);
@@ -400,10 +372,15 @@ function particleSystem(props) {
         clearHandlerDrawing('Polygon');
         flowingFires.forEach((ps) => ps.clear());
         flowingFires.length = 0;
+        // 所有粒子已清除,注销持续渲染需求,恢复静态省 GPU 模式
+        window.__cancelContinuousRender && window.__cancelContinuousRender('particle-system');
+        // 恢复 Cesium 时钟到修改前的状态(尊重场景属性面板/时间滑块的设置)
+        restoreClock();
     }
 
     // 水柱
     function addFountain(position) {
+        ensureClockRunning();
         let particleSystem = new ParticleSystem();
         particleSystem.create(state, position);
         addModel(position, particleSystem);

+ 1 - 1
RuoYi-Vue3/src/supermap-cesium-module/components/special-effects/particle-system/particle-system.vue

@@ -30,7 +30,7 @@
           <option value="1">{{Resource.fireWater}}</option>
         </select>
       </div>
-      <div class="sm-half-L flex-between" v-show="particleSelectedId !== 2">
+      <div class="sm-half-L flex-between">
         <label style="width:auto">
           <input type="checkbox" v-model="visibleModel" />
           {{Resource.isEdit}}

+ 5 - 5
RuoYi-Vue3/src/supermap-cesium-module/components/special-effects/scan-effect/textures.js

@@ -2,26 +2,26 @@ export default [
     {
         name: '线状纹理1',
         type: 'line',
-        url: 'public/img/particleSystem/line-1.jpg'
+        url: '/img/particleSystem/line-1.jpg'
     },
     {
         name: '线状纹理2',
         type: 'line',
-        url: 'public/img/particleSystem/line-2.jpg'
+        url: '/img/particleSystem/line-2.jpg'
     },
     {
         name: '环状纹理1',
         type: 'ring',
-        url: 'public/img/particleSystem/ring-1.jpg'
+        url: '/img/particleSystem/ring-1.jpg'
     },
     {
         name: '环状纹理2',
         type: 'ring',
-        url: 'public/img/particleSystem/ring-2.jpg'
+        url: '/img/particleSystem/ring-2.jpg'
     },
     {
         name: '环状六边形纹理',
         type: 'ring',
-        url: 'public/img/particleSystem/ring-3.jpg'
+        url: '/img/particleSystem/ring-3.jpg'
     },
 ]

+ 6 - 5
RuoYi-Vue3/src/supermap-cesium-module/components/viewer/viewer.js

@@ -6,6 +6,7 @@ import { onBeforeUnmount } from 'vue'
 import createTooltip from '../../js/tool/tooltip2.js'
 import resource from '../../js/local/lang.js'  //语言资源
 import { addSimpleClouds } from '@/cesium-clouds-atmosphere/simpleClouds.js'
+import { PROXY_TIANDITU_TILE, TIANDITU_KEY } from '../../config/server_config.js'
 
 // ── 硬件性能检测:综合 CPU 核心、内存、GPU 型号分 high/medium/low 三档 ──
 function detectPerformanceTier() {
@@ -295,11 +296,11 @@ function initViewer(props, callback) {
   });
 
   // 添加天地图影像底图和注记
-  const tiandituKey = '3fb1e9fda20ee995dc815c8243553ce8';
-  
-  // 添加天地图影像底图(使用HTTPS)
+  // tk 来自外部配置 supermap.config.js,支持打包后修改
+
+  // 添加天地图影像底图(使用HTTPS)—— 通过公司服务器 Nginx 反代,避免客户网络访问受限
   const imageryProvider = new Cesium.WebMapTileServiceImageryProvider({
-    url: `https://t0.tianditu.gov.cn/img_c/wmts?service=WMTS&request=GetTile&version=1.0.0&LAYER=img&tileMatrixSet=c&TileMatrix={TileMatrix}&TileRow={TileRow}&TileCol={TileCol}&style=default&format=tiles&tk=${tiandituKey}`,
+    url: `${PROXY_TIANDITU_TILE}/img_c/wmts?service=WMTS&request=GetTile&version=1.0.0&LAYER=img&tileMatrixSet=c&TileMatrix={TileMatrix}&TileRow={TileRow}&TileCol={TileCol}&style=default&format=tiles&tk=${TIANDITU_KEY}`,
     layer: 'img',
     style: 'default',
     format: 'image/jpeg',
@@ -313,7 +314,7 @@ function initViewer(props, callback) {
   
   // 添加天地图影像注记(使用HTTPS)
   const labelProvider = new Cesium.WebMapTileServiceImageryProvider({
-    url: `https://t0.tianditu.gov.cn/cia_c/wmts?service=WMTS&request=GetTile&version=1.0.0&LAYER=cia&tileMatrixSet=c&TileMatrix={TileMatrix}&TileRow={TileRow}&TileCol={TileCol}&style=default&format=tiles&tk=${tiandituKey}`,
+    url: `${PROXY_TIANDITU_TILE}/cia_c/wmts?service=WMTS&request=GetTile&version=1.0.0&LAYER=cia&tileMatrixSet=c&TileMatrix={TileMatrix}&TileRow={TileRow}&TileCol={TileCol}&style=default&format=tiles&tk=${TIANDITU_KEY}`,
     layer: 'cia',
     style: 'default',
     format: 'image/png',

+ 23 - 7
RuoYi-Vue3/src/supermap-cesium-module/components/wind-field-visualization/wind-field-visualization.vue

@@ -40,11 +40,12 @@
 <script setup>
 import { ref, watch, nextTick, onMounted, onUnmounted } from 'vue'
 import WindField from './windField.js'
+import { PROXY_SLT } from '../../config/server_config.js'
 
 const props = defineProps({
   viewer: { type: Object, default: null },
   visible: { type: Boolean, default: true },
-  dataUrl: { type: String, default: 'https://typhoon.slt.zj.gov.cn/Api/LastWind' }
+  dataUrl: { type: String, default: `${PROXY_SLT}/Api/LastWind` }
 })
 
 const loading = ref(false)
@@ -98,7 +99,9 @@ const init = async () => {
   try {
     windField = new WindField(v, {
       region: { lonMin: 0, latMin: -90, lonMax: 360, latMax: 90 },
-      particleSize: 400,
+      // 粒子网格边长:400→256,粒子数 16万→65536(项目约定下限),顶点数 1056万→432万/帧(减 59%)
+      // 全球视角下 65536 粒子仍足够密集;如需更密可调到 288/320,但性能开销随之上升
+      particleSize: 256,
       timeStep: 0.01,
       trailStep: 0.15,
       maxAge: 500,
@@ -127,14 +130,27 @@ const init = async () => {
       if (!sample) { windInfo.value.visible = false; return }
 
       // 更新点击标记点
+      // 使用 billboard 代替 point:point 在 disableDepthTestDistance=Infinity +
+      // 远距离/全球视角下 pixelSize 渲染会异常放大(Cesium 着色器路径切换的已知问题);
+      // billboard 的 width/height 严格按屏幕像素,行为可预测,且支持穿透地形显示
       if (clickMarker) { try { v.entities.remove(clickMarker) } catch {} }
+      if (!window.__windMarkerIcon) {
+        const c = document.createElement('canvas')
+        c.width = 32; c.height = 32
+        const cx = c.getContext('2d')
+        cx.strokeStyle = '#FF0000'
+        cx.lineWidth = 3
+        cx.beginPath(); cx.arc(16, 16, 13, 0, Math.PI * 2); cx.stroke()
+        cx.fillStyle = '#FFFF00'
+        cx.beginPath(); cx.arc(16, 16, 9, 0, Math.PI * 2); cx.fill()
+        window.__windMarkerIcon = c.toDataURL()
+      }
       clickMarker = v.entities.add({
         position: cartesian,
-        point: {
-          pixelSize: 12,
-          color: Cesium.Color.YELLOW,
-          outlineColor: Cesium.Color.RED,
-          outlineWidth: 2,
+        billboard: {
+          image: window.__windMarkerIcon,
+          width: 16,
+          height: 16,
           disableDepthTestDistance: Number.POSITIVE_INFINITY,
           heightReference: Cesium.HeightReference.CLAMP_TO_GROUND
         }

+ 192 - 165
RuoYi-Vue3/src/supermap-cesium-module/components/wind-field-visualization/windField.js

@@ -17,7 +17,28 @@ import axios from 'axios'
 const Cesium = (typeof window !== 'undefined' && window.Cesium) ? window.Cesium : CesiumNS;
 
 // ============================================================
-// 风场纹理双三次插值(Catmull-Rom)—— 供色阶片元着色器使用
+// 风速色阶(CPU 光栅化用)—— 与原片元着色器 speedColor 完全一致
+// 静态色阶层在加载数据时一次性 CPU 光栅化为 PNG,叠加为
+// SingleTileImageryProvider 静态影像,运行时 GPU 0 持续消耗。
+// ============================================================
+const SPEED_STOPS = [
+  [0.05, 0.1, 0.6],
+  [0.0, 0.7, 1.0],
+  [0.2, 1.0, 0.4],
+  [1.0, 0.9, 0.1],
+  [1.0, 0.3, 0.05]
+];
+function speedColorRGB(t) {
+  if (t <= 0) return SPEED_STOPS[0];
+  if (t >= 1) return SPEED_STOPS[4];
+  const i = Math.min(Math.floor(t / 0.25), 3);
+  const f = (t - i * 0.25) / 0.25;
+  const a = SPEED_STOPS[i], b = SPEED_STOPS[i + 1];
+  return [a[0] + (b[0] - a[0]) * f, a[1] + (b[1] - a[1]) * f, a[2] + (b[2] - a[2]) * f];
+}
+
+// ============================================================
+// 风场纹理双三次插值(Catmull-Rom)—— 供粒子顶点着色器使用
 // ============================================================
 const WIND_BICUBIC_GLSL = `
 uniform vec2 u_windTexSize;
@@ -282,65 +303,6 @@ void main() {
 }
 `;
 
-// ============================================================
-// 底层风速底色着色器(细分网格覆盖 region,片元着色器按像素采样风场纹理)
-// GLSL ES 1.00
-// ============================================================
-const COLOR_VS = `
-attribute vec2 a_uv;
-uniform float u_height;
-uniform vec4 u_region;
-varying vec2 v_uv;
-
-vec3 lonLatToECEF(vec2 ll, float h) {
-  float lon = radians(ll.x);
-  float lat = radians(ll.y);
-  float cosLat = cos(lat);
-  float sinLat = sin(lat);
-  float a = 6378137.0;
-  float f = 1.0 / 298.257223563;
-  float e2 = f * (2.0 - f);
-  float N = a / sqrt(1.0 - e2 * sinLat * sinLat);
-  return vec3((N + h) * cosLat * cos(lon), (N + h) * cosLat * sin(lon), (N * (1.0 - e2) + h) * sinLat);
-}
-
-void main() {
-  v_uv = a_uv;
-  float lon = mix(u_region.x, u_region.z, a_uv.x);
-  float lat = mix(u_region.y, u_region.w, a_uv.y);
-  gl_Position = czm_viewProjection * vec4(lonLatToECEF(vec2(lon, lat), u_height), 1.0);
-}
-`;
-
-const COLOR_FS = `
-uniform sampler2D u_wind;
-uniform float u_alpha;
-uniform vec4 u_region;
-varying vec2 v_uv;
-
-vec3 speedColor(float t) {
-  vec3 c0 = vec3(0.05, 0.1, 0.6);
-  vec3 c1 = vec3(0.0, 0.7, 1.0);
-  vec3 c2 = vec3(0.2, 1.0, 0.4);
-  vec3 c3 = vec3(1.0, 0.9, 0.1);
-  vec3 c4 = vec3(1.0, 0.3, 0.05);
-  if (t < 0.25) return mix(c0, c1, t / 0.25);
-  if (t < 0.5) return mix(c1, c2, (t - 0.25) / 0.25);
-  if (t < 0.75) return mix(c2, c3, (t - 0.5) / 0.25);
-  return mix(c3, c4, (t - 0.75) / 0.25);
-}
-
-void main() {
-  float lon = mix(u_region.x, u_region.z, v_uv.x);
-  float lat = mix(u_region.y, u_region.w, v_uv.y);
-  float s = fract(lon / 360.0);
-  float t = (90.0 - lat) / 180.0;
-  vec2 wind = sampleWindBicubic(u_wind, vec2(s, t));
-  float tSpeed = clamp(length(wind), 0.0, 1.0);
-  gl_FragColor = vec4(speedColor(tSpeed), u_alpha);
-}
-`;
-
 /**
  * 风场可视化类
  */
@@ -378,8 +340,10 @@ export default class WindField {
     // 渲染资源
     this._updateCmd = null;
     this._particleCmd = null;
-    this._colorCmd = null;
     this._primitive = null;
+    // 静态色阶影像(SingleTileImageryProvider + Blob URL)
+    this._colorImageryLayer = null;
+    this._colorBlobUrl = null;
     this._randomSeed = 0;
     this._frameCount = 0;
     this._forceRespawn = false;
@@ -397,10 +361,31 @@ export default class WindField {
       throw new Error('当前环境不支持渲染到 FLOAT 纹理 (EXT_color_buffer_float)');
     }
 
-    const resp = await axios.get(url);
+    const resp = await axios.get(url, {
+      // 显式解析 JSON:axios 默认 transformResponse 在 JSON.parse 失败时会
+      // 静默返回原字符串,导致后续 json.windData 为 undefined 而误报"缺少 windData",
+      // 掩盖真实原因(如代理未命中返回 HTML、响应被截断、502 错误页等)。
+      // 注意:transformResponse 在 axios.get 执行期间被调用,此时外层 const resp 尚未初始化,
+      // 闭包内不能引用 resp(会触发 TDZ "Cannot access resp before initialization"),
+      // HTTP 状态码通过 transformResponse 的第三个参数 status 获取(axios v1.x 支持)。
+      transformResponse: [(data, headers, status) => {
+        if (typeof data !== 'string') return data;
+        try {
+          return JSON.parse(data);
+        } catch (e) {
+          const preview = data.length > 200
+            ? data.slice(0, 200) + '...(truncated, total ' + data.length + ' bytes)'
+            : data;
+          throw new Error(`风场响应不是合法 JSON (解析失败: ${e.message}),HTTP=${status},原始内容前 200 字符: ${preview}`);
+        }
+      }]
+    });
     const json = resp.data;
     const windDataStr = json && json.windData;
-    if (!windDataStr) throw new Error('风场数据格式错误: 缺少 windData');
+    if (!windDataStr) {
+      const keys = (json && typeof json === 'object') ? Object.keys(json).join(',') : `(type=${typeof json})`;
+      throw new Error(`风场数据格式错误: 缺少 windData 字段,HTTP=${resp.status},响应实际 keys=[${keys}]`);
+    }
     if (this._destroyed) return this;
 
     const records = JSON.parse(windDataStr);
@@ -416,6 +401,11 @@ export default class WindField {
     this._createWindTexture();
     this._createParticles();
     this._createPrimitive();
+    // 静态色阶影像(CPU 一次性光栅化 → SingleTileImageryProvider)
+    // 异步生成 PNG Blob,不阻塞粒子动画启动;销毁时如果在生成中会自动丢弃
+    this._createColorImageryLayer().catch((e) => {
+      console.warn('[WindField] 静态色阶影像生成失败(粒子动画不受影响):', e);
+    });
     console.warn('[WindField] init done, particles=', this.options.particleSize ** 2);
     return this;
   }
@@ -581,62 +571,135 @@ export default class WindField {
       ],
       indexBuffer: this._particleIB
     });
-
-    // 底色网格几何
-    this._createColorGeometry();
   }
 
-  _createColorGeometry() {
-    const N = 256;
-    const vertsPerSide = N + 1;
-    const vertexData = new Float32Array(vertsPerSide * vertsPerSide * 2);
-    for (let j = 0; j < vertsPerSide; j++) {
-      for (let i = 0; i < vertsPerSide; i++) {
-        const idx = j * vertsPerSide + i;
-        vertexData[idx * 2] = i / N;
-        vertexData[idx * 2 + 1] = j / N;
-      }
+  // ---------- 静态色阶影像(CPU 一次性光栅化 → SingleTileImageryProvider)----------
+  // 替代原色阶 DrawCommand,运行时 GPU 0 持续消耗(像降雨一样静态叠加)
+  // 原理:加载数据时一次性把 U/V 数据光栅化为 PNG(与降雨等值面同思路),
+  //       叠加为 SingleTileImageryProvider 静态影像,由 Cesium 按 LOD 自动重采样,
+  //       静态场景下不占用任何 GPU 帧渲染时间
+  //
+  // 关键:CPU 端用 Catmull-Rom 双三次插值(与原片元着色器 sampleWindBicubic 等价)
+  // 在【数据→颜色映射前】对 U/V 做插值,输出 4 倍分辨率 canvas,消除双线性在
+  // 数据点附近产生的色彩"平台"导致网格感明显的问题。
+  async _createColorImageryLayer() {
+    if (this._colorImageryLayer || this._destroyed) return;
+
+    const h = this._header;
+    const nx = h.nx, ny = h.ny;
+    const lo1 = h.lo1;
+    const la1 = h.la1;
+    const dx = h.dx || (nx > 1 ? 360 / nx : 1);
+    const dy = h.dy || (ny > 1 ? (la1 > 0 ? 180 / ny : 1) : 1);
+    const la2 = h.la2 !== undefined ? h.la2 : la1 - (ny - 1) * dy;
+
+    // 经度重排:让 canvas x=0 对应 lon=-180(使矩形 west=-180,east=180 一次性覆盖全球,
+    // 避免跨越 ±180 日界线导致 SingleTileImageryProvider 渲染异常)
+    // offset = (180 + lo1) / dx (通常 lo1=0 → offset = nx/2)
+    const xOffset = ((Math.round((180 + lo1) / dx) % nx) + nx) % nx;
+
+    // 4 倍分辨率输出:在数据空间 bicubic 插值后再映射颜色,过渡平滑无平台
+    // 注:cpu 计算量约 16 × nx × ny × SCALE²(4×4 邻域),加载数据时一次性,
+    //     1440×720 数据约 2.5 亿次浮点运算,单核 1-2 秒可完成
+    const SCALE = 4;
+    const outW = nx * SCALE;
+    const outH = ny * SCALE;
+    const canvas = document.createElement('canvas');
+    canvas.width = outW;
+    canvas.height = outH;
+    const ctx = canvas.getContext('2d');
+    const imgData = ctx.createImageData(outW, outH);
+    const data = imgData.data;
+    const u = this._u, v = this._v;
+    const maxSpeed = this._maxSpeed || 1;
+    const alphaByte = Math.round((this.options.colorLayerAlpha ?? 0.42) * 255);
+
+    // Catmull-Rom 权重(与 WIND_BICUBIC_GLSL 中 catmullRom 完全一致)
+    const catmullRom = (t) => {
+      const t2 = t * t, t3 = t2 * t;
+      return [
+        -0.5 * t3 + t2 - 0.5 * t,
+        1.5 * t3 - 2.5 * t2 + 1.0,
+        -1.5 * t3 + 2.0 * t2 + 0.5 * t,
+        0.5 * t3 - 0.5 * t2
+      ];
+    };
+
+    // 行预计算:每行的 4 个 iy 邻域索引 + wy 权重(避免内层重复计算)
+    const rowInfo = new Array(outH);
+    for (let y = 0; y < outH; y++) {
+      // 输出 y → 数据 fy = y / SCALE - 0.5(与片元着色器 coord = uv*size - 0.5 一致)
+      const fy = y / SCALE - 0.5;
+      const iy = Math.floor(fy);
+      const ty = fy - iy;
+      const wy = catmullRom(ty);
+      // 纬度方向 clamp(不环绕,南北界外重复边界数据点)
+      const sy = [
+        Math.max(0, Math.min(ny - 1, iy - 1)),
+        Math.max(0, Math.min(ny - 1, iy)),
+        Math.max(0, Math.min(ny - 1, iy + 1)),
+        Math.max(0, Math.min(ny - 1, iy + 2))
+      ];
+      rowInfo[y] = { sy, wy };
     }
-    const indexData = new Uint32Array(N * N * 6);
-    for (let j = 0; j < N; j++) {
-      for (let i = 0; i < N; i++) {
-        const v0 = j * vertsPerSide + i;
-        const v1 = v0 + 1;
-        const v2 = v0 + vertsPerSide;
-        const v3 = v2 + 1;
-        const o = (j * N + i) * 6;
-        indexData[o] = v0;
-        indexData[o + 1] = v1;
-        indexData[o + 2] = v3;
-        indexData[o + 3] = v0;
-        indexData[o + 4] = v3;
-        indexData[o + 5] = v2;
+
+    for (let y = 0; y < outH; y++) {
+      const { sy, wy } = rowInfo[y];
+      for (let x = 0; x < outW; x++) {
+        const fx = x / SCALE - 0.5;
+        const ix = Math.floor(fx);
+        const tx = fx - ix;
+        const wx = catmullRom(tx);
+
+        let sumU = 0, sumV = 0;
+        for (let dj = 0; dj < 4; dj++) {
+          const wyv = wy[dj];
+          if (wyv === 0) continue;
+          const rowBase = sy[dj] * nx;
+          for (let di = 0; di < 4; di++) {
+            const wxv = wx[di];
+            if (wxv === 0) continue;
+            // 经度环绕(全球数据),并应用 xOffset 重排
+            let sxRaw = (ix + di - 1) % nx;
+            if (sxRaw < 0) sxRaw += nx;
+            const sx = (sxRaw + xOffset) % nx;
+            const w = wxv * wyv;
+            sumU += u[rowBase + sx] * w;
+            sumV += v[rowBase + sx] * w;
+          }
+        }
+
+        const speed = Math.sqrt(sumU * sumU + sumV * sumV);
+        const t = Math.min(1, Math.max(0, speed / maxSpeed));
+        const c = speedColorRGB(t);
+        const p = (y * outW + x) * 4;
+        data[p] = Math.round(c[0] * 255);
+        data[p + 1] = Math.round(c[1] * 255);
+        data[p + 2] = Math.round(c[2] * 255);
+        data[p + 3] = alphaByte;
       }
     }
-    this._colorVB = Cesium.Buffer.createVertexBuffer({
-      context: this.context,
-      typedArray: vertexData,
-      usage: Cesium.BufferUsage.STATIC_DRAW
-    });
-    this._colorIB = Cesium.Buffer.createIndexBuffer({
-      context: this.context,
-      typedArray: indexData,
-      usage: Cesium.BufferUsage.STATIC_DRAW,
-      indexDatatype: Cesium.IndexDatatype.UNSIGNED_INT
-    });
-    this._colorVA = new Cesium.VertexArray({
-      context: this.context,
-      attributes: [{
-        index: 0,
-        vertexBuffer: this._colorVB,
-        componentsPerAttribute: 2,
-        componentDatatype: Cesium.ComponentDatatype.FLOAT,
-        normalize: false,
-        offsetInBytes: 0,
-        strideInBytes: 8
-      }],
-      indexBuffer: this._colorIB
+    ctx.putImageData(imgData, 0, 0);
+
+    // Blob URL → SingleTileImageryProvider 静态叠加(参考云图大 base64 处理思路)
+    const blob = await new Promise((resolve) => canvas.toBlob(resolve, 'image/png'));
+    if (this._destroyed) {
+      // 销毁后异步回调,直接丢弃
+      return;
+    }
+    this._colorBlobUrl = URL.createObjectURL(blob);
+
+    const provider = new Cesium.SingleTileImageryProvider({
+      url: this._colorBlobUrl,
+      rectangle: Cesium.Rectangle.fromDegrees(-180, la2, 180, la1),
+      credit: 'WindField'
     });
+    this._colorImageryLayer = this.viewer.imageryLayers.addImageryProvider(provider);
+    if (this._colorImageryLayer) {
+      this._colorImageryLayer.show = !!this.options.colorLayerVisible;
+      // 置顶,确保色阶在底图之上(粒子是 primitive 独立通道,不受 imageryLayer 顺序影响)
+      try { this.viewer.imageryLayers.raiseToTop(this._colorImageryLayer); } catch (e) {}
+    }
   }
 
   // ---------- 着色器与命令 ----------
@@ -664,14 +727,6 @@ export default class WindField {
       throw new Error('粒子着色器编译失败: ' + (e.message || e));
     }
 
-    // 底色着色器
-    const colorProgram = Cesium.ShaderProgram.fromCache({
-      context: this.context,
-      vertexShaderSource: COLOR_VS,
-      fragmentShaderSource: WIND_BICUBIC_GLSL + COLOR_FS,
-      attributeLocations: { a_uv: 0 }
-    });
-
     // 区域包围球(避免被视锥裁剪)
     const centerLon = (lonMin + lonMax) * 0.5;
     const centerLat = (latMin + latMax) * 0.5;
@@ -708,35 +763,7 @@ export default class WindField {
       owner: this
     });
 
-    // 底色绘制命令(关闭深度测试使色阶不被地形遮挡,背面剔除防止穿透地球)
-    const colorRenderState = Cesium.RenderState.fromCache({
-      blending: {
-        enabled: true,
-        equationRgb: Cesium.BlendEquation.ADD,
-        equationAlpha: Cesium.BlendEquation.ADD,
-        functionSourceRgb: Cesium.BlendFunction.SOURCE_ALPHA,
-        functionSourceAlpha: Cesium.BlendFunction.ONE,
-        functionDestinationRgb: Cesium.BlendFunction.ONE_MINUS_SOURCE_ALPHA,
-        functionDestinationAlpha: Cesium.BlendFunction.ONE_MINUS_SOURCE_ALPHA
-      },
-      depthTest: { enabled: false },
-      depthMask: false,
-      cull: { enabled: true, face: Cesium.CullFace.BACK }
-    });
-    this._colorCmd = new Cesium.DrawCommand({
-      primitiveType: Cesium.PrimitiveType.TRIANGLES,
-      vertexArray: this._colorVA,
-      shaderProgram: colorProgram,
-      renderState: colorRenderState,
-      pass: Cesium.Pass.TRANSLUCENT,
-      cull: false,
-      occlude: false,
-      boundingVolume: regionBS,
-      owner: this
-    });
-
     this._particleProgram = particleProgram;
-    this._colorProgram = colorProgram;
 
     const self = this;
     this._primitive = {
@@ -802,17 +829,9 @@ export default class WindField {
     };
     frameState.commandList.push(this._particleCmd);
 
-    // ---- 底色 DrawCommand ----
-    if (this.options.colorLayerVisible) {
-      this._colorCmd.uniformMap = {
-        u_wind: () => this._windTexture,
-        u_windTexSize: () => this._windTexSize,
-        u_alpha: () => this.options.colorLayerAlpha,
-        u_region: () => regionVec4,
-        u_height: () => 3000
-      };
-      frameState.commandList.push(this._colorCmd);
-    }
+    // 色阶已改为静态 SingleTileImageryProvider(CPU 一次性光栅化为 PNG),
+    // 不再每帧 push colorCmd,由 Cesium imagery 内部按 LOD 重采样渲染,
+    // 静态场景下 GPU 0 持续消耗(与降雨等值面同思路)
 
     // 首帧诊断
     if (!this._diagLogged) {
@@ -831,6 +850,9 @@ export default class WindField {
 
   setColorLayerVisible(visible) {
     this.options.colorLayerVisible = !!visible;
+    if (this._colorImageryLayer) {
+      this._colorImageryLayer.show = !!visible;
+    }
   }
 
   isDestroyed() {
@@ -852,15 +874,20 @@ export default class WindField {
     destroy(this._particleVB);
     destroy(this._particleIB);
     destroy(this._particleVA);
-    destroy(this._colorVB);
-    destroy(this._colorIB);
-    destroy(this._colorVA);
     destroy(this._particleProgram);
-    destroy(this._colorProgram);
+
+    // 清理静态色阶影像图层 + 释放 Blob URL
+    if (this._colorImageryLayer && this.viewer && this.viewer.imageryLayers) {
+      try { this.viewer.imageryLayers.remove(this._colorImageryLayer); } catch (e) {}
+      this._colorImageryLayer = null;
+    }
+    if (this._colorBlobUrl) {
+      try { URL.revokeObjectURL(this._colorBlobUrl); } catch (e) {}
+      this._colorBlobUrl = null;
+    }
 
     this._updateCmd = null;
     this._particleCmd = null;
-    this._colorCmd = null;
     this._primitive = null;
   }
 

+ 16 - 0
RuoYi-Vue3/src/supermap-cesium-module/config/server_config.js

@@ -19,6 +19,22 @@ console.log('超图服务配置:', {
   baseUrl: SUPERMAP_BASE_URL
 })
 
+// 外部 API 反向代理路径前缀(部署在公司服务器 Nginx 反代到外网)
+// 客户浏览器只访问公司服务器一个域名,避免跨域 + 天地图 tk Referer 白名单 + 客户出口防火墙三重问题
+// 代理规则(Nginx 配置见部署文档):
+//   /proxy/tianditu     → https://t0.tianditu.gov.cn   (天地图瓦片 img_c / cia_c)
+//   /proxy/tianditu-api → https://api.tianditu.gov.cn  (天地图地理编码 geocoder)
+//   /proxy/slt          → https://typhoon.slt.zj.gov.cn (浙江水情台风 API)
+// 使用相对路径,部署到任何公网穿透域名都能自动走当前域名,无需重新打包
+export const PROXY_TIANDITU_TILE = '/proxy/tianditu'
+export const PROXY_TIANDITU_API = '/proxy/tianditu-api'
+export const PROXY_SLT = '/proxy/slt'
+
+// 天地图 token:支持打包后外部修改(无需重新打包)
+// 优先级:dist/supermap.config.js 中的 tiandituKey > .env 环境变量 > 默认值
+// 部署后修改:直接编辑 dist/supermap.config.js 的 tiandituKey 字段,浏览器刷新即生效
+export const TIANDITU_KEY = externalConfig.tiandituKey || import.meta.env.VITE_TIANDITU_KEY || '3fb1e9fda20ee995dc815c8243553ce8'
+
 export default [
   {
     id: "vectorData",

+ 444 - 0
RuoYi-Vue3/src/supermap-cesium-module/js/common/clip-model.js

@@ -0,0 +1,444 @@
+/**
+ * glTF Model 裁剪公共工具
+ *
+ * 背景:裁剪分析(clip-plane/clip-box/clip-cross/clip-polygon)原生只支持
+ *   超图 S3M 图层 (scene.layers.layerQueue + layer.setCustomClipPlane/setCustomClipBox)。
+ *   通过 viewer.entities.add({model:{uri}}) 加载的 glTF 模型属于 Cesium
+ *   原生 Model primitive,不在 layerQueue 中,无法被裁剪。
+ *
+ * 本工具用 Cesium 原生 ClippingPlaneCollection 给 glTF Model 添加裁剪能力,
+ * 与 S3M 图层的裁剪逻辑并存:先按原逻辑裁剪 S3M 图层,再额外裁剪 glTF Model。
+ *
+ * Model primitive 访问路径(已验证):
+ *   - viewer.entities.values[*].model._content._model  (EntityGraphics 加载完成后的内部 Model)
+ *   - viewer.scene.primitives.get(i) 中 isinstanceof Cesium.Model 的对象
+ *
+ * ClippingPlaneCollection 说明:
+ *   - 平面裁剪(clip-plane):用三点确定一个平面,生成单个 ClippingPlane
+ *   - Box 裁剪(clip-box/clip-cross):用 6 个 ClippingPlane 围成正方体
+ *   - 多边形裁剪(clip-polygon):原生 Model 不支持多边形裁剪,跳过并提示
+ */
+
+import { storeState } from '../store/store.js'
+
+// 模块加载标记:用 alert 强制弹窗,确保一定能看到(避免 console 被过滤)
+console.log('[clip-model] ✅ clip-model.js 模块已加载')
+try {
+    window.__clipModelLoaded = true
+    window.__clipModel = {
+        // 简单诊断:返回场景中所有 entity 信息
+        diagnose: () => {
+            const v = window.viewer
+            if (!v) return 'viewer 不存在'
+            const ents = v.entities.values
+            const result = ['entities 数量: ' + ents.length]
+            for (let i = 0; i < ents.length; i++) {
+                const e = ents[i]
+                result.push('[' + i + '] name=' + (e.name || '') + ' hasModel=' + (!!e.model))
+                if (e.model) {
+                    result.push('    model._content: ' + (!!e.model._content))
+                    result.push('    model._content._model: ' + (e.model._content && !!e.model._content._model))
+                    const inner = e.model._content && e.model._content._model
+                    if (inner) {
+                        result.push('    innerModel 类型: ' + (inner.constructor && inner.constructor.name))
+                        result.push('    innerModel 有 clippingPlanes: ' + ('clippingPlanes' in inner))
+                        // 赋值测试
+                        try {
+                            const Cesium = window.Cesium
+                            const planes = new Cesium.ClippingPlaneCollection({
+                                planes: [new Cesium.ClippingPlane(new Cesium.Cartesian3(0, 0, 1), 0.0)]
+                            })
+                            inner.clippingPlanes = planes
+                            result.push('    ✅ 赋值成功! 读取: ' + (inner.clippingPlanes ? 'ClippingPlaneCollection' : 'undefined'))
+                            v.scene.requestRender()
+                            // 3 秒后还原
+                            setTimeout(() => {
+                                inner.clippingPlanes = undefined
+                                v.scene.requestRender()
+                                console.log('[clip-model] 诊断: 已还原')
+                            }, 3000)
+                        } catch(e) {
+                            result.push('    ❌ 赋值失败: ' + e.message)
+                        }
+                    }
+                }
+            }
+            // 也看 primitives
+            const primes = v.scene.primitives
+            result.push('primitives 数量: ' + primes.length)
+            for (let j = 0; j < primes.length; j++) {
+                const p = primes.get(j)
+                result.push('  [' + j + '] type=' + (p && p.constructor && p.constructor.name))
+                result.push('      有clippingPlanes: ' + (p && ('clippingPlanes' in p)))
+            }
+            return result.join('\n')
+        }
+    }
+    console.log('[clip-model] window.__clipModel 已挂载,可在控制台调用 window.__clipModel.diagnose()')
+} catch(e) {
+    console.error('[clip-model] 初始化失败:', e)
+}
+
+/**
+ * 查找场景中所有 glTF Model primitive 实例
+ *
+ * 实现说明:
+ *   - Entity 加载的 glTF (viewer.entities.add({model:{uri}})) 内部为 Cesium.Model primitive
+ *     访问路径(已验证):entity.model._content._model
+ *   - Model.fromGltf 直接加载的 Model 直接在 scene.primitives 中
+ *   - EntityGraphics.ModelGraphics 本身没有渲染层 clippingPlanes 支持,必须赋值给内部 Model primitive
+ *
+ * @param {Cesium.Viewer} viewer
+ * @returns {Array<Object>} 支持 clippingPlanes 字段的 Model primitive 数组
+ */
+export function findModelPrimitives(viewer) {
+    const Cesium = window.Cesium
+    const result = []
+    const debug = []
+
+    // 路径1:Entity 加载的 glTF → 取内部 Model primitive
+    try {
+        const ents = viewer.entities.values
+        debug.push('entities.length: ' + ents.length)
+        for (let i = 0; i < ents.length; i++) {
+            const e = ents[i]
+            if (!e.model) continue
+            debug.push('  entity[' + i + '] name=' + (e.name || '') + ' hasModel=true')
+            // 列出 entity.model 的所有内部字段
+            const m = e.model
+            const mKeys = []
+            for (const k in m) {
+                if (typeof m[k] !== 'function') mKeys.push(k + ':' + (m[k] === null ? 'null' : typeof m[k]))
+            }
+            debug.push('    entity.model keys: ' + mKeys.join(','))
+            // 尝试所有可能的路径拿到内部 Model primitive
+            const candidates = [
+                m._content && m._content._model,
+                m._primitive,
+                m._content,
+                m._model,
+                m  // 最后兜底:直接用 entity.model 本身(SuperMap 可能转发)
+            ]
+            for (const c of candidates) {
+                if (!c || typeof c !== 'object') continue
+                if (result.indexOf(c) !== -1) continue
+                const info = {
+                    type: c.constructor && c.constructor.name,
+                    hasClippingPlanes: 'clippingPlanes' in c,
+                    isCesiumModel: !!(Cesium.Model && c instanceof Cesium.Model),
+                    hasGltf: c._gltf !== undefined,
+                    hasResource: c._resource !== undefined,
+                    hasUrl: c._url !== undefined,
+                    hasReadyPromise: typeof c.readyPromise === 'object'
+                }
+                debug.push('    候选: type=' + info.type + ' hasClippingPlanes=' + info.hasClippingPlanes + ' isCesiumModel=' + info.isCesiumModel + ' hasGltf=' + info.hasGltf)
+                // 接受所有"看起来像 Model"的对象
+                if (info.hasClippingPlanes || info.isCesiumModel || info.hasGltf || info.hasResource || info.hasUrl) {
+                    result.push(c)
+                    debug.push('    ✅ 加入结果')
+                }
+            }
+        }
+    } catch (e) {
+        debug.push('从 entities 查找错误: ' + e.message)
+        console.warn('[clip-model] 从 entities 查找 Model 失败:', e.message)
+    }
+
+    // 路径2:直接遍历 primitives 找 Cesium.Model 实例
+    try {
+        const primes = viewer.scene.primitives
+        debug.push('primitives.length: ' + primes.length)
+        for (let i = 0; i < primes.length; i++) {
+            const p = primes.get(i)
+            if (!p) continue
+            const tn = (p.constructor && p.constructor.name) || 'unknown'
+            const hasClipping = 'clippingPlanes' in p
+            const isModel = !!(Cesium.Model && p instanceof Cesium.Model)
+            // 放宽:有 clippingPlanes 字段 + (是 Model 实例 OR 有模型相关字段 OR 类型名不是已知非 Model)
+            const hasModelField = (p._gltf !== undefined || p._resource !== undefined || p._url !== undefined || p._model !== undefined)
+            const nonModelTypes = ['GroundPrimitive', 'ClassificationPrimitive', 'Batched3DModel3DTileset',
+                'Model3DTileset', 'PointCloud3DTileset', 'ImageryLayer', 'Cesium3DTileset',
+                'GlobeSurfaceTileProvider', 'TileCoordinatesImageryProvider', 'GridImageryProvider']
+            const notNonModel = nonModelTypes.indexOf(tn) === -1
+            if (isModel || (hasClipping && (hasModelField || notNonModel))) {
+                if (result.indexOf(p) === -1) {
+                    result.push(p)
+                    debug.push('  primitive[' + i + '] ' + tn + ' ✅ 加入结果')
+                }
+            } else {
+                debug.push('  primitive[' + i + '] ' + tn + ' hasClipping=' + hasClipping + ' 跳过')
+            }
+        }
+    } catch (e) {
+        debug.push('从 primitives 查找错误: ' + e.message)
+        console.warn('[clip-model] 从 primitives 查找 Model 失败:', e.message)
+    }
+
+    console.log('[clip-model] findModelPrimitives 诊断:\n' + debug.join('\n') + '\n找到 ' + result.length + ' 个 Model')
+    return result
+}
+
+/**
+ * 测试 ClippingPlaneCollection 是否可用
+ */
+export function isClippingPlaneSupported() {
+    const Cesium = window.Cesium
+    return !!(Cesium && Cesium.ClippingPlaneCollection && Cesium.ClippingPlane)
+}
+
+/**
+ * 给单个 Model primitive 应用平面裁剪(三点确定一个平面)
+ * @param {Cesium.Model} model
+ * @param {Cesium.Cartesian3[]} positions  三个点 [p0, p1, p2]
+ * @param {Object} [opts]
+ * @param {boolean} [opts.unionClippingRegions=false]  true=裁剪所有平面外侧(交集), false=裁剪任一平面外侧(并集)
+ *   平面裁剪用单平面,两者效果相同;Box 裁剪传 true 保留 Box 内部,传 false 保留 Box 外部
+ */
+export function setModelClipPlane(model, positions, opts = {}) {
+    const Cesium = window.Cesium
+    if (!model || !positions || positions.length < 3) return
+    if (!isClippingPlaneSupported()) return
+
+    // 三点计算平面法向量和距离
+    // positions 结构(垂直平面): [点1地面, 点2地面, 点2上方, 点1上方]
+    // 用 v1 = 点2-点1(水平方向), v2 = 点2上方-点2(纯垂直方向), 避免 v2 含水平分量导致法向量倾斜
+    const v1 = Cesium.Cartesian3.subtract(positions[1], positions[0], new Cesium.Cartesian3())
+    let v2
+    if (positions.length >= 4) {
+        // 4 点结构:用 [2]-[1] 作为纯垂直方向
+        v2 = Cesium.Cartesian3.subtract(positions[2], positions[1], new Cesium.Cartesian3())
+    } else {
+        v2 = Cesium.Cartesian3.subtract(positions[2], positions[0], new Cesium.Cartesian3())
+    }
+    const normal = Cesium.Cartesian3.cross(v1, v2, new Cesium.Cartesian3())
+    const normalLen = Cesium.Cartesian3.magnitude(normal)
+    if (normalLen < 1e-10) return  // 三点共线
+    Cesium.Cartesian3.divideByScalar(normal, normalLen, normal)
+    // opts.flipNormal 可反转裁剪方向(用于 clipMode 切换)
+    if (opts.flipNormal) {
+        Cesium.Cartesian3.negate(normal, normal)
+    }
+    // 平面方程: normal·point + distance = 0
+    // 已知平面上一点 P0,则 distance = -normal · P0
+    const distance = -Cesium.Cartesian3.dot(normal, positions[0])
+
+    try {
+        const planes = [
+            new Cesium.ClippingPlane(normal, distance)
+        ]
+        const collection = new Cesium.ClippingPlaneCollection({
+            planes: planes,
+            unionClippingRegions: opts.unionClippingRegions === true,
+            enabled: true,
+            edgeWidth: 1.0,
+            edgeColor: Cesium.Color.WHITE.withAlpha(0.3)
+        })
+        model.clippingPlanes = collection
+    } catch (e) {
+        console.warn('[clip-model] setModelClipPlane 失败:', e.message)
+    }
+}
+
+/**
+ * 给单个 Model primitive 应用 Box 裁剪(6 个面围成正方体)
+ *
+ * Cesium ClippingPlane 裁剪规则(官方文档 + 实测):
+ *   - 平面方程:dot(normal, point) + distance = 0
+ *   - 被裁剪的条件:dot(normal, point) + distance < 0 (即 normal 反方向那一侧被裁)
+ *   - ClippingPlaneCollection.unionClippingRegions:
+ *       true  = 裁剪区域 = 所有平面裁剪区域的【交集】(必须满足所有平面才裁)
+ *       false = 裁剪区域 = 所有平面裁剪区域的【并集】(任一平面裁剪就裁)
+ *
+ * Box 裁剪推导:
+ *   法向量 n 指向 Box 外部(+X/-X/+Y/-Y/+Z/-Z 六个方向)
+ *   对于 Box 内部点 P_in: dot(n_i, P_in) + distance_i < 0 (在所有 6 个平面的裁剪区域)
+ *   对于 Box 外部点 P_out: 至少有一个平面 dot(n_i, P_out) + distance_i > 0 (在非裁剪区域)
+ *
+ *   裁剪 Box 内部(cutInside=true):
+ *     需要所有平面都裁剪才裁 → unionClippingRegions=true
+ *   裁剪 Box 外部(cutInside=false):
+ *     需要任一平面裁剪就裁 → unionClippingRegions=false
+ *     但此时法向量方向要反过来(指向 Box 内部),让外部点在裁剪区域
+ *
+ * @param {Cesium.Model} model
+ * @param {Object} boxOptions
+ *   - position: Cesium.Cartesian3  Box 中心点(ECEF 世界坐标)
+ *   - dimensions: Cesium.Cartesian3  长/宽/高
+ *   - heading: number  绕 ENU 局部 Z 轴旋转弧度
+ * @param {Object} [opts]
+ *   - cutInside: boolean  true=裁剪 Box 内部(保留外部), false=裁剪 Box 外部(保留内部)
+ */
+export function setModelClipBox(model, boxOptions, opts = {}) {
+    const Cesium = window.Cesium
+    if (!model || !boxOptions || !boxOptions.position || !boxOptions.dimensions) {
+        console.warn('[clip-model] setModelClipBox 参数无效')
+        return
+    }
+    if (!isClippingPlaneSupported()) return
+
+    const position = boxOptions.position
+    const dim = boxOptions.dimensions
+    const heading = boxOptions.heading || 0
+    const halfX = dim.x / 2
+    const halfY = dim.y / 2
+    const halfZ = dim.z / 2
+    const cutInside = opts.cutInside === true
+
+    // 构建 Box 中心点的 ENU 局部坐标系 + heading 旋转
+    // 这样局部 X=东, Y=北, Z=上,旋转后转到 ECEF 世界坐标系
+    const enuToEcef = Cesium.Transforms.eastNorthUpToFixedFrame(position)
+    const rotZ = Cesium.Matrix3.fromRotationZ(heading)
+    const headingMat4 = Cesium.Matrix4.fromRotationTranslation(rotZ, Cesium.Cartesian3.ZERO)
+    const boxToWorld = new Cesium.Matrix4()
+    Cesium.Matrix4.multiply(enuToEcef, headingMat4, boxToWorld)
+
+    // 6 个面的局部定义:法向量(指向 Box 外部) + 该方向上的半边长
+    const localFaces = [
+        { n: new Cesium.Cartesian3(1, 0, 0), d: halfX },   // +X 面
+        { n: new Cesium.Cartesian3(-1, 0, 0), d: halfX },  // -X 面
+        { n: new Cesium.Cartesian3(0, 1, 0), d: halfY },   // +Y 面
+        { n: new Cesium.Cartesian3(0, -1, 0), d: halfY },  // -Y 面
+        { n: new Cesium.Cartesian3(0, 0, 1), d: halfZ },   // +Z 面
+        { n: new Cesium.Cartesian3(0, 0, -1), d: halfZ }    // -Z 面
+    ]
+
+    const planes = []
+    for (const lf of localFaces) {
+        // 1. 法向量方向(cutInside=false 时反向,指向 Box 内部)
+        const localN = cutInside
+            ? lf.n
+            : Cesium.Cartesian3.negate(lf.n, new Cesium.Cartesian3())
+
+        // 2. 将局部法向量转到 ECEF 世界坐标系(boxToWorld 旋转,原点不用平移因为是向量)
+        const worldNormal = new Cesium.Cartesian3()
+        Cesium.Matrix4.multiplyByPointAsVector(boxToWorld, localN, worldNormal)
+        // 归一化(boxToWorld 是正交+平移,向量长度不变,但保险起见)
+        const nLen = Cesium.Cartesian3.magnitude(worldNormal)
+        if (nLen > 1e-10) {
+            Cesium.Cartesian3.divideByScalar(worldNormal, nLen, worldNormal)
+        }
+
+        // 3. 平面上的一个点 P0 = boxToWorld * (lf.n * lf.d)
+        //   boxToWorld = eastNorthUpToFixedFrame(position) 已包含 position 平移
+        //   所以 multiplyByPoint 后即为世界坐标,不能再加 position(否则位置错 2 倍)
+        const localP0 = Cesium.Cartesian3.multiplyByScalar(lf.n, lf.d, new Cesium.Cartesian3())
+        const worldP0 = new Cesium.Cartesian3()
+        Cesium.Matrix4.multiplyByPoint(boxToWorld, localP0, worldP0)
+
+        // 4. 平面方程:dot(normal, point) + distance = 0
+        //   已知平面上一点 P0,则 distance = -dot(normal, P0)
+        const distance = -Cesium.Cartesian3.dot(worldNormal, worldP0)
+
+        try {
+            planes.push(new Cesium.ClippingPlane(worldNormal, distance))
+        } catch (e) {
+            console.warn('[clip-model] 创建 ClippingPlane 失败:', e.message)
+        }
+    }
+
+    try {
+        const collection = new Cesium.ClippingPlaneCollection({
+            planes: planes,
+            // cutInside=true (裁剪内部):unionClippingRegions=true (所有平面裁剪区域交集)
+            // cutInside=false (裁剪外部):unionClippingRegions=false (任一平面裁剪即裁剪)
+            unionClippingRegions: cutInside,
+            enabled: true,
+            edgeWidth: 1.0,
+            edgeColor: Cesium.Color.WHITE.withAlpha(0.3)
+        })
+        model.clippingPlanes = collection
+        // 调试日志
+        console.log('[clip-model] Box 裁剪已应用:', {
+            cutInside: cutInside,
+            unionClippingRegions: cutInside,
+            planeCount: planes.length,
+            position: position,
+            dimensions: dim,
+            heading: heading,
+            firstPlane: planes[0] ? { normal: planes[0].normal, distance: planes[0].distance } : null,
+            // 用 Box 中心点验证裁剪判定
+            centerTest: planes.map(p => {
+                const v = Cesium.Cartesian3.dot(p.normal, position) + p.distance
+                return v < 0 ? '裁剪区域内' : '非裁剪区域'
+            })
+        })
+    } catch (e) {
+        console.warn('[clip-model] setModelClipBox 失败:', e.message)
+    }
+}
+
+/**
+ * 清除 Model 上的所有裁剪
+ * @param {Cesium.Model} model
+ */
+export function clearModelClip(model) {
+    if (!model) return
+    try {
+        if (model.clippingPlanes) {
+            // 销毁旧的 ClippingPlaneCollection 释放资源
+            if (typeof model.clippingPlanes.destroy === 'function') {
+                model.clippingPlanes.destroy()
+            }
+            model.clippingPlanes = undefined
+        }
+    } catch (e) {
+        console.warn('[clip-model] clearModelClip 失败:', e.message)
+    }
+}
+
+/**
+ * 批量:给场景中所有 glTF Model 应用平面裁剪
+ * @param {Cesium.Viewer} viewer
+ * @param {Cesium.Cartesian3[]} positions
+ * @param {Object} [opts]
+ */
+export function setAllModelClipPlane(viewer, positions, opts) {
+    const models = findModelPrimitives(viewer)
+    for (const m of models) {
+        setModelClipPlane(m, positions, opts)
+    }
+    return models.length
+}
+
+/**
+ * 批量:给场景中所有 glTF Model 应用 Box 裁剪
+ * @param {Cesium.Viewer} viewer
+ * @param {Object} boxOptions
+ * @param {Object} [opts]
+ */
+export function setAllModelClipBox(viewer, boxOptions, opts) {
+    const models = findModelPrimitives(viewer)
+    console.log('[clip-model] setAllModelClipBox: 找到 ' + models.length + ' 个 Model, 准备裁剪', { boxOptions, opts })
+    for (const m of models) {
+        console.log('  - Model type:', m && m.constructor && m.constructor.name, 'hasClippingPlanes:', m && ('clippingPlanes' in m), '当前值:', m && m.clippingPlanes)
+        setModelClipBox(m, boxOptions, opts)
+        // 赋值后立即读取,验证是否真的写入
+        console.log('    赋值后读取 clippingPlanes:', m && m.clippingPlanes ? 'ClippingPlaneCollection' : (m ? m.clippingPlanes : 'no model'))
+    }
+    if (models.length === 0) {
+        console.warn('[clip-model] ⚠️ 没找到任何 Model!裁剪不会生效。请检查上面的诊断日志。')
+    }
+    return models.length
+}
+
+/**
+ * 批量:清除场景中所有 glTF Model 的裁剪
+ * @param {Cesium.Viewer} viewer
+ */
+export function clearAllModelClip(viewer) {
+    const models = findModelPrimitives(viewer)
+    for (const m of models) {
+        clearModelClip(m)
+    }
+    return models.length
+}
+
+/**
+ * 检查场景中是否有 glTF Model 可被裁剪
+ * 用于在裁剪面板上提示用户当前场景是否支持 Model 裁剪
+ * @param {Cesium.Viewer} viewer
+ * @returns {boolean}
+ */
+export function hasClippableModel(viewer) {
+    return findModelPrimitives(viewer).length > 0
+}

+ 1 - 1
RuoYi-Vue3/src/supermap-cesium-module/js/common/waterLayer.js

@@ -232,7 +232,7 @@ function loadWaterLayer(url, name, callback) {
                                 shallowColor: new Cesium.Color(0.2, 0.28, 0.26, 0.8),
                                 deepColor: new Cesium.Color(0.05, 0.25, 0.25, 0.95),
                                 normalMap: Cesium.buildModuleUrl('Assets/Textures/waterNormals.jpg'),
-                                skyTexture: 'src/texture/sky.jpg',
+                                skyTexture: '/texture/sky.jpg',
                                 time: 0,
                                 flowSpeed: 0.6,
                                 flowDirection: new Cesium.Cartesian2(-1.0, 0.0),

+ 14 - 10
RuoYi-Vue3/src/supermap-cesium-module/views/layout/aside.vue

@@ -317,7 +317,7 @@
             <el-menu-item 
               :index="'business-scenario-' + scene.sceneId"
               class="model-menu-item"
-              @dblclick="loadFixedScene(scene)"
+              @click="loadFixedScene(scene)"
             >
               <span class="scene-name" :class="{ 'scene-loaded': isSceneLoaded(scene.sceneId) }">{{ scene.sceneName }}</span>
               <el-popconfirm
@@ -341,10 +341,10 @@
           
           <!-- 数据库场景 -->
           <template v-for="scene in businessScenes" :key="scene.sceneId">
-            <el-menu-item 
+            <el-menu-item
               :index="'business-scenario-' + scene.sceneId"
               class="model-menu-item"
-              @dblclick="loadBusinessScenarioFromDB(scene)"
+              @click="loadBusinessScenarioFromDB(scene)"
             >
               <span class="scene-name" :class="{ 'scene-loaded': isSceneLoaded(scene.sceneId) }">{{ scene.sceneName }}</span>
               <el-popconfirm
@@ -418,7 +418,7 @@
         v-if="showWindField"
         :viewer="viewer"
         :visible="showWindField"
-        data-url="https://typhoon.slt.zj.gov.cn/Api/LastWind"
+        :data-url="`${PROXY_SLT}/Api/LastWind`"
       ></WindFieldVisualization>
 
       <!-- 降雨可视化组件 -->
@@ -530,7 +530,7 @@
 
 <script>
 import config from "../../config/views_config.js";  //组件配置
-import server_config from "../../config/server_config.js";  //服务配置
+import server_config, { PROXY_TIANDITU_TILE, PROXY_SLT, TIANDITU_KEY } from "../../config/server_config.js";  //服务配置
 import server_flyTo_config from "../../config/server_position_config.js";  //服务坐标配置
 import layerManagement from "../../js/common/layerManagement.js";  //图层管理封装方法
 import waterLayer from "../../js/common/waterLayer.js";  //水面图层
@@ -598,6 +598,10 @@ export default {
         "http://www.supermapol.com/realspace/services/3D-ZF_normal/rest/realspace",
       config: config,
       server_config: server_config,
+      // 暴露给模板使用:Options API 模板只能访问 data/computed/methods,
+      // 不能直接访问 import 的变量,否则模板里 PROXY_SLT 为 undefined,
+      // 导致 :data-url="`${PROXY_SLT}/Api/LastWind`" 变成 "undefined/Api/LastWind"
+      PROXY_SLT: PROXY_SLT,
       baseLayerObj: server_config[1].children[0],
       terrainLayerObj: null,
       addCustomServices: [],
@@ -2177,7 +2181,7 @@ export default {
         case "TIANDITU":
           // 使用天地图影像底图(使用HTTPS)
           imageryProvider = new Cesium.WebMapTileServiceImageryProvider({
-            url: `https://t0.tianditu.gov.cn/img_c/wmts?service=WMTS&request=GetTile&version=1.0.0&LAYER=img&tileMatrixSet=c&TileMatrix={TileMatrix}&TileRow={TileRow}&TileCol={TileCol}&style=default&format=tiles&tk=3fb1e9fda20ee995dc815c8243553ce8`,
+            url: `${PROXY_TIANDITU_TILE}/img_c/wmts?service=WMTS&request=GetTile&version=1.0.0&LAYER=img&tileMatrixSet=c&TileMatrix={TileMatrix}&TileRow={TileRow}&TileCol={TileCol}&style=default&format=tiles&tk=${TIANDITU_KEY}`,
             layer: 'img',
             style: 'default',
             format: 'image/jpeg',
@@ -2230,10 +2234,10 @@ export default {
         // 重新添加注记图层
         if (type === "TIANDITU") {
           // 当天地图作为底图时,总是添加影像注记图层
-          const tiandituKey = '3fb1e9fda20ee995dc815c8243553ce8';
+          // tk 来自外部配置 supermap.config.js
           labelLayer = imageryLayerCollection.addImageryProvider(
             new Cesium.WebMapTileServiceImageryProvider({
-              url: `https://t0.tianditu.gov.cn/cia_c/wmts?service=WMTS&request=GetTile&version=1.0.0&LAYER=cia&tileMatrixSet=c&TileMatrix={TileMatrix}&TileRow={TileRow}&TileCol={TileCol}&style=default&format=tiles&tk=${tiandituKey}`,
+              url: `${PROXY_TIANDITU_TILE}/cia_c/wmts?service=WMTS&request=GetTile&version=1.0.0&LAYER=cia&tileMatrixSet=c&TileMatrix={TileMatrix}&TileRow={TileRow}&TileCol={TileCol}&style=default&format=tiles&tk=${TIANDITU_KEY}`,
               layer: 'cia',
               style: 'default',
               format: 'image/png',
@@ -2248,10 +2252,10 @@ export default {
           console.log('为天地图添加影像注记图层');
         } else if (!labelLayer) {
           // 其他底图时,如果没有注记图层,则添加
-          const tiandituKey = '3fb1e9fda20ee995dc815c8243553ce8';
+          // tk 来自外部配置 supermap.config.js
           labelLayer = imageryLayerCollection.addImageryProvider(
             new Cesium.WebMapTileServiceImageryProvider({
-              url: `https://t0.tianditu.gov.cn/cia_c/wmts?service=WMTS&request=GetTile&version=1.0.0&LAYER=cia&tileMatrixSet=c&TileMatrix={TileMatrix}&TileRow={TileRow}&TileCol={TileCol}&style=default&format=tiles&tk=${tiandituKey}`,
+              url: `${PROXY_TIANDITU_TILE}/cia_c/wmts?service=WMTS&request=GetTile&version=1.0.0&LAYER=cia&tileMatrixSet=c&TileMatrix={TileMatrix}&TileRow={TileRow}&TileCol={TileCol}&style=default&format=tiles&tk=${TIANDITU_KEY}`,
               layer: 'cia',
               style: 'default',
               format: 'image/png',

+ 4 - 16
RuoYi-Vue3/src/utils/request.js

@@ -1,15 +1,12 @@
 import axios from 'axios'
-import { ElNotification , ElMessageBox, ElMessage, ElLoading } from 'element-plus'
+import { ElNotification, ElMessage, ElLoading } from 'element-plus'
 import { getToken } from '@/utils/auth'
 import errorCode from '@/utils/errorCode'
 import { tansParams, blobValidate } from '@/utils/ruoyi'
 import cache from '@/plugins/cache'
 import { saveAs } from 'file-saver'
-import useUserStore from '@/store/modules/user'
 
 let downloadLoadingInstance
-// 是否显示重新登录
-export let isRelogin = { show: false }
 
 axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
 // 创建axios实例
@@ -82,18 +79,9 @@ service.interceptors.response.use(res => {
       return res.data
     }
     if (code === 401) {
-      if (!isRelogin.show) {
-        isRelogin.show = true
-        ElMessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', { confirmButtonText: '重新登录', cancelButtonText: '取消', type: 'warning' }).then(() => {
-          isRelogin.show = false
-          useUserStore().logOut().then(() => {
-            location.href = '/index'
-          })
-      }).catch(() => {
-        isRelogin.show = false
-      })
-    }
-      return Promise.reject('无效的会话,或者会话已过期,请重新登录。')
+      // 登录验证已禁用,401 视为普通错误静默处理,不再弹窗提示重新登录
+      console.warn('请求返回 401,已忽略登录跳转:' + (res.config ? res.config.url : ''))
+      return Promise.reject('error')
     } else if (code === 500) {
       ElMessage({ message: msg, type: 'error' })
       return Promise.reject(new Error(msg))

+ 20 - 0
RuoYi-Vue3/vite.config.js

@@ -94,6 +94,26 @@ export default defineConfig(({ mode, command }) => {
         allow: ['..']
       },
       proxy: {
+        // 外部 API 反向代理(开发模式,生产环境由 Nginx 转发)
+        // 必须放在 /dev-api 等通用前缀前面,更长前缀优先匹配
+        '/proxy/tianditu-api': {
+          target: 'https://api.tianditu.gov.cn',
+          changeOrigin: true,
+          secure: false,
+          rewrite: (p) => p.replace(/^\/proxy\/tianditu-api/, '')
+        },
+        '/proxy/tianditu': {
+          target: 'https://t0.tianditu.gov.cn',
+          changeOrigin: true,
+          secure: false,
+          rewrite: (p) => p.replace(/^\/proxy\/tianditu/, '')
+        },
+        '/proxy/slt': {
+          target: 'https://typhoon.slt.zj.gov.cn',
+          changeOrigin: true,
+          secure: false,
+          rewrite: (p) => p.replace(/^\/proxy\/slt/, '')
+        },
         // https://cn.vitejs.dev/config/#server-proxy
         '/dev-api': {
           target: baseUrl,

+ 41 - 0
nginx-production.conf

@@ -12,6 +12,47 @@ server {
         try_files $uri $uri/ /index.html;
     }
 
+    # ========== 外部 API 反向代理 ==========
+    # 客户浏览器只访问公司服务器一个域名,避免跨域 + 天地图 tk Referer 白名单 + 客户出口防火墙三重问题
+    # 对应前端 vite.config.js 的 server.proxy 配置和 server_config.js 的 PROXY_* 常量
+    # 注意:location 前缀必须与 server_config.js 中的 PROXY_SLT/PROXY_TIANDITU_TILE/PROXY_TIANDITU_API 完全一致
+    #       proxy_pass 末尾必须带 /,Nginx 才会去掉 location 前缀转发(如 /proxy/slt/Api/LastWind → /Api/LastWind)
+
+    # 浙江水情台风 API(风场/降雨/云图/雷达)
+    location /proxy/slt/ {
+        proxy_pass https://typhoon.slt.zj.gov.cn/;
+        proxy_set_header Host typhoon.slt.zj.gov.cn;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        proxy_set_header X-Forwarded-Proto $scheme;
+        proxy_ssl_server_name on;
+        proxy_connect_timeout 60s;
+        proxy_send_timeout 60s;
+        proxy_read_timeout 60s;
+    }
+
+    # 天地图地理编码 API
+    location /proxy/tianditu-api/ {
+        proxy_pass https://api.tianditu.gov.cn/;
+        proxy_set_header Host api.tianditu.gov.cn;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        proxy_set_header X-Forwarded-Proto $scheme;
+        proxy_ssl_server_name on;
+    }
+
+    # 天地图瓦片(img_c / cia_c)
+    # Referer 头必填:天地图 WAF 会拦截无 Referer 的请求返回 418
+    location /proxy/tianditu/ {
+        proxy_pass https://t0.tianditu.gov.cn/;
+        proxy_set_header Host t0.tianditu.gov.cn;
+        proxy_set_header Referer "https://tianditu.gov.cn/";
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        proxy_set_header X-Forwarded-Proto $scheme;
+        proxy_ssl_server_name on;
+    }
+
     # 后端API代理 - 关键配置
     location /prod-api/ {
         proxy_pass http://localhost:8080/prod-api/;  # 代理到后端

BIN
ruoyi-admin/target/classes/com/ruoyi/web/controller/system/SysProfileController.class


BIN
ruoyi-common/target/classes/com/ruoyi/common/annotation/Excel$ColumnType.class


BIN
ruoyi-common/target/classes/com/ruoyi/common/annotation/Excel$Type.class


BIN
ruoyi-common/target/classes/com/ruoyi/common/annotation/Excel.class


BIN
ruoyi-common/target/classes/com/ruoyi/common/config/RuoYiConfig.class


BIN
ruoyi-common/target/classes/com/ruoyi/common/config/serializer/SensitiveJsonSerializer.class


BIN
ruoyi-common/target/classes/com/ruoyi/common/constant/CacheConstants.class


BIN
ruoyi-common/target/classes/com/ruoyi/common/constant/Constants.class


BIN
ruoyi-common/target/classes/com/ruoyi/common/constant/GenConstants.class


BIN
ruoyi-common/target/classes/com/ruoyi/common/constant/HttpStatus.class


BIN
ruoyi-common/target/classes/com/ruoyi/common/constant/ScheduleConstants$Status.class


BIN
ruoyi-common/target/classes/com/ruoyi/common/constant/ScheduleConstants.class


BIN
ruoyi-common/target/classes/com/ruoyi/common/constant/UserConstants.class


BIN
ruoyi-common/target/classes/com/ruoyi/common/core/controller/BaseController$1.class


BIN
ruoyi-common/target/classes/com/ruoyi/common/core/controller/BaseController.class


BIN
ruoyi-common/target/classes/com/ruoyi/common/core/domain/AjaxResult.class


BIN
ruoyi-common/target/classes/com/ruoyi/common/core/domain/BaseEntity.class


BIN
ruoyi-common/target/classes/com/ruoyi/common/core/domain/R.class


BIN
ruoyi-common/target/classes/com/ruoyi/common/core/domain/TreeEntity.class


BIN
ruoyi-common/target/classes/com/ruoyi/common/core/domain/TreeSelect.class


BIN
ruoyi-common/target/classes/com/ruoyi/common/core/domain/entity/SysDept.class


BIN
ruoyi-common/target/classes/com/ruoyi/common/core/domain/entity/SysDictData.class


BIN
ruoyi-common/target/classes/com/ruoyi/common/core/domain/entity/SysDictType.class


BIN
ruoyi-common/target/classes/com/ruoyi/common/core/domain/entity/SysMenu.class


BIN
ruoyi-common/target/classes/com/ruoyi/common/core/domain/entity/SysRole.class


BIN
ruoyi-common/target/classes/com/ruoyi/common/core/domain/entity/SysUser.class


BIN
ruoyi-common/target/classes/com/ruoyi/common/core/domain/model/LoginBody.class


BIN
ruoyi-common/target/classes/com/ruoyi/common/core/domain/model/LoginUser.class


BIN
ruoyi-common/target/classes/com/ruoyi/common/core/domain/model/RegisterBody.class


BIN
ruoyi-common/target/classes/com/ruoyi/common/core/page/PageDomain.class


BIN
ruoyi-common/target/classes/com/ruoyi/common/core/page/TableDataInfo.class


BIN
ruoyi-common/target/classes/com/ruoyi/common/core/page/TableSupport.class


BIN
ruoyi-common/target/classes/com/ruoyi/common/core/redis/RedisCache.class


BIN
ruoyi-common/target/classes/com/ruoyi/common/core/text/CharsetKit.class


BIN
ruoyi-common/target/classes/com/ruoyi/common/core/text/Convert.class


BIN
ruoyi-common/target/classes/com/ruoyi/common/core/text/StrFormatter.class


BIN
ruoyi-common/target/classes/com/ruoyi/common/enums/BusinessStatus.class


BIN
ruoyi-common/target/classes/com/ruoyi/common/enums/BusinessType.class


BIN
ruoyi-common/target/classes/com/ruoyi/common/enums/DataSourceType.class


BIN
ruoyi-common/target/classes/com/ruoyi/common/enums/DesensitizedType.class


BIN
ruoyi-common/target/classes/com/ruoyi/common/enums/HttpMethod.class


BIN
ruoyi-common/target/classes/com/ruoyi/common/enums/LimitType.class


BIN
ruoyi-common/target/classes/com/ruoyi/common/enums/OperatorType.class


BIN
ruoyi-common/target/classes/com/ruoyi/common/enums/UserStatus.class


BIN
ruoyi-common/target/classes/com/ruoyi/common/exception/DemoModeException.class


BIN
ruoyi-common/target/classes/com/ruoyi/common/exception/GlobalException.class


BIN
ruoyi-common/target/classes/com/ruoyi/common/exception/ServiceException.class


BIN
ruoyi-common/target/classes/com/ruoyi/common/exception/UtilException.class


BIN
ruoyi-common/target/classes/com/ruoyi/common/exception/base/BaseException.class


BIN
ruoyi-common/target/classes/com/ruoyi/common/exception/file/FileException.class


BIN
ruoyi-common/target/classes/com/ruoyi/common/exception/file/FileNameLengthLimitExceededException.class


BIN
ruoyi-common/target/classes/com/ruoyi/common/exception/file/FileSizeLimitExceededException.class


BIN
ruoyi-common/target/classes/com/ruoyi/common/exception/file/FileUploadException.class


BIN
ruoyi-common/target/classes/com/ruoyi/common/exception/file/InvalidExtensionException$InvalidFlashExtensionException.class


BIN
ruoyi-common/target/classes/com/ruoyi/common/exception/file/InvalidExtensionException$InvalidImageExtensionException.class


BIN
ruoyi-common/target/classes/com/ruoyi/common/exception/file/InvalidExtensionException$InvalidMediaExtensionException.class


BIN
ruoyi-common/target/classes/com/ruoyi/common/exception/file/InvalidExtensionException$InvalidVideoExtensionException.class


BIN
ruoyi-common/target/classes/com/ruoyi/common/exception/file/InvalidExtensionException.class


BIN
ruoyi-common/target/classes/com/ruoyi/common/exception/job/TaskException$Code.class


BIN
ruoyi-common/target/classes/com/ruoyi/common/exception/job/TaskException.class


BIN
ruoyi-common/target/classes/com/ruoyi/common/exception/user/BlackListException.class


Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä