瀏覽代碼

Merge branch 'MicroFunction' of http://47.103.92.60:3003/skyversation/qp_onemap_ui into onemap_zmg

DESKTOP-6LTVLN7\Liumouren 21 小時之前
父節點
當前提交
39f3256159
共有 7 個文件被更改,包括 956 次插入252 次删除
  1. 1 1
      public/static/config/config.js
  2. 23 17
      src/api/rwgl.js
  3. 22 19
      src/components/AppVue/numberScroll.vue
  4. 583 17
      src/components/wgn/controlPanel.vue
  5. 12 32
      src/utils/request.js
  6. 1 0
      src/views/Wgn.vue
  7. 314 166
      src/views/rwgl/Index.vue

+ 1 - 1
public/static/config/config.js

@@ -3,7 +3,7 @@ let systemConfig = {
     defaultAccount: {
         username: "user_yztmh_dev",
     },
-    touristUserId: "191", //默认游客用户(user002)id,Oauth中配置
+    touristUserId: "191", //默认游客用户(user_yztmh_dev)id,Oauth中配置
     adminRoleId: "1", //默认管理员角色id,Oauth中配置“系统管理员”角色
     /*
          * 模型id如下

+ 23 - 17
src/api/rwgl.js

@@ -1,12 +1,14 @@
 import {
-    postform
+    postform,
+    post
 } from '../utils/request'
 import content from './content'
 
 
 const dmsPath = systemConfig.dmsDataProxy
 const multiSearch = dmsPath + "/content/multipleFormsOfJointInvestigation"
-const taskSearch = systemConfig.baseServicerPath+"/task/getTask"
+const taskSearch = systemConfig.baseServicerPath + "/task/getTask"
+const taskExecute = "/oneMap/task/execute"
 
 const taskDmsId = systemConfig.columnIds[6]
 
@@ -17,26 +19,26 @@ export function getCName(cName) {
     }))
 }
 
-export function getTasks(page, pageSize, name, status,type) {
+export function getTasks(page, pageSize, name, status, type) {
     let data = {
         "columnId": taskDmsId,
         "autoSelectItem": true,
         "page": page,
         "pageSize": pageSize,
         "columnAlias": "main",
-        "orderBy":"main,c_start_time,desc",
+        "orderBy": "main,c_start_time,desc",
         "conditionsList": JSON.stringify([
-            ...getEqualChecker(name,"c_name"),
-            ...getTypeChecker(status,"c_state"),
-            ...getTypeChecker(type,"c_type"),
+            ...getEqualChecker(name, "c_name"),
+            ...getTypeChecker(status, "c_state"),
+            ...getTypeChecker(type, "c_type"),
         ]),
     }
 
     return resolveDmsMultiTableResult(postform(taskSearch, data));
 }
 
-function getEqualChecker(value,param) {
-    if (value == null||value=="") {
+function getEqualChecker(value, param) {
+    if (value == null || value == "") {
         return []
     }
     return [{
@@ -46,22 +48,22 @@ function getEqualChecker(value,param) {
         "value": `%${value}%`
     }]
 }
-function getTypeChecker(value,param) {
-    
+function getTypeChecker(value, param) {
+
     if (value == null || value.length == 0) {
         return []
     }
     let output = []
     for (let i = 0; i < value.length; i++) {
         const e = value[i];
-        output.push(e+"")
+        output.push(e + "")
     }
     return [{
-            "columnId": "main",
-            "columnName": param,
-            "condition": "in",
-            "value": output
-        }]
+        "columnId": "main",
+        "columnName": param,
+        "condition": "in",
+        "value": output
+    }]
 }
 
 async function resolveResult(result) {
@@ -82,4 +84,8 @@ async function resolveDmsMultiTableResult(result) {
     } else {
         return null;
     }
+}
+
+export function executeTask(taskId) {
+    return post(taskExecute, { taskId });
 }

+ 22 - 19
src/components/AppVue/numberScroll.vue

@@ -3,36 +3,39 @@
 </template>
 
 <script setup>
-import { ref, watch, onMounted } from 'vue'
+import { ref, watch, onMounted } from "vue";
 
 const props = defineProps({
   value: { type: Number, default: 0 },
   duration: { type: Number, default: 1500 },
-  easing: { type: Function, default: (t) => t * (2 - t) }
-})
+  easing: { type: Function, default: (t) => t * (2 - t) },
+});
 
-const displayValue = ref(0)
-const numberEl = ref(null)
+const displayValue = ref(0);
+const numberEl = ref(null);
 
 function animate(start, end) {
-  const startTime = performance.now()
+  const startTime = performance.now();
   const frame = (currentTime) => {
-    const elapsed = currentTime - startTime
-    const progress = Math.min(elapsed / props.duration, 1)
-    displayValue.value = Math.floor(start + (end - start) * props.easing(progress))
-    displayValue.value = displayValue.value.toLocaleString()
+    const elapsed = currentTime - startTime;
+    const progress = Math.min(elapsed / props.duration, 1);
+    displayValue.value = Math.floor(start + (end - start) * props.easing(progress));
+    displayValue.value = displayValue.value.toLocaleString();
     if (progress < 1) {
-      requestAnimationFrame(frame)
+      requestAnimationFrame(frame);
     }
-  }
-  requestAnimationFrame(frame)
+  };
+  requestAnimationFrame(frame);
 }
 
-watch(() => props.value, (newVal, oldVal) => {
-  animate(oldVal, newVal)
-})
+watch(
+  () => props.value,
+  (newVal, oldVal) => {
+    animate(oldVal, newVal);
+  }
+);
 
 onMounted(() => {
-  animate(0, props.value)
-})
-</script>
+  animate(0, props.value);
+});
+</script>

+ 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;

+ 12 - 32
src/utils/request.js

@@ -91,40 +91,20 @@ function get(url, params) {
 }
 
 function post(url, data) {
-  var myHeaders = new Headers();
-  myHeaders.append("Content-Type", "application/json");
-  myHeaders.append("token", ls.get('token'));
-
-  var requestOptions = {
-    method: 'POST',
-    headers: myHeaders,
-    redirect: 'follow'
-  };
-
-  // console.log(url);
-  return fetch(url, requestOptions)
-    .then(response => { return response.json() })
-    .then(result => {
-      // console.log(result);
-      return result;
+  return new Promise((resolve, reject) => {
+    service({
+      method: 'POST',
+      url,
+      data: data,
+      headers: {
+        'Content-Type': 'application/json;'
+      }
+    }).then(res => {
+      resolve(res.data)
     }).catch(err => {
-      return JSON.parse(err)
+      reject(err)
     })
-
-  // return new Promise((resolve, reject) => {
-  //   service({
-  //     method: 'POST',
-  //     url,
-  //     data: data,
-  //     headers: {
-  //       'Content-Type': 'application/json;'
-  //     }
-  //   }).then(res => {
-  //     resolve(res.data)
-  //   }).catch(err => {
-  //     reject(err)
-  //   })
-  // })
+  })
 }
 
 function postform(url, data) {

+ 1 - 0
src/views/Wgn.vue

@@ -686,6 +686,7 @@ export default {
                 message: "任务创建成功",
                 type: "success",
               });
+              // 不直接开始任务,需要到任务管理页面启动
               that.showTaskFrom = false;
             } else {
               that.$message({

+ 314 - 166
src/views/rwgl/Index.vue

@@ -1,181 +1,264 @@
 <template>
   <el-affix :offset="0">
-    <div style="height: 44px;width: 100vw;background: #08224a;"></div>
+    <div style="height: 44px; width: 100vw; background: #08224a"></div>
   </el-affix>
   <el-affix :offset="44">
-  <div class="blue-background">
-    <div class="lighter-container">
-      <div class="left-row">
-        <div>
-          <span>状态:</span>
-          <el-tag size="large" :effect="focusTaskStatus.includes('all') ? 'dark' : ''" type="primary"
-            @click="changeTaskStatus()" style="cursor: pointer;margin: 10px 5px;">
-            全部
-          </el-tag>
-          <template v-for="status in taskStatus" :key="status.index">
-            <el-tag size="large" :effect="focusTaskStatus.includes(status.index) ? 'dark' : ''" type="primary"
-              @click="changeTaskStatus(status)" style="cursor: pointer;margin: 10px 5px;">
-              {{ status.name }}
+    <div class="blue-background">
+      <div class="lighter-container">
+        <div class="left-row">
+          <div>
+            <span>状态:</span>
+            <el-tag
+              size="large"
+              :effect="focusTaskStatus.includes('all') ? 'dark' : ''"
+              type="primary"
+              @click="changeTaskStatus()"
+              style="cursor: pointer; margin: 10px 5px"
+            >
+              全部
             </el-tag>
-          </template>
-        </div>
-        <div>
-          <span>类别:</span>
-          <el-tag size="large" :effect="focusTaskType.includes('all') ? 'dark' : ''" type="primary"
-            @click="changeTaskType()" style="cursor: pointer;margin: 10px 5px;">
-            全部
-          </el-tag>
-          <template v-for="type in taskType" :key="type.index">
-            <el-tag size="large" :effect="focusTaskType.includes(type.index) ? 'dark' : ''" type="primary"
-              @click="changeTaskType(type)" style="cursor: pointer;margin: 10px 5px;">
-              {{ type.name }}
+            <template v-for="status in taskStatus" :key="status.index">
+              <el-tag
+                size="large"
+                :effect="focusTaskStatus.includes(status.index) ? 'dark' : ''"
+                type="primary"
+                @click="changeTaskStatus(status)"
+                style="cursor: pointer; margin: 10px 5px"
+              >
+                {{ status.name }}
+              </el-tag>
+              <template v-for="status in taskStatus" :key="status.index">
+                <el-tag
+                  size="large"
+                  :effect="focusTaskStatus.includes(status.index) ? 'dark' : ''"
+                  type="primary"
+                  @click="changeTaskStatus(status)"
+                  style="cursor: pointer; margin: 10px 5px"
+                >
+                  {{ status.name }}
+                </el-tag>
+              </template>
+            </template>
+          </div>
+          <div>
+            <span>类别:</span>
+            <el-tag
+              size="large"
+              :effect="focusTaskType.includes('all') ? 'dark' : ''"
+              type="primary"
+              @click="changeTaskType()"
+              style="cursor: pointer; margin: 10px 5px"
+            >
+              全部
             </el-tag>
-          </template>
+            <template v-for="type in taskType" :key="type.index">
+              <el-tag
+                size="large"
+                :effect="focusTaskType.includes(type.index) ? 'dark' : ''"
+                type="primary"
+                @click="changeTaskType(type)"
+                style="cursor: pointer; margin: 10px 5px"
+              >
+                {{ type.name }}
+              </el-tag>
+            </template>
+          </div>
+        </div>
+        <div class="row">
+          <el-input
+            class="searcher"
+            v-model="searcher"
+            placeholder="请输入任务名称相关关键字"
+          />
+          <el-button type="primary" @click="pullTaskData(1)">搜索</el-button>
+          <el-button type="primary" @click="reset(), pullTaskData(1)">重置</el-button>
         </div>
       </div>
-      <div class="row">
-        <el-input class="searcher" v-model="searcher" placeholder="请输入任务名称相关关键字" />
-        <el-button type="primary" @click="pullTaskData(1)">搜索</el-button>
-        <el-button type="primary" @click="reset(), pullTaskData(1)">重置</el-button>
-      </div>
-    </div>
-    <div class="lighter-container">
-      <div style="padding-bottom: 10px;">
-        查询到{{ taskNum }}条任务
+      <div class="lighter-container">
+        <div style="padding-bottom: 10px">查询到{{ taskNum }}条任务</div>
+        <el-table table-layout="fixed" row-key="main_id" :data="taskData" class="table">
+          <el-table-column prop="main_c_name" label="名称" />
+          <el-table-column prop="main_c_user_name" label="用户" />
+          <el-table-column prop="main_c_state" label="类型">
+            <template #default="scope">
+              <el-tag
+                effect="dark"
+                v-show="getType(scope.row.main_c_type) != null"
+                disable-transitions
+                >{{ getType(scope.row.main_c_type)?.name ?? "" }}
+              </el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column prop="main_c_state" label="状态">
+            <template #default="scope">
+              <el-tag
+                effect="dark"
+                v-show="getStatus(scope.row.main_c_state) != null"
+                :type="statusStaticInfo[scope.row.main_c_state]?.tagType ?? ''"
+                disable-transitions
+                >{{ getStatus(scope.row.main_c_state)?.name ?? "" }}</el-tag
+              >
+            </template>
+          </el-table-column>
+          <el-table-column prop="main_c_start_time" label="任务开始时间">
+            <template #default="scope">
+              {{ timeFormatter(scope.row.main_c_start_time) }}
+            </template>
+          </el-table-column>
+          <el-table-column prop="main_c_end_time" label="任务结束时间">
+            <template #default="scope">
+              {{ timeFormatter(scope.row.main_c_end_time) }}
+            </template>
+          </el-table-column>
+          <el-table-column prop="main_c_file_name" label="结果">
+            <template #default="scope">
+              <span
+                class="link"
+                v-if="scope.row.main_c_file != null && scope.row.main_c_file_name != null"
+                @click="
+                  downloadWithBlob(scope.row.main_c_file, scope.row.main_c_file_name)
+                "
+              >
+                {{ scope.row.main_c_file_name }}
+              </span>
+            </template>
+          </el-table-column>
+          <el-table-column label="操作" width="420">
+            <template #default="scope">
+              <el-button
+                type="primary"
+                @click="
+                  () => {
+                    dialog = true;
+                    focusTask = scope.row;
+                  }
+                "
+              >
+                查看详情
+              </el-button>
+              <el-button
+                v-if="scope.row.main_c_state == 0"
+                type="success"
+                @click="runTask(scope.row.main_id)"
+              >
+                运行
+              </el-button>
+              <template
+                v-if="scope.row.main_c_file != null && scope.row.main_c_file_name != null"
+              >
+                <el-button
+                  type="primary"
+                  @click="
+                    downloadWithBlob(scope.row.main_c_file, scope.row.main_c_file_name)
+                  "
+                >
+                  下载结果
+                </el-button>
+                <el-button type="primary" @click="preView(scope.row.main_c_file)">
+                  预览结果
+                </el-button>
+              </template>
+            </template>
+          </el-table-column>
+        </el-table>
+        <div class="between-row pagination-container">
+          <div><!--empty div--></div>
+          <el-pagination
+            layout="prev, pager, next"
+            :total="taskNum"
+            @change="(page) => pullTaskData(page)"
+          />
+        </div>
       </div>
-      <el-table table-layout="fixed" row-key="main_id" :data="taskData" class="table">
-        <el-table-column prop="main_c_name" label="名称" />
-        <el-table-column prop="main_c_user_name" label="用户" />
-        <el-table-column prop="main_c_state" label="类型">
-          <template #default="scope">
-            <el-tag effect="dark" v-show="getType(scope.row.main_c_type) != null" disable-transitions>{{
-              getType(scope.row.main_c_type)?.name ?? "" }}
+      <el-dialog v-model="dialog" :show-close="true" width="750">
+        <template #header>
+          <div class="my-header">
+            <span class="second-title">{{ focusTask.main_c_name }}</span>
+          </div>
+        </template>
+        <el-descriptions class="margin-top" label-width="128" :column="1" border>
+          <el-descriptions-item label="任务名称">
+            {{ focusTask.main_c_name }}
+          </el-descriptions-item>
+          <el-descriptions-item label="任务描述">
+            {{ focusTask.main_c_comment }}
+          </el-descriptions-item>
+          <el-descriptions-item label="任务类型">
+            <el-tag
+              effect="dark"
+              v-show="getType(focusTask.main_c_type) != null"
+              disable-transitions
+              >{{ getType(focusTask.main_c_type)?.name ?? "" }}
             </el-tag>
-          </template>
-        </el-table-column>
-        <el-table-column prop="main_c_state" label="状态">
-          <template #default="scope">
-            <el-tag effect="dark" v-show="getStatus(scope.row.main_c_state) != null"
-              :type="statusStaticInfo[scope.row.main_c_state]?.tagType ?? ''" disable-transitions>{{
-                getStatus(scope.row.main_c_state)?.name ?? "" }}</el-tag>
-          </template>
-        </el-table-column>
-        <el-table-column prop="main_c_start_time" label="任务开始时间">
-          <template #default="scope">
-            {{ timeFormatter(scope.row.main_c_start_time) }}
-          </template>
-        </el-table-column>
-        <el-table-column prop="main_c_end_time" label="任务结束时间">
-          <template #default="scope">
-            {{ timeFormatter(scope.row.main_c_end_time) }}
-          </template>
-        </el-table-column>
-        <el-table-column prop="main_c_file_name" label="结果">
-          <template #default="scope">
-            <span class="link" v-if="scope.row.main_c_file != null && scope.row.main_c_file_name != null"
-              @click="downloadWithBlob(scope.row.main_c_file, scope.row.main_c_file_name)">
-              {{ scope.row.main_c_file_name }}
-            </span>
-          </template>
-        </el-table-column>
-        <el-table-column label="操作" width="360">
-          <template #default="scope">
-            <el-button type="primary" @click="
-              () => {
-                dialog = true;
-                focusTask = scope.row;
-              }
-            ">
-              查看详情
-            </el-button>
-            <template v-if="scope.row.main_c_file != null && scope.row.main_c_file_name != null">
-              <el-button type="primary" @click="
-                downloadWithBlob(scope.row.main_c_file, scope.row.main_c_file_name)
-                ">
+          </el-descriptions-item>
+          <el-descriptions-item label="用户名">
+            {{ focusTask.main_c_user_name }}
+          </el-descriptions-item>
+          <el-descriptions-item label="用户id">
+            {{ focusTask.main_c_user_id }}
+          </el-descriptions-item>
+          <el-descriptions-item label="状态">
+            <el-tag
+              effect="dark"
+              :type="statusStaticInfo[focusTask.main_c_state]?.tagType ?? ''"
+              disable-transitions
+              >{{ getStatus(focusTask.main_c_state)?.name ?? "" }}
+            </el-tag>
+          </el-descriptions-item>
+          <el-descriptions-item label="任务开始时间">
+            {{ timeFormatter(focusTask.main_c_start_time) }}
+          </el-descriptions-item>
+          <el-descriptions-item label="任务结束时间">
+            {{ timeFormatter(focusTask.main_c_end_time) }}
+          </el-descriptions-item>
+          <el-descriptions-item label="结果文件">
+            {{ focusTask.main_c_file_name }}
+
+            <template
+              v-if="focusTask.main_c_file != null && focusTask.main_c_file_name != null"
+            >
+              <el-button
+                type="primary"
+                size="small"
+                @click="
+                  downloadWithBlob(focusTask.main_c_file, focusTask.main_c_file_name)
+                "
+              >
                 下载结果
               </el-button>
-              <el-button type="primary" @click="preView(scope.row.main_c_file)">
+              <el-button
+                type="primary"
+                size="small"
+                @click="preView(focusTask.main_c_file)"
+              >
                 预览结果
               </el-button>
             </template>
-          </template>
-        </el-table-column>
-      </el-table>
-      <div class="between-row">
-        <div><!--empty div--></div>
-        <el-pagination layout="prev, pager, next" :total="taskNum" @change="(page) => pullTaskData(page)" />
-      </div>
+          </el-descriptions-item>
+          <el-descriptions-item label="原始数据">
+            <template
+              v-if="
+                focusTask.main_c_source_file_name != null &&
+                focusTask.main_c_source_file != null
+              "
+            >
+              {{ focusTask.main_c_source_file_name }}
+              <br />
+            </template>
+            <template v-if="focusTask.main_c_source_data != null">
+              <div class="hide-scrollbar long-text">
+                {{ truncateText(focusTask.main_c_source_data, 10000) }}
+              </div>
+            </template>
+          </el-descriptions-item>
+        </el-descriptions>
+      </el-dialog>
     </div>
-    <el-dialog v-model="dialog" :show-close="true" width="750">
-      <template #header>
-        <div class="my-header">
-          <span class="second-title">{{ focusTask.main_c_name }}</span>
-        </div>
-      </template>
-      <el-descriptions class="margin-top" label-width="128" :column="1" border>
-        <el-descriptions-item label="任务名称">
-          {{ focusTask.main_c_name }}
-        </el-descriptions-item>
-        <el-descriptions-item label="任务描述">
-          {{ focusTask.main_c_comment }}
-        </el-descriptions-item>
-        <el-descriptions-item label="任务类型">
-          <el-tag effect="dark" v-show="getType(focusTask.main_c_type) != null" disable-transitions>{{
-            getType(focusTask.main_c_type)?.name ?? "" }}
-          </el-tag>
-        </el-descriptions-item>
-        <el-descriptions-item label="用户名">
-          {{ focusTask.main_c_user_name }}
-        </el-descriptions-item>
-        <el-descriptions-item label="用户id">
-          {{ focusTask.main_c_user_id }}
-        </el-descriptions-item>
-        <el-descriptions-item label="状态">
-          <el-tag effect="dark" :type="statusStaticInfo[focusTask.main_c_state]?.tagType ?? ''" disable-transitions>{{
-            getStatus(focusTask.main_c_state)?.name ?? "" }}
-          </el-tag>
-        </el-descriptions-item>
-        <el-descriptions-item label="任务开始时间">
-          {{ timeFormatter(focusTask.main_c_start_time) }}
-        </el-descriptions-item>
-        <el-descriptions-item label="任务结束时间">
-          {{ timeFormatter(focusTask.main_c_end_time) }}
-        </el-descriptions-item>
-        <el-descriptions-item label="结果文件">
-          {{ focusTask.main_c_file_name }}
-
-          <template v-if="focusTask.main_c_file != null && focusTask.main_c_file_name != null">
-            <el-button type="primary" size="small"
-              @click="downloadWithBlob(focusTask.main_c_file, focusTask.main_c_file_name)">
-              下载结果
-            </el-button>
-            <el-button type="primary" size="small" @click="preView(focusTask.main_c_file)">
-              预览结果
-            </el-button>
-          </template>
-        </el-descriptions-item>
-        <el-descriptions-item label="原始数据">
-          <template v-if="
-            focusTask.main_c_source_file_name != null &&
-            focusTask.main_c_source_file != null
-          ">
-            {{ focusTask.main_c_source_file_name }}
-            <br />
-          </template>
-          <template v-if="focusTask.main_c_source_data != null">
-            <div class="hide-scrollbar long-text">
-              {{ truncateText(focusTask.main_c_source_data, 10000) }}
-            </div>
-          </template>
-        </el-descriptions-item>
-      </el-descriptions>
-    </el-dialog>
-  </div>
   </el-affix>
 </template>
 
 <script>
-import { getTasks, getCName } from "@/api/rwgl";
+import { getTasks, getCName, executeTask } from "@/api/rwgl";
 
 export default {
   data() {
@@ -204,12 +287,18 @@ export default {
       focusTask: {},
       dialog: false,
       page: 1,
+      tableHeight: 0,
     };
   },
   mounted() {
     this.pullTaskStatus();
     this.pullTaskType();
     this.pullTaskData(1);
+    this.calculateTableHeight();
+    window.addEventListener("resize", this.calculateTableHeight);
+  },
+  beforeUnmount() {
+    window.removeEventListener("resize", this.calculateTableHeight);
   },
   methods: {
     async pullTaskStatus() {
@@ -218,8 +307,8 @@ export default {
       for (const key of Object.keys(oData)) {
         newData.push({
           index: Number(key),
-          name: oData[key]
-        })
+          name: oData[key],
+        });
       }
       this.taskStatus = newData.sort((a, b) => a.index - b.index);
     },
@@ -262,8 +351,8 @@ export default {
       for (const key of Object.keys(oData)) {
         newData.push({
           index: Number(key),
-          name: oData[key]
-        })
+          name: oData[key],
+        });
       }
       let taskType = newData.sort((a, b) => a.index - b.index);
       for (let i = 0; i < taskType.length; i++) {
@@ -274,7 +363,7 @@ export default {
           break;
         }
       }
-      this.taskType = taskType
+      this.taskType = taskType;
     },
     changeTaskType(types) {
       if (types == null) {
@@ -348,12 +437,62 @@ export default {
     preView(url) {
       window.open("fileView?url=" + systemConfig.dmsDataProxy + url, "_blank");
     },
+    async runTask(taskId) {
+      try {
+        const res = await executeTask(taskId);
+        if (res.code === 200) {
+          this.$message({
+            type: "success",
+            message: res.content || "任务已提交,正在后台执行",
+          });
+          // 先禁用按钮,防止重复点击
+          this.$refs.runTaskBtn.disabled = true;
+          // 刷新任务列表
+          setTimeout(() => {
+            // 执行成功后,刷新任务列表
+            this.pullTaskData(this.page);
+          }, 300);
+        } else {
+          this.$message({
+            type: "error",
+            message: res.message || "执行任务失败",
+          });
+        }
+      } catch (error) {
+        console.error("执行任务失败:", error);
+        this.$message({
+          type: "error",
+          message: "执行任务失败,请稍后重试",
+        });
+      }
+    },
     truncateText(text, maxLength = 40) {
       if (typeof text !== "string" || text.length <= maxLength) {
         return text;
       }
       return text.substring(0, maxLength) + "…";
     },
+    calculateTableHeight() {
+      // 计算表格高度:窗口高度 - header高度 - footer高度 - 页面padding - 其他元素高度
+      const windowHeight = window.innerHeight;
+      const headerHeight = 70; // Header组件高度
+      const footerHeight = 50; // Footer组件高度
+      const pagePadding = 40 * 2; // 页面上下padding
+      const filterAreaHeight = 120; // 筛选条件区域高度
+      const taskCountHeight = 30; // 任务数量文字高度
+      const paginationHeight = 50; // 分页区域高度
+      const containerMargin = 15 * 2; // 容器上下margin
+
+      this.tableHeight =
+        windowHeight -
+        headerHeight -
+        footerHeight -
+        pagePadding -
+        filterAreaHeight -
+        taskCountHeight -
+        paginationHeight -
+        containerMargin;
+    },
   },
 };
 </script>
@@ -455,6 +594,15 @@ body {
   border-radius: 10px;
 }
 
+.task-count {
+  margin-bottom: 10px;
+  color: #fff;
+}
+
+.pagination-container {
+  margin-top: 15px;
+}
+
 .lightblue-container {
   border-radius: 3%;
   padding: 20px;
@@ -498,7 +646,7 @@ body {
   justify-content: flex-start;
 }
 
-.left-row>* {
+.left-row > * {
   margin-right: 15px;
 }
 
@@ -520,8 +668,8 @@ body {
   flex-direction: column-reverse;
 }
 
-.dense-col>*,
-.start-reverse-col>* {
+.dense-col > *,
+.start-reverse-col > * {
   margin: 10px;
 }