瀏覽代碼

优化微功能场景元素渲染能力,支持元素定位、属性调整。

DESKTOP-6LTVLN7\Liumouren 6 天之前
父節點
當前提交
3f783a3e52
共有 1 個文件被更改,包括 583 次插入17 次删除
  1. 583 17
      src/components/wgn/controlPanel.vue

+ 583 - 17
src/components/wgn/controlPanel.vue

@@ -115,12 +115,21 @@
             <div class="vueJsonEditor_tools">
               <span
                 v-if="jsonData && (jsonData.features || jsonData.geometry)"
-                @click="showToMap(jsonData)"
+                @click="showToMap(jsonData, 'input')"
                 >渲染到地图中</span
               >
+              <el-tooltip content="定位到当前入参渲染要素" placement="top">
+                <span
+                  v-if="renderStatus.input"
+                  @click="locateRenderedGeojson('input')"
+                >
+                  定位
+                </span>
+              </el-tooltip>
               <span @click="copyJsonData(jsonData)">copy</span>
             </div>
             <vue-json-editor
+              :key="'json-input-editor-' + inputEditorKey"
               v-model="jsonData"
               :value="jsonData"
               :show-btns="false"
@@ -151,13 +160,22 @@
                   backData.content &&
                   (backData.content.features || backData.content.geometry)
                 "
-                @click="showToMap(backData.content)"
+                @click="showToMap(backData.content, 'output')"
                 >渲染到地图中</span
               >
+              <el-tooltip content="定位到当前返回渲染要素" placement="top">
+                <span
+                  v-if="renderStatus.output"
+                  @click="locateRenderedGeojson('output')"
+                >
+                  定位
+                </span>
+              </el-tooltip>
               <span @click="copyJsonData(backData.content)">copy</span>
             </div>
             <vue-json-editor
               v-if="backData.content"
+              :key="'json-output-editor-' + outputEditorKey"
               v-model="backData.content"
               :value="backData.content"
               @json-change="handleJsonChange2"
@@ -169,6 +187,56 @@
         ></el-tab-pane>
       </el-tabs>
     </div>
+    <el-dialog
+      v-model="propertyDialog.visible"
+      title="要素属性"
+      width="420px"
+      draggable
+      :modal="false"
+      append-to-body
+      class="feature-property-dialog"
+    >
+      <div class="feature-property-content">
+        <div class="feature-property-tip">支持直接编辑属性值,失焦后自动同步 JSON。</div>
+        <div
+          class="feature-property-item"
+          v-for="item in propertyDialog.list"
+          :key="'feature-property-' + item.id"
+        >
+          <div class="feature-property-row">
+            <el-input
+              v-model="item.editKey"
+              class="feature-property-input feature-property-key-input"
+              placeholder="属性名"
+              @change="handlePropertyKeyChange(item)"
+            />
+            <el-button
+              type="danger"
+              plain
+              size="small"
+              class="feature-property-delete-btn"
+              @click="deleteProperty(item)"
+            >
+              删除
+            </el-button>
+          </div>
+          <el-input
+            v-model="item.editValue"
+            class="feature-property-input"
+            placeholder="属性值"
+            @change="handlePropertyValueChange(item)"
+          />
+        </div>
+        <el-empty
+          v-if="!propertyDialog.list.length"
+          description="当前要素无属性信息"
+          :image-size="80"
+        />
+      </div>
+      <template #footer>
+        <el-button type="primary" plain @click="addProperty">+ 新增属性</el-button>
+      </template>
+    </el-dialog>
     <!-- 绘制工具栏 -->
     <div
       class="toolbar"
@@ -511,6 +579,29 @@ export default {
       currentPolygonEntity: null, // 当前要添加镂空的多边形实体
       currentPolygonGeometry: null, // 当前要添加镂空的多边形几何对象
       tempEntity: null, // 临时预览实体
+      // 地图渲染状态(入参/返回)
+      renderStatus: {
+        input: false,
+        output: false,
+      },
+      // 缓存两侧最近一次成功渲染的geojson,用于定位时恢复
+      renderedGeojsonCache: {
+        input: null,
+        output: null,
+      },
+      currentRenderedSource: "",
+      // 地图点击属性弹窗
+      featurePickHandler: null,
+      inputEditorKey: 0,
+      outputEditorKey: 0,
+      propertyIdSeed: 1,
+      propertyDialog: {
+        visible: false,
+        list: [],
+        source: "",
+        propertiesRef: null,
+        featureIndex: -1,
+      },
     };
   },
   mounted() {
@@ -523,6 +614,10 @@ export default {
     if (this.handler) {
       this.handler.destroy();
     }
+    if (this.featurePickHandler) {
+      this.featurePickHandler.destroy();
+      this.featurePickHandler = null;
+    }
   },
   watch: {
     SceneValue(newVal, oldVal) {
@@ -596,30 +691,346 @@ export default {
           });
       }
     },
+    getSourceGeojsonData(source) {
+      return source === "output" ? this.backData.content : this.jsonData;
+    },
     // 将用户输入或后台返回的geojson渲染到地图中
-    showToMap(geojson) {
+    showToMap(geojson, source = "input") {
+      if (!geojson || (!geojson.features && !geojson.geometry)) {
+        this.$message({
+          message: "当前JSON缺少有效几何信息,无法渲染",
+          type: "warning",
+        });
+        return;
+      }
+      this.currentRenderedSource = source;
       // 1. 清除所有地图中的元素
       this.clearAllMap();
       // 2. 将geojson添加到地图中
       this.addToMap(geojson);
+      this.currentRenderedSource = source;
+      this.renderStatus[source] = true;
+      // 仅缓存纯数据,避免响应式对象带来的引用副作用
+      this.renderedGeojsonCache[source] = JSON.parse(JSON.stringify(geojson));
+      this.flyToGeojson(geojson);
+    },
+    // 定位到已渲染要素
+    locateRenderedGeojson(source) {
+      if (!this.renderStatus[source]) {
+        return;
+      }
+      // 当前地图不是该来源数据时,优先恢复对应渲染结果,再飞行定位
+      if (this.currentRenderedSource !== source && this.renderedGeojsonCache[source]) {
+        this.showToMap(this.renderedGeojsonCache[source], source);
+        return;
+      }
+      const sourceGeojson = this.getSourceGeojsonData(source) || this.renderedGeojsonCache[source];
+      this.flyToGeojson(sourceGeojson);
+    },
+    collectGeometryCoordinates(geometry) {
+      if (!geometry || !geometry.coordinates) {
+        return [];
+      }
+      const { type, coordinates } = geometry;
+      const points = [];
+      if (type === "Point") {
+        points.push([coordinates[0], coordinates[1]]);
+      } else if (type === "LineString") {
+        coordinates.forEach((coord) => points.push([coord[0], coord[1]]));
+      } else if (type === "Polygon") {
+        coordinates.forEach((ring) => {
+          ring.forEach((coord) => points.push([coord[0], coord[1]]));
+        });
+      } else if (type === "MultiPolygon") {
+        coordinates.forEach((polygon) => {
+          polygon.forEach((ring) => {
+            ring.forEach((coord) => points.push([coord[0], coord[1]]));
+          });
+        });
+      }
+      return points;
+    },
+    collectGeojsonCoordinates(geojson) {
+      if (!geojson) {
+        return [];
+      }
+      if (geojson.features && Array.isArray(geojson.features)) {
+        return geojson.features.flatMap((feature) =>
+          this.collectGeometryCoordinates(feature.geometry)
+        );
+      }
+      if (geojson.geometry) {
+        return this.collectGeometryCoordinates(geojson.geometry);
+      }
+      return [];
+    },
+    // 飞行定位到geojson范围
+    flyToGeojson(geojson) {
+      if (!viewer || !geojson) {
+        return;
+      }
+      const points = this.collectGeojsonCoordinates(geojson);
+      if (!points.length) {
+        return;
+      }
+      if (points.length === 1) {
+        viewer.camera.flyTo({
+          destination: SkyScenery.Cartesian3.fromDegrees(points[0][0], points[0][1], 1200),
+          duration: 1.1,
+          orientation: {
+            heading: viewer.camera.heading,
+            pitch: SkyScenery.Math.toRadians(-65),
+            roll: 0,
+          },
+        });
+        viewer.scene.requestRender();
+        return;
+      }
+      let minLon = Infinity;
+      let maxLon = -Infinity;
+      let minLat = Infinity;
+      let maxLat = -Infinity;
+      points.forEach((point) => {
+        minLon = Math.min(minLon, point[0]);
+        maxLon = Math.max(maxLon, point[0]);
+        minLat = Math.min(minLat, point[1]);
+        maxLat = Math.max(maxLat, point[1]);
+      });
+      const lonPad = Math.max((maxLon - minLon) * 0.25, 0.0008);
+      const latPad = Math.max((maxLat - minLat) * 0.25, 0.0008);
+      viewer.camera.flyTo({
+        destination: SkyScenery.Rectangle.fromDegrees(
+          minLon - lonPad,
+          minLat - latPad,
+          maxLon + lonPad,
+          maxLat + latPad
+        ),
+        duration: 1.1,
+      });
+      viewer.scene.requestRender();
+    },
+    // 把属性对象格式化为可编辑列表
+    formatFeatureProperties(properties) {
+      if (!properties || typeof properties !== "object") {
+        return [];
+      }
+      return Object.keys(properties).map((key) => {
+        const rawValue = properties[key];
+        const editValue =
+          rawValue === null || rawValue === undefined
+            ? ""
+            : typeof rawValue === "object"
+            ? JSON.stringify(rawValue)
+            : String(rawValue);
+        return {
+          id: this.propertyIdSeed++,
+          originalKey: key,
+          editKey: key,
+          editValue,
+        };
+      });
+    },
+    parsePropertyValue(inputValue) {
+      if (typeof inputValue !== "string") {
+        return inputValue;
+      }
+      const value = inputValue.trim();
+      if (value === "") {
+        return "";
+      }
+      if (value === "true") {
+        return true;
+      }
+      if (value === "false") {
+        return false;
+      }
+      if (value === "null") {
+        return null;
+      }
+      if (!isNaN(Number(value))) {
+        return Number(value);
+      }
+      if ((value.startsWith("{") && value.endsWith("}")) || (value.startsWith("[") && value.endsWith("]"))) {
+        try {
+          return JSON.parse(value);
+        } catch (e) {
+          return inputValue;
+        }
+      }
+      return inputValue;
+    },
+    syncJsonEditorData(source) {
+      if (source === "output") {
+        if (!this.backData.content) {
+          return;
+        }
+        this.backData = {
+          ...this.backData,
+          content: JSON.parse(JSON.stringify(this.backData.content)),
+        };
+      } else {
+        this.jsonData = JSON.parse(JSON.stringify(this.jsonData));
+      }
+      if (source === "output") {
+        this.outputEditorKey += 1;
+      } else {
+        this.inputEditorKey += 1;
+      }
+      this.renderedGeojsonCache[source] = JSON.parse(
+        JSON.stringify(this.getSourceGeojsonData(source))
+      );
+      const latestGeojson = this.getSourceGeojsonData(source);
+      if (
+        latestGeojson &&
+        this.propertyDialog.visible &&
+        this.propertyDialog.source === source
+      ) {
+        if (
+          this.propertyDialog.featureIndex >= 0 &&
+          latestGeojson.features &&
+          latestGeojson.features[this.propertyDialog.featureIndex]
+        ) {
+          if (!latestGeojson.features[this.propertyDialog.featureIndex].properties) {
+            latestGeojson.features[this.propertyDialog.featureIndex].properties = {};
+          }
+          this.propertyDialog.propertiesRef =
+            latestGeojson.features[this.propertyDialog.featureIndex].properties;
+        } else {
+          if (!latestGeojson.properties) {
+            latestGeojson.properties = {};
+          }
+          this.propertyDialog.propertiesRef = latestGeojson.properties;
+        }
+      }
+    },
+    handlePropertyValueChange(item) {
+      if (!this.propertyDialog.propertiesRef || !item) {
+        return;
+      }
+      const currentKey = (item.editKey || "").trim();
+      if (!currentKey) {
+        this.$message({
+          message: "属性名不能为空",
+          type: "warning",
+        });
+        item.editKey = item.originalKey || "";
+        return;
+      }
+      this.propertyDialog.propertiesRef[currentKey] = this.parsePropertyValue(item.editValue);
+      item.originalKey = currentKey;
+      item.editKey = currentKey;
+      this.syncPropertyToEntity(this.propertyDialog.propertiesRef);
+      this.syncJsonEditorData(this.propertyDialog.source || this.currentRenderedSource || "input");
+    },
+    handlePropertyKeyChange(item) {
+      if (!this.propertyDialog.propertiesRef || !item) {
+        return;
+      }
+      const oldKey = item.originalKey;
+      const newKey = (item.editKey || "").trim();
+      if (!newKey) {
+        this.$message({
+          message: "属性名不能为空",
+          type: "warning",
+        });
+        item.editKey = oldKey;
+        return;
+      }
+      if (newKey !== oldKey && Object.prototype.hasOwnProperty.call(this.propertyDialog.propertiesRef, newKey)) {
+        this.$message({
+          message: "属性名已存在,请更换",
+          type: "warning",
+        });
+        item.editKey = oldKey;
+        return;
+      }
+      const oldValue = this.propertyDialog.propertiesRef[oldKey];
+      if (newKey !== oldKey) {
+        delete this.propertyDialog.propertiesRef[oldKey];
+        this.propertyDialog.propertiesRef[newKey] = oldValue;
+      }
+      item.originalKey = newKey;
+      item.editKey = newKey;
+      this.syncPropertyToEntity(this.propertyDialog.propertiesRef);
+      this.syncJsonEditorData(this.propertyDialog.source || this.currentRenderedSource || "input");
+    },
+    deleteProperty(item) {
+      if (!this.propertyDialog.propertiesRef || !item) {
+        return;
+      }
+      const key = (item.originalKey || "").trim();
+      if (key && Object.prototype.hasOwnProperty.call(this.propertyDialog.propertiesRef, key)) {
+        delete this.propertyDialog.propertiesRef[key];
+      }
+      this.propertyDialog.list = this.propertyDialog.list.filter((property) => property.id !== item.id);
+      this.syncPropertyToEntity(this.propertyDialog.propertiesRef);
+      this.syncJsonEditorData(this.propertyDialog.source || this.currentRenderedSource || "input");
+    },
+    addProperty() {
+      if (!this.propertyDialog.propertiesRef) {
+        this.$message({
+          message: "请先点击地图中的已渲染要素",
+          type: "warning",
+        });
+        return;
+      }
+      let index = 1;
+      let newKey = "newKey";
+      while (Object.prototype.hasOwnProperty.call(this.propertyDialog.propertiesRef, newKey)) {
+        newKey = `newKey${index}`;
+        index += 1;
+      }
+      this.propertyDialog.propertiesRef[newKey] = "";
+      this.propertyDialog.list.push({
+        id: this.propertyIdSeed++,
+        originalKey: newKey,
+        editKey: newKey,
+        editValue: "",
+      });
+      this.syncPropertyToEntity(this.propertyDialog.propertiesRef);
+      this.syncJsonEditorData(this.propertyDialog.source || this.currentRenderedSource || "input");
+    },
+    syncPropertyToEntity(propertiesRef) {
+      if (!propertiesRef || !viewer || !this.currentRenderedSource) {
+        return;
+      }
+      const targetSource = this.propertyDialog.source || this.currentRenderedSource;
+      const targetIndex = this.propertyDialog.featureIndex;
+      this.drawnEntities.forEach((entity) => {
+        if (!entity || !entity.__featureRef) {
+          return;
+        }
+        if (
+          entity.__featureRef.source === targetSource &&
+          entity.__featureRef.featureIndex === targetIndex
+        ) {
+          entity.__featureProperties = propertiesRef;
+        }
+      });
     },
     // 将geojson添加到地图中
     addToMap(geojson) {
+      const source = this.currentRenderedSource || "input";
       if (!geojson.features && geojson.geometry) {
         const { type, coordinates } = geojson.geometry;
+        const featureProperties = geojson.properties || {};
+        const featureRefInfo = {
+          source,
+          featureIndex: -1,
+        };
         switch (type) {
           case "Point":
             // 点
-            this.addPoint(coordinates);
+            this.addPoint(coordinates, featureProperties, featureRefInfo);
             break;
           case "LineString":
             // 线
-            this.addLine(coordinates);
+            this.addLine(coordinates, featureProperties, featureRefInfo);
             break;
           case "Polygon":
           case "MultiPolygon":
             // 面
-            this.addPolygon(coordinates);
+            this.addPolygon(coordinates, featureProperties, featureRefInfo);
             break;
           default:
             break;
@@ -628,21 +1039,26 @@ export default {
         const features = geojson.features;
         // 2. 遍历features,根据type添加到地图中
         console.log("features", features);
-        features.forEach((feature) => {
+        features.forEach((feature, featureIndex) => {
           const { type, coordinates } = feature.geometry;
+          const featureProperties = feature.properties || {};
+          const featureRefInfo = {
+            source,
+            featureIndex,
+          };
           switch (type) {
             case "Point":
               // 点
-              this.addPoint(coordinates);
+              this.addPoint(coordinates, featureProperties, featureRefInfo);
               break;
             case "LineString":
               // 线
-              this.addLine(coordinates);
+              this.addLine(coordinates, featureProperties, featureRefInfo);
               break;
             case "Polygon":
             case "MultiPolygon":
               // 面
-              this.addPolygon(coordinates);
+              this.addPolygon(coordinates, featureProperties, featureRefInfo);
               break;
             default:
               break;
@@ -654,7 +1070,7 @@ export default {
       });
     },
     // 添加点到地图中
-    addPoint(coordinates) {
+    addPoint(coordinates, featureProperties = null, featureRefInfo = null) {
       // 1. 解析点的坐标
       console.log("addPoint coordinates", coordinates);
       // 2. 创建点实体
@@ -676,11 +1092,13 @@ export default {
           outlineWidth: 2,
         },
       });
+      pointEntity.__featureProperties = featureProperties;
+      pointEntity.__featureRef = featureRefInfo;
       // 3. 将点实体添加到drawnEntities中
       this.drawnEntities.push(pointEntity);
     },
     // 添加线到地图中
-    addLine(coordinates) {
+    addLine(coordinates, featureProperties = null, featureRefInfo = null) {
       // 1. 解析线的坐标
       console.log("addLine coordinates", coordinates);
       // 2. 处理坐标格式:如果是二维数组则取第一个元素,否则直接使用
@@ -716,6 +1134,7 @@ export default {
 
       // 5. 创建线实体
       const lineEntity = viewer.entities.add({
+        name: "line",
         polyline: {
           show: true,
           positions: positions,
@@ -723,12 +1142,14 @@ export default {
           width: 3,
         },
       });
+      lineEntity.__featureProperties = featureProperties;
+      lineEntity.__featureRef = featureRefInfo;
 
       // 6. 将线实体添加到drawnEntities中
       this.drawnEntities.push(lineEntity);
     },
     // 添加面到地图中
-    addPolygon(coordinates) {
+    addPolygon(coordinates, featureProperties = null, featureRefInfo = null) {
       // 检测是否为MultiPolygon类型(MultiPolygon的坐标是三维数组)
       if (
         Array.isArray(coordinates[0]) &&
@@ -738,15 +1159,15 @@ export default {
         console.log("MultiPolygon coordinates", coordinates);
         // 是MultiPolygon类型,遍历每个Polygon
         coordinates.forEach((polygonCoordinates) => {
-          this.renderSinglePolygon(polygonCoordinates);
+          this.renderSinglePolygon(polygonCoordinates, featureProperties, featureRefInfo);
         });
       } else {
         // 是单个Polygon类型
-        this.renderSinglePolygon(coordinates);
+        this.renderSinglePolygon(coordinates, featureProperties, featureRefInfo);
       }
     },
     // 渲染单个Polygon
-    renderSinglePolygon(coordinates) {
+    renderSinglePolygon(coordinates, featureProperties = null, featureRefInfo = null) {
       // 1. 处理坐标格式:确保获取外部环坐标
       const outerRingCoordinates =
         Array.isArray(coordinates[0]) && Array.isArray(coordinates[0][0])
@@ -811,6 +1232,8 @@ export default {
           extrudedMaterial: SkyScenery.Color.GREEN.withAlpha(0.8),
         },
       });
+      polygonEntity.__featureProperties = featureProperties;
+      polygonEntity.__featureRef = featureRefInfo;
 
       // 5. 将面实体添加到drawnEntities中
       this.drawnEntities.push(polygonEntity);
@@ -864,12 +1287,99 @@ export default {
       this.clearAll();
       this.jsonData = {};
       this.backData = {};
+      this.renderStatus = {
+        input: false,
+        output: false,
+      };
+      this.renderedGeojsonCache = {
+        input: null,
+        output: null,
+      };
+      this.currentRenderedSource = "";
+      this.propertyDialog.visible = false;
+      this.propertyDialog.list = [];
+      this.propertyDialog.source = "";
+      this.propertyDialog.propertiesRef = null;
+      this.propertyDialog.featureIndex = -1;
       this.SceneValue = emit[emit.length - 1];
     },
     // 初始化绘制处理器
     initDrawHandler() {
       // 创建绘制处理器
-      this.handler = new SkyScenery.ScreenSpaceEventHandler(viewer.canvas);
+      if (!this.handler) {
+        this.handler = new SkyScenery.ScreenSpaceEventHandler(viewer.canvas);
+      }
+      this.initFeaturePickHandler();
+    },
+    // 初始化地图要素拾取处理器(用于属性弹框)
+    initFeaturePickHandler() {
+      if (this.featurePickHandler || !viewer || !viewer.canvas) {
+        return;
+      }
+      this.featurePickHandler = new SkyScenery.ScreenSpaceEventHandler(viewer.canvas);
+      this.featurePickHandler.setInputAction((movement) => {
+        // 绘制模式中不触发属性弹框,避免用户操作冲突
+        if (this.currentTool || this.isDrawingHole) {
+          return;
+        }
+        const pickedObject = viewer.scene.pick(movement.position);
+        if (!SkyScenery.defined(pickedObject) || !SkyScenery.defined(pickedObject.id)) {
+          this.propertyDialog.visible = false;
+          this.propertyDialog.list = [];
+          this.propertyDialog.source = "";
+          this.propertyDialog.propertiesRef = null;
+          this.propertyDialog.featureIndex = -1;
+          return;
+        }
+        const entity = pickedObject.id;
+        const featureIndex =
+          entity.__featureRef && typeof entity.__featureRef.featureIndex === "number"
+            ? entity.__featureRef.featureIndex
+            : -1;
+        const propertySource =
+          (entity.__featureRef && entity.__featureRef.source) ||
+          this.currentRenderedSource ||
+          "input";
+        const geojsonData =
+          this.getSourceGeojsonData(propertySource) || this.renderedGeojsonCache[propertySource];
+        let propertiesRef = null;
+        if (geojsonData) {
+          if (
+            entity.__featureRef &&
+            typeof entity.__featureRef.featureIndex === "number" &&
+            entity.__featureRef.featureIndex >= 0 &&
+            geojsonData.features &&
+            geojsonData.features[entity.__featureRef.featureIndex]
+          ) {
+            if (!geojsonData.features[entity.__featureRef.featureIndex].properties) {
+              geojsonData.features[entity.__featureRef.featureIndex].properties = {};
+            }
+            propertiesRef = geojsonData.features[entity.__featureRef.featureIndex].properties;
+          } else {
+            if (!geojsonData.properties) {
+              geojsonData.properties = {};
+            }
+            propertiesRef = geojsonData.properties;
+          }
+        }
+        const propertyList = this.formatFeatureProperties(
+          propertiesRef || entity.__featureProperties || {}
+        );
+        if (propertiesRef || (entity.__featureProperties && typeof entity.__featureProperties === "object")) {
+          this.propertyDialog.source = propertySource;
+          this.propertyDialog.propertiesRef =
+            propertiesRef || entity.__featureProperties || {};
+          this.propertyDialog.featureIndex = featureIndex;
+          this.propertyDialog.list = propertyList;
+          this.propertyDialog.visible = true;
+        } else {
+          this.propertyDialog.visible = false;
+          this.propertyDialog.list = [];
+          this.propertyDialog.source = "";
+          this.propertyDialog.propertiesRef = null;
+          this.propertyDialog.featureIndex = -1;
+        }
+      }, SkyScenery.ScreenSpaceEventType.LEFT_CLICK);
     },
 
     // 激活绘制工具
@@ -1594,6 +2104,12 @@ export default {
       // 清空数组
       this.drawnEntities = [];
       this.geometries = [];
+      this.currentRenderedSource = "";
+      this.propertyDialog.visible = false;
+      this.propertyDialog.list = [];
+      this.propertyDialog.source = "";
+      this.propertyDialog.propertiesRef = null;
+      this.propertyDialog.featureIndex = -1;
       // 取消当前绘制模式
       this.deactivateDraw();
       console.log("已清除所有绘制的元素");
@@ -1679,6 +2195,56 @@ export default {
     border-color: rgba(255, 255, 255, 0.4);
   }
 }
+.feature-property-content {
+  max-height: 360px;
+  overflow: auto;
+  padding-right: 4px;
+}
+.feature-property-tip {
+  color: #9ed2ff;
+  margin-bottom: 12px;
+  font-size: 13px;
+}
+.feature-property-item {
+  padding: 10px 12px;
+  margin-bottom: 8px;
+  border-radius: 8px;
+  background: rgba(61, 132, 205, 0.14);
+  border: 1px solid rgba(111, 186, 255, 0.35);
+}
+.feature-property-row {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  margin-bottom: 8px;
+}
+.feature-property-delete-btn {
+  flex-shrink: 0;
+}
+.feature-property-key-input {
+  width: 180px;
+}
+:deep(.feature-property-input .el-input__wrapper) {
+  background: rgba(8, 34, 74, 0.68);
+  box-shadow: inset 0 0 0 1px rgba(130, 198, 255, 0.4);
+}
+:deep(.feature-property-input .el-input__inner) {
+  color: #ffffff;
+}
+:deep(.feature-property-dialog .el-dialog) {
+  background: rgba(7, 24, 48, 0.95);
+  border: 1px solid rgba(111, 186, 255, 0.5);
+}
+:deep(.feature-property-dialog .el-dialog__title) {
+  color: #e8f4ff;
+  font-weight: 700;
+}
+:deep(.feature-property-dialog .el-dialog__headerbtn .el-dialog__close) {
+  color: #d8ecff;
+}
+:deep(.feature-property-dialog .el-dialog__body) {
+  padding-top: 12px;
+}
 :deep(.ace_editor) {
   height: 600px !important;
   max-height: calc(100vh - 300px) !important;