@yangyongtao/gaea 1.1.19 → 1.1.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yangyongtao/gaea",
3
- "version": "1.1.19",
3
+ "version": "1.1.21",
4
4
  "description": "Gaea 3D visualization component library - Vue 3 components based on Godot WASM engine. Includes WMTS layer loading, camera view control, Vite plugin auto-injection, etc.",
5
5
  "type": "module",
6
6
  "main": "src/index.cjs",
@@ -46,9 +46,17 @@ import Gaea from '../public/index.js';
46
46
  transition: background 0.2s; backdrop-filter: blur(4px);
47
47
  }
48
48
  #gaea-reset-btn:hover { background: rgba(0,0,0,0.7); }
49
+ #gaea-layertree-btn {
50
+ position: fixed; top: 16px; right: 60px; z-index: 99990;
51
+ width: 36px; height: 36px; border-radius: 6px; border: none;
52
+ background: rgba(0,0,0,0.5); color: #fff; cursor: pointer;
53
+ font-size: 16px; display: flex; align-items: center; justify-content: center;
54
+ transition: background 0.2s; backdrop-filter: blur(4px);
55
+ }
56
+ #gaea-layertree-btn:hover { background: rgba(0,0,0,0.7); }
49
57
  /* 屏幕图标样式(对齐 ht_3dview) */
50
58
  .gaea-locationLineStyle {
51
- position: absolute; overflow: hidden; z-index: 999; pointer-events: auto;
59
+ position: absolute; overflow: hidden; z-index: 99; pointer-events: auto;
52
60
  user-select: none; -webkit-user-select: none;
53
61
  }
54
62
  .gaea-locationLineStyle .text-wrapper {
@@ -67,7 +75,7 @@ import Gaea from '../public/index.js';
67
75
  }
68
76
  .gaea-iconscreen {
69
77
  position: absolute; overflow: hidden; width: 169px; height: 55px;
70
- user-select: none; -webkit-user-select: none; z-index: 999; pointer-events: auto;
78
+ user-select: none; -webkit-user-select: none; z-index: 99; pointer-events: auto;
71
79
  }
72
80
  .gaea-iconscreen img {
73
81
  position: absolute; width: 100%; height: 100%;
@@ -146,12 +154,16 @@ const defaultConfig = {
146
154
  { name: '苍南县村边界', range: [15, 19], color: [255, 255, 255], index: 60 },
147
155
  { name: '沿浦海塘工程范围线', range: [0, 21], color: [0, 255, 255], index: 120 },
148
156
  ],
157
+ /** 默认加载的倾斜摄影 3D Tiles(空数组则不加载) */
158
+ default3DTiles: [],
149
159
  /** 默认相机视角 */
150
160
  defaultCameraPosition: '27.3680928977018,120.414139621887,0,109379.690132486,0,0',
151
161
  /** 引擎加载进度回调 */
152
162
  onProgress: null,
153
163
  /** DataHub API 地址(图层树数据接口),为空则只用默认数据 */
154
164
  dataHubApiUrl: '',
165
+ /** 是否显示图层树按钮(右上角重置按钮旁边) */
166
+ showLayerTreeBtn: false,
155
167
  /** 图层树分类数据(默认数据,接口数据会合并进来) */
156
168
  layerCategories: [
157
169
  { key: 'seawall', text: '海塘', visible: true, children: [
@@ -265,6 +277,8 @@ export function getConfig() { return _config; }
265
277
 
266
278
  // ==================== GaeaApp ====================
267
279
 
280
+ console.log('[GaeaMethods] 模块已加载 - setCategoryVisible 可用');
281
+
268
282
  export class GaeaApp {
269
283
  constructor(options = {}) {
270
284
  this._options = options;
@@ -272,10 +286,10 @@ export class GaeaApp {
272
286
  this.tLayers = [];
273
287
  this.layerCategories = this.config.layerCategories || [];
274
288
  this._screenIconList = [];
289
+ this._hiddenCategories = new Set();
275
290
  this._secIsClickArr = [];
276
291
  this._secScreenIconKeyValue = {};
277
292
  this._groundPolygonElements = [];
278
- this._categoryIcons = {};
279
293
  this.tweenGroup = new TWEEN.Group();
280
294
  this._tourConfig = {
281
295
  '沿浦水闸': { direction: [-0.5, 0.3, 0.3], positionList: [
@@ -340,6 +354,7 @@ export class GaeaApp {
340
354
  // 影像/地形就绪即可显示地图,隐藏 loading,避免用户等待模型/接口
341
355
  hideLoading();
342
356
  this._addResetButton();
357
+ this._addLayerTreeButton();
343
358
  console.log('[GaeaApp] 地图已就绪,模型与图层树数据后台加载中...');
344
359
 
345
360
  // 剩余重资源(模型位置、GLTF、图层树接口、屏幕图标)后台异步加载,不阻塞地图显示
@@ -460,7 +475,7 @@ export class GaeaApp {
460
475
  const cfg = this.config;
461
476
 
462
477
  if (cfg.serviceMode === 'online') {
463
- // 在线模式:仅加载天地图影像,不加载地形/贴地线
478
+ // 在线模式:仅加载天地图影像,不加载地形/贴地线/3D Tiles
464
479
  this.addTianditu();
465
480
  } else {
466
481
  // 服务平台模式:WMTS 影像 + 贴地线 + 地形
@@ -475,6 +490,9 @@ export class GaeaApp {
475
490
  try { this.addGroundLines(); } catch (e) { console.error('加载贴地线失败:', e); }
476
491
  // 地形服务
477
492
  try { await this.addInitTerrainServer(); } catch (e) { console.error('加载地形失败:', e); }
493
+ // 倾斜摄影 3D Tiles(isInitLoad 为 true 的自动加载,依赖地形服务)
494
+ const tiles = cfg.default3DTiles || []
495
+ tiles.filter(item => item.isInitLoad).forEach(item => this.add3DTiles(item))
478
496
  }
479
497
 
480
498
  const pos = cfg.defaultCameraPosition;
@@ -487,19 +505,33 @@ export class GaeaApp {
487
505
 
488
506
  // ---- 天地图 ----
489
507
 
490
- /** 加载天地图影像 */
508
+ /** 加载天地图影像 + 注记 */
491
509
  addTianditu() {
492
510
  const cfg = this.config;
493
- const store = new Gaea.TDTWMTSImageStore();
494
- store.ServerUri = cfg.tiandituServerUri || 'https://t{0}.tianditu.gov.cn/img_c/wmts';
495
- store.FormatString = cfg.tiandituFormatString || 'SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=img&STYLE=default&TILEMATRIXSET=c&FORMAT=tiles&TILECOL={0}&TILEROW={1}&TILEMATRIX={2}&tk={3}';
496
- store.ImageExtension = cfg.tiandituImageExtension ?? Gaea.ImageExtensionEnum.JPEG;
497
- store.TK = cfg.tiandituToken;
498
- store.Name = '影像底图';
499
- store.LevelRange = new Gaea.Vector2(cfg.tiandituLevelRange[0], cfg.tiandituLevelRange[1]);
500
- Gaea.World.Instance.DefaultPyramidTileSet.AddImageStore(store);
511
+
512
+ // 影像底图(卫星图)
513
+ const imgStore = new Gaea.TDTWMTSImageStore();
514
+ imgStore.ServerUri = cfg.tiandituServerUri || 'https://t{0}.tianditu.gov.cn/img_c/wmts';
515
+ imgStore.FormatString = cfg.tiandituFormatString || 'SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=img&STYLE=default&TILEMATRIXSET=c&FORMAT=tiles&TILECOL={0}&TILEROW={1}&TILEMATRIX={2}&tk={3}';
516
+ imgStore.ImageExtension = cfg.tiandituImageExtension ?? Gaea.ImageExtensionEnum.JPEG;
517
+ imgStore.TK = cfg.tiandituToken;
518
+ imgStore.Name = '影像底图';
519
+ imgStore.LevelRange = new Gaea.Vector2(cfg.tiandituLevelRange[0], cfg.tiandituLevelRange[1]);
520
+ Gaea.World.Instance.DefaultPyramidTileSet.AddImageStore(imgStore);
501
521
  console.log('天地图影像加载成功');
502
- return store;
522
+
523
+ // 注记图层(道路/地名标注)
524
+ const labelStore = new Gaea.TDTWMTSImageStore();
525
+ labelStore.ServerUri = 'https://t{0}.tianditu.gov.cn/cia_c/wmts';
526
+ labelStore.FormatString = 'SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cia&STYLE=default&TILEMATRIXSET=c&FORMAT=image/png&TILECOL={0}&TILEROW={1}&TILEMATRIX={2}&tk={3}';
527
+ labelStore.ImageExtension = Gaea.ImageExtensionEnum.PNG;
528
+ labelStore.TK = cfg.tiandituToken;
529
+ labelStore.Name = '天地图注记';
530
+ labelStore.LevelRange = new Gaea.Vector2(9, 15);
531
+ Gaea.World.Instance.DefaultPyramidTileSet.AddImageStore(labelStore);
532
+ console.log('天地图注记加载成功');
533
+
534
+ return { imgStore, labelStore };
503
535
  }
504
536
 
505
537
  /** 添加右上角重置按钮 */
@@ -550,6 +582,19 @@ export class GaeaApp {
550
582
  document.body.appendChild(btn);
551
583
  }
552
584
 
585
+ /** 添加图层树按钮 */
586
+ _addLayerTreeButton() {
587
+ if (document.getElementById('gaea-layertree-btn')) return
588
+ const btn = document.createElement('button')
589
+ btn.id = 'gaea-layertree-btn'
590
+ btn.title = '图层控制'
591
+ btn.innerHTML = '☰'
592
+ btn.onclick = () => {
593
+ document.dispatchEvent(new CustomEvent('gaea-toggle-layertree'))
594
+ }
595
+ document.body.appendChild(btn)
596
+ }
597
+
553
598
  // ---- GLTF 模型 ----
554
599
 
555
600
  async _loadGltfModels() {
@@ -806,18 +851,13 @@ export class GaeaApp {
806
851
  };
807
852
  console.log('[GaeaApp] 开始创建屏幕图标... categories:', cats.length);
808
853
  let count = 0;
809
- this._categoryIcons = {};
810
854
  for (const cat of cats) {
811
855
  if (!cat.children?.length) continue;
812
- this._categoryIcons[cat.key] = [];
813
856
  const cfg = catConfig[cat.key] || { img: '', method: 'textImage', visibleRange: [10, 50000], offset: [-40, -35], drawLine: false };
814
857
  for (const child of cat.children) {
815
858
  if (!child.position) continue;
816
859
  count++;
817
860
  try {
818
- const safeId = child.text.replace(/[^a-zA-Z0-9\u4e00-\u9fa5]/g, '_');
819
- const divId = 'gaea-icon-' + safeId;
820
- this._categoryIcons[cat.key].push(divId);
821
861
  this.addScreenIcon({
822
862
  name: child.text,
823
863
  text: child.text,
@@ -828,6 +868,7 @@ export class GaeaApp {
828
868
  visibleRange: cfg.visibleRange,
829
869
  anchorType: cfg.anchorType,
830
870
  drawLine: cfg.drawLine,
871
+ category: cat.key,
831
872
  onClick: (name, data) => {
832
873
  if (cfg.onClick) cfg.onClick(name, { ...child, ...data });
833
874
  },
@@ -836,8 +877,14 @@ export class GaeaApp {
836
877
  console.warn(`[GaeaApp] 图标 ${child.text} 创建失败:`, e.message);
837
878
  }
838
879
  }
880
+ // 根据配置的 visible 决定初始显隐(不影响创建,只控制初始状态)
881
+ if (cat.visible === false) {
882
+ this.setCategoryVisible(cat.key, false);
883
+ }
839
884
  }
840
885
  console.log('[GaeaApp] 屏幕图标创建完成, 共:', count);
886
+ // 通知外部图标已就绪
887
+ document.dispatchEvent(new CustomEvent('gaea-icons-ready'));
841
888
  }
842
889
 
843
890
  // ---- 图层分类显隐控制 ----
@@ -848,23 +895,35 @@ export class GaeaApp {
848
895
  * @param {boolean} visible 是否可见
849
896
  */
850
897
  setCategoryVisible(categoryKey, visible) {
851
- const divIds = this._categoryIcons?.[categoryKey];
852
- if (!divIds?.length) return;
853
- divIds.forEach(divId => {
854
- const el = document.getElementById(divId);
855
- if (el) el.style.display = visible ? '' : 'none';
898
+ // 记录分类显隐状态
899
+ if (visible) {
900
+ this._hiddenCategories.delete(categoryKey)
901
+ } else {
902
+ this._hiddenCategories.add(categoryKey)
903
+ }
904
+ const els = document.querySelectorAll('.gaea-cat-' + categoryKey);
905
+ console.log('[GaeaApp] setCategoryVisible:', categoryKey, visible, '元素数:', els.length);
906
+ els.forEach(el => { el.style.display = visible ? '' : 'none'; });
907
+ // 同步引擎可见性
908
+ const items = this._screenIconList.filter(item => item.category === categoryKey);
909
+ items.forEach(item => {
910
+ if (visible) {
911
+ item.element.UseAutoVisible();
912
+ item.element.EnablePick = true;
913
+ } else {
914
+ item.element.ForceSetVisible(false);
915
+ item.element.EnablePick = false;
916
+ }
856
917
  });
857
918
  }
858
919
 
859
920
  /**
860
- * 获取某个分类的当前可见性(根据第一个图标的 display 状态判断)
921
+ * 获取某个分类的当前可见性(根据 DOM 元素判断)
861
922
  * @param {string} categoryKey
862
923
  * @returns {boolean}
863
924
  */
864
925
  getCategoryVisible(categoryKey) {
865
- const divIds = this._categoryIcons?.[categoryKey];
866
- if (!divIds?.length) return true;
867
- const el = document.getElementById(divIds[0]);
926
+ const el = document.querySelector('.gaea-cat-' + categoryKey);
868
927
  return el ? el.style.display !== 'none' : true;
869
928
  }
870
929
 
@@ -1087,6 +1146,68 @@ export class GaeaApp {
1087
1146
  this.SetPosition(`${x},${y},${z},${h},0,0`);
1088
1147
  }
1089
1148
 
1149
+ /**
1150
+ * 根据名称或 id 定位到海塘
1151
+ * @param {string} name - 海塘名称或 id
1152
+ * @param {string} [tilesetUrl] - 可选,倾斜摄影 3D Tiles 的 tileset.json 地址
1153
+ */
1154
+ flyToSeawall = (name, tilesetUrl) => {
1155
+ const cats = this.layerCategories || []
1156
+ for (const cat of cats) {
1157
+ if (!cat.children?.length) continue
1158
+ const found = cat.children.find(c => c.text === name || c.id === name)
1159
+ if (found) {
1160
+ this.flyToNode(found)
1161
+ if (tilesetUrl) {
1162
+ this.add3DTiles(tilesetUrl)
1163
+ }
1164
+ return
1165
+ }
1166
+ }
1167
+ console.warn(`[GaeaApp] 未找到海塘: ${name}`)
1168
+ };
1169
+
1170
+ /**
1171
+ * 加载倾斜摄影 3D Tiles
1172
+ * @param {Object} item - { jsonUrl: tileset.json地址, tilName?: 名称, height?: 高度偏移, isInitLoad?: 是否初始化加载 }
1173
+ */
1174
+ add3DTiles = (item) => {
1175
+ const tilesetUrl = typeof item === 'string' ? item : item.jsonUrl
1176
+ const name = typeof item === 'string' ? '' : (item.tilName || '')
1177
+ const height = typeof item === 'string' ? 0 : (item.height || 0)
1178
+ const tileset = new Gaea.Gaea3DTileset()
1179
+ tileset.Name = name
1180
+ tileset.TilesetUrl = tilesetUrl
1181
+ tileset.EnablePick = true
1182
+ tileset.UseLight = false
1183
+ tileset.SkipLevelOfDetail = false
1184
+ tileset.IsZUp = true
1185
+ tileset.RecalculateNormals = true
1186
+ tileset.BaseUrl = tilesetUrl.slice(0, tilesetUrl.lastIndexOf('/') + 1)
1187
+ tileset.InitTileSet(name, tilesetUrl)
1188
+ tileset.MaximumMemoryUsage = 512
1189
+ tileset.MaximumScreenSpaceError = 28
1190
+ if (height) {
1191
+ tileset.LocalOffset = new Gaea.Vector3(0, height, 0)
1192
+ }
1193
+ Gaea.World.Instance.RenderableObjectList.AddLast(tileset)
1194
+ console.log('[GaeaApp] 3D Tiles 已加载:', name || tilesetUrl, height ? `高度: ${height}` : '')
1195
+ return tileset
1196
+ };
1197
+
1198
+ /**
1199
+ * 根据经纬度飞到指定位置
1200
+ * @param {number} lat - 纬度
1201
+ * @param {number} lng - 经度
1202
+ * @param {number} [z=0] - 地面高程
1203
+ * @param {number} [alt=500] - 相机高度
1204
+ * @param {number} [pitch=0] - 俯仰角,默认 0(垂直俯视),负值向前倾斜
1205
+ * @param {number} [yaw=0] - 偏航角
1206
+ */
1207
+ flyToPosition = (lat, lng, z = 0, alt = 500, pitch = 0, yaw = 0) => {
1208
+ this.SetPosition(`${lat},${lng},${z},${alt},${pitch},${yaw}`)
1209
+ };
1210
+
1090
1211
  // ---- 沿线飞行 ----
1091
1212
 
1092
1213
  /**
@@ -1210,7 +1331,7 @@ export class GaeaApp {
1210
1331
  * 添加屏幕图标(参考 ht_3dview 三种创建方式)
1211
1332
  * @param {string} opts.method - 'textImage' | 'styled' | 'imageOnly'
1212
1333
  */
1213
- addScreenIcon({ name, text, imgUrl, position, method = 'textImage', offset = [0, 0], visibleRange = [10, 50000], anchorType = 6, drawLine = false, onClick, type = null, divId: externalDivId, elementName, elementChildrenIndex } = {}) {
1334
+ addScreenIcon({ name, text, imgUrl, position, method = 'textImage', offset = [0, 0], visibleRange = [10, 50000], anchorType = 6, drawLine = false, onClick, type = null, category = null, divId: externalDivId, elementName, elementChildrenIndex } = {}) {
1214
1335
  const safeId = name.replace(/[^a-zA-Z0-9\u4e00-\u9fa5]/g, '_');
1215
1336
  const divId = externalDivId || 'gaea-icon-' + safeId;
1216
1337
 
@@ -1264,12 +1385,17 @@ export class GaeaApp {
1264
1385
 
1265
1386
  } // end else (externalDivId)
1266
1387
 
1388
+ // 打分类标记,用于按类别显隐
1389
+ if (category) {
1390
+ container.classList.add('gaea-cat-' + category);
1391
+ }
1392
+
1267
1393
  if (!externalDivId) {
1268
1394
  document.body.appendChild(container);
1269
1395
  }
1270
1396
 
1271
1397
  container.style.position = 'absolute';
1272
- container.style.zIndex = '9999';
1398
+ container.style.zIndex = '99';
1273
1399
  container.style.pointerEvents = 'auto';
1274
1400
  container.style.cursor = 'pointer';
1275
1401
 
@@ -1313,7 +1439,7 @@ export class GaeaApp {
1313
1439
  console.log('[GaeaApp] child:', !!child);
1314
1440
  if (child) {
1315
1441
  child.AddChild(screen, false);
1316
- const screenItem = { element: screen, type: type || null };
1442
+ const screenItem = { element: screen, type: type || null, category };
1317
1443
  this._screenIconList.push(screenItem);
1318
1444
  return screen;
1319
1445
  }
@@ -1324,7 +1450,7 @@ export class GaeaApp {
1324
1450
 
1325
1451
  screen.GeographyPosition = new Gaea.Vector3(position.x, position.y, position.z || 0);
1326
1452
  Gaea.World.Instance.RenderableObjectList.AddLast(screen);
1327
- const screenItem = { element: screen, type: type || null };
1453
+ const screenItem = { element: screen, type: type || null, category };
1328
1454
  this._screenIconList.push(screenItem);
1329
1455
  return screen;
1330
1456
  }
@@ -1339,6 +1465,35 @@ export class GaeaApp {
1339
1465
  Gaea.World.Instance.DefaultCamera.GotoFavorite(uri);
1340
1466
  }
1341
1467
 
1468
+ /**
1469
+ * 添加点位标记到 3D 地图
1470
+ * @param {Object} opts
1471
+ * @param {string} opts.name - 唯一标识名
1472
+ * @param {string} [opts.text] - 显示文本
1473
+ * @param {number} opts.lat - 纬度
1474
+ * @param {number} opts.lng - 经度
1475
+ * @param {number} [opts.z=0] - 高度
1476
+ * @param {string} [opts.imgUrl] - 图标地址
1477
+ * @param {'textImage'|'styled'|'imageOnly'} [opts.method='textImage'] - 显示样式
1478
+ * @param {Array<number>} [opts.offset=[0,0]] - 屏幕偏移
1479
+ * @param {Function} [opts.onClick] - 点击回调
1480
+ * @param {string} [opts.category] - 分类标识(用于图层树显隐控制)
1481
+ * @returns {Gaea.ScreenGUIElement} screen 元素
1482
+ */
1483
+ addMarker({ name, text, lat, lng, z = 0, imgUrl, method = 'textImage', offset = [0, 0], onClick, category } = {}) {
1484
+ return this.addScreenIcon({
1485
+ name,
1486
+ text: text || name,
1487
+ imgUrl,
1488
+ position: { x: lat, y: lng, z },
1489
+ method,
1490
+ offset,
1491
+ onClick,
1492
+ category,
1493
+ type: 'marker'
1494
+ })
1495
+ }
1496
+
1342
1497
  // ---- 引擎动画方法 ----
1343
1498
 
1344
1499
  hiddenScreenWithClass(className) {
@@ -1854,6 +2009,30 @@ export class GaeaApp {
1854
2009
  this._typhoonElement = null;
1855
2010
  }
1856
2011
  };
2012
+
2013
+ /** 显示所有屏幕标记 */
2014
+ showAllMarkers = () => {
2015
+ this._screenIconList.forEach(item => {
2016
+ // 跳过被隐藏分类的标记
2017
+ if (this._hiddenCategories.has(item.category)) return
2018
+ item.element.UseAutoVisible()
2019
+ item.element.EnablePick = true
2020
+ // 同步显示 DOM 元素
2021
+ const el = document.getElementById(item.element.DivID)
2022
+ if (el) el.style.display = ''
2023
+ })
2024
+ };
2025
+
2026
+ /** 隐藏所有屏幕标记 */
2027
+ hideAllMarkers = () => {
2028
+ this._screenIconList.forEach(item => {
2029
+ item.element.ForceSetVisible(false)
2030
+ item.element.EnablePick = false
2031
+ // 同步隐藏 DOM 元素
2032
+ const el = document.getElementById(item.element.DivID)
2033
+ if (el) el.style.display = 'none'
2034
+ })
2035
+ };
1857
2036
  }
1858
2037
 
1859
2038
  export default GaeaApp;
@@ -18,14 +18,9 @@
18
18
  <span v-else>▾</span>
19
19
  </span>
20
20
  <!-- 分类复选框 -->
21
- <label class="category-checkbox" @click.stop>
22
- <input
23
- type="checkbox"
24
- :checked="categoryVisibility[rootNode.key] !== false"
25
- @change="onCategoryToggle(rootNode.key, $event.target.checked)"
26
- />
27
- <span class="checkmark"></span>
28
- </label>
21
+ <div class="category-checkbox" @click.stop="onCategoryToggle(rootNode.key, !categoryVisibility[rootNode.key])">
22
+ <span class="checkmark" :class="{ checked: categoryVisibility[rootNode.key] !== false }"></span>
23
+ </div>
29
24
  <span class="node-text" :class="{ 'expanded-yellow': rootNode.isExpanded }">{{ rootNode.text }}</span>
30
25
  </div>
31
26
 
@@ -63,12 +58,10 @@ function handleHideLayerTree() {
63
58
 
64
59
  onMounted(() => {
65
60
  const raw = props.appInstance?.layerCategories || [];
66
- categories.value = raw
67
- .filter(c => c.visible !== false)
68
- .map(c => ({ ...c, isExpanded: false }));
69
- // 初始化每个分类的可见性状态
61
+ categories.value = raw.map(c => ({ ...c, isExpanded: false }));
62
+ // 初始化每个分类的可见性状态:按配置的 visible 字段
70
63
  for (const cat of categories.value) {
71
- categoryVisibility[cat.key] = props.appInstance?.getCategoryVisible?.(cat.key) ?? true;
64
+ categoryVisibility[cat.key] = cat.visible !== false;
72
65
  }
73
66
  window.addEventListener('gaea-hide-layertree', handleHideLayerTree);
74
67
  });
@@ -78,6 +71,7 @@ onBeforeUnmount(() => {
78
71
  });
79
72
 
80
73
  function onCategoryToggle(categoryKey, checked) {
74
+ console.log('[LayerTree] onCategoryToggle:', categoryKey, checked, 'appInstance:', !!props.appInstance, 'setCategoryVisible:', !!props.appInstance?.setCategoryVisible);
81
75
  categoryVisibility[categoryKey] = checked;
82
76
  props.appInstance?.setCategoryVisible?.(categoryKey, checked);
83
77
  }
@@ -129,8 +123,8 @@ const filteredData = computed(() => {
129
123
  <style scoped>
130
124
  .layer-tree-dialog {
131
125
  position: fixed;
132
- top: 10px;
133
- left: 10px;
126
+ top: 120px;
127
+ right: 20px;
134
128
  width: 300px;
135
129
  background: var(--lt-bg);
136
130
  border-radius: var(--lt-border-radius);
@@ -312,7 +306,6 @@ const filteredData = computed(() => {
312
306
 
313
307
  /* ===== 分类复选框 ===== */
314
308
  .category-checkbox {
315
- position: relative;
316
309
  display: inline-flex;
317
310
  align-items: center;
318
311
  justify-content: center;
@@ -323,13 +316,6 @@ const filteredData = computed(() => {
323
316
  user-select: none;
324
317
  }
325
318
 
326
- .category-checkbox input[type="checkbox"] {
327
- position: absolute;
328
- opacity: 0;
329
- width: 0;
330
- height: 0;
331
- }
332
-
333
319
  .checkmark {
334
320
  width: 100%;
335
321
  height: 100%;
@@ -342,12 +328,12 @@ const filteredData = computed(() => {
342
328
  transition: all 0.15s ease;
343
329
  }
344
330
 
345
- .category-checkbox input:checked + .checkmark {
331
+ .checkmark.checked {
346
332
  background: var(--lt-checkbox-checked-bg);
347
333
  border-color: var(--lt-checkbox-checked-bg);
348
334
  }
349
335
 
350
- .category-checkbox input:checked + .checkmark::after {
336
+ .checkmark.checked::after {
351
337
  content: '';
352
338
  width: 4px;
353
339
  height: 8px;