# 驾驶舱生产部署操作手册 > 面向部署人员(运维/实施)。**只改配置与静态目录,不需要重新构建前端。** > 阅读顺序:先看「一页速查」,按第 2 节改配置,按第 3 节挂数据,最后跑第 5 节自检。 --- ## 一页速查 | 我要做的事 | 改哪里 | 怎么改 | | --- | --- | --- | | 打开/关闭运行时配置 | 产物 `index.html` | 加/删一行 `window.__COCKPIT_CONFIG_URL__ = 'auto'` | | 改后端接口地址 | `dist/cockpit/config.json` → `api.baseUrl` | 填网关地址,如 `/api/cockpit/v1` | | 改数据源(mock / 真接口) | 同上 → `dataSource` | `"mock"`(默认)或 `"real"` | | 配置真接口凭据(已废弃) | — | **2026-09-28 起免鉴权**:驾驶舱 REST 与 `/ws/deviceStatus` 均不要求凭据,`api.token`/`ws.token` 留空即可(旧配置写了也会被前端忽略);安全性由网络层内网隔离保障 | | 换全局底图 | 同上 → `map.basemap.source` | 默认 `natural-earth`(离线);可选 `xyz` / `tianditu` / `arcgis-imagery` | | 挂地形 DEM | 同上 → `map.terrain.url` | 指向含 `layer.json` 的目录 URL | | 挂本地精细影像 | 同上 → `map.overlays[]` | `url`(TMS 用 `{reverseY}`)+ `bounds` | | 挂 3D Tiles / 倾斜摄影 | 同上 → `map.tilesets[]` | 每项一个 `tileset.json` 的 URL | | 挂 GIS 静态目录(nginx) | 服务器 nginx | 见第 3 节的两段 `location` | | 改「目标城市」与归位视角 | `dist/cockpit/config.json` → `map.home` | 见 2.6 | | 开关进入引导(自转星球 + 点击进入) | 同上 → `map.intro.enabled` | 见 2.6 | | 地面/管线视图的倾角与半透明 | 同上 → `map.viewMode` | 见 2.7(不配则用默认:管线 pitch -18°、距离 ×0.35、地形 α0.45、模型 α0.4) | | 框定主场景区(区域限位 + 自建雾气) | 同上 → `map.primaryArea.bounds` | 见 2.9;**不填就等于关掉这两个能力** | | 主场景区限位的运行表现 | — | 见 2.10(进区后最远=`map.home.range`、拖到边界软回弹、返回全球) | | 区内视距雾(色/距离/进出时长) | 同上 → `map.fog` | 见 2.11(默认 18/42 km、`#bdcdd6`;不配也是这一套) | | 粗→精影像交接(渐显时机/时长) | 同上 → `map.handover` | 见 2.12(默认等雾保护 + 瓦片就绪才渐显,700 ms;不配也是这一套) | | 现场调试生产包(显示诊断信息) | 同上 → `map.ui.debug` | `true` 临时打开;`false` 强制干净形态(默认跟随构建) | | 完全关掉三维引擎 | 同上 → `map.cesium.enabled` | `false` → 中央区域退回临时影像 | **三个文件的分工**(先记住这句,后面就不会乱): 1. **`dist/cockpit/config.json`** —— 运行时配置,**部署后改这个**(环境地址、数据源、图层)。 2. **产物 `index.html`** —— 加一行开关,告诉前端「去读上面那个文件」。 3. **服务器上的 GIS 目录 + nginx** —— DEM / 影像 / 3D Tiles 的静态数据。 --- ## 1. 配置是怎么生效的 前端按**四层**取值,后者覆盖前者(合并是深合并,只写要改的字段即可): | 层 | 来源 | 谁改 | 说明 | | --- | --- | --- | --- | | 1 | 内置默认 | 前端 | `dataSource: mock`、`basemap: natural-earth`、不挂地形/影像/3D Tiles | | 2 | 构建期 `VITE_COCKPIT_*` | 前端构建者 | 打进产物,换环境要重建,**不推荐生产用** | | 3 | `cockpit/config.json` | **部署人员** | 本次部署的主入口,改完刷新页面即生效 | | 4 | 宿主注入 `window.__COCKPIT_CONFIG__` | 集成方(qiankun 主应用等) | 整份对象注入,优先级最高;给了就不再读第 3 层 | **打开第 3 层**:默认前端**不探测**配置文件(免得每页刷 404)。二选一: - **部署后改产物**(推荐,无需重建):编辑 `dist/index.html`,在 `` 里加一行 —— ```html ``` `'auto'` = 取应用基址下的 `cockpit/config.json`(`publicPath: './'` 与 qiankun 子路径下都对)。 也可以给绝对 URL(例如配置由后端下发):`window.__COCKPIT_CONFIG_URL__ = 'https://cfg.example.com/cockpit.json';` - **构建期**:`.env` 里写 `VITE_COCKPIT_CONFIG_URL=auto` 再 `yarn build`。 **顶栏能看到状态**:`.cockpit-shell__config` 标签显示 `配置就绪 / 配置缺失 / 配置载入失败` 与数据源; 引擎浮标显示 `底图:… · 地形:… · 叠加影像 N 层`。排查问题时先看这两个地方。 --- ## 2. 改 `dist/cockpit/config.json` 模板见仓库 `public/cockpit/config.example.json`(带逐字段注释)。**只需写要覆盖的字段**: ### 2.1 后端接口 ```jsonc { "dataSource": "real", // 真接口;默认 mock "api": { "baseUrl": "/api/cockpit/v1" }, // 网关地址;同源可写相对路径 "ws": { "url": "wss://内网主机/ws/deviceStatus" } } ``` - **`api.token` / `ws.token` 已废弃(2026-09-28 免鉴权)**:按 ADR 0005 修订,驾驶舱 REST 与 `/ws/deviceStatus` 均不再要求凭据;接口安全由**网络层内网隔离**保障(网关与可直连的应用端口 都须只在内网可达)。旧配置里写了 token 也会被前端忽略。若个别分区出现 401/403(例如网关层 另行加了策略),仍只把受影响分区标「数据不可用」,**不会跳登录页**。 - 想改回 mock 演示:`"dataSource": "mock"`,无需改其它字段。 ### 2.2 全局底图(离线优先) ```jsonc "map": { "basemap": { "source": "natural-earth" } } // 推荐:Cesium 自带离线影像,零外网 ``` | 取值 | 是否离线 | 需要什么 | | --- | --- | --- | | `natural-earth` | ✅ 完全离线 | 无(影像随 `dist/cesium/Assets` 一起部署) | | `xyz` | ✅ 可离线 | `url` 模板,如 `/gis/world/{z}/{x}/{y}.png` | | `tianditu` | ❌ 在线 | `map.tianditu.token`(天地图 tk),开发走 `/tianditu.map` 代理 | | `arcgis-imagery` | ❌ 在线 | 无(免 key,Esri 署名),但需要外网 | | `none` | — | 不配底图,中央区域显示临时影像 | > **内网环境请用 `natural-earth` 或 `xyz`**。本项目优先本地/内网离线数据。 ### 2.3 地形 DEM ```jsonc "map": { "terrain": { "url": "/gis/dem", "requestVertexNormals": true } } ``` - `url` 指向**含 `layer.json` 的目录**(量化网格 quantized-mesh)。 - cesiumlab 导出的 `layer.json` 里 `scheme: "tms"` —— **Cesium 自己会认,服务端不要翻 y**。 - 配错或不存在的现象:浮标显示 `地形加载失败(…)· 回退椭球`,页面照常(不白屏)。 ### 2.4 本地精细影像 ```jsonc "map": { "overlays": [{ "url": "/gis/pic3/{z}/{x}/{reverseY}.png", "minimumLevel": 0, "maximumLevel": 19, "bounds": [109.96215820312366, 27.962869019358852, 111.04568213224277, 29.060971407383555], "credit": "沅陵本地影像" }] } ``` - **TMS 数据必须用 `{reverseY}`**(cesiumlab 导出的 `meta.json` 里 `tiletrans: "tms"`); 若你的切片是标准 XYZ(slippy map),则写 `{y}`。 - `bounds` = `[西, 南, 东, 北]`,抄 `meta.json` 的 bounds。填了它,范围外不会发请求(否则整球视角刷一屏 404)。 - 可以有多个 overlay,按数组顺序从下往上叠。 ### 2.5 3D Tiles / 倾斜摄影 ```jsonc "map": { "tilesets": [{ "url": "/gis/model/tileset.json", "lighting": true }] } ``` `lighting:true` 是显式模型图层开关:Cesium 为该 tileset 根据其地理包围球配置固定局部日光与环境光,不跟随相机改变;省略时不修改现有场景光照。运行时 LOD 必须由根 tileset 的空间子节点、`geometricError` 与 `refine: "REPLACE"` 提供,单个 GLB 旁边放一个 LOD1 文件不会自动切换。范围较大的程序化街区可以使用“粗父节点 LOD1 → 多个细叶子 LOD0”的两级空间树,让远景只请求粗父节点、近景再替换为局部叶子;根节点应保留有效的空 GLB 锚点,避免 Cesium 跳过无内容根节点。 - 每项一个 `tileset.json` 的 URL(**根** tileset,不是分块 json),可选 `maximumScreenSpaceError`。 - 清单请求或后续内容请求失败只记状态、不影响其它图层:状态行显示 `3D Tiles 加载失败(…)`;内容阶段错误由 `tileFailed` 事件捕获,不能只用根 `fromUrl()` 成功判断。 - `geometricError` 应按数据层级和实际误差设置。旧版把一次未加载现象直接归因于数值0,未隔离相机/视锥等变量,不能作为格式硬约束。 - 地面视图需开启 `globe.depthTestAgainstTerrain`。本轮已修复默认值;地面与管线半透明来回切换由像素回归验证,不能用“模型已加载”替代遮挡验收。 - 当前模型状态: 1. **旧程序化体块已被否决,禁止默认启用**:`D:\GIS\GIS\cityMassing\` 保留作历史资料,`map.tilesets` 留空。新 Blender 资产库与示范街区在 `tools/city-massing/house-library/` 和 `tools/city-massing/district/generated/`,仅通过本机 `serve-review.mjs` 提供效果审查,不进入生产包,也未获得完整县城验收。 所有立面/楼层为估计,影像描绘存在视差;**非测绘建筑,不得用于量测/统计/应急决策**。复现、资产与覆盖清单见 `tools/city-massing/README.md`。 2. **甲方原始 3D Tiles**:`D:\GIS\GIS\yuanlingSHP`(cesiumlab `model2tiles` 导出)**缺根 `tileset.json`**、分块是局部坐标;曾补过一版合成根文件,经目测**错位且是白模,不采用** (合成文件已删除)。若日后用 cesiumlab 重导并勾选生成根 tileset,直接填 `map.tilesets` 即可替换示意模型。 ### 2.6 「目标城市」与进入引导 进系统时的落点、以及顶栏「归位」按钮回到的视角,都由 `map.home` 决定;引导层由 `map.intro` 控制。 ```jsonc "map": { "home": { "lon": 110.39028164, // 目标点经度(CGCS2000/天地图口径,与 WGS-84 本项目视为一致) "lat": 28.4436787, // 目标点纬度 "heading": 0, // 朝向(0 = 正北) "pitch": -55, // 俯角(负 = 从上往下看) "range": 12000, // 弹簧臂距离(米):12000 ≈ 县域级;4600 ≈ 街区级 "label": "沅陵县城" // 归位按钮的提示文案 }, "intro": { "enabled": true, // false = 跳过引导,打开即进系统(不需要引导的部署设 false) "title": "点击进入系统", "spinSeconds": 48 // 星球自转一圈的秒数;0 = 不自转 } } ``` **要点**: - `home.lon/lat` 换成别的城市即可;`range` 决定"进入时看多细",**别小于 2000**(再近看不到县城轮廓)。 - 中心高程**不用填**:地形就绪后系统会用 DEM 实测高程(沅陵实测 111.6 m),填了也会被实测值覆盖 —— 这是 为了避免相机按椭球 0 米定位而钻进山体(拉到 120 m 时曾经整屏黑)。 - `intro.enabled=false` 时**不显示**引导层,但「归位」按钮仍在、`home` 仍然生效。 - 需要"进入后记住,下次直接进"的话目前不支持(每次刷新都会显示引导),属已知取舍。 --- ## 2.7 地面视图 / 管线视图 驾驶舱顶栏筛选条里有「地面视图 / 管线视图」两个按钮(全局条件,切一级模式不重置)。 管线视图会**压低相机 + 放宽俯仰**,并把地形与 3D Tiles **压成半透明**,以便露出管线;地面视图还原。 **旋转中心不重置、三键操作完全不变**(左键平移、右键旋转、滚轮伸缩)。 默认值一般够用;要调就写 `map.viewMode`: ```jsonc "map": { "viewMode": { "pipeline": { "pitch": -18, // 管线视图倾角(度,越接近 0 越平视;允许到 +5) "rangeFactor": 0.35, // 弹簧臂压到当前距离的几成 "terrainAlpha": 0.45, // 地形透明度(0–1;1 = 不透明,0 = 全透) "tilesetAlpha": 0.4 // 3D Tiles / 倾斜摄影透明度 } } } ``` 写错类型/范围外的值会被忽略并退回默认(不会把相机顶到奇怪角度)。 --- ## 2.8 界面形态:开发诊断 vs 生产干净 驾驶舱有两套界面形态,**生产构建默认就是干净形态**,不需要配置: | | 调试形态(开发构建默认) | 生产干净形态 | | --- | --- | --- | | 中央图上的「中央一张图」标题、图层占位、实例提示 | 显示 | **收起** | | 引擎状态(底图/地形/影像/3D Tiles) | 顶部药丸,带边框底色 | **贴底半透明小字**(无边框无底色) | | 顶栏「配置就绪 · 数据源」标签、「当前模式」读数 | 显示 | **收起** | | 点位状态图例(颜色语义)、状态灯 | 显示 | **保留** | | 范围/行业/视图筛选、归位按钮、只读视图、时钟 | 显示 | 保留 | 判定规则:`map.ui.debug` 显式给布尔值就听它,否则**跟随构建模式**(`yarn dev` → 调试;`yarn build` 产物 → 干净)。 ```jsonc "map": { "ui": { "debug": false // 想在生产包里临时看诊断信息就写 true;不写则跟随构建模式 } } ``` 故障时颜色语义仍在:引擎失败时那行小字会转成淡红(生产形态没有边框,靠字色传达)。 --- ## 2.9 主场景区(区域限位 + 自建雾气) `map.primaryArea.bounds` 框出**主场景区**:进入后相机被限位在区内、远景罩上自建距离雾。 两个能力(限位、雾)都以这一个字段为开关 —— **不填 = 两个能力一起关闭**,不是"用了某个默认范围"。 ```jsonc "map": { "primaryArea": { "bounds": [110.328749941417, 28.3917230659234, 110.465779226663, 28.4913038992907] } } ``` **顺序是硬要求**:`[西, 南, 东, 北]`(度),与 `map.overlays[].bounds` 完全同口径, 必须满足 `west < east` 且 `south < north`。 - 甲方或数据方常按 `[W, N, E, S]`(纬度降序)给范围 —— **请在写配置时就把两个纬度对调**, 运行时代码不会替你翻转。 - 写反了会怎样:不算"差不多能用",而是判为**配置非法**,与没配一样走关闭路径, 并在引擎浮标与宿主 `data-map-primary-area-reason` 上写明原因(见下)。 - 为什么不自动翻转:翻对了没人会发现配置抄错,翻错了就是拿一段假范围去锁相机 —— 两种都让部署问题在运行期隐身。 **矩形怎么定**:优先按甲方框定的范围。若要以数据覆盖为准,由部署侧离线算 `影像 bounds ∩ DEM layer.json 的 valid_bounds`,围绕 `map.home` 经纬度、东西/南北各折半后写入本字段 (不要指望前端在运行时推导,也不要在源码里写死某个县的地理常量)。 **怎么确认生效**:引擎浮标(调试形态在中央顶部,生产形态在右下角小字)会出现 | 情况 | 浮标文字 | 宿主读数 | | --- | --- | --- | | 已配置且顺序正确 | `主场景区:13.4×11.1 km` | `data-map-primary-area="ready"` | | 没写这个字段 | `主场景区:未配置 · 限位与雾气关闭` | `data-map-primary-area="missing"` | | 顺序写反 | `主场景区:配置无效(顺序应为 [西,南,东,北],不自动翻转) · 限位与雾气关闭` | `data-map-primary-area="invalid"`,原因在 `data-map-primary-area-reason` | | 数值个数/类型不对 | `主场景区:配置无效(bounds 应为四个有限数字) · 限位与雾气关闭` | 同上(`invalid`) | `未配置` 与 `配置无效` **都会**让「进区锁定 / 相机区域限位」(票 37)与「区内视距雾气」(票 38)关闭; 配置问题同时会记进配置服务的 `issues` 清单,便于排查。 --- ## 2.10 进区锁定与相机区域限位(票 37) 配好主场景区之后,**进区即被限位**:最远视野=归位默认视角(`map.home.range`,本部署 12,000 m), 中心点约束在框内,相机不钻地形。运行期表现为: | 位置 | 顶栏 | 相机 | 宿主读数 | | --- | --- | --- | --- | | 区外(自由) | 只有一个「归位/进入」 | 可拉到全球尺度(上限 3×10⁷ m) | `data-map-area-state="global"` | | 区内(锁定) | 「归位」+「返回全球」 | 滚轮最远只到 `map.home.range`;中心被夹在框内 | `data-map-area-state="locked"`、`data-map-range-max="12000"` | - **怎么进区**:点「进入系统」(引导页)或顶栏「归位」。 - **怎么出区**:只有「返回全球」—— 解除限位并飞回自由全球视角(此后完全自由)。 - **拖到边界**:按紧左键可向外推到**框短边 5%**(≈550 m)为止,**松开自动弹回框内**。 - **防入地**:相机绝对高度始终高于其正下方地形 10 m(可微调);不足时**只退距离**,不改倾角、不动中心。 - **点选定位是例外**:监测点散布全县而主场景区只框了县城,所以**点选/定位到框外点位不受中心约束** (镜头照常飞过去);一旦手动按左键拖动,就回到区内规则。 - **全球态闲置 10 秒** → 回引导页(镜头回初始全景 + 星球自转 + 「点击进入系统」),**不重置**一级模式与筛选; `map.intro.enabled=false` 时按自由全球视角启动、不发生这件事。 - 排障:区内却拉得很远 → 看 `data-map-area-state` 是否真是 `locked`(不是 `locked` 就说明引导 或「归位」没走通,或主场景区配置不可用);`data-map-range-max` 与 `map.home.range` 应一致。 --- ## 2.11 区内视距雾(票 38) 进入主场景区后自动加一层**距离雾**:近处地物清晰、远处平滑融入雾色,旋转镜头时不露出远景瑕疵。 只在区内生效,全球/引导态关闭。默认参数就是下面这组,**不配也是它**: ```jsonc "map": { "fog": { "enabled": true, "startDistance": 18000, // 世界射线距离:从这里开始起雾(米) "endDistance": 42000, // 到这里完全遮蔽(米);必须大于 startDistance "color": "#bdcdd6", // 基准雾色(必须是 #rrggbb) "fadeInMs": 900, // 进区渐显时长(本部署现场调校值) "fadeOutMs": 300, // 返回全球淡出时长(本部署现场调校值;要更利落就调小) "altitudeFullM": 14000, // 相机高度 ≤ 此值时雾满强度 "altitudeZeroM": 3000000, // 相机高度 ≥ 此值时雾收到 0 "altitudeSlewMs": 600 // 上面这层收口的**时间限速**:系数走完全程至少这么久 } } ``` > 上面这四个值(`fadeInMs` / `fadeOutMs` / `altitudeFullM` / `altitudeZeroM` / `altitudeSlewMs`) > 是**本部署(沅陵)在真机上调定的**,不是规格默认。代码默认另有一套 > (`domain/fog.js` 的 `FOG_DEFAULTS`:800/400 ms、20/42 km、400 ms)。 > 换部署若没有自己的现场值,删掉 `map.fog` 里这几项就会退回代码默认。 **要点**: - 只会给**有有效几何深度的地物**上雾(B 方案);天空等无深度背景**保留原色**,不会被整片染色。 - **色相要求:冷白灰、不得发绿**。`#bdcdd6` 是起点不是终值 —— 正式色值请在真实画面上复核后调。 写非 `#rrggbb` 的值会被判非法并退回默认(同时记一条配置问题,不会静默生效失败)。 - **相机高度收口**(`altitudeFullM` / `altitudeZeroM`):雾的距离端点只有 18–42 km, 而相机在**高空**时画面上每个片元都远在 42 km 之外 —— 不加收口就会整个地球被涂成雾色。 这两个值一压,**退出时不再出现"地球变白一段时间"**、**进入时也不再有"白→突然一团绿"**。 代码默认取 20/42 km 有依据:42 km 是雾自己的全遮蔽端点(高过它雾已无保护意义), 20 km 以下恢复满强度,而**区内归位视角约 9.8 km 远在下方 → 区内观感完全不受影响**。 **必须 `altitudeZeroM > altitudeFullM`**:写反会退化为"高度系数恒为 1"(等于把这层保护**关掉**)。 配置层会**报一条 issue**(不静默),但**不会替你改值** —— 两个数各自都合法, "要不要在高空保留雾"是部署的取舍(本部署有意把上端放到 3000 km,使飞入全程都有雾)。 - **`altitudeSlewMs` 是必需的,不是可选的润色**:返回全球时相机 `range` 从 12 km 变到 20,000 km(跨 1.6e6 倍),而雾带只有 20–42 km —— **相机头一帧就越过整条带**。 所以"瞬时按高度决定雾浓淡"必然跳变,观感是「雾几乎一瞬间就产生或消失」。 这一项给高度系数加了时间限速:无论相机多快,画面上的雾都不可能比它更快地变化。 写成 0 = 不限速(回到会跳变的旧行为),一般不要这么配。 - 校验:`endDistance` 必须大于 `startDistance`,否则两端语义不成立,配置层会退回默认并记问题。 - **雾盖不住屏幕中心,这是几何决定的**(排障时先知道这条能省很多时间):雾是**距离雾**, 18 km 起步,而区内相机只有约 9.8 km 高 —— 屏幕中心(脚下街区)的视线距离恒 < 18 km, 所以那里**恒无雾**。中心露出粗底图还是精细影像,只看精细层 alpha(`map.handover`)。 **怎么确认生效**:顶栏出现「视距雾 1.00 · 18–42 km」这样的读数(进区后才显示), 宿主读数在 `.map-canvas` 上: | 情况 | 读数 | 顶栏文案 | | --- | --- | --- | | 区内正常 | `data-map-fog="ready"`、`data-fog-phase="locked"`、`data-fog-strength="1"` | `视距雾 1.00 · 18–42 km` | | 区外待命 | `data-fog-phase="global"`、强度 0 | `视距雾待命 · 区内生效` | | 管线视图观察到透明错位(减弱) | `data-fog-degrade="reduced"`、强度被压低 | `视距雾减弱 · 强度 0.35` | | 不支持深度 / 运行失败 | `data-map-fog="unsupported"` | `视距雾不可用 · 无深度支持` | > **`ready` 不等于「矩形边缘已被遮住」。** 它只说明后处理级可执行; > 画面是否真的被遮住属观感验收,读数里 `data-map-fog-covers-edges` **恒为 `no`**, > 前端不提供"已遮缝"这类未经证实的承诺。 **降级与可读性**:如果管线视图下透明对象被错误浓雾盖住,**可读性优先** —— 可在控制台 (DEV)`__cockpitMap.setFogTranslucentArtifacts(true)` 减弱,或 `setFogUserDisabled(true)` 暂关; `false` 恢复。**降级不影响相机区域限位**(票 37 的限位照旧生效)。 正常情况**不会**预先关闭管线视图的雾。 **性能**:规格要求同机同视角下开启雾的 P95 帧耗时增幅 ≤15%。 DEV 下可采样:`__cockpitMap.startFogPerfSample('with-fog')` → 等一会儿 → `__cockpitMap.stopFogPerfSample()` 读 P95。注意它采的是**渲染事件间隔,不是 GPU 时间**, 只能作参考,不能单独判定达标。 --- ## 2.12 粗→精影像交接(票 39) 从全球飞进县城的**全过程不出现肉眼可辨的矩形色块跳变**。做法是把精细叠加影像的显隐 交给一套交接时序:全球/引导态**隐藏**精细层、只留粗底图;进区后**等雾先形成保护**、 且目标视野瓦片可显示,才渐显精细层;返回时**反向交接**。 ```jsonc "map": { "handover": { "enabled": true, // false = 精细层常显(回到票 39 之前的行为) "fadeMs": 500, // 渐显/收起时长(本部署现场调校值;见下方"为什么调这么短") "minEdgeOcclusion": 0.7, // 相机到影像边处,雾的遮蔽率须 ≥ 此值才允许揭示(0–1) "requireTilesLoaded": false// 揭示前是否要求瓦片加载队列为空 } } ``` > 这三个值也是**本部署真机调定**的(不是规格默认)。代码默认是 > `domain/imageryHandover.js` 的 `HANDOVER_DEFAULTS`:380 ms / 0.9 / `true`。 > 换部署若没有自己的现场值,删掉 `map.handover` 就退回默认。 **要点**: - **为什么 `fadeMs` 要调短("进入时会变绿"的修复)**:雾是**距离雾**,18 km 起步; 而区内相机只有约 9.8 km 高 —— 屏幕中心(脚下街区)的视线距离恒 < 18 km, **那里恒无雾,雾在结构上盖不住它**。所以渐显期间,中心是 「粗糙 Natural Earth II(陆地偏绿)↔ 精细影像」的**混合**,窗口越长那段绿越明显。 调短它 = 让混合一闪而过(维护者 2026-09-29 选定此方向)。 改成硬切(`fadeMs: 0`)也能去掉绿,但会丢掉"粗→精"的过渡感,不推荐。 - **`minEdgeOcclusion` 调小 = 更早揭示、但遮边更弱**。它同时是"**相机离矩形边太近就不揭示**" 的门槛:值越小,允许在雾更薄的地方揭示。本部署取 0.7(默认 0.9); 现网参数下区内最坏点(西南角,距影像边 35.9 km)遮蔽率约 0.97,两种阈值都能过。 - **`requireTilesLoaded: false` 的含义与代价**:不再等"瓦片加载队列为空"就允许揭示。 决策 08 当初要求这一条是为了避免"先空白再跳方块";关掉后**牺牲的正是这个保障** (`globe.tilesLoaded` 本来也只是代理信号,见下)。本部署接受该代价 —— 若现场看到揭示后才有方块补上,把它改回 `true` 即可。 - **隐藏期间仍在预取瓦片**("看不见 ≠ 停止加载")。实现上是 `alpha = 0` 且**保持 `show = true`** —— 若改成 `show = false` 会真的停掉预取,等揭示时才开始下载,反而造成"先空白再跳方块"。 可在 DEV 用 `__cockpitMap.getHandoverState().layerProbe` 核对:应为 `[{show: true, alpha: 0}]`。 - **边缘判据与雾参数同源**:`minEdgeOcclusion` 用的是**同一条雾衰减曲线**,不是另写一套阈值。 所以调 `map.fog.startDistance/endDistance` 时,边缘判据自动跟着变,不会各调各的。 现网参数下,相机停在家(县城中心)时到影像最近边约 42 km、遮蔽率 1.0;区内四角最坏约 0.97,都能过闸。 - **降级**:雾不可用(无深度支持/创建失败)时**业务可用优先** —— 仍会渐显精细层并如实提示, 但**不承诺遮住矩形边**。瓦片失败则**保持隐藏**、继续用粗底图。 - 交接被 `enabled: false` 关掉时,精细层**常显**(把 alpha 还到 1),不会停在隐形状态。 **怎么确认生效**:宿主读数在 `.map-canvas` 上(`data-map-handover*`): | 情况 | 读数 | | --- | --- | | 全球/引导态(精细层隐藏) | `data-map-handover="global"`、`data-map-handover-alpha="0"` | | 进区等待保护 | `data-map-handover-block="fog"` 或 `"tiles"`,alpha 仍为 0 | | 已揭示 | `data-map-handover="revealed"`、`data-map-handover-alpha="1"` | | 瓦片加载失败(保持粗底图) | `data-map-handover-block="tilesFailed"` | | 相机贴近影像边(保护不成立) | `data-map-handover-block="edge"` | | 无雾降级(已揭示但不承诺遮边) | `data-map-handover-degrade="yes"` | > **同样不承诺"边缘已被遮住"**:`data-map-handover-tiles-proxy="yes"` 明确标注 > 瓦片信号(`globe.tilesLoaded`)只是**加载队列为空**的代理,**不证明**目标视野逐像素就绪。 > 「全程无肉眼可辨的方形跳变」属观感验收,请在真实画面上确认。 > **已知边界**:矩形**内部**随地形 LOD 的逐级换挡**不由整层 alpha 修复** —— > 若在真实画面上仍可辨,属票 40 评估局部过渡手段的范围,本层不声称已解决。 **性能**:与雾同一预算(合计 P95 增幅 ≤15%)。交接读数在渲染循环里带 diff 缓存, 稳态帧不重写 DOM;进区/返回是离散动作,会立刻定相位(不被稳态节流吞掉)。 --- ## 3. 挂 GIS 静态目录(nginx) DEM / 影像 / 3D Tiles 都是**静态文件**,直接从磁盘服务,**不要放进前端产物**(上百 MB 起)。 ```nginx # 地形 DEM(里面必须有 layer.json) location /gis/dem/ { alias /data/gis/yuanlingDEM/; add_header Cache-Control "public, max-age=86400"; types { application/octet-stream terrain; } default_type application/octet-stream; } # 本地影像切片 location /gis/pic3/ { alias /data/gis/yuanlingPic3/; add_header Cache-Control "public, max-age=86400"; } # 3D Tiles(若启用) location /gis/model/ { alias /data/gis/model/; add_header Cache-Control "public, max-age=86400"; types { model/gltf-binary glb; application/json json; } } ``` **两条硬要求**: 1. **别让 `.terrain` / `.png` / `.glb` / `layer.json` 走 SPA 回退**。回退成 `index.html` 时, Cesium 会报 `Unexpected token '<'`(地形)或整片瓦片加载失败。上面的 `location` 前缀即是隔离手段。 2. **允许跨域**(前端与数据不同源时): ```nginx add_header Access-Control-Allow-Origin "*"; ``` **目录搬走/换机器**:只改 `config.json` 里对应的 `url`,**不用改前端、不用重建**。 --- ## 4. 完整的最小可用配置(内网离线) ```jsonc { "dataSource": "mock", "map": { "basemap": { "source": "natural-earth" }, "home": { "lon": 110.39028164, "lat": 28.4436787, "heading": 0, "pitch": -55, "range": 12000, "label": "沅陵县城" }, "intro": { "enabled": true, "title": "点击进入系统", "spinSeconds": 48 }, "terrain": { "url": "/gis/dem", "requestVertexNormals": true }, "overlays": [{ "url": "/gis/pic3/{z}/{x}/{reverseY}.png", "minimumLevel": 0, "maximumLevel": 19, "bounds": [109.96215820312366, 27.962869019358852, 111.04568213224277, 29.060971407383555] }], "primaryArea": { "bounds": [110.328749941417, 28.3917230659234, 110.465779226663, 28.4913038992907] }, "tilesets": [] } } ``` --- ## 5. 部署后自检(逐条看现象) | 检查 | 期望现象 | 不通过时看哪里 | | --- | --- | --- | | 页面能打开 | `/#/cockpit` 出现驾驶舱外壳,无白屏 | 控制台报错、`index.html` 的配置开关 | | 进入引导 | 先看到自转星球 + 闪烁「点击进入系统」 | `map.intro.enabled` | | 进入落点 | 点一下后镜头**落在目标城市**(不是随机位置) | `map.home.lon/lat/range` | | 归位按钮 | 顶栏「归位」可点,平移后点它回到目标视角 | `map.home`、引擎是否就绪 | | 配置生效 | 顶栏标签「配置就绪 · 数据源 mock/real」 | `config.json` 路径、`__COCKPIT_CONFIG_URL__` | | 全局底图 | 地球有影像(不是黑球) | `map.basemap.source` | | 地形 | 浮标含「地形:dem」;拉近后有山体起伏 | nginx `/gis/dem/`、`layer.json` 能直接下载 | | 本地影像 | 压到目标区域能看到精细影像 | overlay 的 `url`/`{reverseY}`/`bounds` | | 主场景区 | 浮标含「主场景区:N×M km」;没配或顺序错则写明「限位与雾气关闭」 | `map.primaryArea.bounds`(顺序 `[西,南,东,北]`) | | 进区限位 | 点「进入系统/归位」后顶栏出现「返回全球」,滚轮最远只到 `map.home.range` | `data-map-area-state` 应为 `locked`、`data-map-range-max` 应等于 `map.home.range` | | 区内视距雾 | 进区后顶栏出现「视距雾 1.00 · 18–42 km」;远景平滑融入雾色、近景清晰 | `data-map-fog="ready"`、`data-map-fog-phase="locked"`;区外应为 `global` 强度 0 | | 雾的高度收口 | 返回全球时画面**不再整片变白**;飞入时也**不再"白→突然一团绿"** | 相机升过 42 km 后 `data-map-fog-strength` 应为 `0`;区内约 9.8 km 时仍能到 `1` | | 雾不"一瞬间"散掉 | 返回全球时雾**看得出是一段淡出**,不是一闪就没 | 返回后连续采样 `data-map-fog-strength`,应能取到 0 与 1 之间的**中间值**(若只采到 0/1 说明限速没生效) | | 粗→精影像交接 | 全球态只有粗底图;飞入时**雾先起、精细影像后亮**,全程无肉眼可辨的矩形跳变 | `data-map-handover`:全球 `global`→进区 `entering`→`revealed`;`alpha` 由 0 升到 1;`layerProbe` 在隐藏时应为 `show:true, alpha:0` | | 交接的"先收后撤" | 点「返回全球」:影像先变淡收起,**然后**雾才淡出(不应反过来) | 采样 `data-map-handover-alpha` 与 `data-fog-strength` 的时间序:alpha 归 0 之前雾应保持 1 | | 闲置回引导 | 全球态闲置 **10 秒**后回到初始态:镜头回 `0/−60/2×10⁷`、**星球重新自转**、中央闪烁「点击进入系统」 | `__cockpitMap.getLifecycleStats()`:`spin` 应为 `true`、`spinWanted` > 0;`getCameraState()` 的 hpr 应回到 `0/−60/2×10⁷`。**只在 global 态计时**,区内闲置不回 | | 无 404 噪声 | 浏览器网络面板里 `/gis/**` 全是 200 | 多半是 y 轴约定:TMS 未写 `{reverseY}` | | 3D Tiles(若启用) | 浮标含「3D Tiles N 个」 | 根 `tileset.json` 是否存在(见 2.5) | | 真接口联调 | 分区出数据(REST 与 WS 均免鉴权,无需任何凭据);若个别分区 401/403 只该分区「数据不可用」 | 网络层内网隔离是否覆盖所有入口(网关 + 可直连的应用端口) | --- ## 5.1 关联设备点位与状态特效自检 先记录前端提交、运行页面 URL、配置数据源(real/mock)、后端运行版本及时间。`mock` 受控样例通过不等于 real 数据通过;HTTP 200 不等于业务成功,业务错误分区应显示加载失败。不得为走查绕过坐标校验或修改面板 pointer-events。 ### 操作链 1. 进入系统,在「运行监测」的监测点台账打开一个点,再关闭详情,通过地图标记打开同一点。两种入口都应加载点位摘要并定位;点位名称可读,详情连接线指向该标记。 2. 有有效自身坐标的关联设备应出现;无坐标/结构非法/区外的设备只留清单与具体原因,不借父点坐标。**2026-10-12 起坐标准入改为自动判定**(结构合法 + 落在 `map.primaryArea.bounds` 内即可上图,不再要求人工校核来源):真实数据通常大部分可上图,若本项观察到“全部不可上图”,应先核对后端是否已加载含该判定的构建、以及坐标是否确实落在主场景区内,再决定是否记为缺口——不要沿用旧口径直接判成“无可上图样本”。 3. 聚合点显示“聚合 K 台”。hover 高亮预览,click 固定展开且入口变暗;点子设备进入设备详情,该设备持续可见并接替聚合入口。再次 hover/click 可切换另一设备。K>maxFan 时本屏保留上限内设备,其余仍在清单;选中清单尾部设备也应保持可见。 4. 设备详情与短线跟随设备,返回摘要恢复监测点锚点。平移/旋转后再次选设备应保留地形高程,不钻入山体;关闭/换点后无旧设备层或旧定位响应复活。 5. 同一快照翻页不应闪烁设备层;刷新/快照失效应先撤旧层,新范围完整加载后按所选点位重新核对。切四个模式以及引导态,逐项核对暂停/恢复、图例计数和事件回联,不能只检查按钮能点击。 6. 报警/预警/工单外围环应随帧变化,中心图标保持清晰不透明;正常/离线/未知无动画。静止相机时连续截取同一设备区域,比较实际外围环帧差;全屏变化可能来自时钟、地图瓦片或模型加载,不能单独证明动效。 7. 关闭动效只停动画,静态状态色/形和清单不消失。异常台数超过 `map.effects.abnormalLimit`(默认30)时停动画并显示异常数;这与 `map.overlap.maxFan`(默认10)的展开上限是两种不同降级,不能互代。 ### 可观察字段 字段位于 `.cockpit-shell__map .map-canvas`,DEV只读句柄为 `window.__cockpitMap`;生产没有DEV句柄时查看元素属性和真实画面。 | 检查 | 字段/句柄 | 判据 | | --- | --- | --- | | 详情身份 | `data-map-panel-selection`、`data-map-device-picked` | point/equipment 与面板当前对象一致;不把聚合符号当虚构设备 | | 聚合 | `data-map-overlap-state`、`data-map-overlap-count`;`getOverlapState()` | collapsed/expanded/selected,超量为degraded;count为K,`selectedCode`为真实编码 | | 展开子点 | `getOverlapTargets()`、`getAssociatedDevices()` | 可见子点有真实编码,`overlapEntry`只标记中心已选设备;位置偏移仅用于屏幕呈现 | | 设备状态动画 | `data-map-device-effects`、`data-map-device-effects-animated`、`data-map-device-effects-abnormal`;`getDeviceEffects()` | on/off/degraded,台数真实;tick增长只是驱动证据,还要确认可见外围环跨帧变化 | | 面板锚定 | `.cockpit-shell__detail` 的 `data-anchor-code`、`data-anchor-state` | 点位摘要指向点位,设备详情指向设备;离屏暂隐不改身份 | | 位置退化 | 详情/清单的原因、无地图位置列表 | 坐标缺失(MISSING_*)、越界(*_OUT_OF_RANGE)、零坐标、区外(OUTSIDE_PRIMARY_AREA)、地形失败/待确认、资产降级分别记录,不冒充成功 | ### 证据与排障边界 - 性能使用同机、同视口、同视角、相同加载条件的开/关动效重复采样,保留样本数、P50/P95及口径。浏览器长任务/请求延迟不是Cesium GPU帧耗时,不能代替该读数。 - 真实Redis/Kingbase结论需运行实例提交绑定、服务端日志/部署证据与真实接口/WS样本;页面显示“已连接”或只读GET通过不能证明两种存储均联调成功。 - 当前工作区已修:同编码数据更新、父点门控、固定无位置点详情、定位取消与首次设备档继承;定向回归通过,真实样本仍需回验。仍未完成:刷新/失效撤层重载链与折叠聚合动效。完整真实交互、帧差与性能证据齐备前,不将本节操作链记为全通过。 --- ## 6. 常见问题 **Q:改了 `config.json` 没生效?** 确认产物 `index.html` 里有 `window.__COCKPIT_CONFIG_URL__` 那一行;再硬刷新(配置请求带 `cache: 'no-store'`, 但 `index.html` 可能被缓存)。 **Q:地形/影像请求 404?** 先看 URL 能不能直接下载(浏览器地址栏打开 `…/gis/dem/layer.json`)。全 404 = nginx `alias` 路径不对; 个别 404 = 瓦片 y 轴约定不对(TMS 要 `{reverseY}`)或 `bounds` 与数据范围不一致。 **Q:能不能用天地图/ArcGIS?** 可以但有前提:天地图要免费 tk;ArcGIS(`arcgis-imagery`,免 key)要外网。 **本项目约定优先用本地/内网离线数据**,在线源仅作可选补充。 **Q:拉很近画面变黑?** 已修(票 30):中心点高程现在按 **DEM 实测**取(不是椭球 0 米),拉到距离下限 120 m 仍能看到地面。 若仍黑,检查地形是否加载成功(浮标应有「地形:dem」)。 **Q:不想每次进来都点「点击进入系统」?** `map.intro.enabled = false`。归位按钮不受影响。 **Q:进入引导挡住了自动化/走查?** 同上,或由宿主页注入 `window.__COCKPIT_CONFIG__ = { map: { intro: { enabled: false } } }` 只关这一个用例。 **Q:要换一批数据(另一个区县)?** 把新数据按同样形态准备好 → nginx 挂新目录 → 改 `config.json` 的三个 `url`。前端不用动。