Parcourir la source

添加地图渲染配置、优化业务数据展示配置和效果

Lin Qilong il y a 4 mois
Parent
commit
ffa1196d6b
38 fichiers modifiés avec 1874 ajouts et 815 suppressions
  1. 14 1
      pom.xml
  2. 1 2
      ruoyi-admin/pom.xml
  3. 9 2
      ruoyi-api-patform/src/main/java/com/ruoyi/interfaces/controller/BizDataShowConfigController.java
  4. 81 0
      ruoyi-api-patform/src/main/java/com/ruoyi/interfaces/controller/SCSSFMController.java
  5. 26 0
      ruoyi-api-patform/src/main/java/com/ruoyi/interfaces/domain/BizDataApi.java
  6. 27 0
      ruoyi-api-patform/src/main/java/com/ruoyi/interfaces/domain/BizDataApiParam.java
  7. 16 10
      ruoyi-api-patform/src/main/java/com/ruoyi/interfaces/domain/BizDataShowConfig.java
  8. 3 0
      ruoyi-api-patform/src/main/java/com/ruoyi/interfaces/mapper/BizDataShowConfigMapper.java
  9. 1 0
      ruoyi-api-patform/src/main/java/com/ruoyi/interfaces/service/BizDataShowConfigService.java
  10. 68 11
      ruoyi-api-patform/src/main/java/com/ruoyi/interfaces/service/impl/BizDataShowConfigServiceImpl.java
  11. 5 0
      ruoyi-common/pom.xml
  12. 2 0
      ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java
  13. 5 1
      ruoyi-ui/package.json
  14. 10 0
      ruoyi-ui/src/api/standardization/bizDataShowConfig.js
  15. 6 0
      ruoyi-ui/src/api/standardization/modeling.js
  16. BIN
      ruoyi-ui/src/assets/map/img/dyCenter.gif
  17. 0 0
      ruoyi-ui/src/assets/map/json/stnmData.json
  18. 71 17
      ruoyi-ui/src/components/DynamicMap/index.vue
  19. 13 141
      ruoyi-ui/src/utils/biz.js
  20. 21 0
      ruoyi-ui/src/utils/bus.js
  21. 24 0
      ruoyi-ui/src/utils/data.js
  22. 29 0
      ruoyi-ui/src/utils/validate.js
  23. 65 0
      ruoyi-ui/src/views/map/components/bizDataCard.vue
  24. 139 4
      ruoyi-ui/src/views/map/components/iconDropDialog.vue
  25. 459 0
      ruoyi-ui/src/views/map/components/map.vue
  26. 78 0
      ruoyi-ui/src/views/map/hooks/dataSourceManager.js
  27. 85 0
      ruoyi-ui/src/views/map/hooks/popupContent.js
  28. 112 551
      ruoyi-ui/src/views/map/index.vue
  29. 59 0
      ruoyi-ui/src/views/map/utils/jsonToGeojson.js
  30. 157 0
      ruoyi-ui/src/views/map/utils/styleParser.js
  31. 0 1
      ruoyi-ui/src/views/service/info/AeService.vue
  32. 0 1
      ruoyi-ui/src/views/service/info/serviceFile.vue
  33. 53 40
      ruoyi-ui/src/views/standardization/bizDataShowConfig/index.vue
  34. 48 3
      ruoyi-ui/src/views/standardization/bizDataShowConfig/list/index.vue
  35. 56 0
      ruoyi-ui/src/views/standardization/bizDataShowConfig/position/index.vue
  36. 64 22
      ruoyi-ui/src/views/standardization/bizDataShowConfig/show/GwTableTwo.vue
  37. 64 7
      ruoyi-ui/src/views/standardization/bizDataShowConfig/show/index.vue
  38. 3 1
      ruoyi-ui/src/views/standardization/resultsPresentation/index.vue

+ 14 - 1
pom.xml

@@ -23,7 +23,7 @@
         <bitwalker.version>1.21</bitwalker.version>
         <swagger.version>3.0.0</swagger.version>
         <kaptcha.version>2.3.3</kaptcha.version>
-        <mybatis-plus.version>3.5.1</mybatis-plus.version>
+        <mybatis-plus.version>3.5.3.2</mybatis-plus.version>
         <mybatis-spring-boot.version>2.2.0</mybatis-spring-boot.version>
         <pagehelper.boot.version>1.4.7</pagehelper.boot.version>
         <fastjson.version>2.0.53</fastjson.version>
@@ -87,6 +87,13 @@
                 <version>${bitwalker.version}</version>
             </dependency>
 
+            <!-- DM8驱动包 -->
+            <dependency>
+                <groupId>com.dameng</groupId>
+                <artifactId>DmJdbcDriver18</artifactId>
+                <version>8.1.3.140</version>
+            </dependency>
+
             <!-- mybatis 代码生成器 -->
             <dependency>
                 <groupId>com.baomidou</groupId>
@@ -94,6 +101,12 @@
                 <version>${mybatis-plus.version}</version>
             </dependency>
 
+            <dependency>
+                <groupId>com.baomidou</groupId>
+                <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
+                <version>3.5.2</version>
+            </dependency>
+
             <!-- pagehelper 分页插件 -->
             <dependency>
                 <groupId>com.github.pagehelper</groupId>

+ 1 - 2
ruoyi-admin/pom.xml

@@ -41,7 +41,7 @@
         <dependency>
             <groupId>com.dameng</groupId>
             <artifactId>DmJdbcDriver18</artifactId>
-            <version>8.1.1.193</version>
+            <version>8.1.3.140</version>
         </dependency>
 
         <!-- Mysql驱动包 -->
@@ -66,7 +66,6 @@
         <dependency>
             <groupId>com.dameng</groupId>
             <artifactId>DmJdbcDriver18</artifactId>
-            <version>8.1.1.193</version>
         </dependency>
 
         <!-- 核心模块 -->

+ 9 - 2
ruoyi-api-patform/src/main/java/com/ruoyi/interfaces/controller/BizDataShowConfigController.java

@@ -55,7 +55,7 @@ public class BizDataShowConfigController extends BaseController {
         startPage();
         QueryWrapper<BizDataShowConfig> queryWrapper = new QueryWrapper<>();
         queryWrapper
-                .like(StringUtils.isNotBlank(ptServiceAlarm.getName()), "name", ptServiceAlarm.getName());
+                .like(StringUtils.isNotBlank(ptServiceAlarm.getName()), "NAME", ptServiceAlarm.getName());
         List<BizDataShowConfig> list = bizDataShowConfigService.list(queryWrapper);
         return getDataTable(list);
     }
@@ -67,7 +67,8 @@ public class BizDataShowConfigController extends BaseController {
     public AjaxResult list(BizDataShowConfig ptServiceAlarm) {
         QueryWrapper<BizDataShowConfig> queryWrapper = new QueryWrapper<>();
         queryWrapper
-                .like(StringUtils.isNotBlank(ptServiceAlarm.getName()), "name", ptServiceAlarm.getName()).orderByAsc("sort");
+                .like(StringUtils.isNotBlank(ptServiceAlarm.getName()), "NAME", ptServiceAlarm.getName())
+                .orderByAsc("SORT");
         List<BizDataShowConfig> list = bizDataShowConfigService.list(queryWrapper);
         return AjaxResult.success(list);
     }
@@ -82,4 +83,10 @@ public class BizDataShowConfigController extends BaseController {
         return AjaxResult.success(bizDataShowConfigService.getBizDataById(id));
     }
 
+    @PostMapping("/getBizDataByConfig")
+    public AjaxResult getBizDataByConfig(@RequestBody String config) {
+        return AjaxResult.success(bizDataShowConfigService.getBizDataByConfig(config));
+    }
+
+
 }

+ 81 - 0
ruoyi-api-patform/src/main/java/com/ruoyi/interfaces/controller/SCSSFMController.java

@@ -0,0 +1,81 @@
+package com.ruoyi.interfaces.controller;
+
+import com.alibaba.fastjson2.JSONObject;
+import com.alibaba.fastjson2.JSONPath;
+import com.ruoyi.common.core.controller.BaseController;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.utils.JsonUtils;
+import com.ruoyi.common.utils.OkHttpUtils;
+import com.ruoyi.interfaces.service.BizDataShowConfigService;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * Shanghai Coastal Storm Surge Forecast Model
+ * 上海沿海风暴潮顾报模型 自定义接口
+ */
+@RestController
+@RequestMapping("/SCSSFM")
+public class SCSSFMController extends BaseController {
+
+    @Autowired
+    private BizDataShowConfigService bizDataShowConfigService;
+
+    @ApiOperation("根据主键获取数据")
+    @PostMapping(value = "/getCalResultsByStation")
+    public AjaxResult createContainer(@RequestBody Map<String, Object> params) {
+        Map<String, Object> stationParams = new HashMap<>();
+        stationParams.put("projectId", params.get("projectId"));
+        stationParams.put("forecastSchemeId", params.get("forecastSchemeId"));
+        stationParams.put("userName", "shfbc");
+
+        String stationResponseString = OkHttpUtils.executeRequest(
+                "http://49.4.2.185:2111/RiverStrongAPI2.0/StormSurgeForecast/StormSurge/BaseInfo/GetForecastStationInfos",
+                "GET",
+                stationParams,
+                null,
+                null,
+                null
+        );
+        JSONObject stationResponseObj = JsonUtils.jsonToPojo(stationResponseString, JSONObject.class);
+        List<Map<String, Object>> stationList = (List<Map<String, Object>>) JSONPath.eval(stationResponseObj, "$.data");
+
+        String responseString = OkHttpUtils.executeRequest(
+                "http://49.4.2.185:2111/RiverStrongAPI2.0/StormSurgeForecast/StormSurge/Calculate/GetCalResultsByStation",
+                "POST",
+                null,
+                JsonUtils.objectToJson(params),
+                null,
+                null
+        );
+        JSONObject responseObj = JsonUtils.jsonToPojo(responseString, JSONObject.class);
+        List<Map<String, Object>> calResultsList = (List<Map<String, Object>>) JSONPath.eval(responseObj, "$.data");
+
+        calResultsList.forEach(item -> {
+            Map<String, Object> station = stationList.stream()
+                    .filter(s -> String.valueOf(s.get("stationCode")).equals(item.get("stationCode")))
+                    .findFirst()
+                    .orElse(null);
+
+            Optional.ofNullable(station).ifPresent(s -> {
+                item.put("stationType", s.get("stationType"));
+                item.put("sensorType", s.get("sensorType"));
+                item.put("lgtd", s.get("lgtd"));
+                item.put("lttd", s.get("lttd"));
+                item.put("order", s.get("order"));
+            });
+        });
+
+        return AjaxResult.success(calResultsList);
+    }
+
+}

+ 26 - 0
ruoyi-api-patform/src/main/java/com/ruoyi/interfaces/domain/BizDataApi.java

@@ -0,0 +1,26 @@
+package com.ruoyi.interfaces.domain;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import lombok.Data;
+
+import java.util.List;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+@Data
+public class BizDataApi {
+
+    private String url;
+
+    private String method;
+
+    private List<BizDataApiParam> params;
+
+    private List<BizDataApiParam> body;
+
+    private List<BizDataApiParam> cookies;
+
+    private List<BizDataApiParam> headers;
+
+    private String responseResolution;
+
+}

+ 27 - 0
ruoyi-api-patform/src/main/java/com/ruoyi/interfaces/domain/BizDataApiParam.java

@@ -0,0 +1,27 @@
+package com.ruoyi.interfaces.domain;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@NoArgsConstructor
+@Data
+public class BizDataApiParam {
+
+    private String id;
+    private String key;
+    private Object value;
+    private String title;
+    private String type;
+
+    public BizDataApiParam(String key, Object value, String type) {
+        this.key = key;
+        this.value = value;
+        this.type = type;
+    }
+
+    public BizDataApiParam(String key, Object value) {
+        this.key = key;
+        this.value = value;
+    }
+
+}

+ 16 - 10
ruoyi-api-patform/src/main/java/com/ruoyi/interfaces/domain/BizDataShowConfig.java

@@ -1,6 +1,7 @@
 package com.ruoyi.interfaces.domain;
 
 import com.alibaba.fastjson2.JSONObject;
+import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
 import com.fasterxml.jackson.annotation.JsonFormat;
@@ -14,26 +15,31 @@ import java.util.Date;
 @Data
 @AllArgsConstructor
 @NoArgsConstructor
-@TableName
+@TableName("SH_MODEL.BIZ_DATA_SHOW_CONFIG")
+//@TableName("BIZ_DATA_SHOW_CONFIG")
 public class BizDataShowConfig {
 
-    @TableId
-    private Long id;
-
+    @TableId("ID")
+    private String id;
+    @TableField("NAME")
     private String name;
-
+    @TableField("TYPE")
     private String type;
-
+    @TableField("POSITION")
+    private String position;
+    @TableField("QUERY_OPTIONS")
     private String queryOptions;
-
+    @TableField("RENDERING_OPTIONS")
     private String renderingOptions;
-
+    @TableField("SORT")
     private Integer sort;
-
+    @TableField("APP_ID")
+    private String appId;
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    @TableField("CREATE_TIME")
     private Date createTime;
-
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    @TableField("UPDATE_TIME")
     private Date updateTime;
 
     public JSONObject getQueryOptionsData() {

+ 3 - 0
ruoyi-api-patform/src/main/java/com/ruoyi/interfaces/mapper/BizDataShowConfigMapper.java

@@ -1,6 +1,8 @@
 package com.ruoyi.interfaces.mapper;
 
+import com.baomidou.dynamic.datasource.annotation.DS;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.ruoyi.common.enums.DataSourceType;
 import com.ruoyi.interfaces.domain.BizDataShowConfig;
 import org.apache.ibatis.annotations.Mapper;
 
@@ -11,6 +13,7 @@ import org.apache.ibatis.annotations.Mapper;
  * @date 2025-07-15
  */
 @Mapper
+@DS("SLAVE")
 public interface BizDataShowConfigMapper extends BaseMapper<BizDataShowConfig> {
 
 }

+ 1 - 0
ruoyi-api-patform/src/main/java/com/ruoyi/interfaces/service/BizDataShowConfigService.java

@@ -11,4 +11,5 @@ public interface BizDataShowConfigService extends IService<BizDataShowConfig> {
 
     Object getBizDataById(String id);
 
+    Object getBizDataByConfig(String config);
 }

+ 68 - 11
ruoyi-api-patform/src/main/java/com/ruoyi/interfaces/service/impl/BizDataShowConfigServiceImpl.java

@@ -7,20 +7,22 @@ import com.ruoyi.common.exception.CheckException;
 import com.ruoyi.common.utils.JsonUtils;
 import com.ruoyi.common.utils.OkHttpUtils;
 import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.common.utils.uuid.IdUtils;
+import com.ruoyi.interfaces.domain.BizDataApi;
+import com.ruoyi.interfaces.domain.BizDataApiParam;
 import com.ruoyi.interfaces.domain.BizDataShowConfig;
 import com.ruoyi.interfaces.mapper.BizDataShowConfigMapper;
 import com.ruoyi.interfaces.service.BizDataShowConfigService;
 import org.springframework.stereotype.Service;
 
-import java.util.Date;
-import java.util.Map;
-import java.util.Optional;
+import java.util.*;
 
 @Service
 public class BizDataShowConfigServiceImpl extends ServiceImpl<BizDataShowConfigMapper, BizDataShowConfig> implements BizDataShowConfigService {
 
     @Override
     public boolean save(BizDataShowConfig entity) {
+        entity.setId(IdUtils.simpleUUID());
         entity.setCreateTime(new Date());
         return super.save(entity);
     }
@@ -44,18 +46,25 @@ public class BizDataShowConfigServiceImpl extends ServiceImpl<BizDataShowConfigM
         return requestByQueryOptions(bizDataShowConfig.getQueryOptions());
     }
 
+    @Override
+    public Object getBizDataByConfig(String config) {
+        return requestByQueryOptions(config);
+    }
+
     private Object requestByQueryOptions(String queryOptions) {
-        JSONObject apiConfigObj = JsonUtils.jsonToPojo(queryOptions, JSONObject.class);
-        Map<String, Object> params = apiConfigObj.getJSONObject("params");
-        Map<String, Object> cookies = apiConfigObj.getJSONObject("cookies");
-        Map<String, Object> headers = apiConfigObj.getJSONObject("headers");
-        String responseResolution = apiConfigObj.getString("responseResolution");
+        BizDataApi apiConfigObj = JsonUtils.jsonToPojo(queryOptions, BizDataApi.class);
+        // 解析参数格式
+        Map<String, Object> params = parseKeyValueList(apiConfigObj.getParams());
+        Map<String, Object> cookies = parseKeyValueList(apiConfigObj.getCookies());
+        Map<String, Object> headers = parseKeyValueList(apiConfigObj.getHeaders());
+        String body = parseBody(apiConfigObj.getBody());
+        String responseResolution = apiConfigObj.getResponseResolution();
 
         String responseString = OkHttpUtils.executeRequest(
-                apiConfigObj.getString("url"),
-                apiConfigObj.getString("method"),
+                apiConfigObj.getUrl(),
+                apiConfigObj.getMethod(),
                 params,
-                apiConfigObj.getString("body"),
+                body,
                 cookies,
                 headers
         );
@@ -68,4 +77,52 @@ public class BizDataShowConfigServiceImpl extends ServiceImpl<BizDataShowConfigM
         return result;
     }
 
+    /**
+     * 将键值对数组解析为Map
+     */
+    private Map<String, Object> parseKeyValueList(List<BizDataApiParam> keyValueList) {
+        Map<String, Object> resultMap = new HashMap<>();
+        if (keyValueList == null) {
+            return resultMap;
+        }
+
+        for (BizDataApiParam item : keyValueList) {
+            String key = item.getKey();
+            Object value = item.getValue();
+            String type = item.getType();
+
+            // 根据类型处理值
+            if ("array".equals(type) && value instanceof List) {
+                resultMap.put(key, Collections.singletonList(value));
+            } else {
+                resultMap.put(key, value);
+            }
+        }
+        return resultMap;
+    }
+
+    /**
+     * 解析body字段
+     */
+    private String parseBody(List<BizDataApiParam> bodyArray) {
+        if (bodyArray == null) {
+            return null;
+        }
+
+        JSONObject bodyObj = new JSONObject();
+        bodyArray.forEach(item -> {
+            String key = item.getKey();
+            Object value = item.getValue();
+            String type = item.getType();
+
+            // 根据类型处理值
+            if ("array".equals(type) && value instanceof List) {
+                bodyObj.put(key, value);
+            } else {
+                bodyObj.put(key, value);
+            }
+        });
+        return bodyObj.toJSONString();
+    }
+
 }

+ 5 - 0
ruoyi-common/pom.xml

@@ -36,6 +36,11 @@
             <artifactId>mybatis-plus-boot-starter</artifactId>
         </dependency>
 
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
+        </dependency>
+
         <!-- pagehelper 分页插件 -->
         <dependency>
             <groupId>com.github.pagehelper</groupId>

+ 2 - 0
ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java

@@ -112,6 +112,8 @@ public class SecurityConfig {
                             .antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll()
                             .antMatchers("/workflows/**").permitAll()
                             .antMatchers("/websocket/**").permitAll()
+                            // 沿海风暴潮自定义接口
+                            .antMatchers("/SCSSFM/**").permitAll()
 
                             // 除上面外的所有请求全部需要鉴权认证
                             .anyRequest().authenticated();

+ 5 - 1
ruoyi-ui/package.json

@@ -37,9 +37,13 @@
     "json-beautify": "^1.1.1",
     "json-editor-vue3": "^1.1.1",
     "jsonpath": "^1.1.1",
+    "jsonpath-plus": "^10.3.0",
+    "mitt": "^3.0.1",
     "moment": "^2.30.1",
     "nprogress": "0.2.0",
-    "ol": "^10.2.1",
+    "ol": "^10.6.1",
+    "ol-contextmenu": "^5.5.0",
+    "ol-ext": "^4.0.35",
     "pinia": "2.1.7",
     "pinyin": "^4.0.0",
     "qs": "^6.14.0",

+ 10 - 0
ruoyi-ui/src/api/standardization/bizDataShowConfig.js

@@ -36,6 +36,16 @@ export function getBizDataById(id) {
     return request({
         url: `/biz/data/show/config/getBizData/${id}`,
         method: 'get',
+        timeout: 60 * 1000 * 10
+    })
+}
+
+export function getBizDataByConfig(config) {
+    return request({
+        url: '/biz/data/show/config/getBizDataByConfig',
+        method: 'post',
+        data: config,
+        timeout: 60 * 1000 * 10
     })
 }
 

+ 6 - 0
ruoyi-ui/src/api/standardization/modeling.js

@@ -22,6 +22,12 @@ export function addModeling(data) {
     data:data
   });
 }
+export function getModelingById(id) {
+  return request({
+    url: '/md/app/' + id,
+    method: 'get'
+  })
+}
 export function delModeling(id) {
   return request({
     url: '/md/app/' + id,

BIN
ruoyi-ui/src/assets/map/img/dyCenter.gif


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
ruoyi-ui/src/assets/map/json/stnmData.json


+ 71 - 17
ruoyi-ui/src/components/DynamicMap/index.vue

@@ -1,40 +1,50 @@
 <template>
   <div class="dynamic-map-container">
     <el-row class="header">
-      <el-col :span="5">键</el-col>
-      <el-col :span="5">值</el-col>
-      <el-col :span="5">类型</el-col>
-      <el-col :span="5">说明</el-col>
-      <el-col :span="4">操作</el-col>
+      <el-col :span="8">键</el-col>
+      <el-col :span="8">值</el-col>
+      <el-col :span="2">类型</el-col>
+      <el-col :span="4">说明</el-col>
+      <el-col :span="2">操作</el-col>
     </el-row>
     <el-row
         v-for="(item, index) in items"
         :key="item.id"
         class="row"
     >
-      <el-col :span="5">
+      <el-col :span="8">
         <el-input
             placeholder="请输入键"
             v-model="item.key"
             @input="debouncedEmitUpdate"
         />
       </el-col>
-      <el-col :span="5">
+      <el-col :span="8">
+        <div v-if="Array.isArray(item.value)">
+          <!-- 数组值的输入框 -->
+          <div v-for="(val, valIndex) in item.value" :key="valIndex" style="margin-bottom: 5px;display: flex;align-items: center;">
+            <el-input placeholder="请输入值" v-model="item.value[valIndex]" @input="debouncedEmitUpdate"/>
+            <el-button v-if="valIndex !== 0" type="danger" icon="Delete" size="small" @click="removeArrayValue(item, valIndex)"/>
+          </div>
+          <el-button type="primary" size="small" @click="addArrayValue(item)">添加值</el-button>
+        </div>
         <el-input
+            v-else
             placeholder="请输入值"
             v-model="item.value"
             @input="debouncedEmitUpdate"
         />
       </el-col>
-      <el-col :span="5">
-        <el-select v-model="item.type" clearable placeholder="请选择" @change="debouncedEmitUpdate">
+      <el-col :span="2">
+        <el-select v-model="item.type" clearable placeholder="请选择"
+                   @change="(newType) => onTypeChange(item, newType)">
           <el-option v-for="dict in typeOptions" :key="dict.value" :label="dict.label" :value="dict.value"></el-option>
         </el-select>
       </el-col>
-      <el-col :span="5">
+      <el-col :span="4">
         <el-input placeholder="请输入说明" v-model="item.title" @input="debouncedEmitUpdate"/>
       </el-col>
-      <el-col :span="4" class="actions">
+      <el-col :span="2" class="actions">
         <el-button type="danger" icon="Delete" @click="removeItem(index)"/>
       </el-col>
     </el-row>
@@ -55,7 +65,7 @@ import {debounce} from 'lodash-es';
 interface KeyValueItem {
   id: number;
   key: string;
-  value: string;
+  value: string | string[];
   type: string;
   title: string;
 }
@@ -80,6 +90,7 @@ const typeOptions = ref([
   {label: '字符串', value: 'string'},
   {label: '数字', value: 'number'},
   {label: '布尔值', value: 'boolean'},
+  {label: '数组', value: 'array'},
 ])
 
 // 将对象转换为带ID的键值对数组
@@ -91,7 +102,7 @@ const initItems = () => {
     return value.map((item: any) => ({
       id: idCounter++,
       key: item.key || '',
-      value: String(item.value || ''),
+      value: item.type === 'array' ? (Array.isArray(item.value) ? item.value : [String(item.value || '')]) : String(item.value || ''),
       type: item.type || '',
       title: item.title || ''
     }));
@@ -120,7 +131,9 @@ const addNewItem = () => {
   items.value.push({
     id: idCounter++,
     key: '',
-    value: ''
+    value: '',
+    type: '',
+    title: ''
   });
   emit('update:modelValue', props.returnFormat === "map" ? arrayToObject(items.value) : items.value);
 };
@@ -135,10 +148,51 @@ const removeItem = (index: number) => {
 const arrayToObject = (items: KeyValueItem[]) => {
   return items.reduce((obj, item) => {
     if (item.key) {
-      obj[item.key] = item.value;
+      // 处理数组值的情况
+      if (Array.isArray(item.value)) {
+        obj[item.key] = item.value.filter(v => v !== ''); // 过滤空值
+      } else {
+        obj[item.key] = item.value;
+      }
     }
     return obj;
-  }, {} as Record<string, string>);
+  }, {} as Record<string, string | string[]>);
+};
+
+// 修改 el-select 的 change 事件处理
+const onTypeChange = (item: KeyValueItem, newType: string) => {
+  // 当类型切换为 array 时,转换 value 为数组
+  if (newType === 'array' && !Array.isArray(item.value)) {
+    item.value = [item.value || ''];
+  }
+  // 当从 array 切换到其他类型时,取第一个值或空字符串
+  else if (newType !== 'array' && Array.isArray(item.value)) {
+    item.value = item.value.length > 0 ? item.value[0] : '';
+  }
+
+  item.type = newType;
+  debouncedEmitUpdate();
+};
+
+// 添加数组值
+const addArrayValue = (item: KeyValueItem) => {
+  if (!Array.isArray(item.value)) {
+    item.value = [item.value]; // 转换为数组
+  }
+  item.value.push('');
+  debouncedEmitUpdate();
+};
+
+// 删除数组中的某个值
+const removeArrayValue = (item: KeyValueItem, index: number) => {
+  if (Array.isArray(item.value)) {
+    item.value.splice(index, 1);
+    // 如果数组为空,可以选择保留空数组或转换回空字符串
+    if (item.value.length === 0) {
+      item.value = '';
+    }
+    debouncedEmitUpdate();
+  }
 };
 
 // 监听父组件传入值的变化
@@ -169,7 +223,7 @@ watch(() => props.modelValue, (newValue) => {
   if (shouldUpdate) {
     items.value = initItems();
   }
-}, { deep: true });
+}, {deep: true});
 
 
 // 清除防抖器

+ 13 - 141
ruoyi-ui/src/utils/biz.js

@@ -1,148 +1,14 @@
-import request from "@/utils/request";
-import jp from 'jsonpath';
-import qs from "qs";
-import {isArray} from "@/utils/validate";
+import {isArray, isNumber} from "@/utils/validate";
 import {objectMerge} from "@/utils/index";
 import {getYAxisListByUnit} from "@/utils/chart";
 import {formatStringByTemplate} from "@/utils/string";
 import {getBizDataById} from "@/api/standardization/bizDataShowConfig.js";
+import {filterData} from "@/utils/data.js";
 
 /**
- * 查询业务接口信息数据及业务数据
- * @param id              业务编码
- * @returns {Promise<*>}
+ * 获取数据
+ * @param id
  */
-// export async function getBizDataByAid(id, params) {
-//   if (!id) {
-//     return null;
-//   }
-//
-//   let bizData = await getBizConfigById(id).then(r => r.data)
-//
-//   if (params) {
-//     if (!bizData.params) {
-//       bizData.params = {}
-//     }
-//     objectMerge(bizData.params, params)
-//   }
-//
-//   let dataList = await getBizData(bizData)
-//   if (dataList) {
-//     bizData.dataList = dataList
-//   }
-//   return bizData
-// }
-
-
-/**
- * 查询业务数据
- * @param biz 业务详情
- */
-export async function getBizData(biz) {
-    let data = null;
-    if (biz.type === 'api') {
-        // 1. 从接口查询数据
-        data = await getApiData(biz)
-    } else {
-        // 2. 从文件查询数据
-        data = await getFileData(biz)
-    }
-    if (!data) {
-        // 数据请求失败
-        return null;
-    }
-
-    // 转换格式
-    let list = []
-    const keys = Object.keys(biz.resParams || {});
-    if (isArray(data)) {
-        if (keys.length === 0) {
-            return data;
-        }
-        for (let i = 0; i < data.length; i++) {
-            let d = data[i]
-            let dd = {}
-
-            keys.forEach(key => {
-                dd[biz.resParams[key]] = d[key]
-            })
-            list.push(dd);
-        }
-    } else {
-        if (keys.length === 0) {
-            return [data];
-        }
-        let dd = {}
-        keys.forEach(key => {
-            dd[biz.resParams[key]] = data[key]
-        })
-        list.push(dd);
-    }
-
-    return list;
-}
-
-async function getApiData(biz) {
-    let isFormData = false
-    if (biz.header && JSON.stringify(biz.header).indexOf('application/x-www-form-urlencoded') > 0) {
-        isFormData = true
-    }
-    // 1. 查询数据
-    let requestData = {
-        url: biz.url,
-        method: biz.method,
-        headers: biz.header,
-        timeout: 960 * 1000,
-    }
-    if (biz.method === 'GET') {
-        requestData.params = biz.params
-    } else {
-        requestData.data = isFormData ? qs.stringify(biz.params) : biz.params
-    }
-    let r = await request(requestData)
-    // 返回值格式转JSON
-    // r = getJSONResponse(r)
-    // 解析格式
-    return parsingFormat(r, biz.resFormat, biz.params)
-}
-
-async function getFileData(biz) {
-    let r = await request({
-        url: '/att/biz/api/getBizDataByFileUrl',
-        params: {fileUrl: biz.url}
-    })
-    // 返回值格式转JSON
-    // r = getJSONResponse(r)
-    // 解析格式
-    return parsingFormat(r, biz.resFormat, biz.params)
-}
-
-function getJSONResponse(res) {
-    let data = this.x2js.xml2js(r);
-}
-
-/**
- * 解析返回值格式
- * @param r       返回值
- * @param format  解析格式
- * @param params  传参
- * @returns {*}
- */
-function parsingFormat(r, format, params) {
-    format = format || '$.data';
-    const reg = /\{(.+?)\}/;
-    const reg_g = /\{(.+?)\}/g;
-    const result = format.match(reg_g);
-    if (result && result.length > 0) {
-        for (let i = 0; i < result.length; i++) {
-            let item = result[i]
-            let value = params[item.match(reg)[1]];
-            format = format.replace(item, value);
-        }
-    }
-    return jp.query(r, format)[0];
-}
-
 export async function getDataByDataId(id) {
     if (!id) {
         return null
@@ -160,7 +26,7 @@ export async function getChartOption(config) {
         return {}
     }
 
-    const renderingOptions = config.renderingOptionsData
+    const renderingOptions = JSON.parse(config.renderingOptions)
     switch (renderingOptions.chartType) {
         case 'linebar':
             return getBarLineChartOption(renderingOptions, data)
@@ -298,8 +164,14 @@ export async function getFormListData(config) {
     }
 
     const list = []
-    config.renderingOptionsData.columns.forEach(column => {
-        list.push({name: column.value, value: data[column.key]})
+    const columns = JSON.parse(config.renderingOptions)?.columns
+    columns.forEach(column => {
+        let value = filterData(data, column.key)
+        // 数据优化
+        if (isNumber(value)) {
+            value = value.toFixed(2)
+        }
+        list.push({name: column.value, value: value})
     })
     return list;
 }

+ 21 - 0
ruoyi-ui/src/utils/bus.js

@@ -0,0 +1,21 @@
+import mitt from 'mitt'
+
+const bus = mitt()
+export default bus
+
+/*
+// 发布一个事件
+bus.emit('foo', { a: 'b' })
+
+// 订阅一个具体的事件
+bus.on('foo', (e) => console.log('foo', e))
+
+// 订阅所有事件
+bus.on('*', (type, e) => console.log(type, e))
+
+// 取消订阅同名事件
+bus.off('foo') // unlisten
+
+// 取消所有事件
+bus.all.clear()
+* */

+ 24 - 0
ruoyi-ui/src/utils/data.js

@@ -0,0 +1,24 @@
+import {JSONPath} from "jsonpath-plus";
+
+/**
+ * 数据清洗
+ * @param data 数据源
+ * @param path jsonpath
+ * @param type 数据类型
+ */
+export function filterData(data, path, type) {
+    if (!data || !path) {
+        return
+    }
+
+    const filterData = JSONPath({path: path, json: data});
+
+    if (type === 'auto') {
+        return filterData.length === 1 ? filterData[0] : filterData;
+    } else if (type === 'array') {
+        return filterData;
+    } else {
+        return filterData[0];
+    }
+}
+

+ 29 - 0
ruoyi-ui/src/utils/validate.js

@@ -91,3 +91,32 @@ export function isArray(arg) {
   }
   return Array.isArray(arg)
 }
+
+/**
+ * @param {any} val
+ * @returns {Boolean}
+ */
+export function isNumber(val) {
+  return typeof val === 'number' && !isNaN(val)
+}
+
+/**
+ * @param {any} val
+ * @returns {Boolean}
+ */
+export function isDate(val) {
+  // 如果是 Date 对象且有效
+  if (val instanceof Date && !isNaN(val.getTime())) {
+    return true
+  }
+
+  // 如果是数字(时间戳)或字符串,尝试转换为日期
+  if (typeof val === 'number' || typeof val === 'string') {
+    const date = new Date(val)
+    return !isNaN(date.getTime())
+  }
+
+  return false
+}
+
+

+ 65 - 0
ruoyi-ui/src/views/map/components/bizDataCard.vue

@@ -0,0 +1,65 @@
+<template>
+  <div class="biz-data-card"
+       :style="{left: position.left, top: position.top, right: position.right, bottom: position.bottom, height: position.height}">
+    <div class="biz-data-card-header">{{ title }}</div>
+    <div class="biz-data-card-body">
+      <biz-display :config="config" :showTitle="false"></biz-display>
+    </div>
+  </div>
+</template>
+<script setup>
+import BizDisplay from "@/views/standardization/bizDataShowConfig/show/index.vue";
+import {isNumber} from "element-plus/es/utils/index";
+
+const props = defineProps({
+  config: {
+    type: Object,
+    default: () => {
+      return {};
+    }
+  },
+});
+
+const title = ref(null);
+const position = ref({
+  left: "0px",
+  top: "0px",
+  height: "100%"
+});
+
+watch(() => props.config, config => {
+  title.value = config.name
+  const positionConfig = JSON.parse(config.position)
+  const positionData = {}
+  positionData[positionConfig.xdirection] = isNumber(positionConfig.xvalue) ? positionConfig.xvalue + "px" : positionConfig.xvalue
+  positionData[positionConfig.ydirection] = isNumber(positionConfig.yvalue) ? positionConfig.yvalue + "px" : positionConfig.yvalue
+  positionData.height = isNumber(positionConfig.height) ? positionConfig.height + "px" : `calc(${positionConfig.height} - 20px)`
+  position.value = positionData
+}, {immediate: true, deep: true})
+</script>
+<style scoped lang="scss">
+.biz-data-card {
+  position: absolute;
+  height: 100%;
+  width: 20vw;
+  background: rgba(255, 255, 255, 0.9);
+  border-radius: 8px;
+
+  .biz-data-card-header {
+    width: 100%;
+    height: 36px;
+    background: url("@/assets/map/img/left-title.png") no-repeat;
+    background-size: 100% 100%;
+    font-size: 16px;
+    font-family: 'PuHuiTi', sans-serif;
+    font-weight: bolder;
+    color: #fff;
+    text-align: center;
+    line-height: 34px;
+  }
+
+  .biz-data-card-body {
+    height: calc(100% - 36px);
+  }
+}
+</style>

+ 139 - 4
ruoyi-ui/src/views/map/components/iconDropDialog.vue

@@ -1,9 +1,144 @@
 <template>
-  <div class="">
-    
+  <div class="dialog" v-if="showDialog" :style="{ left: dialogPosition.x + 'px', top: dialogPosition.y + 'px' }">
+    <div class="dialog-header" @mousedown="startDialogDrag">风场
+      <button class="close-btn" @click="closeDialog">×</button>
+    </div>
+    <div class="dialog-body"></div>
   </div>
 </template>
+<script setup>
+const showIcon = ref(true);
+const showDialog = ref(true);
+const isDragging = ref(false);
 
-<script setup></script>
+const fcPosition = ref({x: 280, y: 40});
+const dialogPosition = ref({x: 0, y: 0});
 
-<style scoped lang="scss"></style>
+const dragStartPosition = ref({x: 0, y: 0});
+const isDraggingDialog = ref(false);
+const dialogDragStart = ref({x: 0, y: 0});
+
+const startDrag = (event) => {
+  isDragging.value = true;
+  dragStartPosition.value = {
+    x: event.clientX - fcPosition.value.x,
+    y: event.clientY - fcPosition.value.y
+  };
+
+  window.addEventListener('mousemove', onMouseMove);
+  window.addEventListener('mouseup', onMouseUp);
+};
+const onMouseMove = (event) => {
+  if (!isDragging.value) return;
+
+  fcPosition.value = {
+    x: event.clientX - dragStartPosition.value.x,
+    y: event.clientY - dragStartPosition.value.y
+  };
+  dialogPosition.value = {
+    x: fcPosition.value.x,
+    y: fcPosition.value.y
+  }
+};
+const onMouseUp = () => {
+  if (isDragging.value) {
+    showIcon.value = false;
+    showDialog.value = true;
+    isDragging.value = false;
+    window.removeEventListener('mousemove', onMouseMove);
+    window.removeEventListener('mouseup', onMouseUp);
+  }
+};
+const startDialogDrag = (event) => {
+  isDraggingDialog.value = true;
+  dialogDragStart.value = {
+    x: event.clientX - dialogPosition.value.x,
+    y: event.clientY - dialogPosition.value.y
+  };
+
+  window.addEventListener('mousemove', onDialogMouseMove);
+  window.addEventListener('mouseup', onDialogMouseUp);
+};
+const onDialogMouseMove = (event) => {
+  if (!isDraggingDialog.value) return;
+
+  dialogPosition.value = {
+    x: event.clientX - dialogDragStart.value.x,
+    y: event.clientY - dialogDragStart.value.y
+  };
+};
+
+const onDialogMouseUp = () => {
+  isDraggingDialog.value = false;
+  window.removeEventListener('mousemove', onDialogMouseMove);
+  window.removeEventListener('mouseup', onDialogMouseUp);
+};
+
+const closeDialog = () => {
+  showDialog.value = false;
+  showIcon.value = true;
+  fcPosition.value = {
+    x: 280,
+    y: 40
+  }
+};
+
+onUnmounted(() => {
+  window.removeEventListener('mousemove', onMouseMove);
+  window.removeEventListener('mouseup', onMouseUp);
+  window.removeEventListener('mousemove', onDialogMouseMove);
+  window.removeEventListener('mouseup', onDialogMouseUp);
+});
+</script>
+<style scoped lang="scss">
+
+.dialog {
+  position: absolute;
+  width: 300px;
+  background: white;
+  border-radius: 10px;
+  box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
+  overflow: hidden;
+  z-index: 100;
+}
+
+.dialog-header {
+  padding: 5px;
+  background: rgba(52, 152, 219, 0.8);
+  color: white;
+  cursor: move;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.dialog-body {
+  padding: 20px;
+  color: #2c3e50;
+}
+
+.close-btn {
+  background: none;
+  border: none;
+  color: white;
+  font-size: 20px;
+  cursor: pointer;
+}
+
+.drop-indicator {
+  position: absolute;
+  bottom: 20px;
+  left: 50%;
+  transform: translateX(-50%);
+  padding: 10px 20px;
+  background: #2ecc71;
+  color: white;
+  border-radius: 20px;
+  opacity: 0;
+  transition: opacity 0.3s;
+}
+
+.drop-indicator.visible {
+  opacity: 1;
+}
+</style>

+ 459 - 0
ruoyi-ui/src/views/map/components/map.vue

@@ -0,0 +1,459 @@
+<template>
+  <div class="map-index">
+    <div id="mapChart"></div>
+  </div>
+</template>
+<script setup>
+import 'ol/css';
+import {defaults as defaultControls} from 'ol/control';
+import Map from 'ol/Map';
+import View from 'ol/View';
+import TileLayer from "ol/layer/Tile";
+import VectorLayer from "ol/layer/Vector";
+import Overlay from 'ol/Overlay';
+import {XYZ} from 'ol/source';
+
+import {createDynamicStyle} from "@/views/map/utils/styleParser.js";
+import {loadSource} from "../hooks/dataSourceManager.js";
+import bus from "@/utils/bus.js";
+import {getPopupContentByTemplate, loadPopupChartByTemplate} from "@/views/map/hooks/popupContent.js";
+
+
+const props = defineProps({
+  config: Object,
+});
+
+const mapChart = ref(null);
+// 存储弹窗的引用
+const popupOverlays = ref([]);
+
+onMounted(async () => {
+  await initMap();
+  // 初始化完成后执行配置渲染
+  if (props.config) {
+    // toCenter(props.config.center, props.config.zoom);
+    await loadLayers(props.config.layers);
+  }
+});
+
+const initMap = () => {
+  let vecLayer = new TileLayer({
+    source: new XYZ({
+      url: "http://t0.tianditu.gov.cn/vec_w/wmts?" +
+          "SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=vec&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles" +
+          "&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}" +
+          "&tk=9bb941214f10fbf9a3eab43f45cb2b7e",
+    }),
+  });
+  let cvaLayer = new TileLayer({
+    source: new XYZ({
+      url: "http://t0.tianditu.gov.cn/cva_w/wmts?" +
+          "SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cva&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles" +
+          "&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}" +
+          "&tk=9bb941214f10fbf9a3eab43f45cb2b7e",
+    }),
+  });
+
+  mapChart.value = new Map({
+    target: 'mapChart',
+    view: new View({
+      center: [121.472644, 31.231706],
+      zoom: 9,
+      minZoom: 3,
+      maxZoom: 16,
+      projection: 'EPSG:4326',
+    }),
+    layers: [vecLayer, cvaLayer],
+    controls: defaultControls({
+      zoom: false,//不显示放大放小按钮
+      rotate: false,//不显示指北针控件
+      attribution: false,//不显示右下角的地图信息控件
+      scaleLine: false,//不显示比例尺控件
+    })
+  });
+};
+
+const addVectorLayer = async (config) => {
+  loadSource(config).then(source => {
+    // 创建矢量图层
+    const vectorLayer = new VectorLayer({
+      id: config.id,
+      zIndex: config.zIndex,
+      source: source,
+      style: config.style ? createDynamicStyle(config.style) : null,
+      visible: config.visible !== false // 默认可见
+    });
+    // 添加到地图
+    mapChart.value.addLayer(vectorLayer);
+    // 添加事件监听
+    if (config.events) {
+      setupLayerEvents(vectorLayer, config.events);
+    }
+    return vectorLayer;
+  })
+};
+
+// 设置图层事件监听
+const setupLayerEvents = (layer, events) => {
+  // 点击事件处理
+  if (events.click) {
+    mapChart.value.on('click', (event) => {
+      const features = mapChart.value.getFeaturesAtPixel(event.pixel);
+      if (features && features.length > 0) {
+        // 处理点击事件,例如显示弹窗
+        handleFeatureClick(features[0], events.click);
+      }
+    });
+  }
+
+  // 悬停事件处理
+  if (events.hover) {
+    mapChart.value.on('pointermove', (event) => {
+      const features = mapChart.value.getFeaturesAtPixel(event.pixel);
+      if (features && features.length > 0) {
+        // 处理悬停事件,例如高亮显示
+        handleFeatureHover(features[0], events.hover);
+      }
+    });
+  }
+};
+
+// 处理要素点击事件
+const handleFeatureClick = (feature, clickConfig) => {
+  if (clickConfig.action === 'popup') {
+    // 显示弹窗逻辑
+    showFeaturePopup(feature, clickConfig.popupConfig);
+  }
+};
+
+// 处理要素悬停事件
+const handleFeatureHover = (feature, hoverConfig) => {
+  if (hoverConfig.action === 'highlight') {
+    // 高亮显示逻辑
+    highlightFeature(feature);
+  }
+};
+
+// 显示要素弹窗
+const showFeaturePopup = (feature, popupConfig) => {
+  // 清除已存在的弹窗
+  clearPopups();
+
+  const properties = feature.getProperties();
+  const geometry = feature.getGeometry();
+  const coordinates = geometry.getCoordinates();
+
+  // 创建弹窗元素
+  const popupElement = document.createElement('div');
+  popupElement.className = 'feature-popup';
+  // 根据配置生成弹窗内容
+  if (popupConfig && popupConfig.template) {
+    popupElement.innerHTML = getPopupContentByTemplate(popupConfig.template, properties, popupElement);
+  } else {
+    // 生成默认弹窗内容
+    let content = `
+      <div class="popup-header">
+        <h3>${properties.name || properties['测站名称'] || '未知站点'}</h3>
+        <button class="popup-close" onclick="closePopup()">×</button>
+      </div>
+      <div class="popup-content">
+        <div class="popup-info">
+    `;
+    // 添加属性信息
+    Object.keys(properties).forEach(key => {
+      if (key !== 'geometry' && key !== 'name' && key !== 'stationName') {
+        content += `<div class="info-item"><span class="label">${key}:</span><span class="value">${properties[key]}</span></div>`;
+      }
+    });
+    content += `
+        </div>
+      </div>
+    `;
+    popupElement.innerHTML = content;
+  }
+
+  // 添加关闭按钮事件监听
+  const closeBtn = popupElement.querySelector('.popup-close');
+  if (closeBtn) {
+    closeBtn.onclick = () => {
+      clearPopups();
+    };
+  }
+
+  let popupOverlay = new Overlay({
+    element: popupElement,
+    positioning: popupConfig?.positioning || "bottom-center",
+    stopEvent: popupConfig?.stopEvent !== undefined ? popupConfig.stopEvent : true,
+    offset: popupConfig?.offset || [0, -10],
+  });
+
+  popupOverlay.setPosition(coordinates);
+  mapChart.value.addOverlay(popupOverlay);
+  popupOverlays.value.push(popupOverlay);
+
+  if (popupConfig && popupConfig.template) {
+    loadPopupChartByTemplate(popupConfig.template, properties);
+  }
+};
+
+// 高亮要素
+const highlightFeature = (feature) => {
+  // 获取要素的原始样式
+  const originalStyle = feature.get('originalStyle') || feature.getStyle();
+
+  // 保存原始样式以便恢复
+  feature.set('originalStyle', originalStyle);
+
+  // 创建高亮样式
+  const highlightStyle = createDynamicStyle({
+    fill: {
+      color: 'rgba(255, 255, 0, 0.8)'  // 黄色填充
+    },
+    stroke: {
+      color: '#ff0000',  // 红色边框
+      width: 3
+    },
+    circle: {
+      radius: 8,
+      fill: {
+        color: 'rgba(255, 255, 0, 0.8)'
+      },
+      stroke: {
+        color: '#ff0000',
+        width: 3
+      }
+    }
+  });
+
+  // 应用高亮样式
+  feature.setStyle(highlightStyle);
+};
+
+let timerId = null
+
+// 聚焦
+function highlightPoint(center) {
+  // 先清除已存在的高亮图层
+  const existingHighlightOverlay = mapChart.value.getOverlays().getArray().find(ov => ov['id'] === 'highlight-point');
+  if (existingHighlightOverlay) {
+    mapChart.value.removeOverlay(existingHighlightOverlay);
+    if (timerId) {
+      clearTimeout(timerId);
+    }
+  }
+
+  // 创建 Overlay 并绑定元素
+  const overlayElement = document.createElement('div');
+  overlayElement.className = 'highlight-point-overlay';
+
+  const overlay = new Overlay({
+    id: 'highlight-point',
+    element: overlayElement,
+    position: center,
+    stopEvent: false,
+    zIndex: 1000,
+    offset: [-16, -18],
+  });
+
+  mapChart.value.addOverlay(overlay);
+
+  // 3秒后删掉 - 改进的删除方式
+  timerId = setTimeout(() => {
+    if (mapChart.value) {
+      try {
+        const overlayToRemove = mapChart.value.getOverlays().getArray().find(ov => ov['id'] === 'highlight-point');
+        if (overlayToRemove) {
+          mapChart.value.removeOverlay(overlayToRemove);
+        }
+      } catch (error) {
+        console.warn('删除高亮Overlay时出错:', error);
+      }
+    }
+  }, 3000)
+}
+
+
+// 清除所有弹窗
+const clearPopups = () => {
+  popupOverlays.value.forEach(overlay => {
+    mapChart.value.removeOverlay(overlay);
+  });
+  popupOverlays.value = [];
+};
+
+// 跳转到新中心点
+function toCenter(center, zoom, duration = 500, highlight) {
+  if (center && center.length >= 2) {
+    mapChart.value.getView().animate({
+      center: center,
+      zoom: zoom ? zoom : mapChart.value.getView().getZoom(),
+      duration: duration,
+    });
+
+    if (highlight) {
+      highlightPoint(center);
+    }
+  }
+}
+
+async function loadLayers(layers) {
+  for (const layer of layers) {
+    switch (layer.type) {
+      case 'vector':
+        await addVectorLayer(layer)
+    }
+  }
+}
+
+bus.on('show-map-position', ({latitude, longitude}) => {
+  toCenter([longitude, latitude], null, 1500, true)
+})
+
+// 实时更新地图
+watch(() => props.config, async (config) => {
+  // 渲染配置
+  // 1. 跳转中心点
+  toCenter(config.center, config.zoom);
+  // 渲染图层
+  await loadLayers(config.layers);
+}, {deep: true});
+</script>
+<style scoped lang="scss">
+/*滚动条里面轨道*/
+::-webkit-scrollbar-track {
+  background-color: rgba(20, 19, 19, 0);
+}
+
+/*关键设置 tbody出现滚动条*/
+::-webkit-scrollbar-thumb {
+  background-color: rgba(58, 100, 179, 0.5);
+  border-radius: 8px 10px;
+}
+
+::v-deep(.el-scrollbar) {
+  --el-scrollbar-bg-color: rgba(58, 100, 179);
+  --el-scrollbar-hover-bg-color: rgba(58, 100, 179);
+}
+
+.map-index {
+  height: 100%;
+  width: 100%;
+  position: relative;
+
+  #mapChart {
+    height: 100%;
+    width: 100%;
+  }
+
+}
+</style>
+<style lang="scss">
+.custom-popup {
+  background: rgba(255, 255, 255, 0.95);
+  border-radius: 8px;
+  box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
+  border: 1px solid #3498db;
+  width: 172px;
+
+  .popup-title {
+    width: 100%;
+    line-height: 15px;
+    background-color: rgba(255, 23, 0);
+    padding: 10px;
+    text-align: center;
+    color: #fff;
+    font-size: 16px;
+  }
+
+  .popup-top {
+    padding: 5px 10px;
+    width: 100%;
+    background-color: rgba(58, 100, 179);
+    display: flex;
+    flex-direction: column;
+    color: #fff;
+    font-size: 14px;
+  }
+
+  .popup-bottom {
+    padding: 5px 10px;
+    width: 100%;
+    background-color: rgba(71, 146, 211);
+    display: flex;
+    flex-direction: column;
+    color: #fff;
+    font-size: 14px;
+  }
+}
+
+.feature-popup {
+  background: rgba(255, 255, 255, 0.95);
+  border-radius: 8px;
+  box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
+  border: 1px solid #3498db;
+  width: 250px;
+  font-family: Arial, sans-serif;
+
+  .popup-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    background-color: rgba(58, 100, 179);
+    padding: 10px;
+    border-radius: 8px 8px 0 0;
+
+    h3 {
+      margin: 0;
+      color: white;
+      font-size: 14px;
+    }
+
+    .popup-close {
+      background: none;
+      border: none;
+      color: white;
+      font-size: 18px;
+      cursor: pointer;
+    }
+  }
+
+  .popup-content {
+    padding: 10px;
+  }
+
+  .popup-info {
+    .info-item {
+      display: flex;
+      justify-content: space-between;
+      margin-bottom: 5px;
+      font-size: 12px;
+
+      .label {
+        font-weight: bold;
+        color: #333;
+      }
+
+      .value {
+        color: #666;
+      }
+    }
+  }
+
+  .popup-chart {
+    border-top: 1px solid #eee;
+    padding-top: 10px;
+
+    canvas {
+      width: 100%;
+    }
+  }
+}
+
+.highlight-point-overlay {
+  width: 32px;
+  height: 32px;
+  background-image: url('@/assets/map/img/dyCenter.gif');
+  background-size: contain;
+  background-repeat: no-repeat;
+}
+</style>

+ 78 - 0
ruoyi-ui/src/views/map/hooks/dataSourceManager.js

@@ -0,0 +1,78 @@
+import {jsonToFeatures} from '../utils/jsonToGeojson.js'
+import {getBizDataByConfig} from "@/api/standardization/bizDataShowConfig.js";
+import {ImageStatic, Vector as VectorSource} from 'ol/source';
+import {GeoJSON} from 'ol/format';
+
+
+export async function loadSource(config) {
+    const {id, source} = config;
+    switch (source.type) {
+        case 'api':
+            const response = await getBizDataByConfig(JSON.stringify(config.source));
+            const rawData = response.data;
+            const features = jsonToFeatures(rawData, config.source.mapping);
+            return new VectorSource({features})
+        case 'geojson':
+            return setupGeojsonSource(id, config);
+        case 'static-image':
+            return setupImageSource(id, config);
+        case 'static':  // 添加对 static 类型的支持
+            return setupStaticSource(id, config);
+        default:
+            console.warn(`未知的数据源类型: ${source.type}`);
+    }
+}
+
+async function setupApiSource(id, config) {
+    try {
+        return await getBizDataByConfig(JSON.stringify(config.source)).then(response => {
+            const rawData = response.data;
+            const features = jsonToFeatures(rawData, config.source.mapping);
+            return new VectorSource({features})
+        })
+    } catch (error) {
+        console.error(`加载数据源 ${id} 失败:`, error);
+    }
+}
+
+// 设置GeoJSON数据源
+function setupGeojsonSource(id, config) {
+    try {
+        return new VectorSource({
+            features: new GeoJSON().readFeatures(config.source.data, {
+                featureProjection: 'EPSG:3857'
+            })
+        });
+    } catch (error) {
+        console.error(`加载GeoJSON数据源 ${id} 失败:`, error);
+    }
+}
+
+// 设置静态图像数据源
+function setupImageSource(id, config) {
+    try {
+        return new ImageStatic({
+            url: config.source.url,
+            projection: config.source.projection || 'EPSG:3857',
+            imageExtent: config.source.imageExtent
+        });
+    } catch (error) {
+        console.error(`加载静态图像数据源 ${id} 失败:`, error);
+    }
+}
+
+// 添加静态数据源处理函数
+function setupStaticSource(id, config) {
+    try {
+        if (config.source.data && config.source.data.type === 'FeatureCollection') {
+            const features = new GeoJSON().readFeatures(config.source.data, {
+                featureProjection: 'EPSG:4326' // 根据实际情况调整投影
+            });
+            return new VectorSource({features});
+        } else if (Array.isArray(config.source.data)) {
+            return new VectorSource({features: config.source.data});
+        }
+    } catch (error) {
+        console.error(`加载静态数据源 ${id} 失败:`, error);
+    }
+}

+ 85 - 0
ruoyi-ui/src/views/map/hooks/popupContent.js

@@ -0,0 +1,85 @@
+import * as echarts from 'echarts';
+
+
+export function getPopupContentByTemplate(template, properties, popupElement) {
+    switch (template) {
+        case 'SCSSFM_POPUP':
+            popupElement.style.width = '600px';
+            return getPopupContentBySCSSFM(properties, popupElement);
+        default:
+            return getPopupContentBySCSSFM(properties, popupElement);
+    }
+}
+
+export function loadPopupChartByTemplate(template, properties) {
+    let option = null;
+    switch (template) {
+        case 'SCSSFM_POPUP':
+            option = loadSCSSFMPopupChart(properties);
+            break;
+        default:
+            option = loadSCSSFMPopupChart(properties);
+    }
+    const popupChart = echarts.init(document.getElementById("popupChart"));
+    popupChart.setOption(option);
+}
+
+function getPopupContentBySCSSFM(properties) {
+    // 生成默认弹窗内容
+    let content = `
+      <div class="popup-header">
+        <h3>${properties['测站名称']}</h3>
+        <button class="popup-close" onclick="closePopup()">×</button>
+      </div>
+      <div class="popup-content">
+        <div class="popup-info">
+    `;
+    // 添加属性信息
+    Object.keys(properties).forEach(key => {
+        if (key !== 'geometry' && key !== 'name' && key !== 'stationName' && key !== '预报潮位') {
+            content += `<div class="info-item"><span class="label">${key}:</span><span class="value">${properties[key]}</span></div>`;
+        }
+    });
+    content += `
+        </div>
+        <div class="popup-chart">
+             <div id="popupChart" style="width: 100%;height: 200px;"></div>
+        </div>
+      </div>
+    `;
+    return content;
+}
+
+function loadSCSSFMPopupChart(properties) {
+    const data = properties['预报潮位']
+    return {
+        title: {
+            text: '潮位预报'
+        },
+        tooltip: {
+            trigger: 'axis'
+        },
+        grid: {
+            left: '2%',
+            right: '2%',
+            top: '10%',
+            bottom: '2%',
+            containLabel: true
+        },
+        xAxis: {
+            type: 'category',
+            data: data.map(item => item.tm.substring(5, 16).replace(' ', '\n'))
+        },
+        yAxis: {
+            type: 'value'
+        },
+        series: [
+            {
+                data: data.map(item => item.value?.toFixed(2)),
+                showSymbol: false,
+                type: 'line',
+                smooth: true
+            }
+        ]
+    };
+}

+ 112 - 551
ruoyi-ui/src/views/map/index.vue

@@ -1,106 +1,113 @@
 <template>
-  <div class="map-index">
-    <div id="mapChart"></div>
-    <div class="map-left">
-      <div class="left-title">模型列表</div>
-      <div class="left-tree">
-        <el-card v-for="config in bizDataShowConfigList" class="biz-data-config-container">
-          <div style="height: 200px;">
-            <biz-display :config="config"></biz-display>
-          </div>
-        </el-card>
-      </div>
-    </div>
-    <div class="map-right">
-      <div class="right-title">模型数据展示</div>
-      <div class="right-top-title">
-        <img src="@/assets/map/img/站点.png"/>
-        <span>预报站点信息</span>
-      </div>
-      <div class="station-table">
-        <el-scrollbar style="height: 100%;">
-          <table>
-            <thead>
-            <tr>
-              <td class="table-index">序号</td>
-              <td class="table-head">站码</td>
-              <td class="table-head">站名</td>
-              <td class="table-head">实时潮位</td>
-              <td class="table-head">发生时间</td>
-              <td class="table-head">警戒潮位</td>
-              <td class="table-head">距离警戒</td>
-              <td class="table-head">预报潮位</td>
-              <td class="table-head">发生时间</td>
-              <td class="table-head">距离警戒</td>
-            </tr>
-            </thead>
-            <tbody>
-            <tr
-                v-for="(item,index) in StnmData.data"
-                :key="index"
-            >
-              <td class="table-index">{{ index + 1 }}</td>
-              <td class="table-tbody">{{ item.stationName }}</td>
-              <td class="table-tbody">{{ item.stationCode }}</td>
-              <td class="table-tbody"></td>
-              <td class="table-tbody"></td>
-              <td class="table-tbody"></td>
-              <td class="table-tbody"></td>
-              <td class="table-tbody"></td>
-              <td class="table-tbody"></td>
-              <td class="table-tbody"></td>
-            </tr>
-            </tbody>
-          </table>
-        </el-scrollbar>
-      </div>
-    </div>
-    <!-- 可拖拽图标组 -->
-    <div class="map-fcicon" :style="{ left: fcPosition.x + 'px', top: fcPosition.y + 'px' }" v-show="showIcon"
-         @mousedown="startDrag"></div>
-    <div class="dialog" v-if="showDialog" :style="{ left: dialogPosition.x + 'px', top: dialogPosition.y + 'px' }">
-      <div class="dialog-header" @mousedown="startDialogDrag">风场
-        <button class="close-btn" @click="closeDialog">×</button>
-      </div>
-      <div class="dialog-body"></div>
-    </div>
+  <div class="map-container">
+    <gw-map ref="mapRef" :config="mapConfig"></gw-map>
+    <biz-data-card v-for="bizDataShowConfig in bizDataShowConfigList" :key="bizDataShowConfig.id"
+                   :config="bizDataShowConfig"></biz-data-card>
   </div>
 </template>
 <script setup>
-import 'ol/css';
-import {defaults as defaultControls} from 'ol/control';
-import Map from 'ol/Map';
-import View from 'ol/View';
-import TileLayer from "ol/layer/Tile";
-import {Vector as VectorSource, XYZ} from 'ol/source.js';
-import VectorLayer from "ol/layer/Vector";
-import {Point} from "ol/geom";
-import {Icon, Style} from 'ol/style';
-import Feature from 'ol/Feature';
-import Overlay from 'ol/Overlay';
-import StnmData from "@/assets/map/json/stnmData.json";
-import red_trangle from "@/assets/map/img/Ⅳ.png";
 import {getBizDataShowConfigList} from "@/api/standardization/bizDataShowConfig.js";
-import BizDisplay from "@/views/standardization/bizDataShowConfig/show/index.vue";
-
+import BizDataCard from "@/views/map/components/bizDataCard.vue";
+import GwMap from "./components/map.vue"
 
 const bizDataShowConfigList = ref([]);
 
-const mapChart = ref(null);
-const mapCenter = ref([121.472644, 31.231706]);
-const mapZoom = ref(9);
-const stnmVectorLayer = ref(null);
-const popupOverlays = ref([]);// 存储弹窗的引用
-const showIcon = ref(true);
-const showDialog = ref(false);
-const isDragging = ref(false);
-
-const fcPosition = ref({x: 280, y: 40});
-const dialogPosition = ref({x: 0, y: 0});
-
-const dragStartPosition = ref({x: 0, y: 0});
-const isDraggingDialog = ref(false);
-const dialogDragStart = ref({x: 0, y: 0});
+const mapRef = ref(null);
+const mapConfig = ref({
+  zoom: 9,
+  center: [116.397428, 39.90923],
+  layers: [
+    {
+      id: 'weather-stations',
+      type: 'vector', // 指定为矢量图层
+      name: '行政区划面图层',
+      source: {
+        type: 'api', // 数据源类型为API
+        url: 'http://localhost:8082/SCSSFM/getCalResultsByStation',
+        method: 'POST',
+        body: [
+          {key: 'projectId', value: '230103', type: 'string'},
+          {key: 'forecastSchemeId', value: '2301031', type: 'string'},
+          {key: 'calSchemeIds', value: ["20421b55-5383-4ef0-a"], type: 'array'},
+        ],
+        headers: [
+          {
+            key: 'authorization',
+            value: 'Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6IjBhOTE3ZDdhLTUwODktNDg1MC05YTUyLTk1NzRjZWUzYWU5ZSJ9.QuFdSq-_mLFwOlM249-ledRlM4U2_qIRVfdOzGIOnf38XY-QXyaP0k-me2gT1wf5LCjOW0z-zJnO-SnNo78eOg',
+            type: 'string'
+          },
+        ],
+        responseResolution: "$.data",
+        // 定义数据映射规则
+        mapping: {
+          // 指定哪些字段包含几何信息(经度、纬度)
+          geometry: {
+            type: 'Point', // 几何类型
+            coordinates: {
+              longitude: 'lgtd', // 接口中的经度字段名
+              latitude: 'lttd'   // 接口中的纬度字段名
+            }
+          },
+          // 定义要保留的属性字段
+          properties: {
+            '测站编码': 'stationCode',     // 目标属性名: 源数据字段名
+            '测站名称': 'stationName',
+            '预报最大水位': 'calSchemeInfos[0].results[0].maxData.value',
+            '预报最大水位时间': 'calSchemeInfos[0].results[0].maxData.tm',
+            '预报最小水位': 'calSchemeInfos[0].results[0].minData.value',
+            '预报最小水位时间': 'calSchemeInfos[0].results[0].minData.tm',
+            '预报平均水位': 'calSchemeInfos[0].results[0].average',
+            '警戒水位': 'alarmValue',
+            '预报潮位': 'calSchemeInfos[0].results[0].datas',
+          }
+        },
+        // 轮询更新(可选)
+        refreshInterval: 30000 // 30秒刷新一次
+      },
+      visible: true,
+      zIndex: 1000,
+      style: {
+        // 点要素样式
+        point: {
+          image: {
+            type: 'icon',           // circle|icon
+            // type: 'circle',           // circle|icon
+            name: 'red_trangle',
+            radius: 8,
+            fill: {color: '#FF5722'},
+            stroke: {color: '#fff', width: 2}
+          }
+        },
+        // 线要素样式
+        line: {
+          stroke: {
+            color: '#0066ff',
+            width: 3,
+            lineDash: [5, 5]
+          }
+        },
+        // 面要素样式
+        polygon: {
+          fill: {color: 'rgba(0, 102, 255, 0.2)'},
+          stroke: {color: '#0066ff', width: 2}
+        }
+      },
+      events: {
+        click: {
+          action: 'popup',           // 点击触发弹窗
+          popupConfig: {
+            // 弹窗模板ID
+            template: 'SCSSFM_POPUP',
+            offset: [0, -20]
+          }
+        },
+        // hover: {
+        //   action: 'highlight'       // 悬停高亮
+        // }
+      }
+    },
+  ]
+});
 
 function getBizDataConfigList() {
   getBizDataShowConfigList().then(res => {
@@ -110,197 +117,21 @@ function getBizDataConfigList() {
 
 onMounted(() => {
   getBizDataConfigList();
-  initMap();
-});
-
-const initMap = () => {
-  let vecLayer = new TileLayer({
-    source: new XYZ({
-      url: "http://t0.tianditu.gov.cn/vec_w/wmts?" +
-          "SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=vec&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles" +
-          "&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}" +
-          "&tk=9bb941214f10fbf9a3eab43f45cb2b7e",
-    }),
-  });
-  let cvaLayer = new TileLayer({
-    source: new XYZ({
-      url: "http://t0.tianditu.gov.cn/cva_w/wmts?" +
-          "SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cva&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles" +
-          "&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}" +
-          "&tk=9bb941214f10fbf9a3eab43f45cb2b7e",
-    }),
-  });
-  mapChart.value = new Map({
-    target: 'mapChart',
-    view: new View({
-      center: mapCenter.value,
-      zoom: mapZoom.value,
-      minZoom: 3,
-      maxZoom: 16,
-      projection: 'EPSG:4326',
-    }),
-    layers: [vecLayer, cvaLayer],
-    controls: defaultControls({
-      zoom: false,//不显示放大放小按钮
-      rotate: false,//不显示指北针控件
-      attribution: false,//不显示右下角的地图信息控件
-      scaleLine: false,//不显示比例尺控件
-    })
-  });
-  addStnm();
-};
-const addStnm = () => {
-  let features = [];
-  let stationPopData = [];
-  const targetStations = ['绿华山', '横沙', '崇西闸', '金山嘴'];
-  StnmData.data.forEach(item => {
-    targetStations.includes(item.stationName) && stationPopData.push(item);
-    let point = new Point([item.lgtd, item.lttd]);
-    let feature = new Feature({
-      geometry: point,
-      name: item.stationName,
-      properties: item,
-    });
-    var style = new Style({
-      image: new Icon({
-        anchor: [0.5, 0.5],
-        scale: 0.08,
-        src: red_trangle,
-      }),
-    });
-    feature.setStyle(style);
-    features.push(feature);
-  })
-  let vectorSource = new VectorSource({
-    features: features,
-  });
-  stnmVectorLayer.value = new VectorLayer({
-    source: vectorSource,
-  });
-  mapChart.value.addLayer(stnmVectorLayer.value);
-  showPopupInfo(stationPopData);
-};
-// 显示地图坐标信息
-const showPopupInfo = (data) => {
-  data.forEach((station, index) => {
-    // 创建弹窗元素
-    const popupElement = document.createElement('div');
-    popupElement.className = 'custom-popup';
-    popupElement.id = `popup_${station.stationCode}`;
-    // 设置弹窗内容
-    popupElement.innerHTML = `
-        <div class="popup-title">${station.stationName} (${station.stationCode})</div>
-        <div class="popup-top">
-          <span>实时潮位:<span></span></span>
-          <span>发生时间:<span></span></span>
-          <span>警戒潮位:<span></span></span>
-          <span>距离警戒:<span></span></span>
-        </div>
-        <div class="popup-bottom">
-          <span>预报潮位:<span></span></span>
-          <span>发生时间:<span></span></span>
-          <span>距离警戒:<span></span></span>
-        </div>
-    `;
-    let popupOverlay = new Overlay({
-      element: popupElement,
-      positioning: "top-center",
-      stopEvent: false,
-      offset: [0, -200],
-    });
-    const coordinate = [station.lgtd, station.lttd];
-    popupOverlay.setPosition(coordinate);
-    mapChart.value.addOverlay(popupOverlay);
-    // 保存引用
-    popupOverlays.value.push(popupOverlay);
-  });
-};
-// 清除所有弹窗
-const clearPopups = () => {
-  popupOverlays.value.forEach(overlay => {
-    mapChart.value.removeOverlay(overlay);
-  });
-  popupOverlays.value = [];
-};
-
-const startDrag = (event) => {
-  isDragging.value = true;
-  dragStartPosition.value = {
-    x: event.clientX - fcPosition.value.x,
-    y: event.clientY - fcPosition.value.y
-  };
-
-  window.addEventListener('mousemove', onMouseMove);
-  window.addEventListener('mouseup', onMouseUp);
-};
-const onMouseMove = (event) => {
-  if (!isDragging.value) return;
-
-  fcPosition.value = {
-    x: event.clientX - dragStartPosition.value.x,
-    y: event.clientY - dragStartPosition.value.y
-  };
-  dialogPosition.value = {
-    x: fcPosition.value.x,
-    y: fcPosition.value.y
-  }
-};
-const onMouseUp = () => {
-  if (isDragging.value) {
-    showIcon.value = false;
-    showDialog.value = true;
-    isDragging.value = false;
-    window.removeEventListener('mousemove', onMouseMove);
-    window.removeEventListener('mouseup', onMouseUp);
-  }
-};
-const startDialogDrag = (event) => {
-  isDraggingDialog.value = true;
-  dialogDragStart.value = {
-    x: event.clientX - dialogPosition.value.x,
-    y: event.clientY - dialogPosition.value.y
-  };
-
-  window.addEventListener('mousemove', onDialogMouseMove);
-  window.addEventListener('mouseup', onDialogMouseUp);
-};
-const onDialogMouseMove = (event) => {
-  if (!isDraggingDialog.value) return;
-
-  dialogPosition.value = {
-    x: event.clientX - dialogDragStart.value.x,
-    y: event.clientY - dialogDragStart.value.y
-  };
-};
-
-const onDialogMouseUp = () => {
-  isDraggingDialog.value = false;
-  window.removeEventListener('mousemove', onDialogMouseMove);
-  window.removeEventListener('mouseup', onDialogMouseUp);
-};
-
-const closeDialog = () => {
-  showDialog.value = false;
-  showIcon.value = true;
-  fcPosition.value = {
-    x: 280,
-    y: 40
-  }
-};
-
-onUnmounted(() => {
-  window.removeEventListener('mousemove', onMouseMove);
-  window.removeEventListener('mouseup', onMouseUp);
-  window.removeEventListener('mousemove', onDialogMouseMove);
-  window.removeEventListener('mouseup', onDialogMouseUp);
 });
 </script>
 
 <style scoped lang="scss">
-.biz-data-config-container {
+.map-container {
+  height: 100%;
+  width: 100%;
+  position: relative;
+
+  .biz-data-config-container {
+
+    & + .biz-data-config-container {
+      margin-top: 10px;
+    }
 
-  & + .biz-data-config-container {
-    margin-top: 10px;
   }
 
 }
@@ -320,274 +151,4 @@ onUnmounted(() => {
   --el-scrollbar-bg-color: rgba(58, 100, 179);
   --el-scrollbar-hover-bg-color: rgba(58, 100, 179);
 }
-
-.map-index {
-  height: 100%;
-  width: 100%;
-  position: relative;
-
-  #mapChart {
-    height: 100%;
-    width: 100%;
-  }
-
-  .map-left {
-    position: absolute;
-    top: 1%;
-    left: 0.5%;
-    height: 98%;
-    width: 260px;
-    background: rgba(255, 255, 255, 0.9);
-    border-radius: 8px;
-
-    .left-title {
-      width: 100%;
-      height: 36px;
-      background: url("@/assets/map/img/left-title.png") no-repeat;
-      background-size: 100% 100%;
-      font-size: 16px;
-      font-family: 'PuHuiTi', sans-serif;
-      font-weight: bolder;
-      color: #fff;
-      text-align: center;
-      line-height: 34px;
-    }
-  }
-
-  .map-right {
-    position: absolute;
-    top: 1%;
-    right: 0.5%;
-    height: 98%;
-    width: 400px;
-    background: rgba(255, 255, 255, 0.9);
-    border-radius: 8px;
-
-    .right-title {
-      width: 100%;
-      height: 36px;
-      background: url("@/assets/map/img/left-title.png") no-repeat;
-      background-size: 100% 100%;
-      font-size: 16px;
-      font-family: 'PuHuiTi', sans-serif;
-      font-weight: bolder;
-      color: #fff;
-      text-align: center;
-      line-height: 34px;
-    }
-
-    .right-top-title {
-      width: 100%;
-      line-height: 30px;
-      padding: 10px;
-      display: flex;
-
-      img {
-        width: 30px;
-        height: 30px;
-      }
-
-      span {
-        color: #000;
-        font-size: 16px;
-        font-family: 'PuHuiTi', sans-serif;
-        font-weight: bolder;
-        margin-left: 10px;
-      }
-    }
-  }
-}
-
-.station-table {
-  width: 94%;
-  margin: 0 3%;
-
-  .table-index {
-    width: 40px;
-  }
-
-  .table-head {
-    width: 80px;
-  }
-
-  table {
-    width: 100%;
-    border-spacing: 0px;
-    border-collapse: collapse; /* 设置表格边框合并为单线 */
-    border-top: 2px solid #82bcfd;
-    border-bottom: 2px solid #82bcfd;
-
-    thead {
-      background-image: -webkit-linear-gradient(top, #ebf5ff, #fff);
-      color: #000;
-      font-size: 14px;
-      font-family: 'PuHuiTi', sans-serif;
-      font-weight: bold;
-      width: 100%;
-      border-left: 1px solid #d3e8f9;
-      border-right: 1px solid #d3e8f9;
-      border-bottom: 1px solid #d3e8f9;
-
-      td {
-        text-align: center;
-        padding: 10px 0;
-      }
-    }
-
-    tbody {
-      color: #000;
-      font-size: 12px;
-      font-family: 'PuHuiTi', sans-serif;
-      width: 100%;
-      border: 1px solid #d3e8f9;
-      border-top: none;
-
-      tr {
-        line-height: 28px;
-        background-image: -webkit-linear-gradient(top, #d7eaff, #fff);
-      }
-
-      td {
-        /* border-bottom: 1px solid #d4f0fc; */
-        text-align: center;
-        padding: 5px 0;
-      }
-    }
-  }
-
-  table tbody {
-    display: block;
-    height: 20vh !important;
-    overflow-y: scroll;
-  }
-
-  table thead, tbody tr {
-    display: table;
-    width: 100%;
-    table-layout: fixed;
-  }
-}
-
-.map-fcicon {
-  position: absolute;
-  width: 40px;
-  height: 40px;
-  background-image: url("@/assets/map/img/风场.png");
-  background-repeat: no-repeat;
-  background-size: 50%;
-  background-position: center;
-  background-color: rgba(72, 174, 228);
-  color: white;
-  border-radius: 50%;
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  cursor: grab;
-  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
-  transition: transform 0.2s, box-shadow 0.2s;
-  z-index: 10;
-
-  .fc-img {
-    width: 25px;
-    height: 25px;
-  }
-}
-
-.map-fcicon:hover {
-  transform: scale(1.1);
-  box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3);
-}
-
-.map-fcicon:active {
-  cursor: grabbing;
-}
-
-.dialog {
-  position: absolute;
-  width: 300px;
-  background: white;
-  border-radius: 10px;
-  box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
-  overflow: hidden;
-  z-index: 100;
-}
-
-.dialog-header {
-  padding: 5px;
-  background: rgba(52, 152, 219, 0.8);
-  color: white;
-  cursor: move;
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-}
-
-.dialog-body {
-  padding: 20px;
-  color: #2c3e50;
-}
-
-.close-btn {
-  background: none;
-  border: none;
-  color: white;
-  font-size: 20px;
-  cursor: pointer;
-}
-
-.drop-indicator {
-  position: absolute;
-  bottom: 20px;
-  left: 50%;
-  transform: translateX(-50%);
-  padding: 10px 20px;
-  background: #2ecc71;
-  color: white;
-  border-radius: 20px;
-  opacity: 0;
-  transition: opacity 0.3s;
-}
-
-.drop-indicator.visible {
-  opacity: 1;
-}
 </style>
-<style lang="scss">
-.custom-popup {
-  background: rgba(255, 255, 255, 0.95);
-  border-radius: 8px;
-  box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
-  border: 1px solid #3498db;
-  width: 172px;
-
-  .popup-title {
-    width: 100%;
-    line-height: 15px;
-    background-color: rgba(255, 23, 0);
-    padding: 10px;
-    text-align: center;
-    color: #fff;
-    font-size: 16px;
-  }
-
-  .popup-top {
-    padding: 5px 10px;
-    width: 100%;
-    background-color: rgba(58, 100, 179);
-    display: flex;
-    flex-direction: column;
-    color: #fff;
-    font-size: 14px;
-  }
-
-  .popup-bottom {
-    padding: 5px 10px;
-    width: 100%;
-    background-color: rgba(71, 146, 211);
-    display: flex;
-    flex-direction: column;
-    color: #fff;
-    font-size: 14px;
-  }
-}
-</style>

+ 59 - 0
ruoyi-ui/src/views/map/utils/jsonToGeojson.js

@@ -0,0 +1,59 @@
+// 通用JSON转GeoJSON转换器
+import Feature from 'ol/Feature';
+import Point from 'ol/geom/Point';
+import {filterData} from "@/utils/data.js";
+import {isNumber} from "@/utils/validate.js";
+
+export function jsonToFeatures(data, mappingConfig) {
+    const features = [];
+    data.forEach(item => {
+        // 构建几何图形
+        const geometry = buildGeometry(item, mappingConfig.geometry);
+        // 构建属性
+        const properties = buildProperties(item, mappingConfig.properties);
+        const feature = new Feature({
+            geometry: geometry,
+            ...properties,  // 直接将属性展开到 Feature 中
+        });
+        features.push(feature)
+    });
+    return features
+}
+
+function buildGeometry(item, geomMapping) {
+    const {type, coordinates} = geomMapping;
+
+    if (type === 'Point') {
+        const lngField = coordinates.longitude;
+        const latField = coordinates.latitude;
+        // 使用 OpenLayers Point 类创建几何对象
+        return new Point([
+            parseFloat(item[lngField]),
+            parseFloat(item[latField])
+        ]);
+    }
+    // 可以扩展支持LineString、Polygon等
+}
+
+function buildProperties(item, mapping) {
+    const properties = {};
+
+    // 如果没有提供映射配置,返回空属性对象
+    if (!mapping) {
+        return properties;
+    }
+
+    // 遍历映射配置,将原始数据字段映射到属性中
+    Object.keys(mapping).forEach(key => {
+        const sourceKey = mapping[key];
+        let value = filterData(item, sourceKey, "auto");
+        if (value) {
+            if (isNumber(value)) {
+                value = value.toFixed(2)
+            }
+            properties[key] = value;
+        }
+    });
+
+    return properties;
+}

+ 157 - 0
ruoyi-ui/src/views/map/utils/styleParser.js

@@ -0,0 +1,157 @@
+import blue_trangle from "@/assets/map/img/Ⅰ.png";
+import green_trangle from "@/assets/map/img/Ⅱ.png";
+import yellow_trangle from "@/assets/map/img/Ⅲ.png";
+import red_trangle from "@/assets/map/img/Ⅳ.png";
+import pink_trangle from "@/assets/map/img/Ⅴ.png";
+import blue_circle from "@/assets/map/img/blue-circle.png";
+import dy_center from "@/assets/map/img/dyCenter.gif";
+
+import {Circle, Fill, Icon, Stroke, Style, Text} from 'ol/style';
+
+export function createDynamicStyle(styleConfig) {
+    return (feature, resolution) => {
+        const properties = feature.getProperties();
+        const geometryType = feature.getGeometry().getType();
+
+        // 根据要素属性动态计算样式
+        const finalStyle = evaluateStyleRules(styleConfig, properties, resolution, geometryType);
+
+        return new Style({
+            image: finalStyle.image ? createImageStyle(finalStyle.image) : undefined,
+            fill: finalStyle.fill ? new Fill(finalStyle.fill) : undefined,
+            stroke: finalStyle.stroke ? new Stroke(finalStyle.stroke) : undefined,
+            text: finalStyle.text ? createTextStyle(finalStyle.text, properties) : undefined
+        });
+    };
+}
+
+// 支持条件样式规则
+function evaluateStyleRules(styleConfig, properties, resolution, geometryType) {
+    const style = {...styleConfig[geometryType.toLowerCase()]};
+
+    // 处理条件样式
+    if (style.rules) {
+        style.rules.forEach(rule => {
+            if (evaluateCondition(rule.condition, properties, resolution)) {
+                Object.assign(style, rule.style);
+            }
+        });
+    }
+
+    return style;
+}
+
+// 条件评估函数
+function evaluateCondition(condition, properties, resolution) {
+    if (!condition) return true;
+
+    // 支持多种条件类型
+    if (typeof condition === 'function') {
+        return condition(properties, resolution);
+    }
+
+    if (typeof condition === 'object') {
+        const {property, operator, value} = condition;
+        const propValue = properties[property];
+
+        switch (operator) {
+            case '==':
+                return propValue == value;
+            case '===':
+                return propValue === value;
+            case '!=':
+                return propValue != value;
+            case '!==':
+                return propValue !== value;
+            case '>':
+                return propValue > value;
+            case '>=':
+                return propValue >= value;
+            case '<':
+                return propValue < value;
+            case '<=':
+                return propValue <= value;
+            case 'in':
+                return Array.isArray(value) && value.includes(propValue);
+            case 'notIn':
+                return Array.isArray(value) && !value.includes(propValue);
+            case 'contains':
+                return typeof propValue === 'string' && propValue.includes(value);
+            default:
+                return false;
+        }
+    }
+
+    return false;
+}
+
+// 创建图像样式
+function createImageStyle(imageConfig) {
+    if (!imageConfig) return undefined;
+
+    switch (imageConfig.type) {
+        case 'circle':
+            return new Circle({
+                radius: imageConfig.radius || 5,
+                fill: imageConfig.fill ? new Fill(imageConfig.fill) : undefined,
+                stroke: imageConfig.stroke ? new Stroke(imageConfig.stroke) : undefined
+            });
+        case 'icon':
+            return new Icon({
+                anchor: [0.5, 0.5],
+                scale: 0.08,
+                src: getImagePath(imageConfig.name),
+            });
+        default:
+            return undefined;
+    }
+}
+
+// 创建文本样式
+function createTextStyle(textConfig, properties) {
+    if (!textConfig) return undefined;
+
+    const config = {...textConfig};
+
+    // 动态设置文本内容
+    if (config.property) {
+        config.text = properties[config.property];
+    }
+
+    // 支持文本格式化
+    if (config.formatter && typeof config.formatter === 'function') {
+        config.text = config.formatter(properties);
+    }
+
+    return new Text({
+        text: config.text || '',
+        font: config.font,
+        fill: config.fill ? new Fill(config.fill) : undefined,
+        stroke: config.stroke ? new Stroke(config.stroke) : undefined,
+        offsetX: config.offsetX,
+        offsetY: config.offsetY,
+        textAlign: config.textAlign,
+        textBaseline: config.textBaseline
+    });
+}
+
+function getImagePath(name) {
+    switch (name) {
+        case 'blue_trangle':
+            return blue_trangle;
+        case 'green_trangle':
+            return green_trangle;
+        case 'yellow_trangle':
+            return yellow_trangle;
+        case 'red_trangle':
+            return red_trangle;
+        case 'pink_trangle':
+            return pink_trangle;
+        case 'blue_circle':
+            return blue_circle;
+        case 'dy_center':
+            return dy_center;
+        default:
+            return blue_circle;
+    }
+}

+ 0 - 1
ruoyi-ui/src/views/service/info/AeService.vue

@@ -540,7 +540,6 @@ export default {
       const pattern = /\.{1}[A-Za-z]{1,}$/;
       let fileExt = pattern.exec(file.name);
       const checkType = this.fileTypes.includes(file.type);
-      debugger;
       if (!checkType) {
         this.$message.warning(
           "只能上传 .xls、.xlsx、.json、.xml、.csv 格式的文件!"

+ 0 - 1
ruoyi-ui/src/views/service/info/serviceFile.vue

@@ -245,7 +245,6 @@ export default {
       const pattern = /\.{1}[A-Za-z]{1,}$/;
       let fileExt = pattern.exec(file.name);
       const checkType = this.fileTypes.includes(file.type);
-      debugger;
       if (!checkType) {
         this.$message.warning(
           "只能上传 .xls、.xlsx、.json、.xml、.csv 格式的文件!"

+ 53 - 40
ruoyi-ui/src/views/standardization/bizDataShowConfig/index.vue

@@ -1,22 +1,12 @@
 <template>
   <div class="app-container">
-    <el-row justify="space-between">
+    <el-row justify="space-between" style="margin-bottom: 10px;">
       <el-col :span="12">
-        <el-form :model="queryParams" ref="queryRef" :inline="true" label-width="68px">
-          <el-form-item label="模型名称">
-            <el-select v-model="queryParams.mdid" clearable filterable placeholder="请选择" style="width: 180px;">
-              <el-option
-                  v-for="dict in modelOptions"
-                  :key="dict.mdid"
-                  :label="dict.name"
-                  :value="dict.mdid">
-              </el-option>
-            </el-select>
-          </el-form-item>
-          <el-form-item>
-            <el-button type="primary" icon="Search" @click="getData">查询</el-button>
-          </el-form-item>
-        </el-form>
+        <el-page-header title="" @back="jumpPage('/standardization/map')">
+          <template #content>
+            <span class="text-large font-600 mr-3"> {{ homeTitle }} </span>
+          </template>
+        </el-page-header>
       </el-col>
       <el-col :span="12" style="display: flex;justify-content: right;">
         <el-button type="primary" @click="handleAdd" icon="plus">新建业务数据配置</el-button>
@@ -71,34 +61,41 @@
             </el-form-item>
           </el-col>
           <el-col :span="24">
-            <el-form-item label="请求数据" style="max-height: 300px;overflow: auto;">
-              <biz-data-show-config-api v-model="form.queryOptions"></biz-data-show-config-api>
+            <el-form-item label="位置" style="max-height: 300px;">
+              <div style="width: 100%;height: 100%;background-color: #FAFAFA;overflow: auto;border-radius: 10px;">
+                <biz-data-show-config-position v-model="form.position"></biz-data-show-config-position>
+              </div>
+            </el-form-item>
+          </el-col>
+          <el-col :span="24">
+            <el-form-item label="请求数据" style="max-height: 300px;">
+              <div style="width: 100%;height: 100%;background-color: #FAFAFA;overflow: auto;border-radius: 10px;">
+                <biz-data-show-config-api v-model="form.queryOptions"></biz-data-show-config-api>
+              </div>
             </el-form-item>
           </el-col>
           <el-col :span="24">
             <el-form-item label="展示类型">
-              <el-select v-model="form.type" clearable placeholder="请选择">
-                <el-option v-for="dict in typeOptions" :key="dict.value" :label="dict.label"
-                           :value="dict.value"></el-option>
+              <el-select v-model="form.type" clearable placeholder="请选择" style="margin-bottom: 10px;">
+                <el-option v-for="dict in typeOptions" :key="dict.value" :label="dict.label" :value="dict.value"></el-option>
               </el-select>
+              <div style="width:100%;max-height: 300px;overflow: auto;background-color: #FAFAFA;border-radius: 10px;padding: 10px;">
+                <template v-if="form.type === 'chart'">
+                  <biz-data-show-config-chart :columns="columns" v-model="form.renderingOptions"></biz-data-show-config-chart>
+                </template>
+                <template v-if="form.type === 'list'">
+                  <biz-data-show-config-list :columns="columns" v-model="form.renderingOptions"></biz-data-show-config-list>
+                </template>
+                <template v-if="form.type === 'table'">
+                  <biz-data-show-config-table :columns="columns"
+                                              v-model="form.renderingOptions"></biz-data-show-config-table>
+                </template>
+                <template v-if="form.type === 'text'">
+                  <biz-data-show-config-text v-model="form.renderingOptions"></biz-data-show-config-text>
+                </template>
+              </div>
             </el-form-item>
           </el-col>
-          <el-col :span="24" style="max-height: 300px;overflow: auto;">
-            <template v-if="form.type === 'chart'">
-              <biz-data-show-config-chart :columns="columns"
-                                          v-model="form.renderingOptions"></biz-data-show-config-chart>
-            </template>
-            <template v-if="form.type === 'list'">
-              <biz-data-show-config-list :columns="columns" v-model="form.renderingOptions"></biz-data-show-config-list>
-            </template>
-            <template v-if="form.type === 'table'">
-              <biz-data-show-config-table :columns="columns"
-                                          v-model="form.renderingOptions"></biz-data-show-config-table>
-            </template>
-            <template v-if="form.type === 'text'">
-              <biz-data-show-config-text v-model="form.renderingOptions"></biz-data-show-config-text>
-            </template>
-          </el-col>
         </el-row>
       </el-form>
       <div slot="footer">
@@ -116,22 +113,30 @@
 </template>
 <script setup>
 import {onMounted} from 'vue';
-import {deleteBizDataShowConfig, getBizDataShowConfigList, saveBizDataShowConfig } from "@/api/standardization/bizDataShowConfig.js";
+import {
+  deleteBizDataShowConfig,
+  getBizDataShowConfigList,
+  saveBizDataShowConfig
+} from "@/api/standardization/bizDataShowConfig.js";
+import BizDataShowConfigPosition from "./position/index.vue"
 import BizDataShowConfigApi from "./api/index.vue"
 import BizDataShowConfigChart from "./chart/index.vue"
 import BizDataShowConfigList from "./list/index.vue"
 import BizDataShowConfigTable from "./table/index.vue"
 import BizDataShowConfigText from "./text/index.vue"
 import BizDisplay from "./show/index.vue"
+import {jumpPage} from "@/utils/page.js";
+import {getModelingById} from "@/api/standardization/modeling.js";
 
 const {proxy} = getCurrentInstance();
+const route = useRoute();
 
 const queryParams = ref({pageNum: 1, pageSize: 20})
 const tableData = ref([])
-const modelOptions = ref([])
 const dialogVisible = ref(false)
 const columns = ref([])
 const title = ref('新建业务数据配置')
+const homeTitle = ref('')
 const form = ref({
   id: '',
   name: '',
@@ -149,7 +154,7 @@ const rules = ref({
 const typeOptions = ref([
   {label: '图表', value: 'chart'},
   {label: '列表', value: 'list'},
-  {label: '相信信息', value: 'table'}
+  {label: '详细信息', value: 'table'}
 ])
 
 const previewDialogVisible = ref(false)
@@ -163,6 +168,12 @@ function getData() {
   })
 }
 
+function getModeling() {
+  getModelingById(route.params.id).then(res => {
+    homeTitle.value = res.data.appTitle
+  })
+}
+
 function handleAdd() {
   dialogVisible.value = true
   title.value = '新建业务数据配置'
@@ -172,6 +183,7 @@ function handleAdd() {
 function submitForm() {
   proxy.$refs["formRef"].validate(valid => {
     if (valid) {
+      form.value.appId = route.params.id
       saveBizDataShowConfig(form.value).then(res => {
         proxy.$modal.msgSuccess("新增成功");
         cancel();
@@ -216,6 +228,7 @@ function handleDelete(row) {
 
 onMounted(() => {
   getData()
+  getModeling()
 });
 
 

+ 48 - 3
ruoyi-ui/src/views/standardization/bizDataShowConfig/list/index.vue

@@ -1,10 +1,40 @@
 <template>
-  <el-form ref="formRef" :model="form" :rules="rules" label-width="80px">
+  <el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
     <el-form-item label="表头">
       <el-select v-model="form.columns" clearable multiple placeholder="请选择展示字段">
         <el-option v-for="column in columns" :key="column.key" :label="column.value" :value="column"/>
       </el-select>
     </el-form-item>
+    <!-- 添加行点击配置 -->
+    <el-form-item label="点击配置">
+      <el-radio-group v-model="form.clickAction.type">
+        <el-radio label="none">无操作</el-radio>
+        <el-radio label="map">跳转地图</el-radio>
+      </el-radio-group>
+    </el-form-item>
+    <!-- 地图配置项 -->
+    <el-form-item v-if="form.clickAction.type === 'map'" label="经纬度字段">
+      <el-row :gutter="10">
+        <el-col :span="12">
+          <el-select v-model="form.clickAction.longitudeField" placeholder="请选择经度字段" style="width: 120px;">
+            <el-option
+                v-for="column in columns"
+                :key="column.key"
+                :label="column.value"
+                :value="column.key"/>
+          </el-select>
+        </el-col>
+        <el-col :span="12">
+          <el-select v-model="form.clickAction.latitudeField" placeholder="请选择纬度字段" style="width: 120px;">
+            <el-option
+                v-for="column in columns"
+                :key="column.key"
+                :label="column.value"
+                :value="column.key"/>
+          </el-select>
+        </el-col>
+      </el-row>
+    </el-form-item>
   </el-form>
 </template>
 <script setup>
@@ -16,12 +46,27 @@ const props = defineProps({
   modelValue: String,
 });
 const emit = defineEmits(['update:modelValue'])
-const form = ref({columns: []})
+const form = ref({
+  columns: [],
+  clickAction: {
+    type: 'none', // none, map
+    latitudeField: '',  // 纬度字段
+    longitudeField: ''  // 经度字段
+  }
+})
 const rules = ref({})
 
 watch(() => props.modelValue, modelValue => {
   if (modelValue) {
-    form.value = JSON.parse(modelValue)
+    const parsed = JSON.parse(modelValue)
+    form.value = {
+      columns: parsed.columns || [],
+      clickAction: {
+        type: parsed.clickAction?.type || 'none',
+        latitudeField: parsed.clickAction?.latitudeField || '',
+        longitudeField: parsed.clickAction?.longitudeField || ''
+      }
+    }
   }
 }, {immediate: true})
 

+ 56 - 0
ruoyi-ui/src/views/standardization/bizDataShowConfig/position/index.vue

@@ -0,0 +1,56 @@
+<template>
+  <div class="app-container">
+    <el-form ref="formRef" :model="form" :rules="rules" label-width="80px">
+      <el-form-item label="位置(X)">
+        <el-select v-model="form.xdirection" style="width: 150px;margin-right: 10px;">
+          <el-option label="居左" value="left"></el-option>
+          <el-option label="居右" value="right"></el-option>
+        </el-select>
+        <el-input v-model="form.xvalue" style="width: 100px;"></el-input>
+      </el-form-item>
+      <el-form-item label="位置(Y)">
+        <el-select v-model="form.ydirection" style="width: 150px;margin-right: 10px;">
+          <el-option label="居上" value="top"></el-option>
+          <el-option label="居下" value="bottom"></el-option>
+        </el-select>
+        <el-input v-model="form.yvalue" style="width: 100px;"></el-input>
+      </el-form-item>
+      <el-form-item label="高度">
+        <el-input v-model="form.height" style="width: 150px;"></el-input>
+      </el-form-item>
+    </el-form>
+  </div>
+</template>
+<script setup>
+
+const props = defineProps({
+  columns: {
+    type: Array,
+    default: () => []
+  },
+  modelValue: String,
+});
+const emit = defineEmits(['update:modelValue'])
+
+const form = ref({
+  xdirection: 'left',
+  xvalue: '10',
+  ydirection: 'top',
+  yvalue: '10',
+  height: '',
+})
+const rules = ref({})
+
+watch(() => props.modelValue, modelValue => {
+  if (modelValue) {
+    form.value = JSON.parse(modelValue)
+  }
+}, {immediate: true})
+
+watch(() => form, form => {
+  emit('update:modelValue', JSON.stringify(form.value))
+}, {deep: true})
+</script>
+<style scoped lang="scss">
+
+</style>

+ 64 - 22
ruoyi-ui/src/views/standardization/bizDataShowConfig/show/GwTableTwo.vue

@@ -1,28 +1,36 @@
 <template>
-  <table class="gw-descriptions-table">
-    <tbody>
-    <tr v-for="num in rowNumber" :key="num">
-      <template v-for="item in row(num)">
-        <th colspan="1">{{ item.name }}</th>
-        <td v-if="isString(item.value) && item.code !== 'name'" colspan="1">{{ item.value }}</td>
-        <td v-else-if="isString(item.value) && item.code === 'name'" colspan="1">
-          <span class="node-info">
-            {{ item.value }}
-          </span>
-        </td>
-        <td v-else-if="isArray(item.value)" colspan="1">
-          <span v-for="(vl,index) in item.value" :key="index" class="node-info">
-            {{ vl.name }}
-          </span>
-        </td>
-        <td v-else colspan="1"></td>
-      </template>
-    </tr>
-    </tbody>
-  </table>
+  <div class="descriptions-container">
+    <el-descriptions :column="columnNumber" border>
+      <el-descriptions-item v-for="(item, index) in data" :key="index" :label="item.name">
+        {{ item.value ? item.value : '-' }}
+      </el-descriptions-item>
+    </el-descriptions>
+  </div>
+  <!--  <table class="gw-descriptions-table">-->
+  <!--    <tbody>-->
+  <!--    <tr v-for="num in rowNumber" :key="num">-->
+  <!--      <template v-for="item in row(num)">-->
+  <!--        <th colspan="1">{{ item.name }}</th>-->
+  <!--        <td v-if="isString(item.value) && item.code !== 'name'" colspan="1">{{ item.value }}</td>-->
+  <!--        <td v-else-if="isString(item.value) && item.code === 'name'" colspan="1">-->
+  <!--          <span class="node-info">-->
+  <!--            {{ item.value }}-->
+  <!--          </span>-->
+  <!--        </td>-->
+  <!--        <td v-else-if="isArray(item.value)" colspan="1">-->
+  <!--          <span v-for="(vl,index) in item.value" :key="index" class="node-info">-->
+  <!--            {{ vl.name }}-->
+  <!--          </span>-->
+  <!--        </td>-->
+  <!--        <td v-else colspan="1">-->
+  <!--          <span class="empty-value">-</span> &lt;!&ndash; 显示占位符 &ndash;&gt;-->
+  <!--        </td>-->
+  <!--      </template>-->
+  <!--    </tr>-->
+  <!--    </tbody>-->
+  <!--  </table>-->
 </template>
 <script setup>
-import {isArray, isString} from "@/utils/validate";
 
 const props = defineProps({
   data: {
@@ -54,13 +62,38 @@ const rowNumber = computed(() => {
 })
 </script>
 <style lang="scss" scoped>
+.descriptions-container {
+  width: 100%;
+  height: 100%;
+  overflow: auto;
+}
+
 .gw-descriptions-table {
   width: 100%;
   height: 100%;
+  border-collapse: collapse;
+  border-spacing: 0;
+  table-layout: fixed;
+
+  tr:hover {
+    background-color: #f5f7fa;
+
+    td {
+      background-color: inherit;
+    }
+  }
+
+  th, td {
+    border: 1px solid #ebeef5;
+    padding: 12px 10px;
+    //word-wrap: break-word;
+  }
 
   th {
     min-width: 80px;
     text-align: center;
+    background-color: #f5f7fa; // 表头背景色
+    font-weight: bold;
   }
 
   td {
@@ -76,6 +109,10 @@ const rowNumber = computed(() => {
       margin-bottom: 5px;
       cursor: pointer;
       display: inline-block;
+      max-width: 100%;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
 
       &:hover {
         color: #fff;
@@ -86,5 +123,10 @@ const rowNumber = computed(() => {
 
   }
 
+  .empty-value {
+    color: #909399;
+    font-style: italic;
+  }
+
 }
 </style>

+ 64 - 7
ruoyi-ui/src/views/standardization/bizDataShowConfig/show/index.vue

@@ -1,18 +1,24 @@
 <template>
   <div class="biz-display-container">
-    <div v-if="title || $slots.header" class="biz-display-header">
+    <div v-if="headerShow" class="biz-display-header">
       <slot name="header">
         {{ title }}
       </slot>
     </div>
-    <div class="biz-display-body">
+    <div class="biz-display-body" :style="{ height: bodyHeight }">
       <template v-if="displayType === 'chart'">
         <gw-chart :option="data"></gw-chart>
       </template>
       <template v-if="displayType === 'list'">
-        <el-table :data="data" height="100%" stripe>
+        <el-table :data="data" height="100%" stripe
+                  @row-click="handleRowClick">
           <el-table-column v-for="column in columns" :key="column.key" :label="column.value"
-                           :prop="column.key"></el-table-column>
+                           :prop="column.key">
+            <template #default="scope">
+
+              {{ getValueByKey(scope.row, column.key) }}
+            </template>
+          </el-table-column>
         </el-table>
       </template>
       <template v-if="displayType === 'table'">
@@ -28,22 +34,40 @@
 import GwChart from "@/components/chart/GwEchart.vue";
 import {getChartOption, getFormListData, getTableData, getText} from '@/utils/biz'
 import GwTableTwo from "./GwTableTwo.vue";
+import bus from '@/utils/bus'
+import {filterData} from "@/utils/data.js";
+import {isDate, isNumber} from "@/utils/validate.js";
+import {parseTime} from "@/utils/ruoyi.js";
 
 defineExpose({
   loadData
 })
 
+const slots = useSlots();
 const props = defineProps({
   config: {
     type: Object,
-  }
+  },
+  showTitle: {
+    type: Boolean,
+    default: true
+  },
 });
 
+const headerShow = computed(() => (!!title.value || slots.header) && props.showTitle)
+const bodyHeight = computed(() => {
+  if (headerShow.value) {
+    return 'calc(100% - 1rem - 20px)'
+  } else {
+    return '100%'
+  }
+})
 const data = ref(null)
 const title = ref(null)
 const displayType = ref(null)
 const columnNumber = ref(1)
 const columns = ref([])
+const clickConfig = ref(null)
 
 function loadData(config) {
   if (!config) {
@@ -52,6 +76,11 @@ function loadData(config) {
   config.value = config
   displayType.value = config.type
   title.value = config.name
+
+  // 解析点击配置
+  const renderingOptions = JSON.parse(config.renderingOptions || '{}')
+  clickConfig.value = renderingOptions.clickAction || {type: 'none'}
+
   switch (config.type) {
     case 'chart':
       nextTick(() => {
@@ -59,16 +88,44 @@ function loadData(config) {
       })
       return;
     case 'list':
-      columns.value = config.renderingOptionsData.columns
+      columns.value = renderingOptions.columns
+      debugger
       return getTableData(config).then(r => data.value = r);
     case 'table':
-      columnNumber.value = config.renderingOptionsData.columnNumber
+      columnNumber.value = renderingOptions.columnNumber
       return getFormListData(config).then(r => data.value = r);
     case 'text':
       return getText(config).then(r => data.value = r);
   }
 }
 
+// 处理行点击事件
+function handleRowClick(row, column, event) {
+  if (!clickConfig.value || clickConfig.value.type === 'none') {
+    return
+  }
+
+  if (clickConfig.value.type === 'map') {
+    const latitude = row[clickConfig.value.latitudeField]
+    const longitude = row[clickConfig.value.longitudeField]
+
+    if (latitude && longitude) {
+      // 跳转到地图页面,传递经纬度参数
+      bus.emit('show-map-position', {latitude, longitude, highlight: true})
+    }
+  }
+}
+
+function getValueByKey(obj, key) {
+  let value = filterData(obj, key);
+  if (isNumber(value)) {
+    value = value.toFixed(2)
+  } else if (isDate(value)) {
+    value = parseTime(new Date(value), '{m}-{d} {h}')
+  }
+  return value
+}
+
 watch(() => props.config, (config) => {
   if (config) {
     loadData(config)

+ 3 - 1
ruoyi-ui/src/views/standardization/resultsPresentation/index.vue

@@ -96,7 +96,7 @@
 </template>
 
 <script setup>
-import {getModellist} from "@/api/standardization/modeling.js";
+import {getModelingById, getModellist} from "@/api/standardization/modeling.js";
 import {jumpPage} from "@/utils/page.js";
 
 const knowledgeList = ref([])
@@ -126,6 +126,8 @@ function getList() {
     }
   })
 }
+
+
 </script>
 <style lang="scss">
 .knowledge-dialog .el-dialog__body {

Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff