Parcourir la source

Merge branch 'master' of http://39.98.38.2:13000/dumingliang/sh-model-platform

nanjingliujinyu il y a 5 mois
Parent
commit
43538a7af9
37 fichiers modifiés avec 3876 ajouts et 589 suppressions
  1. 12 1
      ruoyi-api-patform/src/main/java/com/ruoyi/interfaces/controller/BizDataShowConfigController.java
  2. 7 1
      ruoyi-api-patform/src/main/java/com/ruoyi/interfaces/domain/BizDataShowConfig.java
  3. 6 0
      ruoyi-api-patform/src/main/java/com/ruoyi/interfaces/domain/GatewayRoutes.java
  4. 6 0
      ruoyi-api-patform/src/main/java/com/ruoyi/interfaces/service/BizDataShowConfigService.java
  5. 45 0
      ruoyi-api-patform/src/main/java/com/ruoyi/interfaces/service/impl/BizDataShowConfigServiceImpl.java
  6. 89 0
      ruoyi-common/src/main/java/com/ruoyi/common/utils/OkHttpUtils.java
  7. 2 0
      ruoyi-ui/package.json
  8. 41 0
      ruoyi-ui/src/api/standardization/bizDataShowConfig.js
  9. 8 0
      ruoyi-ui/src/assets/icons/svg/horizontal-align-bottom.svg
  10. 7 0
      ruoyi-ui/src/assets/icons/svg/horizontal-align-center.svg
  11. 8 0
      ruoyi-ui/src/assets/icons/svg/horizontal-align-top.svg
  12. 8 0
      ruoyi-ui/src/assets/icons/svg/vertical-align-center.svg
  13. 8 0
      ruoyi-ui/src/assets/icons/svg/vertical-align-left.svg
  14. 8 0
      ruoyi-ui/src/assets/icons/svg/vertical-align-right.svg
  15. 72 22
      ruoyi-ui/src/components/DynamicMap/index.vue
  16. 15 1
      ruoyi-ui/src/components/chart/GwEchart.vue
  17. 9 0
      ruoyi-ui/src/components/chart/index.vue
  18. 26 0
      ruoyi-ui/src/router/index.js
  19. 318 0
      ruoyi-ui/src/utils/biz.js
  20. 42 0
      ruoyi-ui/src/utils/chart.js
  21. 23 0
      ruoyi-ui/src/utils/page.js
  22. 86 0
      ruoyi-ui/src/utils/string.js
  23. 284 297
      ruoyi-ui/src/views/map/index.vue
  24. 606 0
      ruoyi-ui/src/views/map/index.vue.bak
  25. 15 11
      ruoyi-ui/src/views/service/gateway/index.vue
  26. 84 0
      ruoyi-ui/src/views/standardization/bizDataShowConfig/api/index.vue
  27. 604 0
      ruoyi-ui/src/views/standardization/bizDataShowConfig/chart/index.vue
  28. 67 0
      ruoyi-ui/src/views/standardization/bizDataShowConfig/chart/xAxis/index.vue
  29. 74 0
      ruoyi-ui/src/views/standardization/bizDataShowConfig/chart/yAxis/index.vue
  30. 230 0
      ruoyi-ui/src/views/standardization/bizDataShowConfig/index.vue
  31. 33 0
      ruoyi-ui/src/views/standardization/bizDataShowConfig/list/index.vue
  32. 90 0
      ruoyi-ui/src/views/standardization/bizDataShowConfig/show/GwTableTwo.vue
  33. 97 0
      ruoyi-ui/src/views/standardization/bizDataShowConfig/show/index.vue
  34. 41 0
      ruoyi-ui/src/views/standardization/bizDataShowConfig/table/index.vue
  35. 31 0
      ruoyi-ui/src/views/standardization/bizDataShowConfig/text/index.vue
  36. 292 256
      ruoyi-ui/src/views/standardization/modelUsing/index.vue
  37. 482 0
      ruoyi-ui/src/views/standardization/resultsPresentation/index.vue

+ 12 - 1
ruoyi-api-patform/src/main/java/com/ruoyi/interfaces/controller/BizDataShowConfigController.java

@@ -13,6 +13,7 @@ import io.swagger.annotations.ApiParam;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
 
+import java.io.IOException;
 import java.util.List;
 
 /**
@@ -66,9 +67,19 @@ public class BizDataShowConfigController extends BaseController {
     public AjaxResult list(BizDataShowConfig ptServiceAlarm) {
         QueryWrapper<BizDataShowConfig> queryWrapper = new QueryWrapper<>();
         queryWrapper
-                .like(StringUtils.isNotBlank(ptServiceAlarm.getName()), "name", ptServiceAlarm.getName());
+                .like(StringUtils.isNotBlank(ptServiceAlarm.getName()), "name", ptServiceAlarm.getName()).orderByAsc("sort");
         List<BizDataShowConfig> list = bizDataShowConfigService.list(queryWrapper);
         return AjaxResult.success(list);
     }
 
+    @PostMapping("/testApi")
+    public AjaxResult testApi(@RequestBody String apiConfig) throws IOException {
+        return AjaxResult.success(bizDataShowConfigService.testApi(apiConfig));
+    }
+
+    @GetMapping("/getBizData/{id}")
+    public AjaxResult getBizDataById(@PathVariable String id) {
+        return AjaxResult.success(bizDataShowConfigService.getBizDataById(id));
+    }
+
 }

+ 7 - 1
ruoyi-api-patform/src/main/java/com/ruoyi/interfaces/domain/BizDataShowConfig.java

@@ -1,19 +1,23 @@
 package com.ruoyi.interfaces.domain;
 
+import com.alibaba.fastjson2.JSONObject;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
 import com.fasterxml.jackson.annotation.JsonFormat;
 import com.ruoyi.common.utils.JsonUtils;
 import lombok.AllArgsConstructor;
 import lombok.Data;
 import lombok.NoArgsConstructor;
-import org.json.JSONObject;
 
 import java.util.Date;
 
 @Data
 @AllArgsConstructor
 @NoArgsConstructor
+@TableName
 public class BizDataShowConfig {
 
+    @TableId
     private Long id;
 
     private String name;
@@ -24,6 +28,8 @@ public class BizDataShowConfig {
 
     private String renderingOptions;
 
+    private Integer sort;
+
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
     private Date createTime;
 

+ 6 - 0
ruoyi-api-patform/src/main/java/com/ruoyi/interfaces/domain/GatewayRoutes.java

@@ -20,4 +20,10 @@ public class GatewayRoutes implements Serializable {
      */
     private String resultSuccessFormat;
 
+    private String auth;
+
+    private String authExpirationTime;
+
+    private String authQueryOptions;
+
 }

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

@@ -3,6 +3,12 @@ package com.ruoyi.interfaces.service;
 import com.baomidou.mybatisplus.extension.service.IService;
 import com.ruoyi.interfaces.domain.BizDataShowConfig;
 
+import java.io.IOException;
+
 public interface BizDataShowConfigService extends IService<BizDataShowConfig> {
 
+    boolean testApi(String apiConfig) throws IOException;
+
+    Object getBizDataById(String id);
+
 }

+ 45 - 0
ruoyi-api-patform/src/main/java/com/ruoyi/interfaces/service/impl/BizDataShowConfigServiceImpl.java

@@ -1,12 +1,20 @@
 package com.ruoyi.interfaces.service.impl;
 
+import com.alibaba.fastjson2.JSONObject;
+import com.alibaba.fastjson2.JSONPath;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+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.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;
 
 @Service
 public class BizDataShowConfigServiceImpl extends ServiceImpl<BizDataShowConfigMapper, BizDataShowConfig> implements BizDataShowConfigService {
@@ -23,4 +31,41 @@ public class BizDataShowConfigServiceImpl extends ServiceImpl<BizDataShowConfigM
         return super.updateById(entity);
     }
 
+    @Override
+    public boolean testApi(String apiConfig) {
+        Object result = requestByQueryOptions(apiConfig);
+        return result != null;
+    }
+
+    @Override
+    public Object getBizDataById(String id) {
+        BizDataShowConfig bizDataShowConfig = this.getById(id);
+        Optional.ofNullable(bizDataShowConfig).orElseThrow(() -> new CheckException("id.no"));
+        return requestByQueryOptions(bizDataShowConfig.getQueryOptions());
+    }
+
+    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");
+
+        String responseString = OkHttpUtils.executeRequest(
+                apiConfigObj.getString("url"),
+                apiConfigObj.getString("method"),
+                params,
+                apiConfigObj.getString("body"),
+                cookies,
+                headers
+        );
+
+        JSONObject responseObj = JsonUtils.jsonToPojo(responseString, JSONObject.class);
+        Object result = responseObj;
+        if (StringUtils.isNotBlank(responseResolution)) {
+            result = JSONPath.eval(responseObj, responseResolution);
+        }
+        return result;
+    }
+
 }

+ 89 - 0
ruoyi-common/src/main/java/com/ruoyi/common/utils/OkHttpUtils.java

@@ -249,4 +249,93 @@ public class OkHttpUtils {
         }
         return returnMap;
     }
+
+    /**
+     * 通用 HTTP 请求方法
+     *
+     * @param url     请求地址
+     * @param method  请求方法 (GET, POST, PUT, DELETE, PATCH)
+     * @param params  查询参数 (用于GET请求的URL参数)
+     * @param body    请求体 (用于POST/PUT等的请求体,可以是JSON或表单数据)
+     * @param cookies Cookie信息
+     * @param headers 请求头
+     * @return String 响应对象
+     * @throws IOException 当请求执行失败时抛出
+     */
+    public static String executeRequest(String url,
+                                        String method,
+                                        Map<String, Object> params,
+                                        String body,
+                                        Map<String, Object> cookies,
+                                        Map<String, Object> headers) {
+        // 构建请求URL(添加查询参数)
+        HttpUrl.Builder urlBuilder = Objects.requireNonNull(HttpUrl.parse(url)).newBuilder();
+        if (params != null) {
+            for (Map.Entry<String, Object> entry : params.entrySet()) {
+                urlBuilder.addQueryParameter(entry.getKey(), String.valueOf(entry.getValue()));
+            }
+        }
+        String requestUrl = urlBuilder.build().toString();
+
+        // 构建请求体
+        RequestBody requestBody = null;
+        if (body != null) {
+            requestBody = RequestBody.create(JSON, body);
+        }
+
+        // 构建请求
+        Request.Builder requestBuilder = new Request.Builder()
+                .url(requestUrl);
+
+        // 设置请求方法
+        switch (method.toUpperCase()) {
+            case "POST":
+                requestBuilder.post(requestBody != null ? requestBody : RequestBody.create(null, new byte[0]));
+                break;
+            case "PUT":
+                requestBuilder.put(requestBody != null ? requestBody : RequestBody.create(null, new byte[0]));
+                break;
+            case "DELETE":
+                requestBuilder.delete(requestBody != null ? requestBody : RequestBody.create(null, new byte[0]));
+                break;
+            case "PATCH":
+                requestBuilder.patch(requestBody != null ? requestBody : RequestBody.create(null, new byte[0]));
+                break;
+            default: // GET
+                requestBuilder.get();
+        }
+
+        // 添加请求头
+        if (headers != null) {
+            for (Map.Entry<String, Object> entry : headers.entrySet()) {
+                requestBuilder.addHeader(entry.getKey(), String.valueOf(entry.getValue()));
+            }
+        }
+
+        // 添加Cookies
+        if (cookies != null) {
+            for (Map.Entry<String, Object> entry : cookies.entrySet()) {
+                requestBuilder.addHeader("Cookie", entry.getKey() + "=" + entry.getValue());
+            }
+        }
+
+        // 执行请求并返回响应
+        Request request = requestBuilder.build();
+        return httpCall(request);
+    }
+
+    /**
+     * 简化方法:执行GET请求
+     */
+    public static String get(String url, Map<String, Object> params, Map<String, Object> headers) throws IOException {
+        return executeRequest(url, "GET", params, null, null, headers);
+    }
+
+    /**
+     * 简化方法:执行POST请求(JSON数据)
+     */
+    public static String postJsons(String url, String jsonData, Map<String, Object> headers) throws IOException {
+        return executeRequest(url, "POST", null, jsonData, null, headers);
+    }
+
 }

+ 2 - 0
ruoyi-ui/package.json

@@ -36,11 +36,13 @@
     "jsencrypt": "3.3.2",
     "json-beautify": "^1.1.1",
     "json-editor-vue3": "^1.1.1",
+    "jsonpath": "^1.1.1",
     "moment": "^2.30.1",
     "nprogress": "0.2.0",
     "ol": "^10.2.1",
     "pinia": "2.1.7",
     "pinyin": "^4.0.0",
+    "qs": "^6.14.0",
     "sortablejs": "^1.15.6",
     "vue": "^3.5.13",
     "vue-clipboard3": "^2.0.0",

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

@@ -0,0 +1,41 @@
+import request from '@/utils/request'
+
+export function saveBizDataShowConfig(data) {
+    return request({
+        url: '/biz/data/show/config',
+        method: 'post',
+        data
+    })
+}
+
+// 查询建模步骤
+export function getBizDataShowConfigList(query) {
+    return request({
+        url: '/biz/data/show/config/list',
+        method: 'get',
+        params: query
+    })
+}
+
+export function deleteBizDataShowConfig(id) {
+    return request({
+        url: `/biz/data/show/config/${id}`,
+        method: 'delete',
+    })
+}
+
+export function testBizDataApi(data) {
+    return request({
+        url: `/biz/data/show/config/testApi`,
+        method: 'post',
+        data
+    })
+}
+
+export function getBizDataById(id) {
+    return request({
+        url: `/biz/data/show/config/getBizData/${id}`,
+        method: 'get',
+    })
+}
+

+ 8 - 0
ruoyi-ui/src/assets/icons/svg/horizontal-align-bottom.svg

@@ -0,0 +1,8 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
+  "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg t="1721024629432" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4783"
+     xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200">
+  <path
+    d="M0 938.688h1024V1024H0v-85.312zM256 0v853.312H0V0h256z m768 0v853.312h-256V0h256z m-384 170.688v682.624H384V170.688h256z"
+    fill="" p-id="4784"></path>
+</svg>

+ 7 - 0
ruoyi-ui/src/assets/icons/svg/horizontal-align-center.svg

@@ -0,0 +1,7 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
+  "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg t="1721024632258" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4931"
+     xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200">
+  <path d="M0 460.16h1024v103.68H0V460.16zM446.848 64v896h-296.32V64h296.32z m426.688 89.6v716.8h-296.32V153.6h296.32z"
+        fill="" p-id="4932"></path>
+</svg>

+ 8 - 0
ruoyi-ui/src/assets/icons/svg/horizontal-align-top.svg

@@ -0,0 +1,8 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
+  "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg t="1721024625618" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4635"
+     xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200">
+  <path
+    d="M1024 85.312H0V0h1024v85.312zM768 1024V170.688h256V1024h-256zM0 1024V170.688h256V1024H0z m384-170.688V170.688h256v682.624H384z"
+    fill="" p-id="4636"></path>
+</svg>

+ 8 - 0
ruoyi-ui/src/assets/icons/svg/vertical-align-center.svg

@@ -0,0 +1,8 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
+  "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg t="1721021976012" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4339"
+     xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200">
+  <path
+    d="M460.16 1024V0h103.68v1024H460.16zM64 577.152h896v296.32H64v-296.32z m89.6-426.688h716.8v296.32H153.6v-296.32z"
+    fill="" p-id="4340"></path>
+</svg>

+ 8 - 0
ruoyi-ui/src/assets/icons/svg/vertical-align-left.svg

@@ -0,0 +1,8 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
+  "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg t="1721021949568" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4043"
+     xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200">
+  <path
+    d="M85.312 0v1024H0V0h85.312zM1024 256H170.688V0H1024v256z m0 768H170.688v-256H1024v256z m-170.688-384H170.688V384h682.624v256z"
+    fill="" p-id="4044"></path>
+</svg>

+ 8 - 0
ruoyi-ui/src/assets/icons/svg/vertical-align-right.svg

@@ -0,0 +1,8 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
+  "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg t="1721021979820" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4487"
+     xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200">
+  <path
+    d="M938.688 1024V0H1024v1024h-85.312zM0 768h853.312v256H0v-256zM0 0h853.312v256H0V0z m170.688 384h682.624v256H170.688V384z"
+    fill="" p-id="4488"></path>
+</svg>

+ 72 - 22
ruoyi-ui/src/components/DynamicMap/index.vue

@@ -1,8 +1,10 @@
 <template>
   <div class="dynamic-map-container">
     <el-row class="header">
-      <el-col :span="10">键</el-col>
-      <el-col :span="10">值</el-col>
+      <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-row>
     <el-row
@@ -10,26 +12,30 @@
         :key="item.id"
         class="row"
     >
-      <el-col :span="10">
+      <el-col :span="5">
         <el-input
             placeholder="请输入键"
             v-model="item.key"
             @input="debouncedEmitUpdate"
         />
       </el-col>
-      <el-col :span="10">
+      <el-col :span="5">
         <el-input
             placeholder="请输入值"
             v-model="item.value"
             @input="debouncedEmitUpdate"
         />
       </el-col>
+      <el-col :span="5">
+        <el-select v-model="item.type" clearable placeholder="请选择" @change="debouncedEmitUpdate">
+          <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-input placeholder="请输入说明" v-model="item.title" @input="debouncedEmitUpdate"/>
+      </el-col>
       <el-col :span="4" class="actions">
-        <el-button
-            type="danger"
-            icon="Delete"
-            @click="removeItem(index)"
-        />
+        <el-button type="danger" icon="Delete" @click="removeItem(index)"/>
       </el-col>
     </el-row>
     <el-button
@@ -50,6 +56,8 @@ interface KeyValueItem {
   id: number;
   key: string;
   value: string;
+  type: string;
+  title: string;
 }
 
 const props = defineProps({
@@ -57,21 +65,46 @@ const props = defineProps({
     type: Object,
     required: true,
     default: () => ({})
-  }
+  },
+  returnFormat: {
+    type: String,
+    default: 'list'
+  },
 });
 
 const emit = defineEmits(['update:modelValue']);
 
 // 用于生成唯一ID
 let idCounter = 0;
+const typeOptions = ref([
+  {label: '字符串', value: 'string'},
+  {label: '数字', value: 'number'},
+  {label: '布尔值', value: 'boolean'},
+])
 
 // 将对象转换为带ID的键值对数组
 const initItems = () => {
-  return Object.entries(props.modelValue || {})
+  const value = props.modelValue || {};
+
+  // 如果传入的是数组(list),则直接映射为 items
+  if (Array.isArray(value)) {
+    return value.map((item: any) => ({
+      id: idCounter++,
+      key: item.key || '',
+      value: String(item.value || ''),
+      type: item.type || '',
+      title: item.title || ''
+    }));
+  }
+
+  // 如果传入的是对象(map),则转换为 items
+  return Object.entries(value)
       .map(([key, value]) => ({
         id: idCounter++,
         key,
-        value: String(value)
+        value: String(value),
+        type: '',
+        title: ''
       }));
 };
 
@@ -79,7 +112,7 @@ const items = ref<KeyValueItem[]>(initItems());
 
 // 防抖更新事件
 const debouncedEmitUpdate = debounce(() => {
-  emit('update:modelValue', arrayToObject(items.value));
+  emit('update:modelValue', props.returnFormat === "map" ? arrayToObject(items.value) : items.value);
 }, 300);
 
 // 添加行
@@ -89,13 +122,13 @@ const addNewItem = () => {
     key: '',
     value: ''
   });
-  emit('update:modelValue', arrayToObject(items.value));
+  emit('update:modelValue', props.returnFormat === "map" ? arrayToObject(items.value) : items.value);
 };
 
 // 删除行
 const removeItem = (index: number) => {
   items.value.splice(index, 1);
-  emit('update:modelValue', arrayToObject(items.value));
+  emit('update:modelValue', props.returnFormat === "map" ? arrayToObject(items.value) : items.value);
 };
 
 // 数组转对象(处理重复键)
@@ -110,17 +143,34 @@ const arrayToObject = (items: KeyValueItem[]) => {
 
 // 监听父组件传入值的变化
 watch(() => props.modelValue, (newValue) => {
-  const currentKeys = Object.keys(arrayToObject(items.value));
-  const newKeys = Object.keys(newValue);
+  // 检查是否需要更新 items
+  let shouldUpdate = false;
+
+  if (Array.isArray(newValue)) {
+    // 如果新值是数组
+    const currentArray = items.value.map(item => ({
+      key: item.key,
+      value: item.value,
+      type: item.type,
+      title: item.title
+    }));
+
+    shouldUpdate = JSON.stringify(newValue) !== JSON.stringify(currentArray);
+  } else {
+    // 如果新值是对象
+    const currentKeys = Object.keys(arrayToObject(items.value));
+    const newKeys = Object.keys(newValue);
+
+    shouldUpdate = newKeys.length !== currentKeys.length ||
+        newKeys.some(key => newValue[key] !== arrayToObject(items.value)[key]);
+  }
 
   // 仅在数据实际变化时更新(避免循环更新)
-  if (
-      newKeys.length !== currentKeys.length ||
-      newKeys.some(key => newValue[key] !== arrayToObject(items.value)[key])
-  ) {
+  if (shouldUpdate) {
     items.value = initItems();
   }
-}, {deep: true});
+}, { deep: true });
+
 
 // 清除防抖器
 onBeforeUnmount(() => {

+ 15 - 1
ruoyi-ui/src/components/chart/GwEchart.vue

@@ -8,6 +8,13 @@ defineExpose({
   loadChart
 })
 
+const props = defineProps({
+  option: {
+    type: Object,
+    default: null
+  }
+})
+
 const gwChartRef = ref(null)
 const gwChart = ref(null)
 const timer = ref(null)
@@ -28,7 +35,9 @@ function loadChart(option) {
   }
   gwChart.value.setOption(option, true);
   setTimeout(() => {
-    gwChart.value.resize();
+    if (gwChart.value) {
+      gwChart.value.resize();
+    }
   }, 1000)
 }
 
@@ -81,6 +90,11 @@ function carousel(timeout = 5000, yAxisChange = false) {
   }
 }
 
+watch(() => props.option, option => {
+  if (option) {
+    nextTick(() => loadChart(option));
+  }
+}, {immediate: true, deep: true})
 </script>
 <style lang="scss" scoped>
 .gw-chart-wrapper {

+ 9 - 0
ruoyi-ui/src/components/chart/index.vue

@@ -0,0 +1,9 @@
+<template>
+
+</template>
+<script setup>
+
+</script>
+<style scoped lang="scss">
+
+</style>

+ 26 - 0
ruoyi-ui/src/router/index.js

@@ -92,6 +92,32 @@ export const constantRoutes = [
             }
         ]
     },
+    {
+        path: '/standardization/modeling/bizDataShowConfig',
+        component: Layout,
+        hidden: true,
+        children: [
+            {
+                path: ':id',
+                component: () => import('@/views/standardization/bizDataShowConfig/index.vue'),
+                name: 'BizDataShowConfig',
+                meta: {title: '业务数据展示', activeMenu: '/standardization/bizDataShowConfig'}
+            }
+        ]
+    },
+    {
+        path: '/standardization',
+        component: Layout,
+        hidden: true,
+        children: [
+            {
+                path: 'map/:id',
+                component: () => import('@/views/map/index.vue'),
+                name: 'standardizationMap',
+                meta: {title: '成果展示', activeMenu: '/standardization/map/show'}
+            }
+        ]
+    },
 ]
 
 // 动态路由,基于用户权限动态去加载

+ 318 - 0
ruoyi-ui/src/utils/biz.js

@@ -0,0 +1,318 @@
+import request from "@/utils/request";
+import jp from 'jsonpath';
+import qs from "qs";
+import {isArray} 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";
+
+/**
+ * 查询业务接口信息数据及业务数据
+ * @param id              业务编码
+ * @returns {Promise<*>}
+ */
+// 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
+    }
+    const bizDataRes = await getBizDataById(id)
+    if (!bizDataRes?.data || bizDataRes?.data?.length === 0) {
+        return null
+    }
+    return bizDataRes.data;
+}
+
+export async function getChartOption(config) {
+    const data = await getDataByDataId(config.id)
+    if (!data || data.length === 0) {
+        return {}
+    }
+
+    const renderingOptions = config.renderingOptionsData
+    switch (renderingOptions.chartType) {
+        case 'linebar':
+            return getBarLineChartOption(renderingOptions, data)
+        case 'pie':
+            return getPieChartOption(renderingOptions, data)
+    }
+}
+
+function getBarLineChartOption(config, data) {
+    let option = {
+        grid: config.grid,
+        tooltip: {
+            trigger: 'axis',
+        },
+    }
+    let xparams = config.xparams;
+    let yparams = config.yparams;
+
+    const legendSelected = {};
+    yparams.forEach(item => {
+        legendSelected[item.name.value] = item.selected
+    })
+    const legend = {
+        data: yparams.map(item => item.name.value),
+        selected: legendSelected,
+    };
+    if (legend.data.length > 30) {
+        legend.type = 'scroll';
+    }
+    option.legend = objectMerge(config.legend, legend);
+
+    option.xAxis = {
+        data: data.map((item) => {
+            return item[xparams[0].key]
+        }),
+        axisLine: {
+            show: true,
+        },
+    }
+    option.xAxis = objectMerge(config.xAxis, option.xAxis)
+
+    let yAxis = getYAxisListByUnit(Array.from(new Set(yparams.map(i => i.unit))));
+    yAxis = yAxis.map(y => objectMerge(config.yAxis, y));
+    option.yAxis = yAxis
+
+    const series = [];
+    yparams.forEach((y, index) => {
+        const serie = {
+            type: y.type,
+            name: y.name.value,
+            yAxisIndex: yAxis.map(i => i.name).indexOf(y.unit),
+            data: data.map(item => item[y.name.key]),
+        }
+        // 柱状图样式配置
+        if (y.type === 'bar') {
+            serie.itemStyle = {}
+            serie.showBackground = config.series.showBackground
+            serie.itemStyle.borderRadius = config.series.itemStyle.borderRadius
+            serie.itemStyle.barWidth = config.barWidth
+            if (config.color && index < config.color.length) {
+                serie.itemStyle.color = config.color[index]
+            }
+        } else if (y.type === 'line') {
+            serie.itemStyle = {}
+            serie.itemStyle.opacity = config.lineShow ? 1 : 0
+            serie.smooth = config.smooth
+            if (config.lineColors && index < config.lineColors.length) {
+                serie.itemStyle.color = config.lineColors[index]
+            }
+            serie.symbolSize = config.symbolSize
+            serie.areaStyle = {}
+            serie.areaStyle.opacity = config.areaShow ? 1 : 0
+            if (config.areaColors && index < config.areaColors.length) {
+                serie.areaStyle.color = config.areaColors[index]
+            }
+        }
+        series.push(serie);
+    });
+    series.forEach(s => {
+        s = objectMerge(config.series, s)
+        // if (s.type === 'bar') {
+        // }
+    })
+    option.series = series
+    return option;
+}
+
+function getPieChartOption(config, data) {
+    let option = {
+        grid: config.grid,
+        tooltip: {
+            trigger: 'item',
+            formatter: '{a} <br/>{b} : {c} ({d}%)'
+        },
+        legend: config.legend,
+        series: []
+    }
+    config.series.data = data.map((d, index) => {
+        const seriesData = {
+            name: d[config.nameField],
+            value: d[config.valueField],
+        }
+
+        if (config.color && index < config.color.length) {
+            seriesData.itemStyle = {color: config.color[index]}
+        }
+        return seriesData
+    })
+
+    config.series.type = 'pie';
+    config.series.name = config.valueField;
+    option.series.push(config.series);
+
+    console.log('option', option)
+    return option;
+}
+
+export async function getTableData(config) {
+    const dataList = await getDataByDataId(config.id)
+    if (!dataList || dataList.length === 0) {
+        return []
+    }
+    return dataList;
+}
+
+export async function getFormListData(config) {
+    const dataList = await getDataByDataId(config.id)
+    if (!dataList || dataList.length === 0) {
+        return []
+    }
+
+    let data = dataList[0]
+    if (!isArray(dataList)) {
+        data = dataList
+    }
+
+    const list = []
+    config.renderingOptionsData.columns.forEach(column => {
+        list.push({name: column.value, value: data[column.key]})
+    })
+    return list;
+}
+
+export async function getText(config) {
+    let dataList = await getDataByDataId(config.dataId)
+    if (!dataList || dataList.length === 0) {
+        return ''
+    }
+
+    if (config.dataFormat === 'OBJECT') {
+        dataList = dataList[0]
+    }
+
+    return formatStringByTemplate(dataList, config.template);
+}

+ 42 - 0
ruoyi-ui/src/utils/chart.js

@@ -0,0 +1,42 @@
+const yAxisList = [
+  {
+    type: 'value',
+    name: 'm',
+    min: (value) => (value.min - 0.1).toFixed(1),
+    max: (value) => (value.max + 0.1).toFixed(1),
+    axisLabel: {
+      formatter: value => value.toFixed(2),
+    },
+  },
+  {
+    type: 'value',
+    name: 'm³/s',
+    min: (value) => (value.min - 10).toFixed(0) < 0 ? 0 : (value.min - 10).toFixed(0),
+    max: (value) => (value.max + 10).toFixed(0),
+    // axisLabel: {formatter: value => value.toFixed(0),},
+  },
+  {
+    type: 'value',
+    name: 'mm',
+    min: (value) => (value.min - 10).toFixed(0) < 0 ? 0 : (value.min - 10).toFixed(0),
+    max: (value) => (value.max + 10).toFixed(0),
+    // axisLabel: {
+    //   formatter: value => value.toFixed(0),
+    // },
+  },
+  {
+    type: 'value',
+    name: '万m³',
+  },
+  {
+    type: 'value',
+    name: 'mg/L',
+    min: (value) => (value.min - 0.1).toFixed(1),
+    max: (value) => (value.max + 0.1).toFixed(1),
+  },
+]
+
+export function getYAxisListByUnit(units) {
+  return yAxisList.filter(item => units.indexOf(item.name) >= 0)
+}
+

+ 23 - 0
ruoyi-ui/src/utils/page.js

@@ -0,0 +1,23 @@
+import router from '@/router'
+
+/**
+ * 跳转页面
+ * @param path    路由地址
+ * @param query   参数
+ * @param isBlank 是否新页面打开
+ */
+export function jumpPage(path, query, isBlank = false) {
+    if (!path) {
+        return;
+    }
+    if (!isBlank) {
+        router.push({path, query})
+    } else {
+        if (path.indexOf('http') !== -1) {
+            window.open(path, '_blank');
+        } else {
+            let routeUrl = router.resolve({path, query});
+            window.open(routeUrl.href, '_blank');
+        }
+    }
+}

+ 86 - 0
ruoyi-ui/src/utils/string.js

@@ -0,0 +1,86 @@
+import {isArray} from "@/utils/validate";
+
+/**
+ * 根据模板生成字符串
+ * @param data      数据
+ * @param template  模板
+ * @returns 字符串
+ */
+export function formatStringByTemplate(data, template) {
+  let result = template
+  if (data) {
+    if (isArray(data)) {
+      if (data.length === 0) {
+        return result;
+      }
+      // 数组
+      return formatStringByList(data, result);
+    } else {
+      return replaceText(result, data);
+    }
+  }
+  return result;
+}
+
+/**
+ * [[{{姓名}}任{{职务}},负责{{工作职责}}]]
+ * @param data
+ * @param template
+ * @returns {*}
+ */
+function formatStringByList(list, template) {
+  let result = template
+  const regex = /\[\[.*?\]\]/g
+  let templates = result.match(regex)
+
+  const keys = Object.keys(list[0]);
+  const templatess = []
+  templates.forEach(t => {
+    let temp = t.substring(2, t.length - 2);
+    let temps = []
+
+    list.forEach(d => temps.push(replaceText(temp, d)))
+
+    let ts = temps.join(';');
+    if (ts.length > 0 && !ts.endsWith('。')) {
+      ts = ts + '。'
+    }
+    templatess.push(ts)
+  })
+  templates.forEach((ts, index) => {
+    result = result.replace(ts, templatess[index])
+  })
+
+  return result;
+}
+
+
+function replaceText2(template, data) {
+  const keys = Object.keys(data);
+  keys.forEach(key => {
+    template = template.replace(new RegExp('{{' + key + '}}', 'g'), data[key] || '');
+  });
+  return template;
+}
+
+function replaceText(template, data) {
+  // 正则表达式,用于匹配{{key}},包括可能存在的空格
+  const regex = /{{\s*([^}]+?)\s*}}/g;
+
+  // 递归函数,用于处理嵌套对象
+  function replaceValue(key) {
+    // 分割键名,以处理嵌套对象
+    const keys = key.split('.');
+    let value = data;
+    for (const k of keys) {
+      // 如果值为undefined或null,则停止查找
+      if (value === undefined || value === null) break;
+      value = value[k];
+    }
+    // 如果找到的值是undefined或null,则替换为空字符串
+    return value === undefined || value === null ? '' : value;
+  }
+
+  // 使用正则表达式替换模板中的所有匹配项
+  return template.replace(regex, (match, key) => replaceValue(key));
+}

+ 284 - 297
ruoyi-ui/src/views/map/index.vue

@@ -4,16 +4,11 @@
     <div class="map-left">
       <div class="left-title">模型列表</div>
       <div class="left-tree">
-        <el-tree
-          :data="data"
-          :props="defaultProps"
-          show-checkbox
-          node-key="id"
-          :default-expanded-keys="[1, 8]"
-          :default-checked-keys="[1]"
-          @node-click="handleNodeClick"
-          @check-change="handleCheckChange"
-        />
+        <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">
@@ -22,163 +17,120 @@
         <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 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">
-        风场
+    <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>
 </template>
-
 <script setup>
 import 'ol/css';
-import {ScaleLine, defaults as defaultControls} from 'ol/control';
+import {defaults as defaultControls} from 'ol/control';
 import Map from 'ol/Map';
 import View from 'ol/View';
 import TileLayer from "ol/layer/Tile";
-import { XYZ, Vector as VectorSource } from 'ol/source.js';
+import {Vector as VectorSource, XYZ} from 'ol/source.js';
 import VectorLayer from "ol/layer/Vector";
-import { LineString, Point } from "ol/geom";
-import { Icon, Style, Text,Circle } from 'ol/style';
-import Fill from "ol/style/Fill";
+import {Point} from "ol/geom";
+import {Icon, Style} from 'ol/style';
 import Feature from 'ol/Feature';
-import Stroke from "ol/style/Stroke";
 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";
+
+
+const bizDataShowConfigList = ref([]);
 
 const mapChart = ref(null);
 const mapCenter = ref([121.472644, 31.231706]);
 const mapZoom = ref(9);
-const data = [
-  {
-    id:1,
-    label: '上海市城区洪涝仿真模型',
-    children: [
-      {
-        id:8,
-        label: '上海市城区洪涝仿真模型-1',
-      },
-    ],
-  },
-  {
-    id:2,
-    label: '上海沿海风暴潮预报模型',
-  },
-  {
-    id:3,
-    label: '黄浦江水系水文分析预报数值模拟',
-  },
-  {
-    id:4,
-    label: '苏州河水系水情预报模型',
-  },
-  {
-    id:5,
-    label: '内涝风险实时预警与预报',
-  },
-  {
-    id:6,
-    label: '上海市中心城区排水系统模型',
-  },
-  {
-    id:7,
-    label: '温带风暴潮预报模型',
-  },
-]
-
-const defaultProps = {
-  children: 'children',
-  label: 'label',
-}
 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 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});
+
+function getBizDataConfigList() {
+  getBizDataShowConfigList().then(res => {
+    bizDataShowConfigList.value = res.data;
+  })
+}
 
 onMounted(() => {
+  getBizDataConfigList();
   initMap();
-  console.log('StnmData',StnmData);
-  
 });
 
-const 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",
+          "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",
+          "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({
+  mapChart.value = new Map({
     target: 'mapChart',
     view: new View({
       center: mapCenter.value,
@@ -192,33 +144,33 @@ const initMap = () => {
       zoom: false,//不显示放大放小按钮
       rotate: false,//不显示指北针控件
       attribution: false,//不显示右下角的地图信息控件
-      scaleLine:false,//不显示比例尺控件
+      scaleLine: false,//不显示比例尺控件
     })
   });
   addStnm();
 };
-const 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 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,
   });
@@ -277,22 +229,22 @@ const startDrag = (event) => {
     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
+    x: fcPosition.value.x,
+    y: fcPosition.value.y
   }
-}; 
+};
 const onMouseUp = () => {
   if (isDragging.value) {
     showIcon.value = false;
@@ -301,32 +253,32 @@ const onMouseUp = () => {
     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;
@@ -335,7 +287,7 @@ const closeDialog = () => {
     y: 40
   }
 };
-        
+
 onUnmounted(() => {
   window.removeEventListener('mousemove', onMouseMove);
   window.removeEventListener('mouseup', onMouseUp);
@@ -345,36 +297,50 @@ onUnmounted(() => {
 </script>
 
 <style scoped lang="scss">
+.biz-data-config-container {
+
+  & + .biz-data-config-container {
+    margin-top: 10px;
+  }
+
+}
+
 /*滚动条里面轨道*/
-::-webkit-scrollbar-track{
-  background-color: rgba(20, 19, 19,0);
+::-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;    
+
+/*关键设置 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);
+
+::v-deep(.el-scrollbar) {
+  --el-scrollbar-bg-color: rgba(58, 100, 179);
+  --el-scrollbar-hover-bg-color: rgba(58, 100, 179);
 }
-.map-index{
+
+.map-index {
   height: 100%;
   width: 100%;
   position: relative;
-  #mapChart{
+
+  #mapChart {
     height: 100%;
     width: 100%;
   }
-  .map-left{
+
+  .map-left {
     position: absolute;
     top: 1%;
     left: 0.5%;
     height: 98%;
     width: 260px;
-    background:rgba(255,255,255,0.9);
+    background: rgba(255, 255, 255, 0.9);
     border-radius: 8px;
-    .left-title{
+
+    .left-title {
       width: 100%;
       height: 36px;
       background: url("@/assets/map/img/left-title.png") no-repeat;
@@ -387,15 +353,17 @@ onUnmounted(() => {
       line-height: 34px;
     }
   }
-  .map-right{
+
+  .map-right {
     position: absolute;
     top: 1%;
     right: 0.5%;
     height: 98%;
     width: 400px;
-    background:rgba(255,255,255,0.9);
+    background: rgba(255, 255, 255, 0.9);
     border-radius: 8px;
-    .right-title{
+
+    .right-title {
       width: 100%;
       height: 36px;
       background: url("@/assets/map/img/left-title.png") no-repeat;
@@ -407,16 +375,19 @@ onUnmounted(() => {
       text-align: center;
       line-height: 34px;
     }
-    .right-top-title{
+
+    .right-top-title {
       width: 100%;
       line-height: 30px;
       padding: 10px;
       display: flex;
-      img{
+
+      img {
         width: 30px;
-        height:30px;
+        height: 30px;
       }
-      span{
+
+      span {
         color: #000;
         font-size: 16px;
         font-family: 'PuHuiTi', sans-serif;
@@ -426,147 +397,160 @@ onUnmounted(() => {
     }
   }
 }
-.station-table{
+
+.station-table {
   width: 94%;
-  margin:0 3%;
-  .table-index{
+  margin: 0 3%;
+
+  .table-index {
     width: 40px;
   }
-  .table-head{
+
+  .table-head {
     width: 80px;
   }
-  table{
+
+  table {
     width: 100%;
     border-spacing: 0px;
     border-collapse: collapse; /* 设置表格边框合并为单线 */
     border-top: 2px solid #82bcfd;
     border-bottom: 2px solid #82bcfd;
-    thead{
+
+    thead {
       background-image: -webkit-linear-gradient(top, #ebf5ff, #fff);
-      color:#000;
-      font-size:14px;
+      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{
+
+      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;
+
+    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;
+    display: block;
+    height: 20vh !important;
+    overflow-y: scroll;
   }
-  table thead,tbody tr {
+
+  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;
-    }
+
+.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 {
@@ -575,17 +559,19 @@ onUnmounted(() => {
   box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
   border: 1px solid #3498db;
   width: 172px;
-  .popup-title{
+
+  .popup-title {
     width: 100%;
     line-height: 15px;
     background-color: rgba(255, 23, 0);
-    padding: 10px;  
+    padding: 10px;
     text-align: center;
     color: #fff;
     font-size: 16px;
   }
-  .popup-top{
-    padding:5px 10px;  
+
+  .popup-top {
+    padding: 5px 10px;
     width: 100%;
     background-color: rgba(58, 100, 179);
     display: flex;
@@ -593,8 +579,9 @@ onUnmounted(() => {
     color: #fff;
     font-size: 14px;
   }
-  .popup-bottom{
-    padding:5px 10px;  
+
+  .popup-bottom {
+    padding: 5px 10px;
     width: 100%;
     background-color: rgba(71, 146, 211);
     display: flex;

+ 606 - 0
ruoyi-ui/src/views/map/index.vue.bak

@@ -0,0 +1,606 @@
+<template>
+  <div class="map-index">
+    <div id="mapChart"></div>
+    <div class="map-left">
+      <div class="left-title">模型列表</div>
+      <div class="left-tree">
+        <el-tree
+          :data="data"
+          :props="defaultProps"
+          show-checkbox
+          node-key="id"
+          :default-expanded-keys="[1, 8]"
+          :default-checked-keys="[1]"
+          @node-click="handleNodeClick"
+          @check-change="handleCheckChange"
+        />
+      </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>
+</template>
+
+<script setup>
+import 'ol/css';
+import {ScaleLine, defaults as defaultControls} from 'ol/control';
+import Map from 'ol/Map';
+import View from 'ol/View';
+import TileLayer from "ol/layer/Tile";
+import { XYZ, Vector as VectorSource } from 'ol/source.js';
+import VectorLayer from "ol/layer/Vector";
+import { LineString, Point } from "ol/geom";
+import { Icon, Style, Text,Circle } from 'ol/style';
+import Fill from "ol/style/Fill";
+import Feature from 'ol/Feature';
+import Stroke from "ol/style/Stroke";
+import Overlay from 'ol/Overlay';
+import StnmData from "@/assets/map/json/stnmData.json";
+import red_trangle from "@/assets/map/img/Ⅳ.png";
+
+const mapChart = ref(null);
+const mapCenter = ref([121.472644, 31.231706]);
+const mapZoom = ref(9);
+const data = [
+  {
+    id:1,
+    label: '上海市城区洪涝仿真模型',
+    children: [
+      {
+        id:8,
+        label: '上海市城区洪涝仿真模型-1',
+      },
+    ],
+  },
+  {
+    id:2,
+    label: '上海沿海风暴潮预报模型',
+  },
+  {
+    id:3,
+    label: '黄浦江水系水文分析预报数值模拟',
+  },
+  {
+    id:4,
+    label: '苏州河水系水情预报模型',
+  },
+  {
+    id:5,
+    label: '内涝风险实时预警与预报',
+  },
+  {
+    id:6,
+    label: '上海市中心城区排水系统模型',
+  },
+  {
+    id:7,
+    label: '温带风暴潮预报模型',
+  },
+]
+
+const defaultProps = {
+  children: 'children',
+  label: 'label',
+}
+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 });
+
+onMounted(() => {
+  initMap();
+  console.log('StnmData',StnmData);
+  
+});
+
+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">
+/*滚动条里面轨道*/
+::-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%;
+  }
+  .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>

+ 15 - 11
ruoyi-ui/src/views/service/gateway/index.vue

@@ -125,24 +125,24 @@
         </el-row>
         <el-row>
           <el-col :span="12">
-            <el-form-item label="是否鉴权" prop="filters">
-              <el-radio-group v-model="form.filters">
+            <el-form-item label="是否鉴权" prop="auth">
+              <el-radio-group v-model="form.auth">
                 <el-radio value="1">是</el-radio>
                 <el-radio value="0">否</el-radio>
               </el-radio-group>
             </el-form-item>
           </el-col>
-          <el-col :span="12">
-            <el-form-item label="状态">
-              <el-radio-group v-model="form.status">
-                <el-radio v-for="dict in [{value: '0', label: '启用'}, {value: '1', label: '停用'}]" :key="dict.value"
-                          :value="dict.value">
-                  {{ dict.label }}
-                </el-radio>
-              </el-radio-group>
+          <el-col v-if="form.auth == 1" :span="12">
+            <el-form-item label="过期时间(分钟)">
+              <el-input-number v-model="form.authExpirationTime" :min="0" controls-position="right"/>
             </el-form-item>
           </el-col>
         </el-row>
+        <el-row v-if="form.auth == 1">
+          <el-col :span="24" style="max-height: 300px;overflow: auto;">
+            <biz-data-show-config-api v-model="form.authQueryOptions"></biz-data-show-config-api>
+          </el-col>
+        </el-row>
       </el-form>
       <template #footer>
         <el-button @click="cancel">取 消</el-button>
@@ -154,6 +154,7 @@
 
 <script setup name="User">
 import {deleteGatewayRouter, getGatewayRoutersList, saveGatewayRouter} from "@/api/gateway/gatewayRouters.js";
+import BizDataShowConfigApi from "@/views/standardization/bizDataShowConfig/api/index.vue";
 
 const {proxy} = getCurrentInstance();
 const {sys_normal_disable} = proxy.useDict("sys_normal_disable");
@@ -237,7 +238,10 @@ function reset() {
       field: 'code',
       code: '200'
     },
-    status: "0"
+    status: "0",
+    auth: "0",
+    authExpirationTime: null,
+    authQueryOptions: null
   };
   proxy.resetForm("userRef");
 }

+ 84 - 0
ruoyi-ui/src/views/standardization/bizDataShowConfig/api/index.vue

@@ -0,0 +1,84 @@
+<template>
+  <div class="app-container">
+    <el-input v-model="form.url" placeholder="请求地址">
+      <template #prepend>
+        <el-select v-model="form.method" placeholder="Select" style="width: 115px">
+          <el-option label="GET" value="GET"/>
+          <el-option label="POST" value="POST"/>
+          <el-option label="PUT" value="PUT"/>
+          <el-option label="DELETE" value="DELETE"/>
+        </el-select>
+      </template>
+      <template #append>
+        <el-button icon="Search" @click="testApi"/>
+      </template>
+    </el-input>
+    <h3>请求参数</h3>
+    <el-tabs v-model="activeName">
+      <el-tab-pane label="Params" name="Params">
+        <dynamic-map v-model="form.params"/>
+      </el-tab-pane>
+      <el-tab-pane label="Body" name="Body">
+        <dynamic-map v-model="form.body"/>
+      </el-tab-pane>
+      <el-tab-pane label="Headers" name="Headers">
+        <dynamic-map v-model="form.headers"/>
+      </el-tab-pane>
+      <el-tab-pane label="Cookies" name="Cookies">
+        <dynamic-map v-model="form.cookies"/>
+      </el-tab-pane>
+    </el-tabs>
+    <h3>响应结果</h3>
+    <el-input v-model="form.responseResolution" placeholder="请求地址"></el-input>
+    <dynamic-map v-model="form.response"/>
+  </div>
+</template>
+<script setup>
+import DynamicMap from "@/components/DynamicMap/index.vue"
+import {testBizDataApi} from "@/api/standardization/bizDataShowConfig.js";
+
+const {proxy} = getCurrentInstance();
+const props = defineProps({
+  columns: {
+    type: Array,
+    default: () => []
+  },
+  modelValue: String,
+});
+const emit = defineEmits(['update:modelValue'])
+
+const form = ref({
+  url: '',
+  method: 'GET',
+  params: [],
+  body: [],
+  headers: [],
+  cookies: [],
+  response: [],
+  responseResolution: '',
+})
+const activeName = ref('Params')
+
+function testApi() {
+  testBizDataApi(form.value).then(res => {
+    if (res.data) {
+      proxy.$modal.msgSuccess("接口连接成功!");
+    } else {
+      proxy.$modal.msgError("接口连接失败!");
+    }
+  })
+}
+
+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>

+ 604 - 0
ruoyi-ui/src/views/standardization/bizDataShowConfig/chart/index.vue

@@ -0,0 +1,604 @@
+<template>
+  <el-form ref="formRef" :model="form" :rules="rules" label-width="80px">
+    <el-form-item label="图表类型">
+      <el-select v-model="form.chartType" placeholder="请选择图表类型" @change="changeChartType">
+        <el-option v-for="chartType in chartTypeOptions" :key="chartType.value" :label="chartType.label"
+                   :value="chartType.value"/>
+      </el-select>
+    </el-form-item>
+    <el-collapse>
+      <el-collapse-item name="1" title="图例">
+        <el-form-item label="显示">
+          <el-radio-group v-model="form.legend.show">
+            <el-radio :value="true">是</el-radio>
+            <el-radio :value="false">否</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="位置(X)">
+          <x-axis-config v-model="form.legend.left"></x-axis-config>
+        </el-form-item>
+        <el-form-item label="位置(Y)">
+          <y-axis-config v-model="form.legend.top"></y-axis-config>
+        </el-form-item>
+        <el-form-item label="布局方式">
+          <el-radio-group v-model="form.legend.orient">
+            <el-radio-button value="vertical">纵向</el-radio-button>
+            <el-radio-button value="horizontal">横向</el-radio-button>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="字体大小">
+          <el-input-number v-model="form.legend.textStyle.fontSize" :min="2"></el-input-number>
+        </el-form-item>
+        <el-form-item label="文字颜色">
+          <el-color-picker v-model="form.legend.textStyle.color" :predefine="predefineColors"
+                           show-alpha></el-color-picker>
+        </el-form-item>
+      </el-collapse-item>
+      <el-collapse-item name="2" title="内边距">
+        <el-form-item label="内边距(左)">
+          <el-input-number v-model="form.grid.left" :min="0"></el-input-number>
+        </el-form-item>
+        <el-form-item label="内边距(上)">
+          <el-input-number v-model="form.grid.top" :min="0"></el-input-number>
+        </el-form-item>
+        <el-form-item label="内边距(右)">
+          <el-input-number v-model="form.grid.right" :min="0"></el-input-number>
+        </el-form-item>
+        <el-form-item label="内边距(下)">
+          <el-input-number v-model="form.grid.bottom" :min="0"></el-input-number>
+        </el-form-item>
+      </el-collapse-item>
+      <el-collapse-item v-if="form.chartType !== 'pie'" name="3" title="x轴">
+        <el-form-item label="显示">
+          <el-radio-group v-model="form.xAxis.show">
+            <el-radio :value="true">是</el-radio>
+            <el-radio :value="false">否</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="名称">
+          <el-input v-model="form.xAxis.name"></el-input>
+        </el-form-item>
+        <el-form-item label="轴线显示">
+          <el-radio-group v-model="form.xAxis.axisLine.show">
+            <el-radio :value="true">是</el-radio>
+            <el-radio :value="false">否</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="轴线颜色">
+          <el-color-picker v-model="form.xAxis.axisLine.lineStyle.color" :predefine="predefineColors"
+                           show-alpha></el-color-picker>
+        </el-form-item>
+        <el-form-item label="刻度线">
+          <el-radio-group v-model="form.xAxis.axisTick.show">
+            <el-radio :value="true">是</el-radio>
+            <el-radio :value="false">否</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="字体颜色">
+          <el-color-picker v-model="form.xAxis.axisLabel.color" :predefine="predefineColors"
+                           show-alpha></el-color-picker>
+        </el-form-item>
+        <el-form-item label="字体大小">
+          <el-input-number v-model="form.xAxis.axisLabel.fontSize" :min="2"></el-input-number>
+        </el-form-item>
+        <el-form-item label="字体倾斜">
+          <el-input-number v-model="form.xAxis.axisLabel.rotate" :min="0"></el-input-number>
+        </el-form-item>
+        <el-form-item label="显示间隔">
+          <el-select v-model="form.xAxis.axisLabel.interval">
+            <el-option label="自动" value="auto"></el-option>
+            <el-option :value="0" label="全部显示"></el-option>
+            <el-option :value="1" label="隔一行"></el-option>
+            <el-option :value="2" label="隔两行"></el-option>
+            <el-option :value="3" label="隔三行"></el-option>
+            <el-option :value="4" label="隔四行"></el-option>
+            <el-option :value="5" label="隔五行"></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="数据">
+          <el-select v-model="form.xparams" multiple placeholder="请选择x轴数据">
+            <el-option v-for="column in columns" :key="column.key" :label="column.value" :value="column"/>
+          </el-select>
+        </el-form-item>
+      </el-collapse-item>
+      <el-collapse-item v-if="form.chartType !== 'pie'" name="4" title="y轴">
+        <el-form-item label="y轴颜色">
+          <el-color-picker v-model="form.yAxis.axisLine.lineStyle.color" :predefine="predefineColors"
+                           show-alpha></el-color-picker>
+        </el-form-item>
+        <el-form-item label="字体颜色">
+          <el-color-picker v-model="form.yAxis.axisLabel.color" :predefine="predefineColors"
+                           show-alpha></el-color-picker>
+        </el-form-item>
+        <el-form-item label="字体大小">
+          <el-input-number v-model="form.yAxis.axisLabel.fontSize" :min="2"></el-input-number>
+        </el-form-item>
+        <el-form-item label="网格线">
+          <el-radio-group v-model="form.yAxis.splitLine.show">
+            <el-radio :value="true">是</el-radio>
+            <el-radio :value="false">否</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="网格线颜色">
+          <el-color-picker v-model="form.yAxis.splitLine.lineStyle.color" :predefine="predefineColors"
+                           show-alpha></el-color-picker>
+        </el-form-item>
+      </el-collapse-item>
+      <el-collapse-item v-if="form.chartType !== 'pie'" name="5" title="数值">
+        <el-form-item label="显示">
+          <el-radio-group v-model="form.series.label.show">
+            <el-radio :value="true">是</el-radio>
+            <el-radio :value="false">否</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="字体大小">
+          <el-input-number v-model="form.series.label.fontSize" :min="2"></el-input-number>
+        </el-form-item>
+        <el-form-item label="字体颜色">
+          <el-color-picker v-model="form.series.label.color" :predefine="predefineColors"
+                           show-alpha></el-color-picker>
+        </el-form-item>
+        <el-form-item label="字体位置">
+          <el-select v-model="form.series.label.position">
+            <el-option label="顶部" value="top"></el-option>
+            <el-option label="左侧" value="left"></el-option>
+            <el-option label="右侧" value="right"></el-option>
+            <el-option label="底部" value="bottom"></el-option>
+            <el-option label="中心" value="inside"></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="字体单位">
+          <el-input v-model="form.series.label.unit"></el-input>
+        </el-form-item>
+        <el-form-item label="数据配置">
+          <el-table :data="form.yparams">
+            <el-table-column label="展示字段">
+              <template #default="scope">
+                <el-select v-model="scope.row.name" placeholder="请选择y轴数据">
+                  <el-option v-for="column in columns" :key="column.key" :label="column.value" :value="column"/>
+                </el-select>
+              </template>
+            </el-table-column>
+            <el-table-column label="默认选中">
+              <template #default="scope">
+                <el-switch v-model="scope.row.selected"></el-switch>
+              </template>
+            </el-table-column>
+            <el-table-column label="单位">
+              <template #default="scope">
+                <el-select v-model="scope.row.unit" placeholder="请选择单位">
+                  <el-option label="m" value="m"/>
+                  <el-option label="m³/s" value="m³/s"/>
+                  <el-option label="mm" value="mm"/>
+                  <el-option label="万m³" value="万m³"/>
+                  <el-option label="mg/L" value="mg/L"/>
+                </el-select>
+              </template>
+            </el-table-column>
+            <el-table-column label="图表类型">
+              <template #default="scope">
+                <el-select v-model="scope.row.type" placeholder="请选择图表类型">
+                  <el-option label="折线图" value="line"/>
+                  <el-option label="柱状图" value="bar"/>
+                </el-select>
+              </template>
+            </el-table-column>
+            <el-table-column label="">
+              <template slot-scope="scope">
+                <el-button icon="el-icon-delete" style="color:red;" text
+                           @click="handleDelYparam(scope.$index)">
+                  删除
+                </el-button>
+              </template>
+            </el-table-column>
+          </el-table>
+          <el-button icon="el-icon-plus" text @click="handleAddYparam">
+            新建展示字段
+          </el-button>
+        </el-form-item>
+      </el-collapse-item>
+      <el-collapse-item v-if="form.chartType !== 'pie'" name="6" title="柱体">
+        <el-form-item label="柱体背景">
+          <el-radio-group v-model="form.series.showBackground">
+            <el-radio :value="true">是</el-radio>
+            <el-radio :value="false">否</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="圆角">
+          <el-input-number v-model="form.series.itemStyle.borderRadius" :min="0"></el-input-number>
+        </el-form-item>
+        <el-form-item label="柱体宽度">
+          <el-input-number v-model="form.barWidth" :min="0"></el-input-number>
+        </el-form-item>
+        <el-form-item label="柱体配色">
+          <el-color-picker v-for="(color, index) in form.color" :key="index"
+                           v-model="form.color[index]"></el-color-picker>
+          <el-button circle icon="el-icon-plus" @click="form.color.push('#b5b7be')"></el-button>
+        </el-form-item>
+      </el-collapse-item>
+      <el-collapse-item v-if="form.chartType !== 'pie'" name="7" title="折线">
+        <el-form-item label="折线显示">
+          <el-radio-group v-model="form.lineShow">
+            <el-radio :value="true">是</el-radio>
+            <el-radio :value="false">否</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="平滑曲线">
+          <el-radio-group v-model="form.smooth">
+            <el-radio :value="true">是</el-radio>
+            <el-radio :value="false">否</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="折线配色">
+          <el-color-picker v-for="(color, index) in form.lineColors" :key="index"
+                           v-model="form.lineColors[index]"></el-color-picker>
+          <el-button circle icon="el-icon-plus" @click="form.lineColors.push('#b5b7be')"></el-button>
+        </el-form-item>
+        <el-form-item label="圆点大小">
+          <el-input-number v-model="form.symbolSize" :max="120" :min="0"></el-input-number>
+        </el-form-item>
+        <el-form-item label="面积显示">
+          <el-radio-group v-model="form.areaShow">
+            <el-radio :value="true">是</el-radio>
+            <el-radio :value="false">否</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="面积配色">
+          <el-color-picker v-for="(color, index) in form.areaColors" :key="index"
+                           v-model="form.areaColors[index]"></el-color-picker>
+          <el-button circle icon="el-icon-plus" @click="form.areaColors.push('#b5b7be')"></el-button>
+        </el-form-item>
+      </el-collapse-item>
+      <el-collapse-item v-if="form.chartType === 'pie'" name="5" title="标签">
+        <el-form-item label="显示">
+          <el-radio-group v-model="form.series.label.show">
+            <el-radio :value="true">是</el-radio>
+            <el-radio :value="false">否</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="格式化">
+          <el-input v-model="form.series.label.formatter"></el-input>
+        </el-form-item>
+        <el-form-item label="背景颜色">
+          <el-color-picker v-model="form.series.label.backgroundColor" :predefine="predefineColors"
+                           show-alpha></el-color-picker>
+        </el-form-item>
+        <el-form-item label="边框">
+          <el-input-number v-model="form.series.label.borderWidth" :min="0"></el-input-number>
+        </el-form-item>
+        <el-form-item label="边框颜色">
+          <el-color-picker v-model="form.series.label.borderColor" :predefine="predefineColors"
+                           show-alpha></el-color-picker>
+        </el-form-item>
+        <el-form-item label="边框圆角">
+          <el-input-number v-model="form.series.label.borderRadius" :min="0"></el-input-number>
+        </el-form-item>
+        <el-form-item label="位置">
+          <el-select v-model="form.series.label.position">
+            <el-option label="内部" value="inside"></el-option>
+            <el-option label="外侧" value="outside"></el-option>
+            <el-option label="中心" value="center"></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="字体大小">
+          <el-input-number v-model="form.series.label.fontSize" :min="2"></el-input-number>
+        </el-form-item>
+        <el-form-item label="颜色">
+          <el-color-picker v-model="form.series.label.color" :predefine="predefineColors"
+                           show-alpha></el-color-picker>
+        </el-form-item>
+      </el-collapse-item>
+      <el-collapse-item v-if="form.chartType === 'pie'" name="6" title="扇区">
+        <el-form-item label="名称字段">
+          <el-select v-model="form.nameField">
+            <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-select v-model="form.valueField">
+            <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-select v-model="form.series.roseType">
+            <el-option :value="false" label="不展示"></el-option>
+            <el-option label="圆心角" value="radius"></el-option>
+            <el-option label="扇区" value="area"></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item v-if="form.series && form.series.radius && form.series.radius[1]" label="外环大小%">
+          <el-input-number v-model="form.series.radius[1]" :max="120" :min="2"></el-input-number>
+        </el-form-item>
+        <el-form-item v-if="form.series && form.series.radius && form.series.radius[0]" label="内环大小%">
+          <el-input-number v-model="form.series.radius[0]" :max="100" :min="0"></el-input-number>
+        </el-form-item>
+        <el-form-item label="圆角">
+          <el-input-number v-model="form.series.itemStyle.borderRadius" :max="120"
+                           :min="0"></el-input-number>
+        </el-form-item>
+        <el-form-item label="配色">
+          <el-color-picker v-for="(color, index) in form.color" :key="index"
+                           v-model="form.color[index]"></el-color-picker>
+          <el-button circle icon="el-icon-plus" @click="form.color.push('#b5b7be')"></el-button>
+        </el-form-item>
+      </el-collapse-item>
+    </el-collapse>
+  </el-form>
+</template>
+<script setup>
+import YAxisConfig from "./yAxis/index.vue";
+import XAxisConfig from "./xAxis/index.vue";
+
+const props = defineProps({
+  columns: {
+    type: Array,
+    default: () => []
+  },
+  modelValue: String,
+})
+const emit = defineEmits(['update:modelValue'])
+
+const form = ref({
+  type: 'chart',
+  chartType: 'linebar',
+  legend: {
+    show: true,
+    left: 'left',
+    top: 'top',
+    orient: 'vertical',
+    textStyle: {
+      fontSize: 12,
+      color: '#b5b7be'
+    }
+  },
+  grid: {
+    left: 0,
+    top: 0,
+    right: 0,
+    bottom: 0,
+    containLabel: true,
+  },
+  xAxis: {
+    show: true,
+    name: '',
+    axisLine: {
+      show: true,
+      lineStyle: {
+        color: '#b5b7be',
+      }
+    },
+    axisTick: {
+      show: true,
+    },
+    axisLabel: {
+      color: '#b5b7be',
+      fontSize: 12,
+      rotate: 0,
+      interval: 'auto'
+    },
+  },
+  yAxis: {
+    axisLine: {
+      lineStyle: {
+        color: '#b5b7be',
+      },
+    },
+    axisLabel: {
+      color: '#b5b7be',
+      fontSize: 12,
+    },
+    splitLine: {
+      show: false,
+      lineStyle: {
+        color: '#b5b7be',
+      },
+    },
+  },
+  series: {
+    showBackground: false,
+    label: {
+      show: true,
+      fontSize: 12,
+      color: '#b5b7be',
+      position: 'top',
+      unit: '',
+    },
+    itemStyle: {
+      borderRadius: 0,
+    },
+  },
+  barWidth: 18,
+  color: [],
+  xparams: [],
+  yparams: [],
+})
+const rules = ref({})
+const chartTypeOptions = ref([
+  {label: '折线/柱状图', value: 'linebar'},
+  {label: '饼/环图', value: 'pie'},
+]);
+const predefineColors = ref([
+  '#ff4500',
+  '#ff8c00',
+  '#ffd700',
+  '#90ee90',
+  '#00ced1',
+  '#1e90ff',
+  '#c71585',
+  'rgba(255, 69, 0, 0.68)',
+  'rgb(255, 120, 0)',
+  'hsv(51, 100, 98)',
+  'hsva(120, 40, 94, 0.5)',
+  'hsl(181, 100%, 37%)',
+  'hsla(209, 100%, 56%, 0.73)',
+  '#c7158577'
+])
+
+function changeChartType(type) {
+  let defaultValue;
+  switch (type) {
+    case "linebar":
+      defaultValue = {
+        type: 'chart',
+        chartType: 'linebar',
+        legend: {
+          show: true,
+          left: 'left',
+          top: 'top',
+          orient: 'vertical',
+          textStyle: {
+            fontSize: 12,
+            color: '#b5b7be'
+          }
+        },
+        grid: {
+          left: 0,
+          top: 0,
+          right: 0,
+          bottom: 0,
+          containLabel: true,
+        },
+        xAxis: {
+          show: true,
+          name: '',
+          axisLine: {
+            show: true,
+            lineStyle: {
+              color: '#b5b7be'
+            }
+          },
+          axisTick: {
+            show: true,
+          },
+          axisLabel: {
+            color: '#b5b7be',
+            fontSize: 12,
+            rotate: 0,
+            interval: 'auto'
+          },
+        },
+        yAxis: {
+          axisLine: {
+            lineStyle: {
+              color: '#b5b7be',
+            },
+          },
+          axisLabel: {
+            color: '#b5b7be',
+            fontSize: 12,
+          },
+          splitLine: {
+            show: false,
+            lineStyle: {
+              color: '#b5b7be',
+            },
+          },
+        },
+        series: {
+          showBackground: false,
+          label: {
+            show: true,
+            fontSize: 12,
+            color: '#b5b7be',
+            position: 'top',
+            unit: '',
+          },
+          itemStyle: {
+            borderRadius: 0,
+          },
+        },
+        barWidth: 18,
+        color: [],
+        lineShow: true,
+        smooth: false,
+        lineColors: [],
+        symbolSize: 2,
+        areaShow: false,
+        areaColors: [],
+        xparams: [],
+        yparams: [],
+      };
+      break
+    case "pie":
+      defaultValue = {
+        legend: {
+          show: true,
+          left: 'left',
+          top: 'top',
+          orient: 'vertical',
+          textStyle: {
+            fontSize: 12,
+            color: '#b5b7be'
+          }
+        },
+        grid: {
+          left: 0,
+          top: 0,
+          right: 0,
+          bottom: 0,
+          containLabel: true,
+        },
+        series: {
+          showBackground: false,
+          label: {
+            show: true,
+            formatter: '{b}-{c}({d}%)',
+            backgroundColor: '',
+            borderWidth: 0,
+            borderColor: '',
+            borderRadius: 0,
+            position: 'outside',
+            fontSize: 12,
+            color: '#b5b7be',
+            roseType: 'false',
+            radius: [0, 60],
+            itemStyle: {
+              borderRadius: 0
+            },
+          },
+          itemStyle: {
+            borderRadius: 0,
+          },
+        },
+        color: [],
+      };
+      break
+    case "gauge":
+    case "radar":
+  }
+  form.value = objectMerge(defaultValue, form.value)
+}
+
+function handleDelYparam(index) {
+  if (form.value.yparams && form.value.yparams.length > 0) {
+    form.value.yparams.splice(index, 1);
+  }
+}
+
+function handleAddYparam() {
+  if (!form.value.yparams) {
+    form.value.yparams = []
+  }
+  let defaultType = '';
+  if (form.value.chartType === 'bar') {
+    defaultType = 'bar';
+  } else if (form.value.chartType === 'line') {
+    defaultType = 'line';
+  }
+
+  form.value.yparams.push({selected: true, unit: '', type: defaultType})
+}
+
+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>

+ 67 - 0
ruoyi-ui/src/views/standardization/bizDataShowConfig/chart/xAxis/index.vue

@@ -0,0 +1,67 @@
+<template>
+  <div style="width: 100%;">
+    <div style="width: 100%; display: flex;">
+      <div :class="{'icon-item-active': position === 'left'}" class="icon-item" @click="changeVerticalAlign('left')">
+        <svg-icon icon-class="vertical-align-left"></svg-icon>
+      </div>
+      <div :class="{'icon-item-active': position === 'center'}" class="icon-item"
+           @click="changeVerticalAlign('center')">
+        <svg-icon icon-class="vertical-align-center"></svg-icon>
+      </div>
+      <div :class="{'icon-item-active': position === 'right'}" class="icon-item" @click="changeVerticalAlign('right')">
+        <svg-icon icon-class="vertical-align-right"></svg-icon>
+      </div>
+      <div :class="{'icon-item-active': edit}" class="icon-item" @click="changeVerticalAlign('edit')">
+        <svg-icon icon-class="edit"></svg-icon>
+      </div>
+    </div>
+    <div v-show="edit" style="margin-top: 10px;">
+      <el-input-number v-model="editValue" :min="0" style="width: 90%;"></el-input-number>
+    </div>
+  </div>
+</template>
+<script setup>
+const props = defineProps({
+  modelValue: String,
+});
+const emit = defineEmits(['update:modelValue'])
+
+const edit = ref(false)
+const editValue = ref(0)
+const position = ref('')
+
+function changeVerticalAlign(value) {
+  if (value === 'edit') {
+    edit.value = true
+    position.value = editValue.value
+  } else {
+    edit.value = false
+    position.value = value
+  }
+}
+
+watch(() => props.modelValue, modelValue => {
+  if (modelValue) {
+    if (['left', 'center', 'right'].includes(modelValue)) {
+      changeVerticalAlign(modelValue)
+    } else {
+      edit.value = true
+      editValue.value = value
+    }
+  }
+}, {immediate: true})
+
+watch(() => position, position => {
+  emit('update:modelValue', position.value)
+}, {deep: true})
+</script>
+<style lang="scss" scoped>
+.icon-item {
+  flex: 1;
+  cursor: pointer;
+}
+
+.icon-item-active, .icon-item:hover {
+  color: #1e90ff;
+}
+</style>

+ 74 - 0
ruoyi-ui/src/views/standardization/bizDataShowConfig/chart/yAxis/index.vue

@@ -0,0 +1,74 @@
+<template>
+  <div style="width: 100%;">
+    <div style="width: 100%; display: flex;">
+      <div :class="{'icon-item-active': position === 'top'}" class="icon-item" @click="changeHorizontalAlign('top')">
+        <svg-icon icon-class="horizontal-align-top"></svg-icon>
+      </div>
+      <div :class="{'icon-item-active': position === 'center'}" class="icon-item"
+           @click="changeHorizontalAlign('center')">
+        <svg-icon icon-class="horizontal-align-center"></svg-icon>
+      </div>
+      <div :class="{'icon-item-active': position === 'bottom'}" class="icon-item"
+           @click="changeHorizontalAlign('bottom')">
+        <svg-icon icon-class="horizontal-align-bottom"></svg-icon>
+      </div>
+      <div :class="{ 'icon-item-active' : edit }" class="icon-item" @click="changeHorizontalAlign('edit')">
+        <svg-icon icon-class="edit"></svg-icon>
+      </div>
+    </div>
+    <div v-show="edit" style="margin-top: 10px;">
+      <el-input-number v-model="editValue" :min="0" style="width: 90%;"></el-input-number>
+    </div>
+  </div>
+</template>
+<script>
+export default {
+  name: 'YAxisConfig',
+  props: ['value'],
+  data() {
+    return {
+      edit: false,
+      editValue: 0,
+      position: '',
+    }
+  },
+  watch: {
+    value: {
+      handler(value) {
+        if (['top', 'center', 'bottom'].includes(value)) {
+          this.changeHorizontalAlign(value)
+        } else {
+          this.edit = true
+          this.editValue = value
+        }
+      },
+      immediate: true
+    },
+    position(value) {
+      this.$emit('input', value)
+    },
+  },
+  methods: {
+    changeHorizontalAlign(position) {
+      if (position === 'edit') {
+        this.edit = true
+        this.position = this.editValue
+      } else {
+        this.edit = false
+        this.position = position
+      }
+    },
+  }
+}
+</script>
+<style lang="scss" scoped>
+.icon-item {
+  flex: 1;
+  cursor: pointer;
+}
+
+.icon-item-active, .icon-item:hover {
+  color: #1e90ff;
+  fill: #1e90ff;
+}
+</style>

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

@@ -0,0 +1,230 @@
+<template>
+  <div class="app-container">
+    <el-row justify="space-between">
+      <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-col>
+      <el-col :span="12" style="display: flex;justify-content: right;">
+        <el-button type="primary" @click="handleAdd" icon="plus">新建业务数据配置</el-button>
+      </el-col>
+    </el-row>
+    <el-table
+        :data="tableData"
+        height="68vh"
+        :cell-style="{ padding: '5px' }"
+        :row-style="{ fontSize: '1rem', textAlign:'center' }"
+        border>
+      <el-table-column align="center" width="200" prop="name" label="标题"></el-table-column>
+      <el-table-column align="center" width="130" prop="type" label="类型">
+        <template #default="scope">
+          <el-tag round v-if="scope.row.type === 'chart'">
+            <TrendCharts style="width: 1em; height: 1em;margin-right: 2px;"/>
+            图表
+          </el-tag>
+          <el-tag round v-if="scope.row.type === 'list'">
+            <List style="width: 1em; height: 1em;margin-right: 2px;"/>
+            列表
+          </el-tag>
+          <el-tag round v-if="scope.row.type === 'table'">
+            <Document style="width: 1em; height: 1em;margin-right: 2px;"/>
+            表单
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column align="center" width="130" prop="sort" label="排序"></el-table-column>
+      <el-table-column align="center" label="操作" width="240">
+        <template #default="scope">
+          <div style="display: flex;justify-content: space-between;">
+            <el-button type="primary" @click="handlePreview(scope.row)" text>预览</el-button>
+            <el-button type="primary" @click="handleUpdate(scope.row)" text>修改</el-button>
+            <el-button type="danger" @click="handleDelete(scope.row)" text>删除</el-button>
+          </div>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <el-dialog :title="title" v-model="dialogVisible" width="50vw" append-to-body destroy-on-close>
+      <el-form ref="formRef" :model="form" :rules="rules" label-width="80px">
+        <el-row>
+          <el-col :span="12">
+            <el-form-item label="标题" prop="name">
+              <el-input v-model="form.name" placeholder="请输入标题"/>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="显示排序" prop="sort">
+              <el-input-number v-model="form.sort" controls-position="right" :min="0"/>
+            </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>
+          </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>
+            </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">
+        <el-button type="primary" @click="submitForm">确 定</el-button>
+        <el-button @click="cancel">取 消</el-button>
+      </div>
+    </el-dialog>
+
+    <el-dialog :title="previewDialogTitle" v-model="previewDialogVisible" width="50vw" append-to-body>
+      <div style="height: 500px;">
+        <biz-display ref="bizDisplayRef"></biz-display>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+<script setup>
+import {onMounted} from 'vue';
+import {deleteBizDataShowConfig, getBizDataShowConfigList, saveBizDataShowConfig } from "@/api/standardization/bizDataShowConfig.js";
+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"
+
+const {proxy} = getCurrentInstance();
+
+const queryParams = ref({pageNum: 1, pageSize: 20})
+const tableData = ref([])
+const modelOptions = ref([])
+const dialogVisible = ref(false)
+const columns = ref([])
+const title = ref('新建业务数据配置')
+const form = ref({
+  id: '',
+  name: '',
+  type: 'list',
+  queryOptions: null,
+  renderingOptions: null,
+  sort: 0
+})
+const rules = ref({
+  cateName: [
+    {required: true, message: '请输入目录名称', trigger: 'blur'},
+    {min: 2, max: 20, message: '长度在 2 到 20 个字符', trigger: 'blur'}
+  ],
+})
+const typeOptions = ref([
+  {label: '图表', value: 'chart'},
+  {label: '列表', value: 'list'},
+  {label: '相信信息', value: 'table'}
+])
+
+const previewDialogVisible = ref(false)
+const previewDialogTitle = ref('')
+const bizDisplayRef = ref(null)
+
+/** 获取模型服务调用次数 */
+function getData() {
+  getBizDataShowConfigList(queryParams.value).then(res => {
+    tableData.value = res.data
+  })
+}
+
+function handleAdd() {
+  dialogVisible.value = true
+  title.value = '新建业务数据配置'
+  form.value.sort = tableData.value.length + 1
+}
+
+function submitForm() {
+  proxy.$refs["formRef"].validate(valid => {
+    if (valid) {
+      saveBizDataShowConfig(form.value).then(res => {
+        proxy.$modal.msgSuccess("新增成功");
+        cancel();
+        getData();
+      });
+    }
+  });
+}
+
+function cancel() {
+  dialogVisible.value = false
+  reset();
+}
+
+function reset() {
+  form.value = {}
+}
+
+function handlePreview(row) {
+  previewDialogVisible.value = true
+  previewDialogTitle.value = row.name
+  nextTick(() => {
+    bizDisplayRef.value.loadData(row)
+  })
+}
+
+function handleUpdate(row) {
+  dialogVisible.value = true
+  title.value = `编辑【${row.name}】业务数据配置`
+  form.value = row
+}
+
+function handleDelete(row) {
+  proxy.$modal.confirm('是否确认删除?').then(function () {
+    return deleteBizDataShowConfig(row.id);
+  }).then(() => {
+    getData();
+    proxy.$modal.msgSuccess("删除成功");
+  }).catch(() => {
+  });
+}
+
+onMounted(() => {
+  getData()
+});
+
+
+watch(() => form.value.queryOptions, queryOptions => {
+  if (queryOptions) {
+    const api = JSON.parse(queryOptions)
+    columns.value = api.response
+  }
+}, {immediate: true, deep: true})
+</script>
+<style scoped lang="scss">
+</style>

+ 33 - 0
ruoyi-ui/src/views/standardization/bizDataShowConfig/list/index.vue

@@ -0,0 +1,33 @@
+<template>
+  <el-form ref="formRef" :model="form" :rules="rules" label-width="80px">
+    <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>
+</template>
+<script setup>
+const props = defineProps({
+  columns: {
+    type: Array,
+    default: () => []
+  },
+  modelValue: String,
+});
+const emit = defineEmits(['update:modelValue'])
+const form = ref({columns: []})
+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>

+ 90 - 0
ruoyi-ui/src/views/standardization/bizDataShowConfig/show/GwTableTwo.vue

@@ -0,0 +1,90 @@
+<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>
+</template>
+<script setup>
+import {isArray, isString} from "@/utils/validate";
+
+const props = defineProps({
+  data: {
+    type: Array,
+    default: [],
+  },
+  columnNumber: {
+    type: Number,
+    default: 3,
+  },
+})
+
+const row = computed(() => {
+  return (num) => {
+    let start = (num - 1) * props.columnNumber;
+    let end = num * props.columnNumber;
+    // if (end >= props.data.length && props.data.length > 1) {
+    //   end = -1;
+    // }
+    return props.data.slice(start, end);
+  }
+})
+
+const rowNumber = computed(() => {
+  if (props.data) {
+    return Math.ceil(props.data.length / props.columnNumber);
+  }
+  return 0;
+})
+</script>
+<style lang="scss" scoped>
+.gw-descriptions-table {
+  width: 100%;
+  height: 100%;
+
+  th {
+    min-width: 80px;
+    text-align: center;
+  }
+
+  td {
+    text-align: left;
+
+    .node-info {
+      color: rgb(5, 164, 182);
+      padding: 5px 10px;
+      border: 1px solid rgb(44, 184, 197);
+      border-radius: 5px;
+      background-color: rgb(229, 247, 248);
+      margin-right: 10px;
+      margin-bottom: 5px;
+      cursor: pointer;
+      display: inline-block;
+
+      &:hover {
+        color: #fff;
+        background-color: rgb(5, 164, 182);
+        border: 1px solid rgb(5, 164, 182);
+      }
+    }
+
+  }
+
+}
+</style>

+ 97 - 0
ruoyi-ui/src/views/standardization/bizDataShowConfig/show/index.vue

@@ -0,0 +1,97 @@
+<template>
+  <div class="biz-display-container">
+    <div v-if="title || $slots.header" class="biz-display-header">
+      <slot name="header">
+        {{ title }}
+      </slot>
+    </div>
+    <div class="biz-display-body">
+      <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-column v-for="column in columns" :key="column.key" :label="column.value"
+                           :prop="column.key"></el-table-column>
+        </el-table>
+      </template>
+      <template v-if="displayType === 'table'">
+        <gw-table-two :columnNumber="columnNumber" :data="data"></gw-table-two>
+      </template>
+      <template v-if="displayType === 'text'">
+        <span v-html="data"></span>
+      </template>
+    </div>
+  </div>
+</template>
+<script setup>
+import GwChart from "@/components/chart/GwEchart.vue";
+import {getChartOption, getFormListData, getTableData, getText} from '@/utils/biz'
+import GwTableTwo from "./GwTableTwo.vue";
+
+defineExpose({
+  loadData
+})
+
+const props = defineProps({
+  config: {
+    type: Object,
+  }
+});
+
+const data = ref(null)
+const title = ref(null)
+const displayType = ref(null)
+const columnNumber = ref(1)
+const columns = ref([])
+
+function loadData(config) {
+  if (!config) {
+    return
+  }
+  config.value = config
+  displayType.value = config.type
+  title.value = config.name
+  switch (config.type) {
+    case 'chart':
+      nextTick(() => {
+        getChartOption(config).then(r => data.value = r);
+      })
+      return;
+    case 'list':
+      columns.value = config.renderingOptionsData.columns
+      return getTableData(config).then(r => data.value = r);
+    case 'table':
+      columnNumber.value = config.renderingOptionsData.columnNumber
+      return getFormListData(config).then(r => data.value = r);
+    case 'text':
+      return getText(config).then(r => data.value = r);
+  }
+}
+
+watch(() => props.config, (config) => {
+  if (config) {
+    loadData(config)
+  }
+}, {immediate: true})
+</script>
+<style lang="scss" scoped>
+.biz-display-container {
+  width: 100%;
+  height: 100%;
+
+  .biz-display-header {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    font-size: 1rem;
+    text-align: center;
+    padding: 10px 0;
+  }
+
+  .biz-display-body {
+    height: calc(100% - 1rem - 20px);
+  }
+
+}
+</style>

+ 41 - 0
ruoyi-ui/src/views/standardization/bizDataShowConfig/table/index.vue

@@ -0,0 +1,41 @@
+<template>
+  <el-form ref="formRef" :model="form" :rules="rules" label-width="80px">
+    <el-form-item label="表单列数">
+      <el-input-number v-model="form.columnNumber" :min="1" controls-position="right"/>
+    </el-form-item>
+    <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>
+</template>
+<script setup>
+const props = defineProps({
+  columns: {
+    type: Array,
+    default: () => []
+  },
+  modelValue: String,
+});
+const emit = defineEmits(['update:modelValue'])
+
+const form = ref({
+  columnNumber: 1,
+  columns: []
+})
+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>

+ 31 - 0
ruoyi-ui/src/views/standardization/bizDataShowConfig/text/index.vue

@@ -0,0 +1,31 @@
+<template>
+  <el-form ref="formRef" :model="form" :rules="rules" label-width="80px">
+    <el-form-item label="文本模板">
+      <el-input v-model="form.template" :autosize="{ minRows: 4, maxRows: 8}" placeholder="请输入模板"
+                type="textarea"></el-input>
+    </el-form-item>
+  </el-form>
+</template>
+<script setup>
+const props = defineProps({
+  columns: {
+    type: Array,
+    default: () => []
+  },
+  modelValue: String,
+});
+const emit = defineEmits(['update:modelValue'])
+
+const form = ref({})
+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>

+ 292 - 256
ruoyi-ui/src/views/standardization/modelUsing/index.vue

@@ -1,112 +1,132 @@
 <template>
-    <div style="width: 98%;display: flex;align-items: center;justify-content: space-between;padding-top: 1%;margin-left: 1%;">
-        <div style="font-size: 20px;"> 
-            模型应用
-        </div>
-        <div style="display: flex;">
-            <el-input size="mini" v-model="dcName" style="width: 100%;margin-left: 1%;" placeholder="搜索项目"/>
-            <el-button type="primary" @click="showAdd" size="mini" style="margin-left:5%;" :icon="Plus">新增 </el-button>
-        </div>
+  <div
+      style="width: 98%;display: flex;align-items: center;justify-content: space-between;padding-top: 1%;margin-left: 1%;">
+    <div style="font-size: 20px;">
+      模型应用
     </div>
-    <div style="width: 98%;display: flex;align-items: center;justify-content: space-between;padding-top: 1%;margin-left: 1%;">
-        <el-select
+    <div style="display: flex;">
+      <el-input size="mini" v-model="dcName" style="width: 100%;margin-left: 1%;" placeholder="搜索项目"/>
+      <el-button type="primary" @click="showAdd" size="mini" style="margin-left:5%;" :icon="Plus">新增</el-button>
+    </div>
+  </div>
+  <div
+      style="width: 98%;display: flex;align-items: center;justify-content: space-between;padding-top: 1%;margin-left: 1%;">
+    <el-select
         v-model="formJi"
         class="noBorSel"
         placeholder="全部"
         style="width: 10%;margin-left: 0%;"
-        >
-            <el-option label="全部" :value="1" />
-            <el-option label="已发布" :value="2" />
-        </el-select>
-    </div>
-    <div style="height: 70vh;width: 98%;margin-left:1%;background-color: transparent;margin-top: 1%;display: flex;flex-wrap: wrap;justify-content: flex-start;gap: 0.65%">
-        <div  v-for="(item,index) in modelList">
-            <div class="coz-card" @mouseenter="setHoverIndex(index)" @click="goFlow(item)" style="cursor: pointer;"  @mouseleave="resetHoverIndex">
-                <div style="display: flex;width: 100%;">
-                    <div style="margin-left: 8%;margin-top: 5%;width: 60%;">
-                        {{ item.appTitle }}
-                    </div>
-                    <img style="width: 90px;height: 90px;margin-left: 0%;margin-top: 8%;border-radius: 12px;" :src="item.appIcon" alt="">
-                </div>
-                <div>
-                    <el-tag style="margin-left: 8%;margin-top: -1%;">应用</el-tag>
-                </div>
-                <div style="display: flex;align-items: center;margin-left: 8%;margin-top: 3%;line-height: 1.5;width: 85%;">
-                    <img style="width: 4%;height: 4%;border-radius: 12px;" src="@/assets/images/touxiang.png" alt="">
-                    <div style="margin-left: 3%;font-size: 12px;">
-                        创建人:{{ item.createBy }} 更新时间:{{item.createTime}}
-                    </div>
-                    <el-icon style="color: #79bbff;margin-left: auto;cursor: pointer;" @click.stop="editModel(item)" v-if="hoverIndex === index"><Edit /></el-icon>
-                    <el-icon style="color: red;margin-left: 2%;cursor: pointer;" @click.stop="delModel(item)" v-if="hoverIndex === index"><Delete /></el-icon>
-                </div>
-            </div>
-        </div>
-    </div>
-    <el-pagination
-    v-if="total>12"
-    small
-    background
-    style="margin-top: 0.8%;float: right;margin-right: 1%;"
-    layout="prev, pager, next"
-    :total="total"
-    v-model="pageNum"
-    @change="changePage"
-    class="mt-4"
-  />
-    <el-dialog
-        :title="title"
-        v-model="isContentVisible"
-        width="30%"
     >
+      <el-option label="全部" :value="1"/>
+      <el-option label="已发布" :value="2"/>
+    </el-select>
+  </div>
+  <div
+      style="height: 70vh;width: 98%;margin-left:1%;background-color: transparent;margin-top: 1%;display: flex;flex-wrap: wrap;justify-content: flex-start;gap: 0.65%">
+    <div v-for="(item,index) in modelList">
+      <div class="coz-card" @mouseenter="setHoverIndex(index)" @click="goFlow(item)" style="cursor: pointer;"
+           @mouseleave="resetHoverIndex">
+        <div style="display: flex;width: 100%;">
+          <div style="margin-left: 8%;margin-top: 5%;width: 60%;">
+            {{ item.appTitle }}
+          </div>
+          <img style="width: 90px;height: 90px;margin-left: 0%;margin-top: 8%;border-radius: 12px;" :src="item.appIcon"
+               alt="">
+        </div>
         <div>
-            <el-form ref="formAddref" :model="formAdd" label-width="100px" class="coz-mg-card" :rules="rulesAdd">
-                <el-form-item label="模型名称:" prop="appTitle">
-                    <el-input v-model="formAdd.appTitle" placeholder="请输入应用名称"></el-input>
-                </el-form-item>
-                <el-form-item label="模型描述:" prop="appNote">
-                    <el-input type="textarea" v-model="formAdd.appNote" placeholder="请输入应用描述"></el-input>
-                </el-form-item>
-                <el-form-item label="图标:">
-                    <div style="display: flex;width: 100%;">
-                        <img v-if="!previewUrl" style="width: 80px;height:80px;margin-left: 0%;margin-top: 0%;border-radius: 12px;" src="@/assets/images/defaultModel.png" alt="">
-                        <img v-if="previewUrl" style="width: 80px;height:80px;margin-left: 0%;margin-top: 0%;border-radius: 12px;" :src="previewUrl" alt="">
-                    </div>
-                    <el-upload
-                            ref="uploadRef"
-                            style="margin-top: 3%;"
-                            :limit="1"
-                            :show-file-list="false"
-                            :file-list="fileList"
-                            :headers="upload.headers"
-                            :on-progress="handlepro"
-                            :on-success="handleFileSuccess"
-                            :on-change="handleChange"
-                            :action="upload.url + '?file=' + upload.updateSupport"
-                            :auto-upload="false"
-                        >
-                        <el-button @click="clearDefault" plain type="primary" size="mini" style="margin-left:auto;width: 80px;" :icon="Upload">上传</el-button>
-                    </el-upload>
-                </el-form-item>
-            </el-form>
+          <el-tag style="margin-left: 8%;margin-top: -1%;">应用</el-tag>
         </div>
-        <template #footer>
-            <el-button @click="isContentVisible = false">取消</el-button>
-            <el-button v-if="isAdd" type="primary" @click="submitAdd">确定</el-button>
-            <el-button v-if="!isAdd" type="primary" @click="subEdit">确定</el-button>
-        </template>
-    </el-dialog>
+        <div style="display: flex;align-items: center;margin-left: 8%;margin-top: 3%;line-height: 1.5;width: 85%;">
+          <img style="width: 4%;height: 4%;border-radius: 12px;" src="@/assets/images/touxiang.png" alt="">
+          <div style="margin-left: 3%;font-size: 12px;">
+            创建人:{{ item.createBy }} 更新时间:{{ item.createTime }}
+          </div>
+          <el-icon style="color: #79bbff;margin-left: auto;cursor: pointer;" @click.stop="handleBizDataShowConfig(item)"
+                   v-if="hoverIndex === index">
+            <TrendCharts/>
+          </el-icon>
+          <el-icon style="color: #79bbff;margin-left: auto;cursor: pointer;" @click.stop="editModel(item)"
+                   v-if="hoverIndex === index">
+            <Edit/>
+          </el-icon>
+          <el-icon style="color: red;margin-left: 2%;cursor: pointer;" @click.stop="delModel(item)"
+                   v-if="hoverIndex === index">
+            <Delete/>
+          </el-icon>
+        </div>
+      </div>
+    </div>
+  </div>
+  <el-pagination
+      v-if="total>12"
+      small
+      background
+      style="margin-top: 0.8%;float: right;margin-right: 1%;"
+      layout="prev, pager, next"
+      :total="total"
+      v-model="pageNum"
+      @change="changePage"
+      class="mt-4"
+  />
+  <el-dialog
+      :title="title"
+      v-model="isContentVisible"
+      width="30%"
+  >
+    <div>
+      <el-form ref="formAddref" :model="formAdd" label-width="100px" class="coz-mg-card" :rules="rulesAdd">
+        <el-form-item label="模型名称:" prop="appTitle">
+          <el-input v-model="formAdd.appTitle" placeholder="请输入应用名称"></el-input>
+        </el-form-item>
+        <el-form-item label="模型描述:" prop="appNote">
+          <el-input type="textarea" v-model="formAdd.appNote" placeholder="请输入应用描述"></el-input>
+        </el-form-item>
+        <el-form-item label="图标:">
+          <div style="display: flex;width: 100%;">
+            <img v-if="!previewUrl" style="width: 80px;height:80px;margin-left: 0%;margin-top: 0%;border-radius: 12px;"
+                 src="@/assets/images/defaultModel.png" alt="">
+            <img v-if="previewUrl" style="width: 80px;height:80px;margin-left: 0%;margin-top: 0%;border-radius: 12px;"
+                 :src="previewUrl" alt="">
+          </div>
+          <el-upload
+              ref="uploadRef"
+              style="margin-top: 3%;"
+              :limit="1"
+              :show-file-list="false"
+              :file-list="fileList"
+              :headers="upload.headers"
+              :on-progress="handlepro"
+              :on-success="handleFileSuccess"
+              :on-change="handleChange"
+              :action="upload.url + '?file=' + upload.updateSupport"
+              :auto-upload="false"
+          >
+            <el-button @click="clearDefault" plain type="primary" size="mini" style="margin-left:auto;width: 80px;"
+                       :icon="Upload">上传
+            </el-button>
+          </el-upload>
+        </el-form-item>
+      </el-form>
+    </div>
+    <template #footer>
+      <el-button @click="isContentVisible = false">取消</el-button>
+      <el-button v-if="isAdd" type="primary" @click="submitAdd">确定</el-button>
+      <el-button v-if="!isAdd" type="primary" @click="subEdit">确定</el-button>
+    </template>
+  </el-dialog>
 </template>
 
 <script setup>
 
-import { onMounted, ref } from 'vue'
-import { Search,RefreshRight,Plus,Download,Upload,Delete,Setting,Minus} from '@element-plus/icons-vue'
-import { getModellist,addModeling,delModeling,editModeling,getModelingDe } from '@/api/standardization/modeling'
-import { getToken } from '@/utils/auth'
+import {onMounted, ref} from 'vue'
+import {Delete, Plus, Upload} from '@element-plus/icons-vue'
+import {addModeling, delModeling, editModeling, getModelingDe, getModellist} from '@/api/standardization/modeling'
+import {getToken} from '@/utils/auth'
 import imagePath from '@/assets/images/defaultModel.png';
-import { useStore } from 'vuex';
-import { computed } from 'vue';
-const { proxy } = getCurrentInstance();
+import {useStore} from 'vuex';
+import router from "@/router/index.js";
+
+const {proxy} = getCurrentInstance();
 const title = ref()
 const heightAll = window.innerHeight
 const isContentVisible = ref(false)
@@ -116,17 +136,16 @@ const modelList = ref([])
 const total = ref(0)
 const hoverIndex = ref(-1)
 const uploadRef = ref()
-const fileList = ref([
-]);
+const fileList = ref([]);
 const formAdd = ref({
-    appTitle: '',
-    appNote: '',
+  appTitle: '',
+  appNote: '',
 });
 const formAddref = ref()
 
 const rulesAdd = reactive({
-  appTitle: [{ required: true, message: '必填', trigger: 'blur' }], 
-  appNote: [{ required: true, message: '必填', trigger: 'blur' }], 
+  appTitle: [{required: true, message: '必填', trigger: 'blur'}],
+  appNote: [{required: true, message: '必填', trigger: 'blur'}],
 });
 const parModel = ref({})
 const parFile = ref({})
@@ -140,7 +159,7 @@ const upload = reactive({
   // 是否更新已经存在的用户数据
   updateSupport: '',
   // 设置上传的请求头部
-  headers: { Authorization: "Bearer " + getToken() },
+  headers: {Authorization: "Bearer " + getToken()},
   // 上传的地址
   url: import.meta.env.VITE_APP_BASE_API + "/common/upload"
 });
@@ -154,179 +173,192 @@ const setHoverIndex = (index) => {
 const resetHoverIndex = () => {
   hoverIndex.value = -1;
 };
-function goFlow(item){
-    store.commit('setId', item.appId);
-    proxy.$router.push({ path: '/standardization/modeling' });
-}
-function editModel(item){
-    parModel.value = item
-    isAdd.value = false
-    isContentVisible.value = true
-    title.value = '修改模型应用'
-    getModelingDe(item.appId).then(res => {
-        if(res.code === 200){
-            formAdd.value.appTitle = res.data.appTitle
-            formAdd.value.appNote = res.data.appNote
-            formAdd.value.appId = res.data.appId
-            previewUrl.value = res.data.appIcon ? res.data.appIcon : imagePath;
-        } else {
-            proxy.$message.error('获取模型详情失败');
-        }
-    }).catch(() => {
-        proxy.$message.error('获取模型详情失败');
-    });
+
+function goFlow(item) {
+  store.commit('setId', item.appId);
+  proxy.$router.push({path: '/standardization/modeling'});
 }
-function subEdit(){
-    if(!parFile.value){
-        formAddref.value.validate(async (valid) => {
-        if(valid){
-                formAdd.value.appIcon = '/profile/upload/2025/08/20/defaultModel_20250820173415A032.png'
-                    editModeling(formAdd.value).then(res => {
-                    if(res.code === 200){
-                        proxy.$message({
-                            message: '修改成功',
-                            type: 'success'
-                        });
-                        isContentVisible.value = false;
-                        getList();
-                    }
-                })
-            }
-        })
-    }
-    else{
-        formAddref.value.validate(async (valid) => {
-            if(valid){
-                nextTick(() => {
-                    uploadRef.value.submit();
-                });
-            }
-        })
+
+function editModel(item) {
+  parModel.value = item
+  isAdd.value = false
+  isContentVisible.value = true
+  title.value = '修改模型应用'
+  getModelingDe(item.appId).then(res => {
+    if (res.code === 200) {
+      formAdd.value.appTitle = res.data.appTitle
+      formAdd.value.appNote = res.data.appNote
+      formAdd.value.appId = res.data.appId
+      previewUrl.value = res.data.appIcon ? res.data.appIcon : imagePath;
+    } else {
+      proxy.$message.error('获取模型详情失败');
     }
+  }).catch(() => {
+    proxy.$message.error('获取模型详情失败');
+  });
 }
-function delModel(item){
-    proxy.$confirm('是否删除该模型应用?', '提示', {
-        confirmButtonText: '确定',
-        cancelButtonText: '取消',
-        type: 'warning'
-      }).then(() => {
-        delModeling(item.appId).then(res => {
-            if(res.code === 200){
-                proxy.$message({
-                    message: '删除成功',
-                    type: 'success'
-                });
-                getList();
-            } else {
-                proxy.$message.error('删除失败');
-            }
-        })
-      }).catch(() => {
-        
-      });
+
+function handleBizDataShowConfig(data) {
+router.push("/standardization/modeling/bizDataShowConfig/" + data.appId)
 }
-function submitAdd(){
-    if(!parFile.value){
-        formAddref.value.validate(async (valid) => {
-        if(valid){
-                formAdd.value.appIcon = '/profile/upload/2025/08/20/defaultModel_20250820173415A032.png'
-                    addModeling(formAdd.value).then(res => {
-                    if(res.code === 200){
-                        proxy.$message({
-                            message: '新增成功',
-                            type: 'success'
-                        });
-                        isContentVisible.value = false;
-                        getList();
-                    } else {
-                        proxy.$message.error('新增失败');
-                    }
-                })
-            }
+
+function subEdit() {
+  if (!parFile.value) {
+    formAddref.value.validate(async (valid) => {
+      if (valid) {
+        formAdd.value.appIcon = '/profile/upload/2025/08/20/defaultModel_20250820173415A032.png'
+        editModeling(formAdd.value).then(res => {
+          if (res.code === 200) {
+            proxy.$message({
+              message: '修改成功',
+              type: 'success'
+            });
+            isContentVisible.value = false;
+            getList();
+          }
         })
-        
-    }
-    else{
-        formAddref.value.validate(async (valid) => {
-            if(valid){
-                nextTick(() => {
-                    uploadRef.value.submit();
-                });
-            }
+      }
+    })
+  } else {
+    formAddref.value.validate(async (valid) => {
+      if (valid) {
+        nextTick(() => {
+          uploadRef.value.submit();
+        });
+      }
+    })
+  }
+}
+
+function delModel(item) {
+  proxy.$confirm('是否删除该模型应用?', '提示', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    type: 'warning'
+  }).then(() => {
+    delModeling(item.appId).then(res => {
+      if (res.code === 200) {
+        proxy.$message({
+          message: '删除成功',
+          type: 'success'
+        });
+        getList();
+      } else {
+        proxy.$message.error('删除失败');
+      }
+    })
+  }).catch(() => {
+
+  });
+}
+
+function submitAdd() {
+  if (!parFile.value) {
+    formAddref.value.validate(async (valid) => {
+      if (valid) {
+        formAdd.value.appIcon = '/profile/upload/2025/08/20/defaultModel_20250820173415A032.png'
+        addModeling(formAdd.value).then(res => {
+          if (res.code === 200) {
+            proxy.$message({
+              message: '新增成功',
+              type: 'success'
+            });
+            isContentVisible.value = false;
+            getList();
+          } else {
+            proxy.$message.error('新增失败');
+          }
         })
-    }
-    
+      }
+    })
+
+  } else {
+    formAddref.value.validate(async (valid) => {
+      if (valid) {
+        nextTick(() => {
+          uploadRef.value.submit();
+        });
+      }
+    })
+  }
+
 }
-function handleFileSuccess(response, file, fileList){
-    console.log("上传成功的响应:", response);
-    if(response.code===200){
-        if(isAdd.value===true){
-            formAdd.value.appIcon = response.fileName
-            addModeling(formAdd.value).then(res => {
-                if(res.code === 200){
-                    proxy.$message({
-                        message: '新增成功',
-                        type: 'success'
-                    });
-                    isContentVisible.value = false;
-                    getList();
-                } else {
-                    proxy.$message.error('新增失败');
-                }
-            }).catch(() => {
-                proxy.$message.error('新增失败');
-            }); 
+
+function handleFileSuccess(response, file, fileList) {
+  console.log("上传成功的响应:", response);
+  if (response.code === 200) {
+    if (isAdd.value === true) {
+      formAdd.value.appIcon = response.fileName
+      addModeling(formAdd.value).then(res => {
+        if (res.code === 200) {
+          proxy.$message({
+            message: '新增成功',
+            type: 'success'
+          });
+          isContentVisible.value = false;
+          getList();
+        } else {
+          proxy.$message.error('新增失败');
         }
-        else{
-            formAdd.value.appIcon = response.fileName
-            editModeling(formAdd.value).then(res => {
-                if(res.code === 200){
-                    proxy.$message({
-                        message: '修改成功',
-                        type: 'success'
-                    });
-                    isContentVisible.value = false;
-                    getList();
-                } else {
-                    proxy.$message.error('修改失败');
-                }
-            }).catch(() => {
-                proxy.$message.error('修改失败');
-            });
+      }).catch(() => {
+        proxy.$message.error('新增失败');
+      });
+    } else {
+      formAdd.value.appIcon = response.fileName
+      editModeling(formAdd.value).then(res => {
+        if (res.code === 200) {
+          proxy.$message({
+            message: '修改成功',
+            type: 'success'
+          });
+          isContentVisible.value = false;
+          getList();
+        } else {
+          proxy.$message.error('修改失败');
         }
+      }).catch(() => {
+        proxy.$message.error('修改失败');
+      });
     }
+  }
 };
-function clearDefault(){
-    uploadRef.value.clearFiles(); 
+
+function clearDefault() {
+  uploadRef.value.clearFiles();
 }
-function handleChange(file, fileList){
-    console.log(file, fileList)
-    parFile.value = file
-    const rawFile = file.raw;
-    previewUrl.value = URL.createObjectURL(rawFile);
+
+function handleChange(file, fileList) {
+  console.log(file, fileList)
+  parFile.value = file
+  const rawFile = file.raw;
+  previewUrl.value = URL.createObjectURL(rawFile);
 }
-function showAdd(){
-    isAdd.value = true
-    isContentVisible.value = true
-    title.value = '新增模型应用'
+
+function showAdd() {
+  isAdd.value = true
+  isContentVisible.value = true
+  title.value = '新增模型应用'
 }
+
 function changePage(val) {
-    console.log(val)
-    pageNum.value = val,
-    getList()
+  console.log(val)
+  pageNum.value = val,
+      getList()
 }
+
 function getList() {
-    var par = {
-        pageSize: 12, 
-        pageNum: pageNum.value,
-    }
-    getModellist(par).then(response => {
-        modelList.value = response.rows
-        total.value = response.total
-    })
+  var par = {
+    pageSize: 12,
+    pageNum: pageNum.value,
+  }
+  getModellist(par).then(response => {
+    modelList.value = response.rows
+    total.value = response.total
+  })
 }
+
 onMounted(() => {
-    getList()
+  getList()
 })
 </script>
 
@@ -335,6 +367,7 @@ onMounted(() => {
   height: 100px !important;
   width: 100px !important;
 }
+
 .coz-card {
   margin-top: 5%;
   height: 20vh;
@@ -346,13 +379,16 @@ onMounted(() => {
   border-color: #e9e9eb;
   transition: all 150ms ease-out;
 }
+
 .coz-card:hover {
   box-shadow: 0 10px 12px 0 rgba(28, 31, 35, 0.06);
 }
+
 /* 自定义类名需保留 */
 .coz-stroke-primary {
   stroke: var(--primary-color); /* 需替换为实际颜色值 */
 }
+
 .coz-mg-card {
   margin: 10px; /* 根据实际需求调整 */
 }

+ 482 - 0
ruoyi-ui/src/views/standardization/resultsPresentation/index.vue

@@ -0,0 +1,482 @@
+<template>
+  <div class="knowledge-library">
+    <div class="knowledge-library-container">
+      <div :style="{ display: computedDisplay }"
+           class="knowledge-empty">
+        <div class="knowledge-empty-header">
+          <div class="knowledge-empty-header-left">
+            <div class="knowledge-empty-header-title">1分钟创建属于你的专属知识库📚</div>
+            <div class="knowledge-empty-header-desc">新建你的专属领域知识,在应用中引用知识与你进行对话</div>
+          </div>
+        </div>
+        <div class="knowledge-empty-body">
+          <div class="knowledge-empty-body-showStep">
+            <div class="knowledge-empty-body-top">
+              <svg-icon class="file-svg" icon-class="file"/>
+              <div>
+                <div class="knowledge-empty-body-title">知识上传</div>
+                <div class="knowledge-empty-body-subTitle">支持Docx、PDF、Xlsx、URL类型知识上传</div>
+              </div>
+            </div>
+            <div class="knowledge-empty-body-bottom">
+              <div class="knowledge-empty-body-step">1</div>
+              <div class="knowledge-empty-body-line">
+                <svg aria-hidden="true" class="svg_icon svg-icon knowledge-empty-body-icon">
+                  <use xlink:href="#icon-wda-nextstep"></use>
+                </svg>
+              </div>
+            </div>
+          </div>
+          <div class="knowledge-empty-body-showStep">
+            <div class="knowledge-empty-body-top">
+              <svg-icon class="file-svg" icon-class="file"/>
+              <div>
+                <div class="knowledge-empty-body-title">知识配置</div>
+                <div class="knowledge-empty-body-subTitle">选择知识类型,精准解析知识,可供引用</div>
+              </div>
+            </div>
+            <div class="knowledge-empty-body-bottom">
+              <div class="knowledge-empty-body-step">2</div>
+              <div class="knowledge-empty-body-line">
+                <svg aria-hidden="true" class="svg_icon svg-icon knowledge-empty-body-icon">
+                  <use xlink:href="#icon-wda-nextstep"></use>
+                </svg>
+              </div>
+            </div>
+          </div>
+          <div class="knowledge-empty-body-showStep">
+            <div class="knowledge-empty-body-top">
+              <svg-icon class="file-svg" icon-class="file"/>
+              <div>
+                <div class="knowledge-empty-body-title">完成创建</div>
+                <div class="knowledge-empty-body-subTitle">在应用中引用已创建的知识库与你进行对话</div>
+              </div>
+            </div>
+            <div class="knowledge-empty-body-bottom">
+              <div class="knowledge-empty-body-step">3</div>
+              <div class="knowledge-empty-body-line last"><i class="iconfont  knowledge-empty-body-icon"></i></div>
+            </div>
+          </div>
+        </div>
+      </div>
+      <div :style="{ display: !computedDisplay }" class="knowledge_library_list-content noHelp">
+        <el-row :gutter="12">
+          <el-col v-for="(item, index) in knowledgeList" :key="item.appId" :span="4">
+            <div class="knowledge_library_item knowledge_library_item_background_blue">
+              <div class="knowledge_library_item_action" style="display: none;">
+                <el-dropdown>
+                    <span class="el-dropdown-link el-dropdown-selfdefine">
+                       <More style="width: 1em; height: 1em;color: #fff;"/>
+                    </span>
+                  <el-dropdown-menu slot="dropdown" class="knowledge_library_item_drop">
+                    <el-dropdown-item command="edit">编辑</el-dropdown-item>
+                    <el-dropdown-item command="delete" style="color: #F56C6C;">删除</el-dropdown-item>
+                  </el-dropdown-menu>
+                </el-dropdown>
+              </div>
+              <div style="width: 100%;height: 100%;" @click="jumpPage(`/standardization/map/${item.appId}`)">
+                <div class="knowledge_library_item_icon">
+                  <img :src="item.appIcon" class="icon_item_img" alt=""/>
+                </div>
+                <div class="knowledge_library_item_name">{{ item.appTitle }}</div>
+                <div class="knowledge_library_item_desc">{{ item.appNote }}</div>
+                <div class="knowledge_library_item_jump">
+                  <div class="jump_statistic">
+                    <Timer style="width: 1em; height: 1em;"/>&nbsp;
+                    {{ item.createTime }}&nbsp;
+                  </div>
+                </div>
+              </div>
+            </div>
+          </el-col>
+        </el-row>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import {getModellist} from "@/api/standardization/modeling.js";
+import {jumpPage} from "@/utils/page.js";
+
+const knowledgeList = ref([])
+const computedDisplay = ref('none')
+const backgrounds = [
+  {name: 'blue', value: 'linear-gradient(28.78deg, rgb(79, 110, 230) -14.87%, rgb(109, 106, 230) 109.41%)'},
+  {name: 'red', value: 'linear-gradient(28.78deg, rgb(252, 34, 107) -14.87%, rgb(245, 48, 137) 109.41%)'},
+  {name: 'orange', value: 'linear-gradient(28.78deg, rgb(255, 119, 89) -14.87%, rgb(250, 144, 73) 109.41%)'},
+  {name: 'purple', value: 'linear-gradient(28.78deg, rgb(119, 93, 230) -14.87%, rgb(149, 75, 254) 109.41%)'},
+  {name: 'sky', value: 'linear-gradient(28.78deg, rgb(12, 177, 210) -14.87%, rgb(0, 188, 225) 109.41%)'},
+]
+const icons = ref([
+  'ai', 'algorithm', 'component', 'files', 'flood', 'international', 'monitor'
+])
+
+onMounted(() => {
+  getList()
+})
+
+function getList() {
+  getModellist().then(res => {
+    knowledgeList.value = res.rows
+    if (!knowledgeList.value || knowledgeList.value.length === 0) {
+      computedDisplay.value = 'block'
+    } else {
+      computedDisplay.value = 'none'
+    }
+  })
+}
+</script>
+<style lang="scss">
+.knowledge-dialog .el-dialog__body {
+  padding: 0 20px;
+}
+
+</style>
+<style lang="scss" scoped>
+.knowledge-library {
+  width: 100%;
+  height: 100%;
+
+  .knowledge-library-container {
+    width: 100%;
+    height: 100%;
+    padding: 40px 40px 0 40px;
+
+
+    .knowledge-empty {
+      width: 100%;
+      min-height: 336px;
+      border-radius: 8px;
+      border: 1px solid hsla(0, 0%, 87.8%, .6);
+      background: #f5f5f5;
+      padding: 24px;
+      text-align: left;
+      box-sizing: border-box;
+
+      .knowledge-empty-header {
+        display: flex;
+        justify-content: space-between;
+
+        .knowledge-empty-header-title {
+          color: #131212;
+          font-size: 24px;
+          line-height: 40px;
+          margin-bottom: 8px;
+        }
+
+        .knowledge-empty-header-desc {
+          color: #616161;
+          font-size: 14px;
+          line-height: 24px;
+        }
+
+      }
+
+      .knowledge-empty-body {
+        min-height: 188px;
+        margin-top: 30px;
+        display: grid;
+        grid-template-columns: 1fr 1fr 1fr;
+        border-radius: 8px;
+        background: #fff;
+        padding: 50px 0 36px 5%;
+
+        .knowledge-empty-body-showStep {
+
+
+          display: -moz-box;
+          display: flex;
+          flex-direction: column;
+          justify-content: space-between;
+          margin-right: 20px;
+
+          .knowledge-empty-body-top {
+            display: flex;
+            align-items: center;
+
+            .file-svg {
+              font-size: 2rem;
+              margin-right: 20px;
+            }
+
+            .knowledge-empty-body-title {
+              color: #000;
+              font-size: 1rem;
+              font-weight: 500;
+              line-height: 20px;
+            }
+
+            .knowledge-empty-body-subTitle {
+              margin-top: 8px;
+              color: #616161;
+              font-size: 12px;
+              font-weight: 400;
+              line-height: 16px;
+              max-width: 9.16667vw;
+            }
+
+          }
+
+          .knowledge-empty-body-bottom {
+            display: flex;
+            align-items: center;
+
+            .knowledge-empty-body-step {
+              width: 26px;
+              height: 26px;
+              line-height: 26px;
+              text-align: center;
+              font-size: 14px;
+              background-color: #e8e9eb;
+              border-radius: 50%;
+              margin-right: 22px;
+              margin-left: 3px;
+            }
+
+            .knowledge-empty-body-line {
+              background: linear-gradient(315deg, #d9d9d9, hsla(0, 0%, 85.1%, 0));
+              flex: 1;
+              height: 2px;
+              position: relative;
+            }
+          }
+
+        }
+
+
+      }
+
+    }
+
+    .knowledge_library_item {
+      height: 260px;
+      border-radius: 12px;
+      padding: 16px;
+      position: relative;
+      transition: all .2s ease-out;
+      text-align: left;
+      cursor: pointer;
+      margin-bottom: 12px;
+
+      &:hover {
+
+        .knowledge_library_item_action {
+          color: #fff;
+          opacity: 1;
+          background: rgba(0, 0, 0, .2);
+          display: flex;
+          align-items: center;
+          justify-content: center;
+        }
+
+
+        .knowledge_library_item_name, .knowledge_library_item_desc, .knowledge_library_item_jump {
+          color: #fff;
+          fill: #fff;
+        }
+      }
+
+      .knowledge_library_item_action {
+        position: absolute;
+        top: 16px;
+        right: 16px;
+        width: 24px;
+        height: 24px;
+        line-height: 24px;
+        text-align: center;
+        border-radius: 4px;
+        transform: rotate(90deg);
+        color: #fff;
+        opacity: 0;
+        z-index: 2;
+
+        .el-icon-more {
+          color: #fff;
+        }
+
+      }
+
+      .knowledge_library_item_icon {
+        width: 45px;
+        height: 45px;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        margin-bottom: 24px;
+        border-radius: 6px;
+        transition: all .2s ease-out;
+        flex: none;
+        color: #fff;
+
+        .icon_item_img {
+          width: 100%;
+          height: 100%;
+        }
+
+      }
+
+      .knowledge_library_item_name {
+        font-weight: 500;
+        font-size: 20px;
+        line-height: 28px;
+        color: #000;
+        margin-bottom: 10px;
+        transition: all .2s ease-out;
+      }
+
+      .knowledge_library_item_desc {
+        font-size: 1rem;
+        line-height: 20px;
+        color: #131212;
+        opacity: .8;
+        transition: all .2s ease-out;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        display: -webkit-box;
+      }
+
+      .knowledge_library_item_jump {
+        position: absolute;
+        left: 16px;
+        bottom: 16px;
+        display: flex;
+        gap: 16px;
+        color: #5e5e66;
+        font-size: 12px;
+        fill: #5e5e66;
+
+        .jump_statistic {
+          display: flex;
+          align-items: center;
+
+        }
+
+      }
+
+    }
+
+    .knowledge_library_item_background_blue {
+      background-color: #e3edfd;
+      background-image: radial-gradient(circle at 25% 0, #c2d8ff, rgba(227, 237, 253, 0) 70.75%), radial-gradient(circle at 75% 95%, #c2d8ff, rgba(227, 237, 253, 0) 50.75%);
+
+      &:hover {
+        background-color: #7eadff;
+      }
+    }
+
+    .knowledge_library_item_background_red {
+      background-color: #ffe9f0;
+      background-image: radial-gradient(circle at 25% 0, #ffbdd1, rgba(255, 233, 240, 0) 70.75%), radial-gradient(circle at 75% 95%, #ffbdd1, rgba(255, 233, 240, 0) 50.75%);
+
+      &:hover {
+        background-color: #ff96b6;
+      }
+    }
+
+    .knowledge_library_item_background_orange {
+      background-color: #ffefeb;
+      background-image: radial-gradient(circle at 25% 0, #ffd9d1, rgba(255, 239, 235, 0) 70.75%), radial-gradient(circle at 75% 95%, #ffd9d1, rgba(255, 239, 235, 0) 50.75%);
+
+      &:hover {
+        background-color: #ff9983;
+      }
+    }
+
+    .knowledge_library_item_background_purple {
+      background-color: #f3f0ff;
+      background-image: radial-gradient(circle at 25% 0, #ccc7ff, rgba(243, 240, 255, 0) 70.75%), radial-gradient(circle at 75% 95%, #ccc7ff, rgba(243, 240, 255, 0) 50.75%);
+
+      &:hover {
+        background-color: #9897ff;
+      }
+
+    }
+
+    .knowledge_library_item_background_sky {
+      background-color: #ebf8ff;
+      background-image: radial-gradient(circle at 25% 0, #c4efff, rgba(235, 248, 255, 0) 70.75%), radial-gradient(circle at 75% 95%, #c4efff, rgba(235, 248, 255, 0) 50.75%);
+
+      &:hover {
+        background-color: #74e1f7;
+      }
+
+    }
+
+  }
+
+}
+
+.choose-icon {
+  display: flex;
+  gap: 14px;
+
+  .choose-icon_left {
+    width: 64px;
+    height: 64px;
+    border-radius: 6px;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    color: #fff;
+  }
+
+  .choose-icon_right {
+    box-sizing: border-box;
+    background: #f5f5f5;
+    border-radius: 6px;
+    height: 192px;
+    width: 100%;
+    display: flex;
+    flex-direction: column;
+    padding: 20px;
+
+    .icon_item_svg {
+      color: #fff;
+      fill: #fff;
+    }
+
+    .choose-icon_right_background {
+      margin-bottom: 6px;
+    }
+
+
+    .choose-icon_title {
+      font-weight: 600;
+      font-size: 12px;
+      height: 18px;
+      line-height: 18px;
+      color: #000;
+    }
+
+    .choose-icon_background, .choose-icon_icon {
+      display: flex;
+      gap: 24px;
+      padding-top: 12px;
+
+      .choose-icon_background_item, .choose-icon_icon_item {
+        width: 32px;
+        height: 32px;
+        border-radius: 6px;
+        cursor: pointer;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+
+        .choose-icon_background_choose, .choose-icon_icon_choose {
+          width: 20px;
+          height: 20px;
+          border-radius: 50%;
+          display: flex;
+          justify-content: center;
+          align-items: center;
+          color: #fff;
+        }
+
+      }
+
+    }
+
+  }
+}
+</style>