77681 8 часов назад
Родитель
Сommit
725e6ce1f0
44 измененных файлов с 1341 добавлено и 821 удалено
  1. 0 6
      gw-admin/pom.xml
  2. 2 2
      gw-admin/src/main/resources/application-druid.yml
  3. 2 2
      gw-admin/src/main/resources/application-test.yml
  4. 2 2
      gw-admin/src/main/resources/application.yml
  5. 1 1
      gw-framework/src/main/java/com/goldenwater/framework/aspectj/DataScopeAspect.java
  6. 2 2
      gw-quartz/src/main/resources/mapper/quartz/SysJobLogMapper.xml
  7. 2 2
      gw-system/src/main/resources/mapper/system/SysConfigMapper.xml
  8. 2 2
      gw-system/src/main/resources/mapper/system/SysDeptMapper.xml
  9. 2 2
      gw-system/src/main/resources/mapper/system/SysDictTypeMapper.xml
  10. 4 4
      gw-system/src/main/resources/mapper/system/SysMenuMapper.xml
  11. 2 2
      gw-system/src/main/resources/mapper/system/SysRoleMapper.xml
  12. 3 3
      gw-system/src/main/resources/mapper/system/SysUserMapper.xml
  13. 2 5
      gw-ui/.env.development
  14. 2 5
      gw-ui/.env.production
  15. 2 5
      gw-ui/.env.staging
  16. 2 0
      gw-ui/package.json
  17. 0 262
      gw-ui/src/composables/useArcGisMap.js
  18. 0 7
      gw-ui/src/utils/gis-config.js
  19. 98 5
      gw-ui/src/views/slgc/cms/article/detail.vue
  20. 206 25
      gw-ui/src/views/slgc/cms/article/index.vue
  21. 48 7
      gw-ui/src/views/slgc/cms/category/index.vue
  22. 40 9
      gw-ui/src/views/slgc/dike/index.vue
  23. 36 18
      gw-ui/src/views/slgc/doc/index.vue
  24. 43 72
      gw-ui/src/views/slgc/gis/index.vue
  25. 19 23
      gw-ui/src/views/slgc/gis/monitor.vue
  26. 17 22
      gw-ui/src/views/slgc/gis/water.vue
  27. 21 21
      gw-ui/src/views/slgc/gis/wiu.vue
  28. 29 10
      gw-ui/src/views/slgc/law/index.vue
  29. 13 21
      gw-ui/src/views/slgc/monitor/index.vue
  30. 46 18
      gw-ui/src/views/slgc/pust/index.vue
  31. 35 10
      gw-ui/src/views/slgc/reservoir/index.vue
  32. 4 35
      gw-ui/src/views/slgc/unit/edit.vue
  33. 26 10
      gw-ui/src/views/slgc/unit/file.vue
  34. 6 10
      gw-ui/src/views/slgc/unit/index.vue
  35. 12 25
      gw-ui/src/views/slgc/unit/national.vue
  36. 29 3
      gw-ui/src/views/slgc/unit/record/index.vue
  37. 55 22
      gw-ui/src/views/slgc/waga/index.vue
  38. 84 25
      gw-ui/src/views/slgc/wco/dike/index.vue
  39. 72 27
      gw-ui/src/views/slgc/wco/pust/index.vue
  40. 82 23
      gw-ui/src/views/slgc/wco/reservoir/index.vue
  41. 75 25
      gw-ui/src/views/slgc/wco/waga/index.vue
  42. 4 4
      gw-ui/src/views/slgc/zggc/detail/index.vue
  43. 75 30
      gw-ui/src/views/slgc/zggc/portal.vue
  44. 134 7
      gw-ui/src/views/slgc/zggc/video/index.vue

+ 0 - 6
gw-admin/pom.xml

@@ -30,12 +30,6 @@
             <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
         </dependency>
 
-         <!-- Mysql驱动包 -->
-        <dependency>
-            <groupId>com.mysql</groupId>
-            <artifactId>mysql-connector-j</artifactId>
-        </dependency>
-
          <!-- 达梦数据库驱动 -->
         <dependency>
             <groupId>com.dameng</groupId>

+ 2 - 2
gw-admin/src/main/resources/application-druid.yml

@@ -20,9 +20,9 @@ spring:
     druid:
       # 主库数据源
       master:
-        url: jdbc:dm://192.168.0.145:30236/SLGC_RUN?allowMultiQueries=true
+        url: jdbc:dm://39.98.38.2:5236/SLGC_RUN?allowMultiQueries=true
         username: SLGC_RUN
-        password: Gw./16001
+        password: Gw./1601
       # 从库数据源
       slave:
         # 从数据源开关/默认关闭

+ 2 - 2
gw-admin/src/main/resources/application-test.yml

@@ -33,9 +33,9 @@ spring:
     druid:
       # 主库数据源
       master:
-        url: jdbc:dm://192.168.0.145:30236/SLGC_RUN?allowMultiQueries=true
+        url: jdbc:dm://192.168.0.149:5236/SLGC_RUN?allowMultiQueries=true
         username: SLGC_RUN
-        password: Gw./16001
+        password: Gw./1601
       # 从库数据源
       slave:
         # 从数据源开关/默认关闭

+ 2 - 2
gw-admin/src/main/resources/application.yml

@@ -37,7 +37,7 @@ spring:
     # 国际化资源文件路径
     basename: i18n/messages
   profiles:
-    active: druid
+    active: test
 
 # token配置
 token:
@@ -59,7 +59,7 @@ mybatis:
 
 # PageHelper分页插件
 pagehelper:
-  helperDialect: mysql
+  helperDialect: dm
   supportMethodsArguments: true
   params: count=countSql
 

+ 1 - 1
gw-framework/src/main/java/com/goldenwater/framework/aspectj/DataScopeAspect.java

@@ -111,7 +111,7 @@ public class DataScopeAspect
             }
             else if (Constants.Dept.DATA_SCOPE_DEPT_AND_CHILD.equals(dataScope))
             {
-                sqlString.append(StringUtils.format(" OR {}.{} IN ( SELECT dept_id FROM sys_dept WHERE dept_id = {} or find_in_set( {} , ancestors ) )", deptAlias, deptField, user.getDeptId(), user.getDeptId()));
+                sqlString.append(StringUtils.format(" OR {}.{} IN ( SELECT dept_id FROM sys_dept WHERE dept_id = {} or INSTR(',' || ancestors || ',', ',' || {} || ',') > 0 )", deptAlias, deptField, user.getDeptId(), user.getDeptId()));
             }
             else if (Constants.Dept.DATA_SCOPE_SELF.equals(dataScope))
             {

+ 2 - 2
gw-quartz/src/main/resources/mapper/quartz/SysJobLogMapper.xml

@@ -21,8 +21,8 @@
             <if test="jobGroup != null and jobGroup != ''">AND job_group = #{jobGroup}</if>
             <if test="status != null and status != ''">AND status = #{status}</if>
             <if test="invokeTarget != null and invokeTarget != ''">AND invoke_target like concat('%', #{invokeTarget}, '%') </if>
-            <if test="params.beginTime != null and params.beginTime != ''">and date_format(create_time,'%Y%m%d') &gt;= date_format(#{params.beginTime},'%Y%m%d')</if>
-            <if test="params.endTime != null and params.endTime != ''">and date_format(create_time,'%Y%m%d') &lt;= date_format(#{params.endTime},'%Y%m%d')</if>
+            <if test="params.beginTime != null and params.beginTime != ''">and TO_CHAR(create_time, 'YYYYMMDD') &gt;= TO_CHAR(#{params.beginTime}, 'YYYYMMDD')</if>
+            <if test="params.endTime != null and params.endTime != ''">and TO_CHAR(create_time, 'YYYYMMDD') &lt;= TO_CHAR(#{params.endTime}, 'YYYYMMDD')</if>
         </where>
         order by create_time desc
     </select>

+ 2 - 2
gw-system/src/main/resources/mapper/system/SysConfigMapper.xml

@@ -26,8 +26,8 @@
             <if test="configName != null and configName != ''">AND config_name like concat('%', #{configName}, '%')</if>
             <if test="configType != null and configType != ''">AND config_type = #{configType}</if>
             <if test="configKey != null and configKey != ''">AND config_key like concat('%', #{configKey}, '%')</if>
-            <if test="params.beginTime != null and params.beginTime != ''">and date_format(create_time,'%Y%m%d') &gt;= date_format(#{params.beginTime},'%Y%m%d')</if>
-            <if test="params.endTime != null and params.endTime != ''">and date_format(create_time,'%Y%m%d') &lt;= date_format(#{params.endTime},'%Y%m%d')</if>
+            <if test="params.beginTime != null and params.beginTime != ''">and TO_CHAR(create_time, 'YYYYMMDD') &gt;= TO_CHAR(#{params.beginTime}, 'YYYYMMDD')</if>
+            <if test="params.endTime != null and params.endTime != ''">and TO_CHAR(create_time, 'YYYYMMDD') &lt;= TO_CHAR(#{params.endTime}, 'YYYYMMDD')</if>
         </where>
     </select>
     <select id="selectConfigById" parameterType="Long" resultMap="SysConfigResult"><include refid="selectConfigVo"/> where config_id = #{configId}</select>

+ 2 - 2
gw-system/src/main/resources/mapper/system/SysDeptMapper.xml

@@ -41,8 +41,8 @@
     </select>
     <select id="checkDeptExistUser" parameterType="Long" resultType="int">select count(1) from sys_user where dept_id = #{deptId} and del_flag = '0'</select>
     <select id="hasChildByDeptId" parameterType="Long" resultType="int">select count(1) from sys_dept where del_flag = '0' and parent_id = #{deptId}</select>
-    <select id="selectChildrenDeptById" parameterType="Long" resultMap="SysDeptResult">select * from sys_dept where FIND_IN_SET(#{deptId}, ancestors)</select>
-    <select id="selectNormalChildrenDeptById" parameterType="Long" resultType="int">select count(*) from sys_dept where status = 0 and del_flag = '0' and FIND_IN_SET(#{deptId}, ancestors)</select>
+    <select id="selectChildrenDeptById" parameterType="Long" resultMap="SysDeptResult">select * from sys_dept where INSTR(',' || ancestors || ',', ',' || #{deptId} || ',') > 0</select>
+    <select id="selectNormalChildrenDeptById" parameterType="Long" resultType="int">select count(*) from sys_dept where status = 0 and del_flag = '0' and INSTR(',' || ancestors || ',', ',' || #{deptId} || ',') > 0</select>
     <select id="checkDeptNameUnique" resultMap="SysDeptResult"><include refid="selectDeptVo"/> where dept_name=#{deptName} and parent_id = #{parentId} and del_flag = '0' limit 1</select>
     <insert id="insertDept" parameterType="SysDept">
         insert into sys_dept(

+ 2 - 2
gw-system/src/main/resources/mapper/system/SysDictTypeMapper.xml

@@ -18,8 +18,8 @@
             <if test="dictName != null and dictName != ''">AND dict_name like concat('%', #{dictName}, '%')</if>
             <if test="status != null and status != ''">AND status = #{status}</if>
             <if test="dictType != null and dictType != ''">AND dict_type like concat('%', #{dictType}, '%')</if>
-            <if test="params.beginTime != null and params.beginTime != ''">and date_format(create_time,'%Y%m%d') &gt;= date_format(#{params.beginTime},'%Y%m%d')</if>
-            <if test="params.endTime != null and params.endTime != ''">and date_format(create_time,'%Y%m%d') &lt;= date_format(#{params.endTime},'%Y%m%d')</if>
+            <if test="params.beginTime != null and params.beginTime != ''">and TO_CHAR(create_time, 'YYYYMMDD') &gt;= TO_CHAR(#{params.beginTime}, 'YYYYMMDD')</if>
+            <if test="params.endTime != null and params.endTime != ''">and TO_CHAR(create_time, 'YYYYMMDD') &lt;= TO_CHAR(#{params.endTime}, 'YYYYMMDD')</if>
         </where>
     </select>
     <select id="selectDictTypeAll" resultMap="SysDictTypeResult"><include refid="selectDictTypeVo"/></select>

+ 4 - 4
gw-system/src/main/resources/mapper/system/SysMenuMapper.xml

@@ -25,7 +25,7 @@
         <result property="remark" column="remark"/>
     </resultMap>
     <sql id="selectMenuVo">
-        select menu_id, menu_name, parent_id, order_num, path, component, query, route_name, is_frame, is_cache, menu_type, visible, status, IFNULL(perms,'') as perms, icon, create_time from sys_menu
+        select menu_id, menu_name, parent_id, order_num, path, component, query, route_name, is_frame, is_cache, menu_type, visible, status, COALESCE(perms,'') as perms, icon, create_time from sys_menu
     </sql>
     <select id="selectMenuList" parameterType="SysMenu" resultMap="SysMenuResult">
         <include refid="selectMenuVo"/>
@@ -37,11 +37,11 @@
         order by parent_id, order_num
     </select>
     <select id="selectMenuTreeAll" resultMap="SysMenuResult">
-        select distinct m.menu_id, m.parent_id, m.menu_name, m.path, m.component, m.query, m.route_name, m.visible, m.status, IFNULL(m.perms,'') as perms, m.is_frame, m.is_cache, m.menu_type, m.icon, m.order_num, m.create_time
+        select distinct m.menu_id, m.parent_id, m.menu_name, m.path, m.component, m.query, m.route_name, m.visible, m.status, COALESCE(m.perms,'') as perms, m.is_frame, m.is_cache, m.menu_type, m.icon, m.order_num, m.create_time
         from sys_menu m where m.menu_type in ('M', 'C') and m.status = 0 order by m.parent_id, m.order_num
     </select>
     <select id="selectMenuListByUserId" parameterType="SysMenu" resultMap="SysMenuResult">
-        select distinct m.menu_id, m.parent_id, m.menu_name, m.path, m.component, m.query, m.route_name, m.visible, m.status, IFNULL(m.perms,'') as perms, m.is_frame, m.is_cache, m.menu_type, m.icon, m.order_num, m.create_time
+        select distinct m.menu_id, m.parent_id, m.menu_name, m.path, m.component, m.query, m.route_name, m.visible, m.status, COALESCE(m.perms,'') as perms, m.is_frame, m.is_cache, m.menu_type, m.icon, m.order_num, m.create_time
         from sys_menu m left join sys_role_menu rm on m.menu_id = rm.menu_id
         left join sys_user_role ur on rm.role_id = ur.role_id
         left join sys_role ro on ur.role_id = ro.role_id
@@ -52,7 +52,7 @@
         order by m.parent_id, m.order_num
     </select>
     <select id="selectMenuTreeByUserId" parameterType="Long" resultMap="SysMenuResult">
-        select distinct m.menu_id, m.parent_id, m.menu_name, m.path, m.component, m.query, m.route_name, m.visible, m.status, IFNULL(m.perms,'') as perms, m.is_frame, m.is_cache, m.menu_type, m.icon, m.order_num, m.create_time
+        select distinct m.menu_id, m.parent_id, m.menu_name, m.path, m.component, m.query, m.route_name, m.visible, m.status, COALESCE(m.perms,'') as perms, m.is_frame, m.is_cache, m.menu_type, m.icon, m.order_num, m.create_time
         from sys_menu m left join sys_role_menu rm on m.menu_id = rm.menu_id
         left join sys_user_role ur on rm.role_id = ur.role_id
         left join sys_role ro on ur.role_id = ro.role_id

+ 2 - 2
gw-system/src/main/resources/mapper/system/SysRoleMapper.xml

@@ -32,8 +32,8 @@
         <if test="roleName != null and roleName != ''">AND r.role_name like concat('%', #{roleName}, '%')</if>
         <if test="status != null and status != ''">AND r.status = #{status}</if>
         <if test="roleKey != null and roleKey != ''">AND r.role_key like concat('%', #{roleKey}, '%')</if>
-        <if test="params.beginTime != null and params.beginTime != ''">and date_format(r.create_time,'%Y%m%d') &gt;= date_format(#{params.beginTime},'%Y%m%d')</if>
-        <if test="params.endTime != null and params.endTime != ''">and date_format(r.create_time,'%Y%m%d') &lt;= date_format(#{params.endTime},'%Y%m%d')</if>
+        <if test="params.beginTime != null and params.beginTime != ''">and TO_CHAR(r.create_time, 'YYYYMMDD') &gt;= TO_CHAR(#{params.beginTime}, 'YYYYMMDD')</if>
+        <if test="params.endTime != null and params.endTime != ''">and TO_CHAR(r.create_time, 'YYYYMMDD') &lt;= TO_CHAR(#{params.endTime}, 'YYYYMMDD')</if>
         ${params.dataScope}
         order by r.role_sort
     </select>

+ 3 - 3
gw-system/src/main/resources/mapper/system/SysUserMapper.xml

@@ -58,9 +58,9 @@
         <if test="userName != null and userName != ''">AND u.user_name like concat('%', #{userName}, '%')</if>
         <if test="status != null and status != ''">AND u.status = #{status}</if>
         <if test="phonenumber != null and phonenumber != ''">AND u.phonenumber like concat('%', #{phonenumber}, '%')</if>
-        <if test="params.beginTime != null and params.beginTime != ''">AND date_format(u.create_time,'%Y%m%d') &gt;= date_format(#{params.beginTime},'%Y%m%d')</if>
-        <if test="params.endTime != null and params.endTime != ''">AND date_format(u.create_time,'%Y%m%d') &lt;= date_format(#{params.endTime},'%Y%m%d')</if>
-        <if test="deptId != null and deptId != 0">AND (u.dept_id = #{deptId} OR u.dept_id IN ( SELECT t.dept_id FROM sys_dept t WHERE FIND_IN_SET(#{deptId}, t.ancestors) ))</if>
+        <if test="params.beginTime != null and params.beginTime != ''">AND TO_CHAR(u.create_time, 'YYYYMMDD') &gt;= TO_CHAR(#{params.beginTime}, 'YYYYMMDD')</if>
+        <if test="params.endTime != null and params.endTime != ''">AND TO_CHAR(u.create_time, 'YYYYMMDD') &lt;= TO_CHAR(#{params.endTime}, 'YYYYMMDD')</if>
+        <if test="deptId != null and deptId != 0">AND (u.dept_id = #{deptId} OR u.dept_id IN ( SELECT t.dept_id FROM sys_dept t WHERE INSTR(',' || t.ancestors || ',', ',' || #{deptId} || ',') > 0 ))</if>
         ${params.dataScope}
     </select>
     <select id="selectAllocatedList" parameterType="SysUser" resultMap="SysUserResult">

+ 2 - 5
gw-ui/.env.development

@@ -13,8 +13,5 @@ VITE_SERVICE_BASE_TITLE = '/gw-api'
 # 后端接口基础路径
 VITE_SERVER_URL = 'http://127.0.0.1:8901/slgc-run'
 
-# GIS地图引擎: leaflet(默认) / arcgis(需内网ArcGIS服务)
-VITE_GIS_ENGINE = 'arcgis'
-
-# ArcGIS JS API 加载地址 (内网GIS服务)
-VITE_ARCGIS_URL = 'http://10.8.4.136:9090/tba-gis/static/arcgis4_7'
+# GIS地图引擎: openlayers
+VITE_GIS_ENGINE = 'openlayers'

+ 2 - 5
gw-ui/.env.production

@@ -13,11 +13,8 @@ VITE_SERVICE_BASE_TITLE = '/gw-api'
 # 后端接口基础路径
 VITE_SERVER_URL = 'http://39.98.38.2:18055/gw-api'
 
-# GIS地图引擎: leaflet(默认) / arcgis(需内网ArcGIS服务)
-VITE_GIS_ENGINE = 'arcgis'
-
-# ArcGIS JS API 加载地址 (内网GIS服务)
-VITE_ARCGIS_URL = 'http://10.8.4.136:9090/tba-gis/static/arcgis4_7'
+# GIS地图引擎: openlayers
+VITE_GIS_ENGINE = 'openlayers'
 
 # 是否在打包时开启压缩,支持 gzip 和 brotli
 VITE_BUILD_COMPRESS = gzip

+ 2 - 5
gw-ui/.env.staging

@@ -15,11 +15,8 @@ VITE_SERVICE_BASE_TITLE = '/gw-api'
 # 后端接口基础路径
 VITE_SERVER_URL = 'http://39.98.38.2:18055/gw-api'
 
-# GIS地图引擎: leaflet(默认) / arcgis(需内网ArcGIS服务)
-VITE_GIS_ENGINE = 'arcgis'
-
-# ArcGIS JS API 加载地址 (内网GIS服务)
-VITE_ARCGIS_URL = 'http://10.8.4.136:9090/tba-gis/static/arcgis4_7'
+# GIS地图引擎: openlayers
+VITE_GIS_ENGINE = 'openlayers'
 
 # 是否在打包时开启压缩,支持 gzip 和 brotli
 VITE_BUILD_COMPRESS = gzip

+ 2 - 0
gw-ui/package.json

@@ -34,7 +34,9 @@
     "jsencrypt": "3.3.2",
     "leaflet": "^1.9.4",
     "nprogress": "0.2.0",
+    "ol": "^10.9.0",
     "pinia": "3.0.4",
+    "proj4": "^2.20.9",
     "sortablejs": "^1.15.0",
     "vue": "3.5.26",
     "vue-cropper": "1.1.1",

+ 0 - 262
gw-ui/src/composables/useArcGisMap.js

@@ -1,262 +0,0 @@
-import { ref, onBeforeUnmount } from 'vue'
-import { ARCGIS_CSS, ARCGIS_INIT, BASEMAP_URL, WKID, DEFAULT_EXTENT, TBA_BASIN_PATHS } from '@/utils/gis-config'
-
-let arcgisLoaded = false
-let arcgisLoading = null
-
-function loadArcGIS() {
-  if (arcgisLoaded) return Promise.resolve()
-  if (arcgisLoading) return arcgisLoading
-
-  arcgisLoading = new Promise((resolve, reject) => {
-    if (document.querySelector(`link[href="${ARCGIS_CSS}"]`)) {
-      arcgisLoaded = true
-      resolve()
-      return
-    }
-
-    const link = document.createElement('link')
-    link.rel = 'stylesheet'
-    link.href = ARCGIS_CSS
-    document.head.appendChild(link)
-
-    if (window.require) {
-      arcgisLoaded = true
-      resolve()
-      return
-    }
-
-    const script = document.createElement('script')
-    script.src = ARCGIS_INIT
-    script.async = true
-    script.onload = () => {
-      arcgisLoaded = true
-      resolve()
-    }
-    script.onerror = () => {
-      arcgisLoading = null
-      reject(new Error('ArcGIS JS API 加载失败'))
-    }
-    document.head.appendChild(script)
-  })
-
-  return arcgisLoading
-}
-
-function requireModules(modules) {
-  return loadArcGIS().then(() => {
-    return new Promise((resolve, reject) => {
-      try {
-        window.require(modules, function (...args) {
-          resolve(args)
-        })
-      } catch (e) {
-        reject(e)
-      }
-    })
-  })
-}
-
-export function useArcGisMap() {
-  const map = ref(null)
-  const view = ref(null)
-  const coord = ref({ lng: 0, lat: 0 })
-
-  let MapConstructor = null
-  let MapViewConstructor = null
-  let TileLayer = null
-  let GraphicsLayer = null
-  let Graphic = null
-  let Point = null
-  let SpatialReference = null
-  let SimpleMarkerSymbol = null
-  let SimpleLineSymbol = null
-  let Polyline = null
-  let Extent = null
-
-  async function ensureModules() {
-    if (MapConstructor) return
-    const [
-      Map,
-      MapView,
-      TileLayerCls,
-      GraphicsLayerCls,
-      GraphicCls,
-      PointCls,
-      SpatialRefCls,
-      SimpleMarkerSymbolCls,
-      SimpleLineSymbolCls,
-      PolylineCls,
-      ExtentCls,
-    ] = await requireModules([
-      'esri/Map',
-      'esri/views/MapView',
-      'esri/layers/TileLayer',
-      'esri/layers/GraphicsLayer',
-      'esri/Graphic',
-      'esri/geometry/Point',
-      'esri/geometry/SpatialReference',
-      'esri/symbols/SimpleMarkerSymbol',
-      'esri/symbols/SimpleLineSymbol',
-      'esri/geometry/Polyline',
-      'esri/geometry/Extent',
-    ])
-    MapConstructor = Map
-    MapViewConstructor = MapView
-    TileLayer = TileLayerCls
-    GraphicsLayer = GraphicsLayerCls
-    Graphic = GraphicCls
-    Point = PointCls
-    SpatialReference = SpatialRefCls
-    SimpleMarkerSymbol = SimpleMarkerSymbolCls
-    SimpleLineSymbol = SimpleLineSymbolCls
-    Polyline = PolylineCls
-    Extent = ExtentCls
-  }
-
-  async function getModule(path) {
-    await ensureModules()
-    return new Promise(resolve => window.require([path], m => resolve(m)))
-  }
-
-  async function initMap(containerId, center, zoom) {
-    await ensureModules()
-
-    const basemapLayer = new TileLayer({
-      url: BASEMAP_URL,
-    })
-
-    const mapInstance = new MapConstructor({
-      layers: [basemapLayer],
-    })
-
-    const viewInstance = new MapViewConstructor({
-      map: mapInstance,
-      container: containerId,
-      popup: {
-        dockEnabled: false,
-        dockOptions: { buttonEnabled: false, breakpoint: false },
-      },
-    })
-
-    viewInstance.when(() => {
-      const ext = new Extent({
-        ...DEFAULT_EXTENT,
-        spatialReference: new SpatialReference({ wkid: WKID }),
-      })
-      viewInstance.extent = ext
-      if (center) {
-        viewInstance.center = center
-      }
-      if (zoom) {
-        viewInstance.zoom = zoom
-      }
-    })
-
-    viewInstance.on('pointer-move', (event) => {
-      const point = viewInstance.toMap({ x: event.x, y: event.y })
-      coord.value = { lng: point.longitude, lat: point.latitude }
-    })
-
-    map.value = mapInstance
-    view.value = viewInstance
-
-    return { map: mapInstance, view: viewInstance }
-  }
-
-  function createGraphicsLayer(id) {
-    return new GraphicsLayer({ id })
-  }
-
-  function createPointMarker(lat, lng, symbol, attributes) {
-    const point = new Point({
-      longitude: lng,
-      latitude: lat,
-      spatialReference: new SpatialReference({ wkid: WKID }),
-    })
-    return new Graphic({
-      geometry: point,
-      symbol: symbol || createDefaultSymbol(),
-      attributes: attributes || {},
-    })
-  }
-
-  function createCircleSymbol(color, radius, opacity) {
-    return new SimpleMarkerSymbol({
-      style: 'circle',
-      color: [parseInt(color.slice(1, 3), 16), parseInt(color.slice(3, 5), 16), parseInt(color.slice(5, 7), 16), opacity != null ? opacity : 204],
-      outline: { color: [255, 255, 255], width: 1.5 },
-      size: radius || 14,
-    })
-  }
-
-  function createDefaultSymbol() {
-    return new SimpleMarkerSymbol({
-      style: 'circle',
-      color: [64, 158, 255, 204],
-      outline: { color: [255, 255, 255], width: 1.5 },
-      size: 14,
-    })
-  }
-
-  function openPopup(viewInstance, title, content, location) {
-    viewInstance.popup.open({
-      title,
-      content,
-      location: location || viewInstance.center,
-    })
-  }
-
-  async function addBasinBoundary() {
-    if (!map.value || !Polyline || !SimpleLineSymbol || !Graphic) return
-    const paths = TBA_BASIN_PATHS.map(p => ({ longitude: p[0], latitude: p[1] }))
-    const polyline = new Polyline({
-      paths: [TBA_BASIN_PATHS],
-      spatialReference: new SpatialReference({ wkid: WKID }),
-    })
-    const symbol = new SimpleLineSymbol({
-      color: [72, 149, 239, 180],
-      width: 2,
-      style: 'solid',
-    })
-    const graphic = new Graphic({ geometry: polyline, symbol })
-    const layerId = 'tba-basin-boundary'
-    let layer = map.value.findLayerById(layerId)
-    if (!layer) {
-      layer = new GraphicsLayer({ id: layerId })
-      map.value.add(layer)
-    }
-    layer.add(graphic)
-  }
-
-  function destroy() {
-    if (view.value) {
-      view.value.destroy()
-      view.value = null
-    }
-    if (map.value) {
-      map.value.destroy()
-      map.value = null
-    }
-  }
-
-  onBeforeUnmount(() => {
-    destroy()
-  })
-
-  return {
-    map,
-    view,
-    coord,
-    initMap,
-    ensureModules,
-    getModule,
-    createGraphicsLayer,
-    createPointMarker,
-    createCircleSymbol,
-    createDefaultSymbol,
-    openPopup,
-    addBasinBoundary,
-    destroy,
-  }
-}

+ 0 - 7
gw-ui/src/utils/gis-config.js

@@ -1,10 +1,3 @@
-export const ARCGIS_URL = import.meta.env.VITE_ARCGIS_URL || 'http://10.8.4.136:9090/tba-gis/static/arcgis4_7'
-
-export const ARCGIS_CSS = `${ARCGIS_URL}/esri/css/main.css`
-export const ARCGIS_INIT = `${ARCGIS_URL}/init.js?r=1`
-
-export const BASEMAP_URL = 'http://10.8.4.128/server/rest/services/OneMap/TH_XZQH_2000/MapServer'
-
 export const WKID = 4490
 
 export const DEFAULT_EXTENT = {

+ 98 - 5
gw-ui/src/views/slgc/cms/article/detail.vue

@@ -12,26 +12,52 @@
 
       <div v-loading="loading">
         <template v-if="detail.aid">
+          <!-- 文章标题 -->
+          <h1 v-if="detail.title" class="article-title" :style="{ color: detail.titleColor || '#303133' }">
+            {{ detail.title }}
+          </h1>
+
           <div v-if="detail.subtitle" style="text-align:center;color:#606266;font-size:14px;margin-bottom:8px">
             {{ detail.subtitle }}
           </div>
+
           <div class="article-meta">
             <span>作者:{{ detail.pubUser || '-' }}</span>
-            <span>分类:{{ detail.cateId }}</span>
+            <span>分类:{{ getCateName(detail.cateId) }}</span>
             <span>发布日期:{{ detail.pubDate || '-' }}</span>
             <span>点击量:{{ detail.viewCount || 0 }}</span>
           </div>
+
           <div style="margin:12px 0">
             <el-tag size="small" :type="detail.status === '1' ? 'success' : 'info'" style="margin-right:8px">
               {{ statusLabel(detail.status) }}
             </el-tag>
-            <el-tag size="small" :type="detail.pubPlatform === 'weixin' ? 'success' : 'info'" style="margin-right:8px">
-              {{ detail.pubPlatform === 'weixin' ? '微信' : 'PC' }}
+            <el-tag size="small" :type="platformTagType(detail.pubPlatform)" style="margin-right:8px">
+              {{ platformLabel(detail.pubPlatform) }}
             </el-tag>
             <el-tag size="small" :type="detail.type === 'link' ? 'warning' : ''">
               {{ detail.type === 'link' ? '链接' : '图文' }}
             </el-tag>
           </div>
+
+          <!-- 工具栏:字号切换 + 打印 + 分享 -->
+          <div class="article-toolbar">
+            <el-space>
+              <span style="color:#909399;font-size:13px">字号:</span>
+              <el-button size="small" :type="fontSize === 'small' ? 'primary' : 'info'" text @click="fontSize = 'small'">小</el-button>
+              <el-button size="small" :type="fontSize === 'medium' ? 'primary' : 'info'" text @click="fontSize = 'medium'">中</el-button>
+              <el-button size="small" :type="fontSize === 'large' ? 'primary' : 'info'" text @click="fontSize = 'large'">大</el-button>
+            </el-space>
+            <el-space>
+              <el-button size="small" @click="handlePrint">
+                <el-icon><Printer /></el-icon> 打印
+              </el-button>
+              <el-button size="small" @click="shareToWeibo">微博</el-button>
+              <el-button size="small" @click="shareToWechat">微信</el-button>
+              <el-button size="small" @click="shareToQQ">QQ</el-button>
+            </el-space>
+          </div>
+
           <div v-if="detail.keyword" style="color:#909399;font-size:13px;margin-bottom:12px">
             关键字:{{ detail.keyword }}
           </div>
@@ -39,7 +65,7 @@
             <img :src="detail.thumbnail" style="max-width:100%;max-height:300px;border-radius:4px" />
           </div>
           <el-divider />
-          <div class="article-content" v-html="detail.content || '文章内容加载中...'"></div>
+          <div class="article-content" :class="'fs-' + fontSize" v-html="detail.content || '文章内容加载中...'"></div>
         </template>
       </div>
     </el-card>
@@ -47,18 +73,72 @@
 </template>
 
 <script setup name="CmsDetail">
-import { getArticleDetail } from '@/api/slgc/cms/index'
+import { getArticleDetail, getCateTree } from '@/api/slgc/cms/index'
+import { Printer } from '@element-plus/icons-vue'
 
 const route = useRoute()
 const loading = ref(true)
 const detail = ref({})
+const cateTreeData = ref([])
+const fontSize = ref('medium')
 
 function statusLabel(status) {
   const map = { '0': '草稿', '1': '已发布', '2': '已撤回', '3': '已删除' }
   return map[status] || '未知'
 }
 
+function platformLabel(val) {
+  const map = { '1': 'PC', '2': '微信', '3': 'PC + 微信' }
+  return map[val] || val || '-'
+}
+
+function platformTagType(val) {
+  const map = { '1': 'info', '2': 'success', '3': '' }
+  return map[val] || 'info'
+}
+
+/** 获取分类名称(从树形数据中查找) */
+function getCateName(cateId) {
+  if (!cateId) return '-'
+  const flatMap = {}
+  function walk(nodes) {
+    if (!nodes) return
+    for (const n of nodes) {
+      flatMap[n.cateId] = n.cateName
+      if (n.children) walk(n.children)
+    }
+  }
+  walk(cateTreeData.value)
+  return flatMap[cateId] || cateId
+}
+
+function handlePrint() {
+  window.print()
+}
+
+function shareToWeibo() {
+  const url = encodeURIComponent(window.location.href)
+  const title = encodeURIComponent(detail.value.title || '')
+  window.open(`https://service.weibo.com/share/share.php?url=${url}&title=${title}`, '_blank')
+}
+
+function shareToWechat() {
+  // 微信分享通常通过二维码,这里弹出提示
+  ElMessage.info('请使用微信扫描页面二维码分享')
+}
+
+function shareToQQ() {
+  const url = encodeURIComponent(window.location.href)
+  const title = encodeURIComponent(detail.value.title || '')
+  window.open(`https://connect.qq.com/widget/shareqq/index.html?url=${url}&title=${title}`, '_blank')
+}
+
 onMounted(() => {
+  // 加载分类树
+  getCateTree().then(res => {
+    cateTreeData.value = res.data || []
+  })
+
   const id = route.query.id
   if (id) {
     loading.value = true
@@ -76,6 +156,19 @@ onMounted(() => {
 
 <style scoped>
 .card-header { font-weight: 600; font-size: 16px; }
+.article-title { font-size: 22px; font-weight: 600; margin: 0 0 12px; text-align: center; }
 .article-meta { display: flex; gap: 24px; color: #999; font-size: 14px; }
+.article-toolbar {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin: 12px 0;
+  padding: 8px 12px;
+  background: #f5f7fa;
+  border-radius: 4px;
+}
 .article-content { font-size: 15px; line-height: 1.8; color: #333; }
+.article-content.fs-small { font-size: 13px; }
+.article-content.fs-medium { font-size: 15px; }
+.article-content.fs-large { font-size: 18px; }
 </style>

+ 206 - 25
gw-ui/src/views/slgc/cms/article/index.vue

@@ -1,10 +1,33 @@
 <template>
   <div class="app-container">
+    <el-row :gutter="16">
+      <el-col :span="6">
+        <el-card shadow="never" class="cate-sidebar">
+          <template #header>
+            <div class="card-header">
+              <span>文章分类</span>
+            </div>
+          </template>
+          <div class="cate-tree-wrapper">
+            <el-tree
+              ref="cateTreeRef"
+              :data="sidebarTreeData"
+              :props="{ label: 'cateName', children: 'children' }"
+              node-key="cateId"
+              highlight-current
+              default-expand-all
+              :expand-on-click-node="false"
+              @node-click="handleCateNodeClick"
+            />
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :span="18">
     <el-card shadow="never">
       <template #header>
         <div class="card-header">
           <span>内容管理</span>
-          <el-button type="primary" size="small" @click="handleAdd">新增文章</el-button>
+          <el-button v-if="queryParams.status === '0' || queryParams.status === '1'" type="primary" size="small" @click="handleAdd">新增文章</el-button>
         </div>
       </template>
 
@@ -59,25 +82,25 @@
       <transition name="el-fade-in">
         <div v-if="selectedRows.length > 0" class="batch-toolbar">
           <span class="batch-info">已选择 <strong>{{ selectedRows.length }}</strong> 项</span>
-          <el-button size="small" type="success" @click="handleBatchPublish">
+          <el-button v-if="queryParams.status === '0' || queryParams.status === '2'" size="small" type="success" @click="handleBatchPublish">
             <el-icon><Check /></el-icon> 批量发布
           </el-button>
-          <el-button size="small" type="warning" @click="handleBatchTop">
+          <el-button v-if="queryParams.status === '1'" size="small" type="warning" @click="handleBatchTop">
             <el-icon><Top /></el-icon> 批量置顶
           </el-button>
-          <el-button size="small" type="info" @click="handleBatchRecall">
+          <el-button v-if="queryParams.status === '1'" size="small" type="info" @click="handleBatchRecall">
             <el-icon><RefreshLeft /></el-icon> 批量撤回
           </el-button>
-          <el-button size="small" type="danger" @click="handleBatchDelete">
+          <el-button v-if="queryParams.status === '0' || queryParams.status === '1' || queryParams.status === '2'" size="small" type="danger" @click="handleBatchDelete">
             <el-icon><Delete /></el-icon> 批量删除
           </el-button>
           <el-button v-if="queryParams.status === '3'" size="small" type="danger" @click="handleBatchDrop">
             <el-icon><DeleteFilled /></el-icon> 批量清空
           </el-button>
-          <el-button size="small" type="primary" @click="handleBatchCopy">
+          <el-button v-if="queryParams.status === '1'" size="small" type="primary" @click="handleBatchCopy">
             <el-icon><CopyDocument /></el-icon> 批量复制
           </el-button>
-          <el-button size="small" type="primary" @click="handleBatchMove">
+          <el-button v-if="queryParams.status === '1'" size="small" type="primary" @click="handleBatchMove">
             <el-icon><FolderOpened /></el-icon> 批量移动
           </el-button>
           <el-button size="small" text @click="clearSelection">取消选择</el-button>
@@ -106,11 +129,9 @@
           <template #default="s">{{ getCateName(s.row.cateId) }}</template>
         </el-table-column>
         <el-table-column prop="pubUser" label="作者" width="100" />
-        <el-table-column prop="pubPlatform" label="发布平台" width="90">
+        <el-table-column prop="pubPlatform" label="发布平台" width="120">
           <template #default="s">
-            <el-tag size="small" :type="s.row.pubPlatform === 'weixin' ? 'success' : 'info'">
-              {{ s.row.pubPlatform === 'weixin' ? '微信' : 'PC' }}
-            </el-tag>
+            {{ pubPlatformLabel(s.row.pubPlatform) }}
           </template>
         </el-table-column>
         <el-table-column prop="type" label="类型" width="80">
@@ -144,6 +165,8 @@
         @pagination="getList"
       />
     </el-card>
+      </el-col>
+    </el-row>
 
     <!-- 新增/编辑文章对话框 -->
     <el-dialog :title="title" v-model="open" width="900px" append-to-body top="3vh" destroy-on-close>
@@ -189,10 +212,10 @@
           </el-col>
           <el-col :span="8">
             <el-form-item label="发布平台" prop="pubPlatform">
-              <el-select v-model="form.pubPlatform" placeholder="请选择" style="width:100%">
-                <el-option label="PC" value="pc" />
-                <el-option label="微信" value="weixin" />
-              </el-select>
+              <el-checkbox-group v-model="form.pubPlatform">
+                <el-checkbox label="1">PC</el-checkbox>
+                <el-checkbox label="2">微信</el-checkbox>
+              </el-checkbox-group>
             </el-form-item>
           </el-col>
         </el-row>
@@ -210,6 +233,14 @@
           </el-col>
         </el-row>
 
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="责任编辑">
+              <el-input v-model="form.auditUser" placeholder="请输入责任编辑" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
         <el-row :gutter="20">
           <el-col :span="12">
             <el-form-item label="缩略图">
@@ -237,7 +268,11 @@
           </el-col>
         </el-row>
 
-        <el-form-item label="正文内容">
+        <el-form-item v-if="form.type === 'link'" label="链接地址" prop="url">
+          <el-input v-model="form.url" placeholder="请输入转载链接,如 https://example.com" />
+        </el-form-item>
+
+        <el-form-item v-if="form.type === 'article'" label="正文内容">
           <Editor v-model="form.content" :height="320" />
         </el-form-item>
 
@@ -281,6 +316,7 @@
           </p>
           <div style="text-align:center;color:#999;font-size:13px;margin-bottom:16px">
             <span>作者:{{ detailData.pubUser }}</span> &nbsp;|&nbsp;
+            <span>责任编辑:{{ detailData.auditUser }}</span> &nbsp;|&nbsp;
             <span>分类:{{ getCateName(detailData.cateId) }}</span> &nbsp;|&nbsp;
             <span>发布日期:{{ detailData.pubDate }}</span> &nbsp;|&nbsp;
             <span>点击量:{{ detailData.viewCount || 0 }}</span>
@@ -289,9 +325,10 @@
             <el-tag size="small" :type="statusTagType(detailData.status)" style="margin-right:8px">
               {{ statusLabel(detailData.status) }}
             </el-tag>
-            <el-tag size="small" :type="detailData.pubPlatform === 'weixin' ? 'success' : 'info'" style="margin-right:8px">
-              {{ detailData.pubPlatform === 'weixin' ? '微信' : 'PC' }}
-            </el-tag>
+            <template v-if="detailData.pubPlatform">
+              <el-tag v-if="detailData.pubPlatform === '1' || detailData.pubPlatform === '3'" size="small" style="margin-right:8px">PC</el-tag>
+              <el-tag v-if="detailData.pubPlatform === '2' || detailData.pubPlatform === '3'" size="small" type="success" style="margin-right:8px">微信</el-tag>
+            </template>
             <el-tag size="small" :type="detailData.type === 'link' ? 'warning' : ''">
               {{ detailData.type === 'link' ? '链接' : '图文' }}
             </el-tag>
@@ -303,7 +340,24 @@
             <img :src="detailData.thumbnail" style="max-width:100%;max-height:200px;border-radius:4px" />
           </div>
           <el-divider />
-          <div style="font-size:15px;line-height:1.8;white-space:pre-line" v-html="detailData.content"></div>
+          <div class="article-toolbar">
+            <div class="toolbar-group">
+              <span class="toolbar-label">字号:</span>
+              <el-button :type="fontSize === 18 ? 'primary' : ''" size="small" @click="fontSize = 18">大</el-button>
+              <el-button :type="fontSize === 16 ? 'primary' : ''" size="small" @click="fontSize = 16">中</el-button>
+              <el-button :type="fontSize === 14 ? 'primary' : ''" size="small" @click="fontSize = 14">小</el-button>
+            </div>
+            <div class="toolbar-group">
+              <el-button size="small" icon="Printer" @click="handlePrint">打印</el-button>
+            </div>
+            <div class="toolbar-group">
+              <span class="toolbar-label">分享:</span>
+              <el-button size="small" @click="shareToWeibo">微博</el-button>
+              <el-button size="small" @click="shareToWechat">微信</el-button>
+              <el-button size="small" @click="shareToQQ">QQ</el-button>
+            </div>
+          </div>
+          <div :style="{ fontSize: fontSize + 'px', lineHeight: '1.8', whiteSpace: 'pre-line' }" v-html="detailData.content"></div>
           <div v-if="detailFiles.length > 0" class="mt-16">
             <el-divider />
             <div class="card-header" style="margin-bottom:8px">附件列表</div>
@@ -354,6 +408,7 @@ const list = ref([])
 const fileList = ref([])
 const detailData = ref(null)
 const detailFiles = ref([])
+const fontSize = ref(15) // 文章正文字号:大18、中16、小14
 const selectedRows = ref([])
 const cateTreeData = ref([])
 const tableRef = ref(null)
@@ -361,6 +416,7 @@ const formRef = ref(null)
 const thumbUploadRef = ref(null)
 const uploadRef = ref(null)
 const moveTreeRef = ref(null)
+const cateTreeRef = ref(null)
 const moveDialog = reactive({
   visible: false,
   title: '选择目标分类',
@@ -380,6 +436,10 @@ const queryParams = reactive({
 })
 const defaultSort = ref({ prop: 'pubDate', order: 'descending' })
 
+const sidebarTreeData = computed(() => {
+  return [{ cateId: '', cateName: '全部', children: cateTreeData.value }]
+})
+
 const statusCounts = reactive({ 0: 0, 1: 0, 2: 0, 3: 0 })
 
 const form = ref({})
@@ -388,11 +448,49 @@ const formRules = {
   title: [{ required: true, message: '请输入文章标题', trigger: 'blur' }],
   cateId: [{ required: true, message: '请选择文章分类', trigger: 'change' }],
   type: [{ required: true, message: '请选择文章类型', trigger: 'change' }],
-  pubPlatform: [{ required: true, message: '请选择发布平台', trigger: 'change' }]
+  pubPlatform: [{
+    required: true,
+    message: '请选择发布平台',
+    trigger: 'change',
+    validator: (rule, value, callback) => {
+      if (!value || value.length === 0) {
+        callback(new Error('请选择发布平台'))
+      } else {
+        callback()
+      }
+    }
+  }],
+  auditUser: [{ required: true, message: '请输入责任编辑', trigger: 'blur' }],
+  url: [{
+    validator: (rule, value, callback) => {
+      if (form.value.type === 'link' && (!value || !value.trim())) {
+        callback(new Error('请输入链接地址'))
+      } else {
+        callback()
+      }
+    },
+    trigger: 'blur'
+  }]
 }
 
 const titleColorPresets = ['#ff4500', '#ff6600', '#e6a23c', '#303133', '#000000', '#e91e63', '#2196f3', '#4caf50']
 
+/** 发布平台 编码/解码 (数组 ↔ 后端值 1/2/3) */
+function arrayToPubPlatform(arr) {
+  if (!arr || arr.length === 0) return ''
+  if (arr.includes('1') && arr.includes('2')) return '3'
+  return arr[0]
+}
+function pubPlatformToArray(val) {
+  if (val === '3') return ['1', '2']
+  if (val === '1' || val === '2') return [val]
+  return []
+}
+function pubPlatformLabel(val) {
+  const map = { '1': 'PC', '2': '微信', '3': 'PC + 微信' }
+  return map[val] || val || '-'
+}
+
 /** 状态工具函数 */
 function statusLabel(status) {
   const map = { '0': '草稿', '1': '已发布', '2': '已撤回', '3': '已删除' }
@@ -473,9 +571,22 @@ function resetQuery() {
   queryParams.title = undefined
   queryParams.cateId = undefined
   queryParams.status = undefined
+  cateTreeRef.value?.setCurrentKey('')
   handleQuery()
 }
 
+/** 分类树节点点击(侧边栏) */
+function handleCateNodeClick(data) {
+  queryParams.cateId = data.cateId || undefined
+  queryParams.pageNum = 1
+  getList()
+}
+
+/** 同步侧边栏树高亮与 queryParams.cateId */
+watch(() => queryParams.cateId, (val) => {
+  cateTreeRef.value?.setCurrentKey(val || '')
+})
+
 /** 表格选中变化 */
 function handleSelectionChange(rows) {
   selectedRows.value = rows
@@ -510,7 +621,7 @@ function handleDetail(row) {
 
 /** 新增 */
 function handleAdd() {
-  form.value = { type: 'article', pubPlatform: 'pc' }
+  form.value = { type: 'article', pubPlatform: [], auditUser: '', url: '' }
   fileList.value = []
   title.value = '新增文章'
   open.value = true
@@ -520,6 +631,7 @@ function handleAdd() {
 function handleEdit(row) {
   getArticleDetail(row.aid).then(res => {
     form.value = { ...res.data }
+    form.value.pubPlatform = pubPlatformToArray(res.data.pubPlatform)
     fileList.value = res.data?.files ? [...res.data.files] : []
     title.value = '编辑文章'
     open.value = true
@@ -577,8 +689,9 @@ function removeThumbnail() {
 function submitForm() {
   formRef.value?.validate((valid) => {
     if (!valid) return
-    const method = form.value.aid && title.value === '编辑文章' ? updateArticle : addArticle
-    method(form.value).then(() => {
+    const submitData = { ...form.value, pubPlatform: arrayToPubPlatform(form.value.pubPlatform) }
+    const method = submitData.aid && title.value === '编辑文章' ? updateArticle : addArticle
+    method(submitData).then(() => {
       proxy.$modal.msgSuccess('操作成功')
       open.value = false
       getList()
@@ -592,6 +705,30 @@ function cancel() {
   open.value = false
 }
 
+/** 打印文章 */
+function handlePrint() {
+  window.print()
+}
+
+/** 分享到微博 */
+function shareToWeibo() {
+  const url = encodeURIComponent(location.href)
+  const title = encodeURIComponent(detailData.value?.title || '')
+  window.open(`https://service.weibo.com/share/share.php?url=${url}&title=${title}`, '_blank')
+}
+
+/** 分享到微信(提示扫描二维码) */
+function shareToWechat() {
+  proxy.$modal.msgInfo('请使用微信扫描页面二维码分享')
+}
+
+/** 分享到QQ */
+function shareToQQ() {
+  const url = encodeURIComponent(location.href)
+  const title = encodeURIComponent(detailData.value?.title || '')
+  window.open(`https://connect.qq.com/widget/shareqq/index.html?url=${url}&title=${title}`, '_blank')
+}
+
 /** ====== 批量操作 ====== */
 function getSelectedAids() {
   return selectedRows.value.map(r => r.aid).join(',')
@@ -684,7 +821,8 @@ function confirmMoveOrCopy() {
         title: r.title, subtitle: r.subtitle, cateId: targetCateId,
         status: 0, type: r.type, content: r.content,
         pubPlatform: r.pubPlatform, titleColor: r.titleColor,
-        summary: r.summary, source: r.source, keyword: r.keyword
+        summary: r.summary, source: r.source, keyword: r.keyword,
+        auditUser: r.auditUser
       }))
       batchAddArticle(items).then(() => {
         proxy.$modal.msgSuccess('批量复制成功')
@@ -812,4 +950,47 @@ onMounted(() => {
 .thumb-actions {
   margin-top: 4px;
 }
+
+.cate-sidebar {
+  height: 100%;
+}
+.cate-sidebar :deep(.el-card__body) {
+  padding: 0;
+}
+.cate-tree-wrapper {
+  padding: 8px 0;
+  overflow-y: auto;
+  max-height: calc(100vh - 260px);
+}
+.cate-tree-wrapper :deep(.el-tree-node__content) {
+  height: 36px;
+  padding-left: 12px;
+}
+.cate-tree-wrapper :deep(.el-tree-node.is-current > .el-tree-node__content) {
+  background-color: #ecf5ff;
+  color: #409eff;
+  font-weight: 600;
+}
+
+.article-toolbar {
+  display: flex;
+  align-items: center;
+  gap: 16px;
+  padding: 10px 12px;
+  margin-bottom: 12px;
+  background: #f5f7fa;
+  border: 1px solid #e4e7ed;
+  border-radius: 4px;
+  flex-wrap: wrap;
+}
+.toolbar-group {
+  display: flex;
+  align-items: center;
+  gap: 4px;
+}
+.toolbar-label {
+  font-size: 13px;
+  color: #606266;
+  margin-right: 2px;
+}
 </style>

+ 48 - 7
gw-ui/src/views/slgc/cms/category/index.vue

@@ -4,7 +4,11 @@
       <template #header>
         <div class="card-header"><span>栏目分类</span><el-button type="primary" size="small" style="float:right" @click="handleAdd">新增分类</el-button></div>
       </template>
-      <el-table v-loading="loading" :data="list" stripe>
+      <div style="margin-bottom:12px;display:flex;align-items:center;gap:12px">
+        <el-input v-model="queryParams.cateName" placeholder="请输入分类名称" clearable style="width:220px" @keyup.enter="loadData" @clear="loadData" />
+        <el-button type="primary" icon="Search" @click="loadData">搜索</el-button>
+      </div>
+      <el-table v-loading="loading" :data="list" row-key="cateId" :tree-props="{ children: 'children' }" default-expand-all stripe>
         <el-table-column prop="cateId" label="ID" width="80" />
         <el-table-column prop="cateName" label="分类名称" min-width="160" />
         <el-table-column prop="cateMemo" label="分类说明" min-width="200" show-overflow-tooltip />
@@ -18,6 +22,9 @@
     </el-card>
     <el-dialog v-model="open" :title="title" width="450px">
       <el-form :model="form" label-width="80px">
+        <el-form-item label="上级分类">
+          <el-tree-select v-model="form.catePid" :data="cateTree" :props="{ label:'cateName', value:'cateId' }" check-strictly clearable placeholder="留空则为顶级分类" style="width:100%" />
+        </el-form-item>
         <el-form-item label="分类名称"><el-input v-model="form.cateName" placeholder="请输入分类名称" /></el-form-item>
         <el-form-item label="分类说明"><el-input v-model="form.cateMemo" type="textarea" :rows="3" placeholder="请输入分类说明" /></el-form-item>
         <el-form-item label="排序"><el-input-number v-model="form.cateSeq" :min="0" :max="9999" style="width:100%" /></el-form-item>
@@ -31,18 +38,20 @@
 </template>
 
 <script setup name="CmsCategory">
-import { getCateList, getCate, addCate, updateCate, delCate } from '@/api/slgc/cms/index'
+import { getCateList, getCateTree, getCate, addCate, updateCate, delCate } from '@/api/slgc/cms/index'
 const { proxy } = getCurrentInstance()
 const loading = ref(true)
 const list = ref([])
 const open = ref(false)
 const title = ref('')
-const form = ref({ cateId: '', cateName: '', cateMemo: '', cateSeq: 0 })
+const cateTree = ref([])
+const queryParams = ref({ cateName: '' })
+const form = ref({ cateId: '', catePid: '', cateName: '', cateMemo: '', cateSeq: 0 })
 
-function handleAdd() { form.value = { cateId: '', cateName: '', cateMemo: '', cateSeq: 0 }; title.value = '新增分类'; open.value = true }
+function handleAdd() { form.value = { cateId: '', catePid: '', cateName: '', cateMemo: '', cateSeq: 0 }; title.value = '新增分类'; open.value = true }
 function handleEdit(row) {
   getCate(row.cateId).then(res => {
-    form.value = { cateId: res.data.cateId, cateName: res.data.cateName, cateMemo: res.data.cateMemo || '', cateSeq: res.data.cateSeq || 0 }
+    form.value = { cateId: res.data.cateId, catePid: res.data.catePid || '', cateName: res.data.cateName, cateMemo: res.data.cateMemo || '', cateSeq: res.data.cateSeq || 0 }
     title.value = '编辑分类'; open.value = true
   })
 }
@@ -56,7 +65,39 @@ function submitForm() {
   const action = form.value.cateId ? updateCate(form.value) : addCate(form.value)
   action.then(() => { ElMessage.success('操作成功'); open.value = false; loadData() })
 }
-function loadData() { getCateList().then(res => { list.value = res.rows; loading.value = false }) }
-onMounted(() => { loadData() })
+function flatToTree(flat, idKey = 'cateId', parentKey = 'catePid') {
+  const map = {}, roots = []
+  flat.forEach(i => { map[i[idKey]] = { ...i, children: [] } })
+  flat.forEach(i => {
+    if (map[i[parentKey]]) map[i[parentKey]].children.push(map[i[idKey]])
+    else roots.push(map[i[idKey]])
+  })
+  roots.forEach(function clean(n) { if (!n.children.length) delete n.children; else n.children.forEach(clean) })
+  return roots
+}
+function filterTree(tree, kw) {
+  const key = kw.toLowerCase()
+  return tree.reduce((acc, node) => {
+    const kids = node.children ? filterTree(node.children, key) : []
+    const selfMatch = node.cateName.toLowerCase().includes(key)
+    if (selfMatch || kids.length) {
+      const item = { ...node }
+      if (kids.length) item.children = kids
+      else delete item.children
+      acc.push(item)
+    }
+    return acc
+  }, [])
+}
+function loadData() {
+  loading.value = true
+  getCateTree().then(res => {
+    const tree = res.data || res.rows || []
+    list.value = queryParams.value.cateName ? filterTree(tree, queryParams.value.cateName) : tree
+    loading.value = false
+  })
+}
+function loadTree() { getCateTree().then(res => { cateTree.value = res.data || res.rows || [] }) }
+onMounted(() => { loadData(); loadTree() })
 </script>
 <style scoped>.card-header { font-weight: 600; font-size: 16px; }</style>

+ 40 - 9
gw-ui/src/views/slgc/dike/index.vue

@@ -15,6 +15,15 @@
         <el-form-item label="起始位置">
           <el-input v-model="queryParams.startLoc" placeholder="请输入起始位置" clearable @keyup.enter="handleQuery" />
         </el-form-item>
+        <el-form-item label="堤防型式">
+          <el-select v-model="queryParams.dikePatt" placeholder="请选择" clearable>
+            <el-option label="土堤" value="1" />
+            <el-option label="砌石堤" value="2" />
+            <el-option label="土石混合堤" value="3" />
+            <el-option label="钢筋混凝土防洪墙" value="4" />
+            <el-option label="其他" value="9" />
+          </el-select>
+        </el-form-item>
         <el-form-item>
           <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
           <el-button icon="Refresh" @click="resetQuery">重置</el-button>
@@ -33,10 +42,15 @@
           </template>
         </el-table-column>
         <el-table-column prop="dikeType" label="类型" width="100" />
-        <el-table-column prop="engStat" label="状态" width="80">
+        <el-table-column prop="dikePatt" label="堤防型式" width="100">
           <template #default="scope">
-            <el-tag :type="scope.row.engStat === '1' ? 'success' : 'warning'" size="small">
-              {{ scope.row.engStat === '1' ? '正常' : scope.row.engStat === '0' ? '停用' : scope.row.engStat }}
+            {{ dikePattMap[scope.row.dikePatt] || scope.row.dikePatt }}
+          </template>
+        </el-table-column>
+        <el-table-column prop="runStat" label="运行状况" width="100">
+          <template #default="scope">
+            <el-tag :type="scope.row.runStat === '1' ? 'success' : scope.row.runStat === '2' ? 'warning' : 'info'" size="small">
+              {{ runStatMap[scope.row.runStat] || scope.row.runStat }}
             </el-tag>
           </template>
         </el-table-column>
@@ -98,10 +112,24 @@
         </el-row>
         <el-row>
           <el-col :span="12">
-            <el-form-item label="工程状态">
-              <el-select v-model="form.engStat" placeholder="请选择状态">
-                <el-option label="正常" value="1" />
-                <el-option label="停用" value="0" />
+            <el-form-item label="堤防型式">
+              <el-select v-model="form.dikePatt" placeholder="请选择堤防型式">
+                <el-option label="土堤" value="1" />
+                <el-option label="砌石堤" value="2" />
+                <el-option label="土石混合堤" value="3" />
+                <el-option label="钢筋混凝土防洪墙" value="4" />
+                <el-option label="其他" value="9" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row>
+          <el-col :span="12">
+            <el-form-item label="运行状况">
+              <el-select v-model="form.runStat" placeholder="请选择状态">
+                <el-option label="在用良好" value="1" />
+                <el-option label="在用故障" value="2" />
+                <el-option label="停用" value="3" />
               </el-select>
             </el-form-item>
           </el-col>
@@ -140,7 +168,7 @@ const title = ref('')
 const dikeList = ref([])
 const queryParams = reactive({
   pageNum: 1, pageSize: 10,
-  dikeName: undefined, startLoc: undefined
+  dikeName: undefined, startLoc: undefined, dikePatt: undefined
 })
 const form = ref({})
 const statsVisible = ref(false)
@@ -148,6 +176,8 @@ const statsLoading = ref(false)
 const statsData = ref([])
 
 const gradeMap = { '1': '一级', '2': '二级', '3': '三级', '4': '四级', '5': '五级' }
+const dikePattMap = { '1': '土堤', '2': '砌石堤', '3': '土石混合堤', '4': '钢筋混凝土防洪墙', '9': '其他' }
+const runStatMap = { '1': '在用良好', '2': '在用故障', '3': '停用' }
 
 function getList() {
   loading.value = true
@@ -162,6 +192,7 @@ function handleQuery() { queryParams.pageNum = 1; getList() }
 function resetQuery() {
   queryParams.dikeName = undefined
   queryParams.startLoc = undefined
+  queryParams.dikePatt = undefined
   handleQuery()
 }
 
@@ -193,7 +224,7 @@ function submitForm() {
 function cancel() { open.value = false }
 
 function handleAdd() {
-  form.value = { engStat: '1' }
+  form.value = { runStat: '1' }
   title.value = '新增堤防'
   open.value = true
 }

+ 36 - 18
gw-ui/src/views/slgc/doc/index.vue

@@ -3,7 +3,7 @@
     <el-row :gutter="16">
       <el-col :span="6">
         <el-card shadow="never">
-          <template #header><div class="card-header"><span>文档分类</span><el-button size="small" circle @click="handleAddCate">+</el-button></div></template>
+          <template #header><div class="card-header"><span>文档分类</span><el-button size="small" circle @click="handleAddCate" v-hasRole="['admin']">+</el-button></div></template>
           <el-tree :data="cateTree" :props="{label:'cateName'}" default-expand-all highlight-current
             @node-click="handleCateClick"
             :expand-on-click-node="false">
@@ -11,8 +11,8 @@
               <span class="custom-tree-node">
                 <span>{{ node.label }}</span>
                 <span class="tree-actions">
-                  <el-button type="primary" link size="small" @click.stop="handleEditCate(data)">编辑</el-button>
-                  <el-button type="danger" link size="small" @click.stop="handleDeleteCate(data)">删除</el-button>
+                  <el-button type="primary" link size="small" @click.stop="handleEditCate(data)" v-hasRole="['admin']">编辑</el-button>
+                  <el-button type="danger" link size="small" @click.stop="handleDeleteCate(data)" v-hasRole="['admin']">删除</el-button>
                 </span>
               </span>
             </template>
@@ -21,24 +21,36 @@
       </el-col>
       <el-col :span="18">
         <el-card shadow="never">
-          <template #header><div class="card-header"><span>文档列表</span><el-button type="primary" size="small" @click="handleUpload">上传文档</el-button></div></template>
+          <template #header><div class="card-header"><span>文档列表</span><el-button type="primary" size="small" @click="handleUpload" v-hasRole="['admin']">上传文档</el-button></div></template>
           <el-table :data="fileList" stripe v-loading="loading" @selection-change="handleSelectionChange">
-            <el-table-column type="selection" width="55" />
+            <el-table-column type="selection" width="55" v-hasRole="['admin']" />
             <el-table-column prop="fileId" label="序号" width="60" />
             <el-table-column prop="fileName" label="标题" min-width="200" show-overflow-tooltip />
             <el-table-column prop="fileSize" label="大小" width="80" />
             <el-table-column prop="fileTime" label="上传时间" width="160" />
-            <el-table-column label="操作" width="240" fixed="right">
+            <el-table-column prop="pubUser" label="上传人" width="100" />
+            <el-table-column label="操作" width="280" fixed="right">
               <template #default="s">
+                <el-button type="success" link @click="handleView(s.row)">查看</el-button>
                 <el-button type="primary" link @click="handleDownload(s.row)">下载</el-button>
-                <el-button type="warning" link @click="handleRename(s.row)">重命名</el-button>
-                <el-button type="warning" link @click="handleMove(s.row)">移动</el-button>
-                <el-button type="danger" link @click="handleDelete(s.row)">删除</el-button>
+                <el-button type="warning" link @click="handleRename(s.row)" v-hasRole="['admin']">重命名</el-button>
+                <el-button type="warning" link @click="handleMove(s.row)" v-hasRole="['admin']">移动</el-button>
+                <el-button type="danger" link @click="handleDelete(s.row)" v-hasRole="['admin']">删除</el-button>
               </template>
             </el-table-column>
           </el-table>
+          <el-pagination
+            class="pagination-wrap"
+            v-model:current-page="pageNum"
+            v-model:page-size="pageSize"
+            :page-sizes="[10, 20, 50, 100]"
+            :total="total"
+            layout="total, sizes, prev, pager, next, jumper"
+            @size-change="loadFileList"
+            @current-change="loadFileList"
+          />
           <div style="margin-top: 12px" v-if="selectedRows.length > 0">
-            <el-button type="warning" size="small" @click="handleBatchMove">批量移动 ({{ selectedRows.length }})</el-button>
+            <el-button type="warning" size="small" @click="handleBatchMove" v-hasRole="['admin']">批量移动 ({{ selectedRows.length }})</el-button>
           </div>
         </el-card>
       </el-col>
@@ -97,7 +109,8 @@ const selectedRows = ref([])
 const moveVisible = ref(false); const moveTargetCateId = ref(''); const moveFileIds = ref([])
 const cateEditVisible = ref(false); const cateEditTitle = ref('')
 const cateEditForm = ref({ cateId: '', cateName: '', cateMemo: '', cateSeq: 0 })
-function handleCateClick(data) { currentCateId.value = data.cateId; loadFileList(data.cateId) }
+const pageNum = ref(1); const pageSize = ref(20); const total = ref(0)
+function handleCateClick(data) { currentCateId.value = data.cateId; pageNum.value = 1; loadFileList() }
 function handleAddCate() {
   cateEditForm.value = { cateId: '', cateName: '', cateMemo: '', cateSeq: 0 }
   cateEditTitle.value = '新增分类'; cateEditVisible.value = true
@@ -125,11 +138,14 @@ function beforeUpload(file) {
   return true
 }
 function handleUploadSuccess(res) {
-  if (res.code === 200) { ElMessage.success('上传成功'); loadFileList(currentCateId.value) }
+  if (res.code === 200) { ElMessage.success('上传成功'); loadFileList() }
   else { ElMessage.error(res.msg || '上传失败') }
 }
 function handleUploadError() { ElMessage.error('上传失败') }
 function handleSelectionChange(rows) { selectedRows.value = rows }
+function handleView(row) {
+  window.open(`/common/download?fileName=${encodeURIComponent(row.filePath)}`, '_blank')
+}
 function handleDownload(row) {
   const url = `/common/download?fileName=${encodeURIComponent(row.filePath)}`
   const a = document.createElement('a')
@@ -144,7 +160,7 @@ function handleRename(row) {
   }).then(({ value }) => {
     updateDocFile({ fileId: row.fileId, fileName: value.trim() }).then(() => {
       ElMessage.success('重命名成功')
-      loadFileList(currentCateId.value)
+      loadFileList()
     })
   }).catch(() => {})
 }
@@ -158,15 +174,16 @@ function handleMoveCateClick(data) { moveTargetCateId.value = data.cateId }
 function submitMove() {
   if (!moveTargetCateId.value) { ElMessage.warning('请选择目标分类'); return }
   moveDocFile({ fileIds: moveFileIds.value.join(','), fileCate: moveTargetCateId.value }).then(() => {
-    ElMessage.success('移动成功'); moveVisible.value = false; loadFileList(currentCateId.value)
+    ElMessage.success('移动成功'); moveVisible.value = false; loadFileList()
   })
 }
 function handleDelete(row) {
-  proxy.$modal.confirm('确认删除"' + row.fileName + '"?').then(() => { delDocFile(row.fileId).then(() => { ElMessage.success('删除成功'); loadFileList(currentCateId.value) }) })
+  proxy.$modal.confirm('确认删除"' + row.fileName + '"?').then(() => { delDocFile(row.fileId).then(() => { ElMessage.success('删除成功'); loadFileList() }) })
 }
-function loadFileList(cateId) {
-  const params = cateId ? { fileCate: cateId } : {}
-  getDocFileList(params).then(r => { fileList.value = r.rows; loading.value = false })
+function loadFileList() {
+  const params = { pageNum: pageNum.value, pageSize: pageSize.value }
+  if (currentCateId.value) params.fileCate = currentCateId.value
+  getDocFileList(params).then(r => { fileList.value = r.rows; total.value = r.total || 0; loading.value = false })
 }
 function loadCateTree() { getDocCateTree().then(r => { cateTree.value = r.data || r.rows || [] }) }
 onMounted(() => { loadCateTree(); loadFileList() })
@@ -176,4 +193,5 @@ onMounted(() => { loadCateTree(); loadFileList() })
 .custom-tree-node { flex: 1; display: flex; align-items: center; justify-content: space-between; font-size: 14px; padding-right: 8px; }
 .tree-actions { display: none; }
 .custom-tree-node:hover .tree-actions { display: inline; }
+.pagination-wrap { margin-top: 12px; display: flex; justify-content: flex-end; }
 </style>

+ 43 - 72
gw-ui/src/views/slgc/gis/index.vue

@@ -142,9 +142,14 @@ import { Location, Folder, List } from '@element-plus/icons-vue'
 import { getDikeList, getReservoirList, getPustList, getWagaList } from '@/api/slgc/gc/index'
 import { getWindList } from '@/api/slgc/wind/index'
 import { getTyphoonInfoList } from '@/api/slgc/typhoon/index'
-import { useArcGisMap } from '@/composables/useArcGisMap'
+import { useMap } from '@/composables/useMap'
+import { fromLonLat } from 'ol/proj.js'
+import LineString from 'ol/geom/LineString.js'
+import Feature from 'ol/Feature.js'
+import Point from 'ol/geom/Point.js'
+import { Style, Circle as CircleStyle, Fill, Stroke, Text as TextStyle } from 'ol/style.js'
 
-const { map, view, coord, initMap, ensureModules, createGraphicsLayer, createPointMarker, createCircleSymbol, destroy, addBasinBoundary } = useArcGisMap()
+const { map, view, coord, initMap, createGraphicsLayer, createPointMarker, createCircleSymbol, destroy, addBasinBoundary, openPopup, closePopup } = useMap()
 
 const mapRef = ref(null)
 const tableRef = ref(null)
@@ -275,19 +280,6 @@ function extractCity(r) {
   return r.region || r.city || r.行政区划名称 || r.REGION || r.adminDiv || r.ADMIN_DIV || r.area || r.district || ''
 }
 
-const loadedModules = {}
-
-async function getModule(path) {
-  if (loadedModules[path]) return loadedModules[path]
-  await ensureModules()
-  return new Promise(resolve => {
-    window.require([path], m => {
-      loadedModules[path] = m
-      resolve(m)
-    })
-  })
-}
-
 function getVisibleMarkers() {
   return filteredMarkers.value
 }
@@ -412,12 +404,11 @@ function loadWind() {
         const speed = parseFloat(d.WNDV) || 0
         const color = speed > 10 ? '#F56C6C' : speed > 5 ? '#E6A23C' : '#67C23A'
         const symbol = createCircleSymbol(color, (5 + Math.min(speed, 12)) * 2)
-        const graphic = createPointMarker(lat, lng, symbol, d)
         const stnm = d.STNM || '站' + i
-        graphic.popupTemplate = {
-          title: stnm,
-          content: `风速: ${d.WNDV || '-'} m/s<br/>风力: ${d.WNDPWR || '-'}<br/>平均风速: ${d.AVGWNDV || '-'}<br/>时间: ${d.TM || '-'}<br/><div style="margin-top:8px;border-top:1px solid #eee;padding-top:6px;text-align:right"><a href="javascript:void(0)" style="color:#409EFF;font-size:12px;text-decoration:none" onclick="window.open('/slgc/hydrology?stnm=${encodeURIComponent(stnm)}', '_blank')">查看历史图表 &gt;</a></div>`,
-        }
+        const graphic = createPointMarker(lat, lng, symbol, {
+          ...d, _popupTitle: stnm,
+          _popupContent: `风速: ${d.WNDV || '-'} m/s<br/>风力: ${d.WNDPWR || '-'}<br/>平均风速: ${d.AVGWNDV || '-'}<br/>时间: ${d.TM || '-'}<br/><div style="margin-top:8px;border-top:1px solid #eee;padding-top:6px;text-align:right"><a href="javascript:void(0)" style="color:#409EFF;font-size:12px;text-decoration:none" onclick="window.open('/slgc/hydrology?stnm=${encodeURIComponent(stnm)}', '_blank')">查看历史图表 &gt;</a></div>`,
+        })
         windLayer.add(graphic)
       })
     })
@@ -438,39 +429,23 @@ function loadTyphoon() {
       if (typhoonLayer) typhoonLayer.removeAll()
       if (!list.length) return
 
-      const TextSymbol = await getModule('esri/symbols/TextSymbol')
-      const Polyline = await getModule('esri/geometry/Polyline')
-      const SimpleLineSymbol = await getModule('esri/symbols/SimpleLineSymbol')
-      const Graphic = await getModule('esri/Graphic')
-      const Point = await getModule('esri/geometry/Point')
-      const SpatialReference = await getModule('esri/geometry/SpatialReference')
-
       list.slice(0, 5).forEach((t, i) => {
         const baseLng = 124 + i * 4
         const baseLat = 28 - i * 2.5
 
-        const textSymbol = new TextSymbol({
-          text: '\u{1F300}',
-          color: [245, 108, 108],
-          fontSize: 22,
-          fontWeight: 'bold',
-          fontFamily: 'sans-serif',
+        const markerStyle = new Style({
+          text: new TextStyle({
+            text: '\u{1F300}',
+            font: 'bold 22px sans-serif',
+            fill: new Fill({ color: '#F56C6C' }),
+          }),
         })
-        const point = new Point({
-          longitude: baseLng,
-          latitude: baseLat,
-          spatialReference: new SpatialReference({ wkid: 4490 }),
-        })
-        const markerGraphic = new Graphic({
-          geometry: point,
-          symbol: textSymbol,
-          attributes: { name: t.TFNAME || '台风' + (i + 1) },
-        })
-        markerGraphic.popupTemplate = {
-          title: t.TFNAME || '台风' + (i + 1),
-          content: `编号: ${t.TFID || '-'}<br/>开始: ${t.STRTIME || '-'}<br/>结束: ${t.ENDTIME || '-'}`,
-        }
-        typhoonLayer.add(markerGraphic)
+        const markerFeat = new Feature(new Point(fromLonLat([baseLng, baseLat], 'EPSG:4490')))
+        markerFeat.setStyle(markerStyle)
+        markerFeat.set('name', t.TFNAME || '台风' + (i + 1))
+        markerFeat.set('_popupTitle', t.TFNAME || '台风' + (i + 1))
+        markerFeat.set('_popupContent', `编号: ${t.TFID || '-'}<br/>开始: ${t.STRTIME || '-'}<br/>结束: ${t.ENDTIME || '-'}`)
+        typhoonLayer.add(markerFeat)
 
         const paths = [
           [baseLng - 1.5, baseLat - 1.5],
@@ -479,19 +454,12 @@ function loadTyphoon() {
           [baseLng + 0.8, baseLat + 0.8],
           [baseLng + 1.5, baseLat + 1.5],
         ]
-        const polyline = new Polyline({
-          paths: [paths],
-          spatialReference: new SpatialReference({ wkid: 4490 }),
-        })
-        const lineSymbol = new SimpleLineSymbol({
-          color: [245, 108, 108],
-          width: 2,
-          style: 'dash',
-          cap: 'round',
-          join: 'round',
-        })
-        const lineGraphic = new Graphic({ geometry: polyline, symbol: lineSymbol })
-        typhoonLayer.add(lineGraphic)
+        const lineCoords = paths.map(p => fromLonLat(p, 'EPSG:4490'))
+        const lineFeat = new Feature(new LineString(lineCoords))
+        lineFeat.setStyle(new Style({
+          stroke: new Stroke({ color: '#F56C6C', width: 2, lineDash: [5, 5] }),
+        }))
+        typhoonLayer.add(lineFeat)
       })
     })
     .catch(() => {
@@ -538,7 +506,7 @@ function onToggleTyphoon() {
 
 function onTableRowClick(row) {
   if (!view.value || !row.lng || !row.lat) return
-  view.value.goTo({ center: [row.lng, row.lat], zoom: 12 })
+  view.value.animate({ center: fromLonLat([row.lng, row.lat], 'EPSG:4490'), zoom: 12 })
   currentMarker.value = row
   detailDialogVisible.value = true
   detailTab.value = 'basic'
@@ -558,22 +526,25 @@ async function initGisMap() {
   engLayer = createGraphicsLayer('eng-layer')
   windLayer = createGraphicsLayer('wind-layer')
   typhoonLayer = createGraphicsLayer('typhoon-layer')
-  map.value.add(engLayer)
-  map.value.add(windLayer)
-  map.value.add(typhoonLayer)
   windLayer.visible = false
   typhoonLayer.visible = false
 
-  view.value.on('click', function(event) {
-    view.value.hitTest(event).then(function(response) {
-      const result = response.results.find(r => r.graphic && r.graphic.attributes && r.graphic.attributes.type)
-      if (result) {
-        const attrs = result.graphic.attributes
+  map.value.on('click', function(event) {
+    const feature = map.value.forEachFeatureAtPixel(event.pixel, f => f)
+    if (feature) {
+      const attrs = feature.get('_attrs')
+      if (attrs && attrs.type) {
         currentMarker.value = attrs
         detailDialogVisible.value = true
         detailTab.value = 'basic'
+      } else {
+        const title = feature.get('_popupTitle')
+        const content = feature.get('_popupContent')
+        if (title && content) {
+          openPopup(map.value, title, content, event.coordinate)
+        }
       }
-    })
+    }
   })
 
   loadEngData()
@@ -584,7 +555,7 @@ function refreshMap() {
   engLayer = null
   windLayer = null
   typhoonLayer = null
-  coord.value = null
+  coord.value = { lng: 0, lat: 0 }
   errorMsg.value = ''
   initGisMap()
 }

+ 19 - 23
gw-ui/src/views/slgc/gis/monitor.vue

@@ -64,19 +64,20 @@
 <script setup name="GisMonitor">
 import { getMonitorList, getRealtimeTbadd } from '@/api/slgc/monitor/index'
 import { getRealtimeData } from '@/api/slgc/hydrology/index'
-import { useArcGisMap } from '@/composables/useArcGisMap'
+import { useMap } from '@/composables/useMap'
 
 const { proxy } = getCurrentInstance()
 
 const {
   coord,
-  view,
+  map,
   initMap,
   createGraphicsLayer,
   createPointMarker,
   createCircleSymbol,
   openPopup,
-} = useArcGisMap()
+  removeLayer,
+} = useMap()
 
 const loading = ref(true)
 const keyword = ref('')
@@ -187,7 +188,6 @@ function renderMarkers() {
     } else {
       const gl = createGraphicsLayer(key)
       layerInstances[key] = gl
-      view.value.map.add(gl)
       gl.visible = layer.visible
     }
   }
@@ -239,7 +239,6 @@ function renderWarningMarkers() {
   } else {
     const gl = createGraphicsLayer(key)
     layerInstances[key] = gl
-    view.value.map.add(gl)
     gl.visible = false
   }
 
@@ -269,7 +268,6 @@ async function loadArcGisLayer(layerKey, mapServerId) {
     } else {
       const gl = createGraphicsLayer(layerKey)
       layerInstances[layerKey] = gl
-      view.value.map.add(gl)
     }
 
     const gl = layerInstances[layerKey]
@@ -323,7 +321,7 @@ watch(
         if (l.visible && !instance) {
           loadArcGisLayer(l.key, ARCGIS_SERVER_IDS[l.key])
         } else if (!l.visible && instance) {
-          view.value.map.remove(instance)
+          removeLayer(instance)
           delete layerInstances[l.key]
         } else if (instance) {
           instance.visible = l.visible
@@ -339,26 +337,24 @@ watch(
 )
 
 function setupClickHandler() {
-  if (!view.value) return
-  view.value.on('click', (event) => {
-    view.value.hitTest(event).then(response => {
-      const result = response.results.find(r => r.graphic)
-      if (!result) return
-      const graphic = result.graphic
-      const attrs = graphic.attributes
-      if (!attrs) return
-      if (attrs._arcgis) {
-        const title = `ArcGIS: ${attrs['水库名称'] || '-'}`
-        const content = constructArcGisPopupContent(attrs)
-        openPopup(view.value, title, content, graphic.geometry)
+  if (!map.value) return
+  map.value.on('click', (event) => {
+    map.value.forEachFeatureAtPixel(event.pixel, (feature) => {
+      const props = feature.getProperties()
+      if (!props) return true
+      if (props._arcgis) {
+        const title = `ArcGIS: ${props['水库名称'] || '-'}`
+        const content = constructArcGisPopupContent(props)
+        openPopup(map.value, title, content, feature.getGeometry())
       } else {
         openPopup(
-          view.value,
-          `站点: ${attrs.station || '-'}`,
-          `水位: ${attrs.waterLevel || '-'}m<br/>流量: ${attrs.flow || '-'}m³/s<br/>状态: ${attrs.status || '-'}`,
-          graphic.geometry
+          map.value,
+          `站点: ${props.station || '-'}`,
+          `水位: ${props.waterLevel || '-'}m<br/>流量: ${props.flow || '-'}m³/s<br/>状态: ${props.status || '-'}`,
+          feature.getGeometry()
         )
       }
+      return true
     })
   })
 }

+ 17 - 22
gw-ui/src/views/slgc/gis/water.vue

@@ -91,14 +91,15 @@ import { getStationList, getWaterQualityData } from '@/api/slgc/hydrology/index'
 import { getSyqStation } from '@/api/slgc/syq/index'
 import { getAllSWS } from '@/api/slgc/waterSource/index'
 import { ZONE_CONFIG, ZONE_COLORS, ZONE_LABELS } from '@/utils/gis-config'
-import { useArcGisMap } from '@/composables/useArcGisMap'
+import { useMap } from '@/composables/useMap'
 
 const {
   map, view, coord,
-  initMap: initArcGisMap, ensureModules,
+  initMap,
   createGraphicsLayer, createPointMarker, createCircleSymbol,
   openPopup, destroy, addBasinBoundary,
-} = useArcGisMap()
+  removeLayer,
+} = useMap()
 
 const loading = ref(false)
 const keyword = ref('')
@@ -145,7 +146,7 @@ function buildPopupContent(s, zoneCfg) {
 function renderMarkers() {
   zones.forEach(z => {
     if (z.layer) {
-      map.value.remove(z.layer)
+      removeLayer(z.layer)
       z.layer = null
     }
   })
@@ -178,7 +179,6 @@ function renderMarkers() {
       layer.add(graphic)
     })
 
-    map.value.add(layer)
     if (!z.visible) {
       layer.visible = false
     }
@@ -208,25 +208,20 @@ function onSearch() {
   updateTable()
 }
 
-async function initMap() {
-  await ensureModules()
-  const result = await initArcGisMap('viewDiv', [31.2, 120.8], 9)
+async function setupMap() {
+  await initMap('viewDiv', [31.2, 120.8], 9)
   await addBasinBoundary()
 
-  result.view.popup.autoOpenEnabled = false
-
-  result.view.on('click', (event) => {
-    result.view.hitTest(event).then(response => {
-      if (response.results.length > 0) {
-        const graphic = response.results[0].graphic
-        if (graphic && graphic.attributes && graphic.attributes.name) {
-          const attrs = graphic.attributes
-          const zoneCfg = zones.find(z => z.key === attrs.zone)
-          const content = buildPopupContent(attrs, zoneCfg)
-          openPopup(result.view, attrs.name, content, graphic.geometry)
-        }
+  map.value.on('click', (event) => {
+    const feature = map.value.forEachFeatureAtPixel(event.pixel, (f) => f)
+    if (feature) {
+      const attrs = feature.getProperties()
+      if (attrs.name) {
+        const zoneCfg = zones.find(z => z.key === attrs.zone)
+        const content = buildPopupContent(attrs, zoneCfg)
+        openPopup(map.value, attrs.name, content, feature.getGeometry())
       }
-    })
+    }
   })
 
   if (stations.value.length) {
@@ -270,7 +265,7 @@ function refreshData() {
 }
 
 onMounted(() => {
-  initMap()
+  setupMap()
   loadData()
 })
 </script>

+ 21 - 21
gw-ui/src/views/slgc/gis/wiu.vue

@@ -123,14 +123,16 @@ import { getStationList, getRealtimeData, getWaterQualityData } from '@/api/slgc
 import { getSyqNewest, getSjBasinNewest, getSzAutoNewest, getSzLzNewest } from '@/api/slgc/tbaGx/index'
 import { getSyqStation } from '@/api/slgc/syq/index'
 import { WIU_DATA_TYPES, BUSINESS_UNITS } from '@/utils/gis-config'
-import { useArcGisMap } from '@/composables/useArcGisMap'
+import { fromLonLat } from 'ol/proj.js'
+import { useMap } from '@/composables/useMap'
 
 const {
   map, view, coord,
   initMap, addBasinBoundary,
   createGraphicsLayer, createPointMarker, createCircleSymbol,
   openPopup,
-} = useArcGisMap()
+  findLayer, removeLayer,
+} = useMap()
 
 const currentUnit = ref('unit0')
 const keyword = ref('')
@@ -142,7 +144,7 @@ function onUnitChange() {
   dataTypes.forEach(dt => {
     dt.checked = false
     if (dt.layer && map.value) {
-      map.value.removeLayer(dt.layer)
+      removeLayer(dt.layer)
       dt.layer = null
     }
   })
@@ -310,7 +312,7 @@ function onLayerCheckChange(dt) {
   if (dt.checked) {
     loadLayerData(dt)
   } else {
-    removeLayer(dt)
+    clearDataTypeLayer(dt)
   }
 }
 
@@ -392,12 +394,11 @@ function buildDataItem(row, dt, coords) {
 }
 
 function renderLayerItems(items, dt) {
-  if (dt.layer && map.value.findLayerById(dt.layer.id)) {
-    map.value.remove(dt.layer)
+  if (dt.layer && findLayer(dt.layer.id)) {
+    removeLayer(dt.layer)
   }
   const layer = createGraphicsLayer(`wiu-${dt.key}`)
   dt.layer = layer
-  map.value.add(layer)
 
   items.forEach(item => {
     const symbol = createCircleSymbol(dt.color, 14)
@@ -476,10 +477,10 @@ function parseFloatValue(val) {
   return isNaN(n) ? '-' : n
 }
 
-function removeLayer(dt) {
+function clearDataTypeLayer(dt) {
   if (dt.layer && map.value) {
-    const existing = map.value.findLayerById(dt.layer.id)
-    if (existing) map.value.remove(existing)
+    const existing = findLayer(dt.layer.id)
+    if (existing) removeLayer(existing)
     dt.layer = null
   }
   delete typeDataMap[dt.key]
@@ -488,7 +489,7 @@ function removeLayer(dt) {
 function refreshAll() {
   dataTypes.forEach(dt => {
     if (dt.checked) {
-      removeLayer(dt)
+      clearDataTypeLayer(dt)
       loadLayerData(dt)
     }
   })
@@ -500,10 +501,10 @@ function onSearch() {
 
 function onTableRowClick(row) {
   if (!view.value || !row.lng || !row.lat) return
-  view.value.goTo({ center: [row.lng, row.lat], zoom: 12 })
+  view.value.animate({ center: fromLonLat([row.lng, row.lat], 'EPSG:4490'), zoom: 12 })
   const fields = POPUP_FIELDS[row.typeKey] || []
   const content = buildPopupContent(row, fields)
-  openPopup(view.value, row.stnm, content, [row.lng, row.lat])
+  openPopup(map.value, row.stnm, content, [row.lng, row.lat])
 }
 
 function buildPopupContent(item, fields) {
@@ -530,16 +531,15 @@ function buildPopupContent(item, fields) {
 }
 
 function setupClickHandler() {
-  if (!view.value) return
-  view.value.on('click', (event) => {
-    view.value.hitTest(event).then(response => {
-      const result = response.results.find(r => r.graphic && r.graphic.attributes && r.graphic.attributes.stnm)
-      if (!result) return
-      const graphic = result.graphic
-      const attrs = graphic.attributes
+  if (!map.value) return
+  map.value.on('click', (event) => {
+    map.value.forEachFeatureAtPixel(event.pixel, (feature) => {
+      const attrs = feature.getProperties()
+      if (!attrs.stnm) return
       const fields = POPUP_FIELDS[attrs.typeKey] || []
       const content = buildPopupContent(attrs, fields)
-      openPopup(view.value, attrs.stnm, content, graphic.geometry)
+      openPopup(map.value, attrs.stnm, content)
+      return true
     })
   })
 }

+ 29 - 10
gw-ui/src/views/slgc/law/index.vue

@@ -3,15 +3,15 @@
     <el-row :gutter="16">
       <el-col :span="6">
         <el-card shadow="never">
-          <template #header><div class="card-header"><span>法规分类</span><el-button size="small" circle @click="handleAddCate">+</el-button></div></template>
+          <template #header><div class="card-header"><span>法规分类</span><el-button size="small" circle @click="handleAddCate" v-hasRole="['admin']">+</el-button></div></template>
           <el-tree :data="cateTree" :props="{label:'cateName'}" default-expand-all highlight-current
             @node-click="handleCateClick" :expand-on-click-node="false">
             <template #default="{ node, data }">
               <span class="custom-tree-node">
                 <span>{{ node.label }}</span>
                 <span class="tree-actions">
-                  <el-button type="primary" link size="small" @click.stop="handleEditCate(data)">编辑</el-button>
-                  <el-button type="danger" link size="small" @click.stop="handleDeleteCate(data)">删除</el-button>
+                  <el-button type="primary" link size="small" @click.stop="handleEditCate(data)" v-hasRole="['admin']">编辑</el-button>
+                  <el-button type="danger" link size="small" @click.stop="handleDeleteCate(data)" v-hasRole="['admin']">删除</el-button>
                 </span>
               </span>
             </template>
@@ -20,13 +20,14 @@
       </el-col>
       <el-col :span="18">
         <el-card shadow="never">
-          <template #header><div class="card-header"><span>法规列表</span><el-button type="primary" size="small" @click="handleAdd">新增法规</el-button></div></template>
+          <template #header><div class="card-header"><span>法规列表</span><el-button type="primary" size="small" @click="handleAdd" v-hasRole="['admin']">新增法规</el-button></div></template>
           <el-table :data="lawList" stripe v-loading="loading">
             <el-table-column prop="aid" label="序号" width="60" />
             <el-table-column prop="title" label="标题" min-width="200" show-overflow-tooltip />
             <el-table-column prop="subtitle" label="副标题" min-width="150" show-overflow-tooltip />
             <el-table-column prop="cateId" label="分类ID" width="80" />
             <el-table-column prop="pubDate" label="发布日期" width="140" />
+            <el-table-column prop="pubUser" label="发布人" width="100" />
             <el-table-column prop="status" label="状态" width="80">
               <template #default="s"><el-tag size="small">{{ s.row.status === '1' ? '已发布' : '草稿' }}</el-tag></template>
             </el-table-column>
@@ -34,11 +35,21 @@
             <el-table-column label="操作" width="180" fixed="right">
               <template #default="s">
                 <el-button type="primary" link @click="handleView(s.row)">查看</el-button>
-                <el-button type="primary" link @click="handleEdit(s.row)">编辑</el-button>
-                <el-button type="danger" link @click="handleDelete(s.row)">删除</el-button>
+                <el-button type="primary" link @click="handleEdit(s.row)" v-hasRole="['admin']">编辑</el-button>
+                <el-button type="danger" link @click="handleDelete(s.row)" v-hasRole="['admin']">删除</el-button>
               </template>
             </el-table-column>
           </el-table>
+          <el-pagination
+            class="pagination-wrap"
+            v-model:current-page="pageNum"
+            v-model:page-size="pageSize"
+            :page-sizes="[10, 20, 50, 100]"
+            :total="total"
+            layout="total, sizes, prev, pager, next, jumper"
+            @size-change="loadLawList"
+            @current-change="loadLawList"
+          />
         </el-card>
       </el-col>
     </el-row>
@@ -94,9 +105,16 @@ const { proxy } = getCurrentInstance()
 const loading = ref(true); const open = ref(false); const title = ref('')
 const viewOpen = ref(false); const viewData = ref({})
 const cateTree = ref([]); const lawList = ref([]); const form = ref({})
+const currentCateId = ref('')
 const cateOpen = ref(false); const cateTitle = ref(''); const cateForm = ref({ cateId: '', cateName: '', cateMemo: '', cateSeq: 0 })
+const pageNum = ref(1); const pageSize = ref(20); const total = ref(0)
 
-function handleCateClick(d) { getLawList({ cateId: d.cateId }).then(r => { lawList.value = r.rows }) }
+function handleCateClick(d) { currentCateId.value = d.cateId; pageNum.value = 1; loadLawList() }
+function loadLawList() {
+  const params = { pageNum: pageNum.value, pageSize: pageSize.value }
+  if (currentCateId.value) params.cateId = currentCateId.value
+  getLawList(params).then(r => { lawList.value = r.rows; total.value = r.total || 0; loading.value = false })
+}
 function handleAdd() { form.value = {}; title.value = '新增法规'; open.value = true }
 function handleView(r) {
   getLawDetail(r.aid).then(res => { viewData.value = res.data; viewOpen.value = true })
@@ -104,8 +122,8 @@ function handleView(r) {
 function handleEdit(r) {
   getLawDetail(r.aid).then(res => { form.value = res.data; title.value = '编辑法规'; open.value = true })
 }
-function handleDelete(r) { proxy.$modal.confirm('确认删除?').then(() => { delLaw(r.aid).then(() => { ElMessage.success('删除成功'); getLawList().then(r2 => { lawList.value = r2.rows }) }) }) }
-function submitForm() { saveLaw(form.value).then(() => { proxy.$modal.msgSuccess('操作成功'); open.value = false; getLawList().then(r => { lawList.value = r.rows }) }) }
+function handleDelete(r) { proxy.$modal.confirm('确认删除?').then(() => { delLaw(r.aid).then(() => { ElMessage.success('删除成功'); loadLawList() }) }) }
+function submitForm() { saveLaw(form.value).then(() => { proxy.$modal.msgSuccess('操作成功'); open.value = false; loadLawList() }) }
 
 function handleAddCate() { cateForm.value = { cateId: '', cateName: '', cateMemo: '', cateSeq: 0 }; cateTitle.value = '新增分类'; cateOpen.value = true }
 function handleEditCate(data) {
@@ -127,7 +145,7 @@ function submitCateForm() {
 function loadCateTree() { getLawCateTree().then(r => { cateTree.value = r.data || r.rows || [] }) }
 onMounted(() => {
   loadCateTree()
-  getLawList().then(r => { lawList.value = r.rows; loading.value = false })
+  loadLawList()
 })
 </script>
 <style scoped>
@@ -136,4 +154,5 @@ onMounted(() => {
 .tree-actions { display: none; }
 .custom-tree-node:hover .tree-actions { display: inline; }
 .law-content { margin-top: 16px; padding: 16px; border: 1px solid #e5e5e5; border-radius: 4px; line-height: 1.8; max-height: 400px; overflow-y: auto; }
+.pagination-wrap { margin-top: 12px; display: flex; justify-content: flex-end; }
 </style>

+ 13 - 21
gw-ui/src/views/slgc/monitor/index.vue

@@ -39,16 +39,19 @@
 <script setup name="MonitorIndex">
 import { getPortalData } from '@/api/slgc/zggc/index'
 import { ref, onMounted, nextTick } from 'vue'
-import { useArcGisMap } from '@/composables/useArcGisMap'
+import { fromLonLat } from 'ol/proj.js'
+import { useMap } from '@/composables/useMap'
 
 const {
+  map,
   view,
   initMap,
   createGraphicsLayer,
   createPointMarker,
   createCircleSymbol,
   addBasinBoundary,
-} = useArcGisMap()
+  openPopup,
+} = useMap()
 
 const mapLoading = ref(true)
 const stationList = ref([])
@@ -92,12 +95,8 @@ async function selectStation(i) {
   const name = st.name || st.stationName
   const coords = stationCoords[name]
   if (!coords || !view.value) return
-  await view.value.goTo({ center: coords, zoom: 11 })
-  view.value.popup.open({
-    title: name,
-    content: popupContent(st),
-    location: coords,
-  })
+  await view.value.animate({ center: fromLonLat(coords, 'EPSG:4490'), zoom: 11 })
+  openPopup(map.value, name, popupContent(st), fromLonLat(coords, 'EPSG:4490'))
 }
 
 async function renderMap() {
@@ -105,7 +104,6 @@ async function renderMap() {
   await addBasinBoundary()
 
   const layer = createGraphicsLayer('stations')
-  view.value.map.add(layer)
 
   stationList.value.forEach((st, i) => {
     const name = st.name || st.stationName
@@ -117,21 +115,15 @@ async function renderMap() {
     layer.add(g)
   })
 
-  view.value.on('click', (evt) => {
-    view.value.hitTest(evt).then((res) => {
-      const hit = res.results.find(
-        (r) => r.graphic && r.graphic.attributes && r.graphic.attributes[STATION_IDX] !== undefined
-      )
-      if (!hit) return
-      const idx = hit.graphic.attributes[STATION_IDX]
+  map.value.on('click', (evt) => {
+    map.value.forEachFeatureAtPixel(evt.pixel, (feature) => {
+      const idx = feature.get(STATION_IDX)
+      if (idx === undefined || idx === null) return
       activeStation.value = idx
       const st = stationList.value[idx]
       const name = st.name || st.stationName
-      view.value.popup.open({
-        title: name,
-        content: popupContent(st),
-        location: hit.graphic.geometry,
-      })
+      openPopup(map.value, name, popupContent(st), feature.getGeometry().getCoordinates())
+      return true
     })
   })
 

+ 46 - 18
gw-ui/src/views/slgc/pust/index.vue

@@ -3,12 +3,12 @@
     <el-card shadow="never">
       <template #header>
         <div class="card-header">
-          <span>水闸工程管理</span>
-          <el-button type="primary" size="small" style="float:right" @click="handleAdd">新增水闸</el-button>
+          <span>泵站工程管理</span>
+          <el-button type="primary" size="small" style="float:right" @click="handleAdd">新增泵站</el-button>
         </div>
       </template>
       <el-form :model="queryParams" :inline="true" size="small">
-        <el-form-item label="水闸名称"><el-input v-model="queryParams.pustName" placeholder="请输入" clearable @keyup.enter="handleQuery" /></el-form-item>
+        <el-form-item label="泵站名称"><el-input v-model="queryParams.pustName" placeholder="请输入" clearable @keyup.enter="handleQuery" /></el-form-item>
         <el-form-item label="管理单位"><el-input v-model="queryParams.admDep" placeholder="请输入" clearable @keyup.enter="handleQuery" /></el-form-item>
         <el-form-item>
           <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
@@ -18,13 +18,21 @@
       </el-form>
       <el-table v-loading="loading" :data="list" stripe>
         <el-table-column prop="pustCode" label="编码" width="100" />
-        <el-table-column prop="pustName" label="水闸名称" min-width="140" />
+        <el-table-column prop="pustName" label="泵站名称" min-width="140" />
         <el-table-column prop="pustLoc" label="位置" width="140" />
-        <el-table-column prop="pustType" label="类型" width="80" />
+        <el-table-column prop="pustType" label="类型" width="100">
+          <template #default="s">{{ pustTypeMap[s.row.pustType] || s.row.pustType }}</template>
+        </el-table-column>
         <el-table-column prop="insFlow" label="流量(m³/s)" width="110" />
-        <el-table-column prop="engGrad" label="等级" width="60" />
+        <el-table-column prop="engGrad" label="工程等别" width="80">
+          <template #default="s">{{ engGradMap[s.row.engGrad] || s.row.engGrad }}</template>
+        </el-table-column>
         <el-table-column prop="engStat" label="状态" width="70">
-          <template #default="s"><el-tag :type="s.row.engStat === '1' ? 'success' : 'warning'" size="small">{{ s.row.engStat === '1' ? '正常' : s.row.engStat === '0' ? '停用' : s.row.engStat }}</el-tag></template>
+          <template #default="s">
+            <el-tag :type="s.row.engStat === '1' ? 'success' : 'warning'" size="small">
+              {{ s.row.engStat === '1' ? '正常' : s.row.engStat === '0' ? '停用' : s.row.engStat }}
+            </el-tag>
+          </template>
         </el-table-column>
         <el-table-column prop="updDate" label="更新日期" width="120" />
         <el-table-column label="操作" width="200" fixed="right">
@@ -40,20 +48,38 @@
     <el-dialog :title="title" v-model="open" width="720px" append-to-body>
       <el-form :model="form" label-width="130px">
         <el-row>
-          <el-col :span="12"><el-form-item label="水闸编码"><el-input v-model="form.pustCode" :disabled="title === '编辑水闸'" /></el-form-item></el-col>
-          <el-col :span="12"><el-form-item label="水闸名称"><el-input v-model="form.pustName" /></el-form-item></el-col>
+          <el-col :span="12"><el-form-item label="泵站编码"><el-input v-model="form.pustCode" :disabled="title === '编辑泵站'" /></el-form-item></el-col>
+          <el-col :span="12"><el-form-item label="泵站名称"><el-input v-model="form.pustName" /></el-form-item></el-col>
         </el-row>
         <el-row>
           <el-col :span="12"><el-form-item label="位置"><el-input v-model="form.pustLoc" /></el-form-item></el-col>
-          <el-col :span="12"><el-form-item label="类型"><el-input v-model="form.pustType" /></el-form-item></el-col>
+          <el-col :span="12"><el-form-item label="类型">
+            <el-select v-model="form.pustType" placeholder="请选择类型">
+              <el-option label="排水泵站" value="1" />
+              <el-option label="供水泵站" value="2" />
+              <el-option label="供排结合泵站" value="3" />
+            </el-select>
+          </el-form-item></el-col>
         </el-row>
         <el-row>
           <el-col :span="12"><el-form-item label="装机流量(m³/s)"><el-input-number v-model="form.insFlow" :min="0" :step="0.1" /></el-form-item></el-col>
           <el-col :span="12"><el-form-item label="装机功率(kW)"><el-input-number v-model="form.insPow" :min="0" :step="0.1" /></el-form-item></el-col>
         </el-row>
         <el-row>
-          <el-col :span="12"><el-form-item label="水泵数量"><el-input-number v-model="form.pumpNum" :min="0" /></el-form-item></el-col>
-          <el-col :span="12"><el-form-item label="工程状态"><el-select v-model="form.engStat"><el-option label="正常" value="1" /><el-option label="停用" value="0" /></el-select></el-form-item></el-col>
+          <el-col :span="12"><el-form-item label="工程等别">
+            <el-select v-model="form.engGrad" placeholder="请选择等别">
+              <el-option label="Ⅰ等" value="1" />
+              <el-option label="Ⅱ等" value="2" />
+              <el-option label="Ⅲ等" value="3" />
+              <el-option label="Ⅳ等" value="4" />
+              <el-option label="Ⅴ等" value="5" />
+            </el-select>
+          </el-form-item></el-col>
+          <el-col :span="12"><el-form-item label="工程状态">
+            <el-select v-model="form.engStat">
+              <el-option label="正常" value="1" /><el-option label="停用" value="0" />
+            </el-select>
+          </el-form-item></el-col>
         </el-row>
         <el-row>
           <el-col :span="12"><el-form-item label="管理单位"><el-input v-model="form.admDep" /></el-form-item></el-col>
@@ -66,7 +92,7 @@
       </template>
     </el-dialog>
 
-    <el-dialog v-model="statsVisible" title="水闸类型分布统计" width="600px" append-to-body>
+    <el-dialog v-model="statsVisible" title="泵站类型分布统计" width="600px" append-to-body>
       <el-table :data="statsData" stripe v-loading="statsLoading">
         <el-table-column prop="name" label="类型" min-width="120" />
         <el-table-column prop="value" label="数量" width="100" />
@@ -86,22 +112,24 @@ const form = ref({})
 const statsVisible = ref(false)
 const statsLoading = ref(false)
 const statsData = ref([])
+const pustTypeMap = { '1': '排水泵站', '2': '供水泵站', '3': '供排结合泵站' }
+const engGradMap = { '1': 'Ⅰ等', '2': 'Ⅱ等', '3': 'Ⅲ等', '4': 'Ⅳ等', '5': 'Ⅴ等' }
 function getList() { loading.value = true; getPustList(queryParams).then(res => { list.value = res.rows; total.value = res.total; loading.value = false }) }
 function handleQuery() { queryParams.pageNum = 1; getList() }
 function resetQuery() { queryParams.pustName = undefined; queryParams.admDep = undefined; handleQuery() }
-function handleDetail(row) { getPust(row.pustCode).then(res => { form.value = res.data; title.value = '水闸详情'; open.value = true }) }
-function handleEdit(row) { getPust(row.pustCode).then(res => { form.value = res.data; title.value = '编辑水闸'; open.value = true }) }
-function submitForm() { const method = form.value.pustCode && title.value === '编辑水闸' ? updatePust : addPust; method(form.value).then(() => { proxy.$modal.msgSuccess('操作成功'); open.value = false; getList() }) }
+function handleDetail(row) { getPust(row.pustCode).then(res => { form.value = res.data; title.value = '泵站详情'; open.value = true }) }
+function handleEdit(row) { getPust(row.pustCode).then(res => { form.value = res.data; title.value = '编辑泵站'; open.value = true }) }
+function submitForm() { const method = form.value.pustCode && title.value === '编辑泵站' ? updatePust : addPust; method(form.value).then(() => { proxy.$modal.msgSuccess('操作成功'); open.value = false; getList() }) }
 function cancel() { open.value = false }
 
 function handleAdd() {
   form.value = { engStat: '1' }
-  title.value = '新增水闸'
+  title.value = '新增泵站'
   open.value = true
 }
 
 function handleDelete(row) {
-  proxy.$modal.confirm('确认删除水闸"' + row.pustName + '"?').then(() => {
+  proxy.$modal.confirm('确认删除泵站"' + row.pustName + '"?').then(() => {
     delPust(row.pustCode).then(() => {
       proxy.$modal.msgSuccess('删除成功')
       getList()

+ 35 - 10
gw-ui/src/views/slgc/reservoir/index.vue

@@ -26,13 +26,18 @@
         <el-table-column prop="resCode" label="编码" width="100" />
         <el-table-column prop="resName" label="水库名称" min-width="140" />
         <el-table-column prop="resLoc" label="位置" width="140" />
-        <el-table-column prop="engScal" label="规模" width="90" />
-        <el-table-column prop="totCap" label="总库容(万m³)" width="120" />
-        <el-table-column prop="watShedArea" label="集水面积(km²)" width="120" />
-        <el-table-column prop="engStat" label="状态" width="80">
+        <el-table-column prop="resType" label="类型" width="90">
+          <template #default="scope">{{ resTypeMap[scope.row.resType] || scope.row.resType }}</template>
+        </el-table-column>
+        <el-table-column prop="engGrad" label="工程等别" width="80">
+          <template #default="scope">{{ engGradMap[scope.row.engGrad] || scope.row.engGrad }}</template>
+        </el-table-column>
+        <el-table-column prop="totCap" label="总库容(万m³)" width="110" />
+        <el-table-column prop="watShedArea" label="集水面积(km²)" width="110" />
+        <el-table-column prop="runStat" label="运行状况" width="100">
           <template #default="scope">
-            <el-tag :type="scope.row.engStat === '1' ? 'success' : 'warning'" size="small">
-              {{ scope.row.engStat === '1' ? '正常' : scope.row.engStat === '0' ? '停用' : scope.row.engStat }}
+            <el-tag :type="scope.row.runStat === '1' ? 'success' : scope.row.runStat === '2' ? 'warning' : 'info'" size="small">
+              {{ runStatMap[scope.row.runStat] || scope.row.runStat }}
             </el-tag>
           </template>
         </el-table-column>
@@ -57,9 +62,24 @@
         </el-row>
         <el-row>
           <el-col :span="12"><el-form-item label="位置"><el-input v-model="form.resLoc" /></el-form-item></el-col>
-          <el-col :span="12"><el-form-item label="类型"><el-input v-model="form.resType" /></el-form-item></el-col>
+          <el-col :span="12"><el-form-item label="类型">
+            <el-select v-model="form.resType" placeholder="请选择类型">
+              <el-option label="山丘水库" value="1" />
+              <el-option label="平原水库" value="2" />
+              <el-option label="地下水库" value="3" />
+            </el-select>
+          </el-form-item></el-col>
         </el-row>
         <el-row>
+          <el-col :span="12"><el-form-item label="工程等别">
+            <el-select v-model="form.engGrad" placeholder="请选择等别">
+              <el-option label="Ⅰ等" value="1" />
+              <el-option label="Ⅱ等" value="2" />
+              <el-option label="Ⅲ等" value="3" />
+              <el-option label="Ⅳ等" value="4" />
+              <el-option label="Ⅴ等" value="5" />
+            </el-select>
+          </el-form-item></el-col>
           <el-col :span="12"><el-form-item label="总库容(万m³)"><el-input-number v-model="form.totCap" :min="0" :step="0.01" /></el-form-item></el-col>
           <el-col :span="12"><el-form-item label="正常库容(万m³)"><el-input-number v-model="form.normPoolStagCap" :min="0" :step="0.01" /></el-form-item></el-col>
         </el-row>
@@ -69,8 +89,10 @@
         </el-row>
         <el-row>
           <el-col :span="12"><el-form-item label="集水面积(km²)"><el-input-number v-model="form.watShedArea" :min="0" :step="0.1" /></el-form-item></el-col>
-          <el-col :span="12"><el-form-item label="工程状态">
-            <el-select v-model="form.engStat"><el-option label="正常" value="1" /><el-option label="停用" value="0" /></el-select>
+          <el-col :span="12"><el-form-item label="运行状况">
+            <el-select v-model="form.runStat">
+              <el-option label="在用良好" value="1" /><el-option label="在用故障" value="2" /><el-option label="停用" value="3" />
+            </el-select>
           </el-form-item></el-col>
         </el-row>
         <el-row>
@@ -141,6 +163,9 @@ const form = ref({})
 const statsVisible = ref(false)
 const statsLoading = ref(false)
 const statsData = ref([])
+const resTypeMap = { '1': '山丘水库', '2': '平原水库', '3': '地下水库' }
+const engGradMap = { '1': 'Ⅰ等', '2': 'Ⅱ等', '3': 'Ⅲ等', '4': 'Ⅳ等', '5': 'Ⅴ等' }
+const runStatMap = { '1': '在用良好', '2': '在用故障', '3': '停用' }
 const personVisible = ref(false)
 const personTab = ref('admin')
 const personData = ref({})
@@ -167,7 +192,7 @@ function submitForm() {
 function cancel() { open.value = false }
 
 function handleAdd() {
-  form.value = { engStat: '1' }
+  form.value = { runStat: '1' }
   title.value = '新增水库'
   open.value = true
 }

+ 4 - 35
gw-ui/src/views/slgc/unit/edit.vue

@@ -16,31 +16,19 @@
               <el-input v-model="form.name" placeholder="请输入单位名称" />
             </el-form-item>
           </el-col>
-          <el-col :span="12">
-            <el-form-item label="单位类型" prop="type">
-              <el-select v-model="form.type" placeholder="请选择">
-                <el-option label="供水" value="供水" />
-                <el-option label="灌溉" value="灌溉" />
-                <el-option label="工业用水" value="工业用水" />
-                <el-option label="生活用水" value="生活用水" />
-              </el-select>
-            </el-form-item>
-          </el-col>
-        </el-row>
-        <el-row>
           <el-col :span="12">
             <el-form-item label="省份" prop="province">
               <el-input v-model="form.province" placeholder="请输入省份" />
             </el-form-item>
           </el-col>
+        </el-row>
+        <el-row>
           <el-col :span="12">
             <el-form-item label="城市" prop="city">
               <el-input v-model="form.city" placeholder="请输入城市" />
             </el-form-item>
           </el-col>
-        </el-row>
-        <el-row>
-          <el-col :span="24">
+          <el-col :span="12">
             <el-form-item label="详细地址" prop="address">
               <el-input v-model="form.address" placeholder="请输入详细地址" />
             </el-form-item>
@@ -76,26 +64,11 @@
               <el-input v-model="form.fax" placeholder="请输入传真号码" />
             </el-form-item>
           </el-col>
-          <el-col :span="12">
-            <el-form-item label="所在区域" prop="area">
-              <el-input v-model="form.area" placeholder="请输入所在区域" />
-            </el-form-item>
-          </el-col>
-        </el-row>
-        <el-row>
           <el-col :span="12">
             <el-form-item label="批准日期" prop="approvalDate">
               <el-date-picker v-model="form.approvalDate" type="date" value-format="YYYY-MM-DD" placeholder="请选择批准日期" style="width:100%" />
             </el-form-item>
           </el-col>
-          <el-col :span="12">
-            <el-form-item label="状态" prop="status">
-              <el-radio-group v-model="form.status">
-                <el-radio value="正常">正常</el-radio>
-                <el-radio value="暂停">暂停</el-radio>
-              </el-radio-group>
-            </el-form-item>
-          </el-col>
         </el-row>
         <el-form-item>
           <el-button type="primary" :loading="submitLoading" @click="submitForm">保 存</el-button>
@@ -117,7 +90,6 @@ const isEdit = computed(() => !!route.query.id)
 
 const form = ref({
   name: '',
-  type: '',
   province: '',
   city: '',
   address: '',
@@ -126,14 +98,11 @@ const form = ref({
   contacts: '',
   telphone: '',
   fax: '',
-  area: '',
-  approvalDate: '',
-  status: '正常'
+  approvalDate: ''
 })
 
 const rules = {
   name: [{ required: true, message: '请输入单位名称', trigger: 'blur' }],
-  type: [{ required: true, message: '请选择单位类型', trigger: 'change' }],
   province: [{ required: true, message: '请输入省份', trigger: 'blur' }],
   city: [{ required: true, message: '请输入城市', trigger: 'blur' }],
   address: [{ required: true, message: '请输入详细地址', trigger: 'blur' }],

+ 26 - 10
gw-ui/src/views/slgc/unit/file.vue

@@ -7,18 +7,26 @@
             <el-button text @click="$router.back()">← 返回</el-button>
             <span>单位档案</span>
           </el-space>
-          <el-upload
-            ref="uploadRef"
-            :action="uploadUrl"
-            :data="{ fileCate: 'UNIT-' + unitId }"
-            :show-file-list="false"
-            :before-upload="beforeUpload"
-            :on-success="handleUploadSuccess"
-            :on-error="handleUploadError">
-            <el-button type="primary" size="small">上传文件</el-button>
-          </el-upload>
+          <el-button type="primary" size="small" @click="uploadDialogVisible = true">上传文件</el-button>
         </div>
       </template>
+      <el-dialog v-model="uploadDialogVisible" title="上传文件" width="480px" :close-on-click-modal="false" @closed="resetUploadForm">
+        <el-form label-width="80px">
+          <el-form-item label="标题">
+            <el-input v-model="fileTitle" placeholder="请输入文件标题" maxlength="100" />
+          </el-form-item>
+        </el-form>
+        <el-upload
+          ref="uploadRef"
+          :action="uploadUrl"
+          :data="{ fileCate: 'UNIT-' + unitId, fileTitle: fileTitle }"
+          :show-file-list="false"
+          :before-upload="beforeUpload"
+          :on-success="handleUploadSuccess"
+          :on-error="handleUploadError">
+          <el-button type="primary" size="small">选择文件</el-button>
+        </el-upload>
+      </el-dialog>
       <div v-if="fileList.length === 0">
         <el-empty description="暂无档案文件" />
       </div>
@@ -45,6 +53,8 @@ const unitId = ref(route.query.id || '')
 const fileList = ref([])
 const hoverId = ref('')
 const uploadUrl = ref('/slgc/doc-file/upload')
+const uploadDialogVisible = ref(false)
+const fileTitle = ref('')
 
 function loadFiles() {
   if (!unitId.value) return
@@ -77,12 +87,18 @@ function beforeUpload(file) {
 function handleUploadSuccess(res) {
   if (res.code === 200) {
     ElMessage.success('上传成功')
+    uploadDialogVisible.value = false
+    fileTitle.value = ''
     loadFiles()
   } else {
     ElMessage.error(res.msg || '上传失败')
   }
 }
 
+function resetUploadForm() {
+  fileTitle.value = ''
+}
+
 function handleUploadError() {
   ElMessage.error('上传失败')
 }

+ 6 - 10
gw-ui/src/views/slgc/unit/index.vue

@@ -25,17 +25,19 @@
       <el-table v-loading="loading" :data="list" stripe :row-class-name="rowClassName" @selection-change="handleSelectionChange">
         <el-table-column type="selection" width="50" />
         <el-table-column prop="id" label="ID" width="60" />
-        <el-table-column prop="name" label="单位名称" min-width="160" />
+        <el-table-column prop="name" label="单位名称" min-width="160" show-overflow-tooltip />
         <el-table-column prop="province" label="省" width="80" />
         <el-table-column prop="city" label="市" width="80" />
+        <el-table-column prop="address" label="地址" min-width="150" show-overflow-tooltip />
         <el-table-column prop="contacts" label="联系人" width="100" />
         <el-table-column prop="telphone" label="联系电话" width="130" />
-        <el-table-column prop="fax" label="传真" width="120" />
+        <el-table-column prop="longitude" label="经度" width="100" />
+        <el-table-column prop="latitude" label="纬度" width="100" />
         <el-table-column prop="approvalDate" label="批准日期" width="120" />
         <el-table-column label="操作" width="240" fixed="right">
           <template #default="s">
             <el-button type="warning" link icon="Folder" @click="$router.push('/slgc/unit/file?id=' + s.row.id)">附件</el-button>
-            <el-button type="primary" link @click="$router.push('/slgc/unit/record?id=' + s.row.id)">用水记录</el-button>
+            <el-button type="primary" link @click="$router.push('/slgc/unit/record?id=' + s.row.id)">复核记录</el-button>
             <el-button type="primary" link icon="Edit" @click="handleEdit(s.row)">编辑</el-button>
             <el-button type="danger" link icon="Delete" @click="handleDelete(s.row)">删除</el-button>
           </template>
@@ -99,13 +101,7 @@ function rowClassName({ row }) {
 function getList() {
   loading.value = true
   getUnitList(queryParams).then(res => {
-    let rows = res.rows || []
-    if (queryParams.overdue === 'yes') {
-      rows = rows.filter(r => isOverdue(r))
-    } else if (queryParams.overdue === 'no') {
-      rows = rows.filter(r => !isOverdue(r))
-    }
-    list.value = rows
+    list.value = res.rows || []
     total.value = res.total
     loading.value = false
   })

+ 12 - 25
gw-ui/src/views/slgc/unit/national.vue

@@ -55,6 +55,7 @@
 
 <script setup name="NationalUnit">
 import { getCurrentInstance } from 'vue'
+import { getUnitList, getUnit } from '@/api/slgc/unit/index'
 
 const { proxy } = getCurrentInstance()
 const loading = ref(false)
@@ -65,31 +66,15 @@ const detail = ref({})
 const queryParams = reactive({ pageNum: 1, pageSize: 10, name: undefined, year: undefined })
 const yearOptions = [2020, 2021, 2022, 2023, 2024, 2025]
 
-const mockData = [
-  { id: 1, name: '太湖流域管理局', location: '上海', level: '一级', year: 2020, status: '有效', projectCount: 12, mainProjects: '太浦河工程、望虞河工程', manageScope: '太湖流域', address: '上海市虹口区纪念路480号' },
-  { id: 2, name: '江苏省水利厅', location: '南京', level: '一级', year: 2021, status: '有效', projectCount: 8, mainProjects: '长江堤防工程、秦淮河治理', manageScope: '江苏省境内', address: '南京市鼓楼区上海路5号' },
-  { id: 3, name: '浙江省水利厅', location: '杭州', level: '一级', year: 2021, status: '有效', projectCount: 10, mainProjects: '钱塘江干堤、新安江水库', manageScope: '浙江省境内', address: '杭州市上城区梅花碑7号' },
-  { id: 4, name: '安徽省水利厅', location: '合肥', level: '二级', year: 2022, status: '有效', projectCount: 6, mainProjects: '淮河干流治理、巢湖防洪', manageScope: '安徽省境内', address: '合肥市包河区九华山路48号' },
-  { id: 5, name: '江西省水利厅', location: '南昌', level: '二级', year: 2022, status: '有效', projectCount: 7, mainProjects: '鄱阳湖治理、赣江堤防', manageScope: '江西省境内', address: '南昌市西湖区中山西路68号' },
-  { id: 6, name: '福建省水利厅', location: '福州', level: '一级', year: 2023, status: '有效', projectCount: 9, mainProjects: '闽江防洪工程、九龙江治理', manageScope: '福建省境内', address: '福州市鼓楼区东大路229号' },
-  { id: 7, name: '山东省水利厅', location: '济南', level: '二级', year: 2020, status: '有效', projectCount: 5, mainProjects: '黄河山东段治理、南四湖防洪', manageScope: '山东省境内', address: '济南市历下区历山路127号' },
-  { id: 8, name: '广东省水利厅', location: '广州', level: '一级', year: 2021, status: '有效', projectCount: 11, mainProjects: '珠江堤防、东江治理工程', manageScope: '广东省境内', address: '广州市天河区天寿路116号' },
-]
-
 function getList() {
   loading.value = true
-  let data = [...mockData]
-  if (queryParams.name) {
-    data = data.filter(r => r.name.includes(queryParams.name))
-  }
-  if (queryParams.year) {
-    data = data.filter(r => r.year === queryParams.year)
-  }
-  total.value = data.length
-  const start = (queryParams.pageNum - 1) * queryParams.pageSize
-  const end = start + queryParams.pageSize
-  list.value = data.slice(start, end)
-  loading.value = false
+  getUnitList(queryParams).then(res => {
+    list.value = res.rows || []
+    total.value = res.total || 0
+    loading.value = false
+  }).catch(() => {
+    loading.value = false
+  })
 }
 
 function handleQuery() {
@@ -104,8 +89,10 @@ function resetQuery() {
 }
 
 function handleView(row) {
-  detail.value = row
-  open.value = true
+  getUnit(row.id).then(res => {
+    detail.value = res.data || {}
+    open.value = true
+  })
 }
 
 onMounted(() => { getList() })

+ 29 - 3
gw-ui/src/views/slgc/unit/record/index.vue

@@ -5,12 +5,16 @@
         <div class="card-header">
           <el-space>
             <el-button text @click="$router.back()">← 返回</el-button>
-            <span>审核记录</span>
+            <span>复核记录</span>
+          </el-space>
+          <el-space>
+            <el-button v-if="selectedRows.length" type="danger" size="small" icon="Delete" @click="handleBatchDelete">批量删除 ({{ selectedRows.length }})</el-button>
+            <el-button type="primary" size="small" @click="handleAdd">新增记录</el-button>
           </el-space>
-          <el-button type="primary" size="small" @click="handleAdd">新增记录</el-button>
         </div>
       </template>
-      <el-table v-loading="loading" :data="list" stripe>
+      <el-table v-loading="loading" :data="list" stripe @selection-change="handleSelectionChange">
+        <el-table-column type="selection" width="50" />
         <el-table-column prop="id" label="ID" width="60" />
         <el-table-column prop="approvalNumber" label="审核编号" min-width="160" />
         <el-table-column prop="approvalDate" label="审核日期" width="160" />
@@ -52,6 +56,7 @@ const { proxy } = getCurrentInstance()
 const route = useRoute()
 const loading = ref(true); const total = ref(0)
 const list = ref([])
+const selectedRows = ref([])
 const queryParams = reactive({ pageNum: 1, pageSize: 10, unitId: route.query.id })
 
 const dialogVisible = ref(false)
@@ -95,6 +100,27 @@ function handleDelete(row) {
   })
 }
 
+function handleSelectionChange(rows) {
+  selectedRows.value = rows
+}
+
+function handleBatchDelete() {
+  const count = selectedRows.value.length
+  proxy.$modal.confirm(`确认删除选中的 ${count} 条审核记录?`).then(async () => {
+    try {
+      for (const row of selectedRows.value) {
+        await delRecord(row.id)
+      }
+      proxy.$modal.msgSuccess('批量删除成功')
+      selectedRows.value = []
+      getList()
+    } catch {
+      proxy.$modal.msgError('部分删除失败')
+      getList()
+    }
+  })
+}
+
 function submitRecord() {
   recordFormRef.value.validate(valid => {
     if (!valid) return

+ 55 - 22
gw-ui/src/views/slgc/waga/index.vue

@@ -3,12 +3,12 @@
     <el-card shadow="never">
       <template #header>
         <div class="card-header">
-          <span>泵站工程管理</span>
-          <el-button type="primary" size="small" style="float:right" @click="handleAdd">新增泵站</el-button>
+          <span>水闸工程管理</span>
+          <el-button type="primary" size="small" style="float:right" @click="handleAdd">新增水闸</el-button>
         </div>
       </template>
       <el-form :model="queryParams" :inline="true" size="small">
-        <el-form-item label="泵站名称"><el-input v-model="queryParams.wagaName" placeholder="请输入" clearable @keyup.enter="handleQuery" /></el-form-item>
+        <el-form-item label="水闸名称"><el-input v-model="queryParams.wagaName" placeholder="请输入" clearable @keyup.enter="handleQuery" /></el-form-item>
         <el-form-item label="管理单位"><el-input v-model="queryParams.admDep" placeholder="请输入" clearable @keyup.enter="handleQuery" /></el-form-item>
         <el-form-item>
           <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
@@ -18,13 +18,21 @@
       </el-form>
       <el-table v-loading="loading" :data="list" stripe>
         <el-table-column prop="wagaCode" label="编码" width="100" />
-        <el-table-column prop="wagaName" label="泵站名称" min-width="140" />
+        <el-table-column prop="wagaName" label="水闸名称" min-width="140" />
         <el-table-column prop="wagaLoc" label="位置" width="140" />
-        <el-table-column prop="wagaType" label="类型" width="80" />
-        <el-table-column prop="insPow" label="功率(kW)" width="100" />
-        <el-table-column prop="engGrad" label="等级" width="60" />
-        <el-table-column prop="engStat" label="状态" width="70">
-          <template #default="s"><el-tag :type="s.row.engStat === '1' ? 'success' : 'warning'" size="small">{{ s.row.engStat === '1' ? '正常' : s.row.engStat === '0' ? '停用' : s.row.engStat }}</el-tag></template>
+        <el-table-column prop="wagaType" label="类型" width="100">
+          <template #default="s">{{ wagaTypeMap[s.row.wagaType] || s.row.wagaType }}</template>
+        </el-table-column>
+        <el-table-column prop="engGrad" label="工程等别" width="80">
+          <template #default="s">{{ engGradMap[s.row.engGrad] || s.row.engGrad }}</template>
+        </el-table-column>
+        <el-table-column prop="lockDisc" label="最大过闸流量(m³/s)" width="130" />
+        <el-table-column prop="runStat" label="运行状况" width="100">
+          <template #default="s">
+            <el-tag :type="s.row.runStat === '1' ? 'success' : s.row.runStat === '2' ? 'warning' : 'info'" size="small">
+              {{ runStatMap[s.row.runStat] || s.row.runStat }}
+            </el-tag>
+          </template>
         </el-table-column>
         <el-table-column prop="updDate" label="更新日期" width="120" />
         <el-table-column label="操作" width="200" fixed="right">
@@ -40,19 +48,41 @@
     <el-dialog :title="title" v-model="open" width="720px" append-to-body>
       <el-form :model="form" label-width="130px">
         <el-row>
-          <el-col :span="12"><el-form-item label="泵站编码"><el-input v-model="form.wagaCode" :disabled="title === '编辑泵站'" /></el-form-item></el-col>
-          <el-col :span="12"><el-form-item label="泵站名称"><el-input v-model="form.wagaName" /></el-form-item></el-col>
+          <el-col :span="12"><el-form-item label="水闸编码"><el-input v-model="form.wagaCode" :disabled="title === '编辑水闸'" /></el-form-item></el-col>
+          <el-col :span="12"><el-form-item label="水闸名称"><el-input v-model="form.wagaName" /></el-form-item></el-col>
         </el-row>
         <el-row>
           <el-col :span="12"><el-form-item label="位置"><el-input v-model="form.wagaLoc" /></el-form-item></el-col>
-          <el-col :span="12"><el-form-item label="类型"><el-input v-model="form.wagaType" /></el-form-item></el-col>
+          <el-col :span="12"><el-form-item label="类型">
+            <el-select v-model="form.wagaType" placeholder="请选择类型">
+              <el-option label="分(泄)洪闸" value="1" />
+              <el-option label="节制闸" value="2" />
+              <el-option label="排(退)水闸" value="3" />
+              <el-option label="引(进)水闸" value="4" />
+              <el-option label="挡潮闸" value="5" />
+              <el-option label="船闸" value="6" />
+              <el-option label="其他" value="9" />
+            </el-select>
+          </el-form-item></el-col>
         </el-row>
         <el-row>
-          <el-col :span="12"><el-form-item label="装机功率(kW)"><el-input-number v-model="form.insPow" :min="0" :step="0.1" /></el-form-item></el-col>
-          <el-col :span="12"><el-form-item label="设计总装机(kW)"><el-input-number v-model="form.desTotInsCap" :min="0" :step="0.1" /></el-form-item></el-col>
+          <el-col :span="12"><el-form-item label="工程等别">
+            <el-select v-model="form.engGrad" placeholder="请选择等别">
+              <el-option label="Ⅰ等" value="1" />
+              <el-option label="Ⅱ等" value="2" />
+              <el-option label="Ⅲ等" value="3" />
+              <el-option label="Ⅳ等" value="4" />
+              <el-option label="Ⅴ等" value="5" />
+            </el-select>
+          </el-form-item></el-col>
+          <el-col :span="12"><el-form-item label="最大过闸流量(m³/s)"><el-input-number v-model="form.lockDisc" :min="0" :step="0.1" /></el-form-item></el-col>
         </el-row>
         <el-row>
-          <el-col :span="12"><el-form-item label="工程状态"><el-select v-model="form.engStat"><el-option label="正常" value="1" /><el-option label="停用" value="0" /></el-select></el-form-item></el-col>
+          <el-col :span="12"><el-form-item label="运行状况">
+            <el-select v-model="form.runStat">
+              <el-option label="在用良好" value="1" /><el-option label="在用故障" value="2" /><el-option label="停用" value="3" />
+            </el-select>
+          </el-form-item></el-col>
           <el-col :span="12"><el-form-item label="管理单位"><el-input v-model="form.admDep" /></el-form-item></el-col>
         </el-row>
       </el-form>
@@ -62,7 +92,7 @@
       </template>
     </el-dialog>
 
-    <el-dialog v-model="statsVisible" title="泵站类型分布统计" width="600px" append-to-body>
+    <el-dialog v-model="statsVisible" title="水闸类型分布统计" width="600px" append-to-body>
       <el-table :data="statsData" stripe v-loading="statsLoading">
         <el-table-column prop="name" label="类型" min-width="120" />
         <el-table-column prop="value" label="数量" width="100" />
@@ -82,22 +112,25 @@ const form = ref({})
 const statsVisible = ref(false)
 const statsLoading = ref(false)
 const statsData = ref([])
+const wagaTypeMap = { '1': '分(泄)洪闸', '2': '节制闸', '3': '排(退)水闸', '4': '引(进)水闸', '5': '挡潮闸', '6': '船闸', '9': '其他' }
+const engGradMap = { '1': 'Ⅰ等', '2': 'Ⅱ等', '3': 'Ⅲ等', '4': 'Ⅳ等', '5': 'Ⅴ等' }
+const runStatMap = { '1': '在用良好', '2': '在用故障', '3': '停用' }
 function getList() { loading.value = true; getWagaList(queryParams).then(res => { list.value = res.rows; total.value = res.total; loading.value = false }) }
 function handleQuery() { queryParams.pageNum = 1; getList() }
 function resetQuery() { queryParams.wagaName = undefined; queryParams.admDep = undefined; handleQuery() }
-function handleDetail(row) { getWaga(row.wagaCode).then(res => { form.value = res.data; title.value = '泵站详情'; open.value = true }) }
-function handleEdit(row) { getWaga(row.wagaCode).then(res => { form.value = res.data; title.value = '编辑泵站'; open.value = true }) }
-function submitForm() { const method = form.value.wagaCode && title.value === '编辑泵站' ? updateWaga : addWaga; method(form.value).then(() => { proxy.$modal.msgSuccess('操作成功'); open.value = false; getList() }) }
+function handleDetail(row) { getWaga(row.wagaCode).then(res => { form.value = res.data; title.value = '水闸详情'; open.value = true }) }
+function handleEdit(row) { getWaga(row.wagaCode).then(res => { form.value = res.data; title.value = '编辑水闸'; open.value = true }) }
+function submitForm() { const method = form.value.wagaCode && title.value === '编辑水闸' ? updateWaga : addWaga; method(form.value).then(() => { proxy.$modal.msgSuccess('操作成功'); open.value = false; getList() }) }
 function cancel() { open.value = false }
 
 function handleAdd() {
-  form.value = { engStat: '1' }
-  title.value = '新增泵站'
+  form.value = { runStat: '1' }
+  title.value = '新增水闸'
   open.value = true
 }
 
 function handleDelete(row) {
-  proxy.$modal.confirm('确认删除泵站"' + row.wagaName + '"?').then(() => {
+  proxy.$modal.confirm('确认删除水闸"' + row.wagaName + '"?').then(() => {
     delWaga(row.wagaCode).then(() => {
       proxy.$modal.msgSuccess('删除成功')
       getList()

+ 84 - 25
gw-ui/src/views/slgc/wco/dike/index.vue

@@ -13,6 +13,14 @@
         <el-form-item label="起始位置">
           <el-input v-model="queryParams.startLoc" placeholder="请输入起始位置" clearable @keyup.enter="handleQuery" />
         </el-form-item>
+        <el-form-item label="所在位置">
+          <el-input v-model="queryParams.key" placeholder="请输入所在位置" clearable @keyup.enter="handleQuery" />
+        </el-form-item>
+        <el-form-item label="堤防级别">
+          <el-select v-model="queryParams.dikeGrad" placeholder="请选择堤防级别" clearable @keyup.enter="handleQuery">
+            <el-option label="1级" value="1" /><el-option label="2级" value="2" /><el-option label="3级" value="3" /><el-option label="4级" value="4" /><el-option label="5级" value="5" />
+          </el-select>
+        </el-form-item>
         <el-form-item>
           <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
           <el-button icon="Refresh" @click="resetQuery">重置</el-button>
@@ -24,7 +32,9 @@
         <el-table-column prop="dikeName" label="堤防名称" min-width="160" />
         <el-table-column prop="startLoc" label="起始位置" width="140" />
         <el-table-column prop="dikeLen" label="长度(km)" width="100" />
-        <el-table-column prop="dikeGrad" label="等级" width="80" />
+        <el-table-column prop="dikeGrad" label="等级" width="80">
+          <template #default="s">{{ dikeGradMap[s.row.dikeGrad] || s.row.dikeGrad }}</template>
+        </el-table-column>
         <el-table-column prop="engStat" label="状态" width="80">
           <template #default="s"><el-tag :type="s.row.engStat === '1' ? 'success' : 'warning'" size="small">{{ s.row.engStat === '1' ? '正常' : s.row.engStat === '0' ? '停用' : s.row.engStat }}</el-tag></template>
         </el-table-column>
@@ -38,35 +48,83 @@
       <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
     </el-card>
 
-    <el-dialog :title="title" v-model="open" width="720px" append-to-body>
-      <el-form :model="form" label-width="110px">
+    <el-dialog :title="title" v-model="open" width="900px" append-to-body>
+      <el-form :model="form" label-width="140px">
+        <el-divider content-position="left">基本信息</el-divider>
+        <el-row>
+          <el-col :span="8"><el-form-item label="堤防名称"><el-input v-model="form.dikeName" disabled /></el-form-item></el-col>
+          <el-col :span="8"><el-form-item label="堤防代码"><el-input v-model="form.dikeCode" disabled /></el-form-item></el-col>
+          <el-col :span="8"><el-form-item label="堤防级别">
+            <el-select v-model="form.dikeGrad" disabled>
+              <el-option label="未知" value="0" /><el-option label="1级" value="1" /><el-option label="2级" value="2" /><el-option label="3级" value="3" /><el-option label="4级" value="4" /><el-option label="5级" value="5" /><el-option label="5级以下" value="9" />
+            </el-select>
+          </el-form-item></el-col>
+        </el-row>
+        <el-row>
+          <el-col :span="8"><el-form-item label="堤防类型">
+            <el-select v-model="form.dikeType" disabled>
+              <el-option label="河(江)堤" value="1" /><el-option label="湖堤" value="2" /><el-option label="海堤" value="3" /><el-option label="围(圩、圈)" value="4" />
+            </el-select>
+          </el-form-item></el-col>
+          <el-col :span="8"><el-form-item label="堤防型式">
+            <el-select v-model="form.dikePatt" disabled>
+              <el-option label="土堤" value="1" /><el-option label="砌石堤" value="2" /><el-option label="土石混合堤" value="3" /><el-option label="钢筋混凝土防洪墙" value="4" /><el-option label="其他" value="9" />
+            </el-select>
+          </el-form-item></el-col>
+          <el-col :span="8"><el-form-item label="堤防长度(m)"><el-input v-model="form.dikeLen" disabled /></el-form-item></el-col>
+        </el-row>
+        <el-row>
+          <el-col :span="8"><el-form-item label="起点所在位置"><el-input v-model="form.startLoc" disabled /></el-form-item></el-col>
+          <el-col :span="8"><el-form-item label="终点所在位置"><el-input v-model="form.endLoc" disabled /></el-form-item></el-col>
+          <el-col :span="8"><el-form-item label="工程任务"><el-input v-model="form.engTask" disabled /></el-form-item></el-col>
+        </el-row>
+
+        <el-divider content-position="left">桩号与高程</el-divider>
+        <el-row>
+          <el-col :span="8"><el-form-item label="堤防起点桩号"><el-input v-model="form.dikeStartNum" disabled /></el-form-item></el-col>
+          <el-col :span="8"><el-form-item label="堤防终点桩号"><el-input v-model="form.dikeEndNum" disabled /></el-form-item></el-col>
+          <el-col :span="8"><el-form-item label="起点堤顶高程(m)"><el-input v-model="form.startDikeTopEl" disabled /></el-form-item></el-col>
+        </el-row>
+        <el-row>
+          <el-col :span="8"><el-form-item label="终点堤顶高程(m)"><el-input v-model="form.endDikeTopEl" disabled /></el-form-item></el-col>
+          <el-col :span="8"><el-form-item label="堤防最小高度(m)"><el-input v-model="form.dikeHeigMin" disabled /></el-form-item></el-col>
+          <el-col :span="8"><el-form-item label="堤防最大高度(m)"><el-input v-model="form.dikeHeigMax" disabled /></el-form-item></el-col>
+        </el-row>
         <el-row>
-          <el-col :span="12"><el-form-item label="堤防编码"><el-input v-model="form.dikeCode" disabled /></el-form-item></el-col>
-          <el-col :span="12"><el-form-item label="堤防名称"><el-input v-model="form.dikeName" disabled /></el-form-item></el-col>
+          <el-col :span="8"><el-form-item label="堤顶最小宽度(m)"><el-input v-model="form.dikeTopWidMin" disabled /></el-form-item></el-col>
+          <el-col :span="8"><el-form-item label="堤顶最大宽度(m)"><el-input v-model="form.dikeTopWidMax" disabled /></el-form-item></el-col>
         </el-row>
+
+        <el-divider content-position="left">经纬度信息</el-divider>
+        <el-row>
+          <el-col :span="6"><el-form-item label="起点经度"><el-input v-model="form.startLong" disabled /></el-form-item></el-col>
+          <el-col :span="6"><el-form-item label="起点纬度"><el-input v-model="form.startLat" disabled /></el-form-item></el-col>
+          <el-col :span="6"><el-form-item label="终点经度"><el-input v-model="form.endLong" disabled /></el-form-item></el-col>
+          <el-col :span="6"><el-form-item label="终点纬度"><el-input v-model="form.endLat" disabled /></el-form-item></el-col>
+        </el-row>
+
+        <el-divider content-position="left">建设与管理</el-divider>
         <el-row>
-          <el-col :span="12"><el-form-item label="起始位置"><el-input v-model="form.startLoc" disabled /></el-form-item></el-col>
-          <el-col :span="12"><el-form-item label="终点位置"><el-input v-model="form.endLoc" disabled /></el-form-item></el-col>
+          <el-col :span="8"><el-form-item label="工程建设情况">
+            <el-select v-model="form.engStat" disabled>
+              <el-option label="未建" value="0" /><el-option label="在建" value="1" /><el-option label="已建" value="2" />
+            </el-select>
+          </el-form-item></el-col>
+          <el-col :span="8"><el-form-item label="开工时间"><el-input v-model="form.startDate" disabled /></el-form-item></el-col>
+          <el-col :span="8"><el-form-item label="建成时间"><el-input v-model="form.compDate" disabled /></el-form-item></el-col>
         </el-row>
         <el-row>
-          <el-col :span="12"><el-form-item label="长度(km)"><el-input-number v-model="form.dikeLen" :min="0" :step="0.1" disabled /></el-form-item></el-col>
-          <el-col :span="12">
-            <el-form-item label="等级">
-              <el-select v-model="form.dikeGrad" placeholder="请选择等级" disabled>
-                <el-option label="一级" value="1" /><el-option label="二级" value="2" /><el-option label="三级" value="3" /><el-option label="四级" value="4" /><el-option label="五级" value="5" />
-              </el-select>
-            </el-form-item>
-          </el-col>
+          <el-col :span="8"><el-form-item label="归口管理部门">
+            <el-select v-model="form.admDep" disabled>
+              <el-option label="水利部门" value="1" /><el-option label="电力部门" value="2" /><el-option label="农业部门" value="3" /><el-option label="林业部门" value="4" /><el-option label="城建部门" value="5" /><el-option label="航运部门" value="6" /><el-option label="环保部门" value="7" /><el-option label="其他部门" value="9" />
+            </el-select>
+          </el-form-item></el-col>
+          <el-col :span="8"><el-form-item label="运行状况"><el-input v-model="form.runStat" disabled /></el-form-item></el-col>
+          <el-col :span="8"><el-form-item label="备注"><el-input v-model="form.note" disabled /></el-form-item></el-col>
         </el-row>
         <el-row>
-          <el-col :span="12">
-            <el-form-item label="工程状态">
-              <el-select v-model="form.engStat" placeholder="请选择状态" disabled>
-                <el-option label="正常" value="1" /><el-option label="停用" value="0" />
-              </el-select>
-            </el-form-item>
-          </el-col>
-          <el-col :span="12"><el-form-item label="堤防类型"><el-input v-model="form.dikeType" disabled /></el-form-item></el-col>
+          <el-col :span="8"><el-form-item label="属性采集时间"><el-input v-model="form.collDate" disabled /></el-form-item></el-col>
+          <el-col :span="8"><el-form-item label="属性更新时间"><el-input v-model="form.updDate" disabled /></el-form-item></el-col>
         </el-row>
       </el-form>
       <template #footer><el-button @click="cancel">关 闭</el-button></template>
@@ -87,11 +145,12 @@ import { getDike, delDike, getDikeCountType } from '@/api/slgc/dike/index'
 const { proxy } = getCurrentInstance()
 const loading = ref(true); const total = ref(0); const open = ref(false); const title = ref('')
 const list = ref([]); const form = ref({})
-const queryParams = reactive({ pageNum: 1, pageSize: 10, dikeName: undefined, startLoc: undefined })
+const queryParams = reactive({ pageNum: 1, pageSize: 10, dikeName: undefined, startLoc: undefined, key: undefined, dikeGrad: undefined })
+const dikeGradMap = { '0': '未知', '1': '1级', '2': '2级', '3': '3级', '4': '4级', '5': '5级', '9': '5级以下' }
 const statsVisible = ref(false); const statsLoading = ref(false); const statsData = ref([])
 function getList() { loading.value = true; getWcoDikeList(queryParams).then(res => { list.value = res.rows; total.value = res.total; loading.value = false }) }
 function handleQuery() { queryParams.pageNum = 1; getList() }
-function resetQuery() { queryParams.dikeName = undefined; queryParams.startLoc = undefined; handleQuery() }
+function resetQuery() { queryParams.dikeName = undefined; queryParams.startLoc = undefined; queryParams.key = undefined; queryParams.dikeGrad = undefined; handleQuery() }
 function handleDetail(row) { getDike(row.dikeCode).then(res => { form.value = res.data; title.value = '详情'; open.value = true }) }
 function handleDelete(row) { proxy.$modal.confirm('确认删除堤防"' + row.dikeName + '"?').then(() => { delDike(row.dikeCode).then(() => { proxy.$modal.msgSuccess('删除成功'); getList() }) }) }
 function handleStats() { statsVisible.value = true; statsLoading.value = true; getDikeCountType().then(res => { statsData.value = res.data || []; statsLoading.value = false }).catch(() => { statsLoading.value = false }) }

+ 72 - 27
gw-ui/src/views/slgc/wco/pust/index.vue

@@ -3,12 +3,20 @@
     <el-card shadow="never">
       <template #header>
         <div class="card-header">
-          <el-space><el-button text @click="$router.back()">← 返回</el-button><span>WCO 水闸工程</span></el-space>
+          <el-space><el-button text @click="$router.back()">← 返回</el-button><span>WCO 泵站工程</span></el-space>
         </div>
       </template>
       <el-form :model="queryParams" :inline="true" size="small">
-        <el-form-item label="水闸名称">
-          <el-input v-model="queryParams.pustName" placeholder="请输入水闸名称" clearable @keyup.enter="handleQuery" />
+        <el-form-item label="泵站名称">
+          <el-input v-model="queryParams.pustName" placeholder="请输入泵站名称" clearable @keyup.enter="handleQuery" />
+        </el-form-item>
+        <el-form-item label="所在位置">
+          <el-input v-model="queryParams.key" placeholder="请输入所在位置" clearable @keyup.enter="handleQuery" />
+        </el-form-item>
+        <el-form-item label="泵站类型">
+          <el-select v-model="queryParams.pustType" placeholder="请选择泵站类型" clearable @keyup.enter="handleQuery">
+            <el-option label="排水泵站" value="1" /><el-option label="供水泵站" value="2" /><el-option label="供排结合泵站" value="3" />
+          </el-select>
         </el-form-item>
         <el-form-item>
           <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
@@ -18,9 +26,11 @@
       </el-form>
       <el-table v-loading="loading" :data="list" stripe>
         <el-table-column prop="pustCode" label="编码" width="100" />
-        <el-table-column prop="pustName" label="水闸名称" min-width="140" />
+        <el-table-column prop="pustName" label="泵站名称" min-width="140" />
         <el-table-column prop="pustLoc" label="位置" width="140" />
-        <el-table-column prop="pustType" label="类型" width="80" />
+        <el-table-column prop="pustType" label="类型" width="80">
+          <template #default="s">{{ pustTypeMap[s.row.pustType] || s.row.pustType }}</template>
+        </el-table-column>
         <el-table-column prop="insFlow" label="流量(m³/s)" width="110" />
         <el-table-column prop="engStat" label="状态" width="80">
           <template #default="s"><el-tag :type="s.row.engStat === '1' ? 'success' : 'warning'" size="small">{{ s.row.engStat === '1' ? '正常' : s.row.engStat === '0' ? '停用' : s.row.engStat }}</el-tag></template>
@@ -35,39 +45,73 @@
       <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
     </el-card>
 
-    <el-dialog :title="title" v-model="open" width="720px" append-to-body>
-      <el-form :model="form" label-width="130px">
+    <el-dialog :title="title" v-model="open" width="900px" append-to-body>
+      <el-form :model="form" label-width="140px">
+        <el-divider content-position="left">基本信息</el-divider>
+        <el-row>
+          <el-col :span="8"><el-form-item label="泵站名称"><el-input v-model="form.pustName" disabled /></el-form-item></el-col>
+          <el-col :span="8"><el-form-item label="泵站代码"><el-input v-model="form.pustCode" disabled /></el-form-item></el-col>
+          <el-col :span="8"><el-form-item label="泵站所在位置"><el-input v-model="form.pustLoc" disabled /></el-form-item></el-col>
+        </el-row>
         <el-row>
-          <el-col :span="12"><el-form-item label="水闸编码"><el-input v-model="form.pustCode" disabled /></el-form-item></el-col>
-          <el-col :span="12"><el-form-item label="水闸名称"><el-input v-model="form.pustName" disabled /></el-form-item></el-col>
+          <el-col :span="8"><el-form-item label="泵站类型">
+            <el-select v-model="form.pustType" disabled>
+              <el-option label="排水泵站" value="1" /><el-option label="供水泵站" value="2" /><el-option label="供排结合泵站" value="3" />
+            </el-select>
+          </el-form-item></el-col>
+          <el-col :span="8"><el-form-item label="工程等别">
+            <el-select v-model="form.engGrad" disabled>
+              <el-option label="Ⅰ" value="1" /><el-option label="Ⅱ" value="2" /><el-option label="Ⅲ" value="3" /><el-option label="Ⅳ" value="4" /><el-option label="Ⅴ" value="5" />
+            </el-select>
+          </el-form-item></el-col>
+          <el-col :span="8"><el-form-item label="工程规模">
+            <el-select v-model="form.engScal" disabled>
+              <el-option label="大(1)型" value="1" /><el-option label="大(2)型" value="2" /><el-option label="中型" value="3" /><el-option label="小(1)型" value="4" /><el-option label="小(2)型" value="5" />
+            </el-select>
+          </el-form-item></el-col>
         </el-row>
         <el-row>
-          <el-col :span="12"><el-form-item label="位置"><el-input v-model="form.pustLoc" disabled /></el-form-item></el-col>
-          <el-col :span="12"><el-form-item label="类型"><el-input v-model="form.pustType" disabled /></el-form-item></el-col>
+          <el-col :span="8"><el-form-item label="工程任务"><el-input v-model="form.engTask" disabled /></el-form-item></el-col>
+          <el-col :span="8"><el-form-item label="主要建筑物级别"><el-input v-model="form.mainBuildGrad" disabled /></el-form-item></el-col>
+          <el-col :span="8"><el-form-item label="设计扬程(m)"><el-input v-model="form.desHead" disabled /></el-form-item></el-col>
         </el-row>
+
+        <el-divider content-position="left">经纬度信息</el-divider>
         <el-row>
-          <el-col :span="12"><el-form-item label="装机流量(m³/s)"><el-input-number v-model="form.insFlow" :min="0" :step="0.1" disabled /></el-form-item></el-col>
-          <el-col :span="12"><el-form-item label="装机功率(kW)"><el-input-number v-model="form.insPow" :min="0" :step="0.1" disabled /></el-form-item></el-col>
+          <el-col :span="8"><el-form-item label="泵站经度"><el-input v-model="form.pustLong" disabled /></el-form-item></el-col>
+          <el-col :span="8"><el-form-item label="泵站纬度"><el-input v-model="form.pustLat" disabled /></el-form-item></el-col>
         </el-row>
+
+        <el-divider content-position="left">装机参数</el-divider>
+        <el-row>
+          <el-col :span="8"><el-form-item label="装机流量(m³/s)"><el-input v-model="form.insFlow" disabled /></el-form-item></el-col>
+          <el-col :span="8"><el-form-item label="装机功率(kW)"><el-input v-model="form.insPow" disabled /></el-form-item></el-col>
+          <el-col :span="8"><el-form-item label="水泵数量(台)"><el-input v-model="form.pumpNum" disabled /></el-form-item></el-col>
+        </el-row>
+
+        <el-divider content-position="left">建设与管理</el-divider>
         <el-row>
-          <el-col :span="12"><el-form-item label="水泵数量"><el-input-number v-model="form.pumpNum" :min="0" disabled /></el-form-item></el-col>
-          <el-col :span="12">
-            <el-form-item label="工程状态">
-              <el-select v-model="form.engStat" placeholder="请选择状态" disabled>
-                <el-option label="正常" value="1" /><el-option label="停用" value="0" />
-              </el-select>
-            </el-form-item>
-          </el-col>
+          <el-col :span="8"><el-form-item label="工程建设情况">
+            <el-select v-model="form.engStat" disabled>
+              <el-option label="未建" value="0" /><el-option label="在建" value="1" /><el-option label="已建" value="2" />
+            </el-select>
+          </el-form-item></el-col>
+          <el-col :span="8"><el-form-item label="开工时间"><el-input v-model="form.startDate" disabled /></el-form-item></el-col>
+          <el-col :span="8"><el-form-item label="建成时间"><el-input v-model="form.compDate" disabled /></el-form-item></el-col>
         </el-row>
         <el-row>
-          <el-col :span="12"><el-form-item label="管理单位"><el-input v-model="form.admDep" disabled /></el-form-item></el-col>
-          <el-col :span="12"><el-form-item label="开工日期"><el-input v-model="form.startDate" type="date" disabled /></el-form-item></el-col>
+          <el-col :span="8"><el-form-item label="归口管理部门">
+            <el-select v-model="form.admDep" disabled>
+              <el-option label="水利部门" value="1" /><el-option label="电力部门" value="2" /><el-option label="农业部门" value="3" /><el-option label="林业部门" value="4" /><el-option label="城建部门" value="5" /><el-option label="航运部门" value="6" /><el-option label="环保部门" value="7" /><el-option label="其他部门" value="9" />
+            </el-select>
+          </el-form-item></el-col>
+          <el-col :span="16"><el-form-item label="备注"><el-input v-model="form.note" disabled /></el-form-item></el-col>
         </el-row>
       </el-form>
       <template #footer><el-button @click="cancel">关 闭</el-button></template>
     </el-dialog>
 
-    <el-dialog v-model="statsVisible" title="水闸类型分布统计" width="600px" append-to-body>
+    <el-dialog v-model="statsVisible" title="泵站类型分布统计" width="600px" append-to-body>
       <el-table :data="statsData" stripe v-loading="statsLoading">
         <el-table-column prop="name" label="类型" min-width="120" />
         <el-table-column prop="value" label="数量" width="100" />
@@ -82,13 +126,14 @@ import { getPust, delPust, getPustCountType } from '@/api/slgc/pust/index'
 const { proxy } = getCurrentInstance()
 const loading = ref(true); const total = ref(0); const open = ref(false); const title = ref('')
 const list = ref([]); const form = ref({})
-const queryParams = reactive({ pageNum: 1, pageSize: 10, pustName: undefined })
+const queryParams = reactive({ pageNum: 1, pageSize: 10, pustName: undefined, key: undefined, pustType: undefined })
+const pustTypeMap = { '1': '排水泵站', '2': '供水泵站', '3': '供排结合泵站' }
 const statsVisible = ref(false); const statsLoading = ref(false); const statsData = ref([])
 function getList() { loading.value = true; getWcoPustList(queryParams).then(res => { list.value = res.rows; total.value = res.total; loading.value = false }) }
 function handleQuery() { queryParams.pageNum = 1; getList() }
-function resetQuery() { queryParams.pustName = undefined; handleQuery() }
+function resetQuery() { queryParams.pustName = undefined; queryParams.key = undefined; queryParams.pustType = undefined; handleQuery() }
 function handleDetail(row) { getPust(row.pustCode).then(res => { form.value = res.data; title.value = '详情'; open.value = true }) }
-function handleDelete(row) { proxy.$modal.confirm('确认删除水闸"' + row.pustName + '"?').then(() => { delPust(row.pustCode).then(() => { proxy.$modal.msgSuccess('删除成功'); getList() }) }) }
+function handleDelete(row) { proxy.$modal.confirm('确认删除泵站"' + row.pustName + '"?').then(() => { delPust(row.pustCode).then(() => { proxy.$modal.msgSuccess('删除成功'); getList() }) }) }
 function handleStats() { statsVisible.value = true; statsLoading.value = true; getPustCountType().then(res => { statsData.value = res.data || []; statsLoading.value = false }).catch(() => { statsLoading.value = false }) }
 function cancel() { open.value = false }
 onMounted(() => { getList() })

+ 82 - 23
gw-ui/src/views/slgc/wco/reservoir/index.vue

@@ -10,6 +10,14 @@
         <el-form-item label="水库名称">
           <el-input v-model="queryParams.resName" placeholder="请输入水库名称" clearable @keyup.enter="handleQuery" />
         </el-form-item>
+        <el-form-item label="所在位置">
+          <el-input v-model="queryParams.key" placeholder="请输入所在位置" clearable @keyup.enter="handleQuery" />
+        </el-form-item>
+        <el-form-item label="工程规模">
+          <el-select v-model="queryParams.engScal" placeholder="请选择工程规模" clearable @keyup.enter="handleQuery">
+            <el-option label="大(1)型" value="1" /><el-option label="大(2)型" value="2" /><el-option label="中型" value="3" /><el-option label="小(1)型" value="4" /><el-option label="小(2)型" value="5" />
+          </el-select>
+        </el-form-item>
         <el-form-item>
           <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
           <el-button icon="Refresh" @click="resetQuery">重置</el-button>
@@ -20,7 +28,9 @@
         <el-table-column prop="resCode" label="编码" width="100" />
         <el-table-column prop="resName" label="水库名称" min-width="140" />
         <el-table-column prop="resLoc" label="位置" width="140" />
-        <el-table-column prop="engScal" label="规模" width="80" />
+        <el-table-column prop="engScal" label="规模" width="80">
+          <template #default="s">{{ engScalMap[s.row.engScal] || s.row.engScal }}</template>
+        </el-table-column>
         <el-table-column prop="totCap" label="总库容(万m³)" width="120" />
         <el-table-column prop="engStat" label="状态" width="80">
           <template #default="s"><el-tag :type="s.row.engStat === '1' ? 'success' : 'warning'" size="small">{{ s.row.engStat === '1' ? '正常' : s.row.engStat === '0' ? '停用' : s.row.engStat }}</el-tag></template>
@@ -35,37 +45,85 @@
       <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
     </el-card>
 
-    <el-dialog :title="title" v-model="open" width="720px" append-to-body>
-      <el-form :model="form" label-width="130px">
+    <el-dialog :title="title" v-model="open" width="900px" append-to-body>
+      <el-form :model="form" label-width="150px">
+        <el-divider content-position="left">基本信息</el-divider>
+        <el-row>
+          <el-col :span="8"><el-form-item label="水库名称"><el-input v-model="form.resName" disabled /></el-form-item></el-col>
+          <el-col :span="8"><el-form-item label="水库代码"><el-input v-model="form.resCode" disabled /></el-form-item></el-col>
+          <el-col :span="8"><el-form-item label="水库所在位置"><el-input v-model="form.resLoc" disabled /></el-form-item></el-col>
+        </el-row>
         <el-row>
-          <el-col :span="12"><el-form-item label="水库编码"><el-input v-model="form.resCode" disabled /></el-form-item></el-col>
-          <el-col :span="12"><el-form-item label="水库名称"><el-input v-model="form.resName" disabled /></el-form-item></el-col>
+          <el-col :span="8"><el-form-item label="水库类型">
+            <el-select v-model="form.resType" disabled>
+              <el-option label="山丘水库" value="1" /><el-option label="平原水库" value="2" /><el-option label="地下水库" value="3" />
+            </el-select>
+          </el-form-item></el-col>
+          <el-col :span="8"><el-form-item label="工程等别">
+            <el-select v-model="form.engGrad" disabled>
+              <el-option label="Ⅰ" value="1" /><el-option label="Ⅱ" value="2" /><el-option label="Ⅲ" value="3" /><el-option label="Ⅳ" value="4" /><el-option label="Ⅴ" value="5" />
+            </el-select>
+          </el-form-item></el-col>
+          <el-col :span="8"><el-form-item label="工程规模">
+            <el-select v-model="form.engScal" disabled>
+              <el-option label="大(1)型" value="1" /><el-option label="大(2)型" value="2" /><el-option label="中型" value="3" /><el-option label="小(1)型" value="4" /><el-option label="小(2)型" value="5" />
+            </el-select>
+          </el-form-item></el-col>
         </el-row>
+
+        <el-divider content-position="left">建设与管理</el-divider>
         <el-row>
-          <el-col :span="12"><el-form-item label="位置"><el-input v-model="form.resLoc" disabled /></el-form-item></el-col>
-          <el-col :span="12"><el-form-item label="类型"><el-input v-model="form.resType" disabled /></el-form-item></el-col>
+          <el-col :span="8"><el-form-item label="工程建设情况">
+            <el-select v-model="form.engStat" disabled>
+              <el-option label="未建" value="0" /><el-option label="在建" value="1" /><el-option label="已建" value="2" />
+            </el-select>
+          </el-form-item></el-col>
+          <el-col :span="8"><el-form-item label="开工时间"><el-input v-model="form.startDate" disabled /></el-form-item></el-col>
+          <el-col :span="8"><el-form-item label="建成时间"><el-input v-model="form.compDate" disabled /></el-form-item></el-col>
         </el-row>
         <el-row>
-          <el-col :span="12"><el-form-item label="总库容(万m³)"><el-input-number v-model="form.totCap" :min="0" :step="0.01" disabled /></el-form-item></el-col>
-          <el-col :span="12"><el-form-item label="正常库容(万m³)"><el-input-number v-model="form.normPoolStagCap" :min="0" :step="0.01" disabled /></el-form-item></el-col>
+          <el-col :span="8"><el-form-item label="归口管理部门">
+            <el-select v-model="form.admDep" disabled>
+              <el-option label="水利部门" value="1" /><el-option label="电力部门" value="2" /><el-option label="农业部门" value="3" /><el-option label="林业部门" value="4" /><el-option label="城建部门" value="5" /><el-option label="航运部门" value="6" /><el-option label="环保部门" value="7" /><el-option label="其他部门" value="9" />
+            </el-select>
+          </el-form-item></el-col>
+          <el-col :span="16"><el-form-item label="备注"><el-input v-model="form.note" disabled /></el-form-item></el-col>
         </el-row>
+
+        <el-divider content-position="left">经纬度信息</el-divider>
         <el-row>
-          <el-col :span="12"><el-form-item label="防洪库容(万m³)"><el-input-number v-model="form.storFlCap" :min="0" :step="0.01" disabled /></el-form-item></el-col>
-          <el-col :span="12"><el-form-item label="调洪库容(万m³)"><el-input-number v-model="form.flcoCap" :min="0" :step="0.01" disabled /></el-form-item></el-col>
+          <el-col :span="6"><el-form-item label="左下角经度"><el-input v-model="form.lowLeftLong" disabled /></el-form-item></el-col>
+          <el-col :span="6"><el-form-item label="左下角纬度"><el-input v-model="form.lowLeftLat" disabled /></el-form-item></el-col>
+          <el-col :span="6"><el-form-item label="右上角经度"><el-input v-model="form.upRightLong" disabled /></el-form-item></el-col>
+          <el-col :span="6"><el-form-item label="右上角纬度"><el-input v-model="form.upRightLat" disabled /></el-form-item></el-col>
         </el-row>
+
+        <el-divider content-position="left">水文参数</el-divider>
+        <el-row>
+          <el-col :span="8"><el-form-item label="坝址控制流域面积(km²)"><el-input v-model="form.watShedArea" disabled /></el-form-item></el-col>
+          <el-col :span="8"><el-form-item label="坝址多年平均径流量(10⁴m³)"><el-input v-model="form.daadMulAverRuof" disabled /></el-form-item></el-col>
+          <el-col :span="8"><el-form-item label="防洪高水位(m)"><el-input v-model="form.uppLevFlco" disabled /></el-form-item></el-col>
+        </el-row>
+        <el-row>
+          <el-col :span="8"><el-form-item label="正常蓄水位(m)"><el-input v-model="form.normWatLev" disabled /></el-form-item></el-col>
+          <el-col :span="8"><el-form-item label="正常蓄水位水面面积(km²)"><el-input v-model="form.normPoolStagArea" disabled /></el-form-item></el-col>
+          <el-col :span="8"><el-form-item label="正常蓄水位库容(10⁴m³)"><el-input v-model="form.normPoolStagCap" disabled /></el-form-item></el-col>
+        </el-row>
+        <el-row>
+          <el-col :span="8"><el-form-item label="主汛期防洪限制水位(m)"><el-input v-model="form.flLowLimLev" disabled /></el-form-item></el-col>
+          <el-col :span="8"><el-form-item label="防洪限制水位库容(10⁴m³)"><el-input v-model="form.flLowLimLevCap" disabled /></el-form-item></el-col>
+          <el-col :span="8"><el-form-item label="死水位(m)"><el-input v-model="form.deadLev" disabled /></el-form-item></el-col>
+        </el-row>
+
+        <el-divider content-position="left">库容信息</el-divider>
         <el-row>
-          <el-col :span="12"><el-form-item label="集水面积(km²)"><el-input-number v-model="form.watShedArea" :min="0" :step="0.1" disabled /></el-form-item></el-col>
-          <el-col :span="12">
-            <el-form-item label="工程状态">
-              <el-select v-model="form.engStat" placeholder="请选择状态" disabled>
-                <el-option label="正常" value="1" /><el-option label="停用" value="0" />
-              </el-select>
-            </el-form-item>
-          </el-col>
+          <el-col :span="6"><el-form-item label="总库容(10⁴m³)"><el-input v-model="form.totCap" disabled /></el-form-item></el-col>
+          <el-col :span="6"><el-form-item label="调洪库容(10⁴m³)"><el-input v-model="form.storFlCap" disabled /></el-form-item></el-col>
+          <el-col :span="6"><el-form-item label="防洪库容(10⁴m³)"><el-input v-model="form.flcoCap" disabled /></el-form-item></el-col>
+          <el-col :span="6"><el-form-item label="兴利库容(10⁴m³)"><el-input v-model="form.benResCap" disabled /></el-form-item></el-col>
         </el-row>
         <el-row>
-          <el-col :span="12"><el-form-item label="管理单位"><el-input v-model="form.admDep" disabled /></el-form-item></el-col>
-          <el-col :span="12"><el-form-item label="开工日期"><el-input v-model="form.startDate" type="date" disabled /></el-form-item></el-col>
+          <el-col :span="6"><el-form-item label="死库容(10⁴m³)"><el-input v-model="form.deadCap" disabled /></el-form-item></el-col>
         </el-row>
       </el-form>
       <template #footer><el-button @click="cancel">关 闭</el-button></template>
@@ -86,11 +144,12 @@ import { getReservoir, delReservoir, getReservoirCountType } from '@/api/slgc/re
 const { proxy } = getCurrentInstance()
 const loading = ref(true); const total = ref(0); const open = ref(false); const title = ref('')
 const list = ref([]); const form = ref({})
-const queryParams = reactive({ pageNum: 1, pageSize: 10, resName: undefined })
+const queryParams = reactive({ pageNum: 1, pageSize: 10, resName: undefined, key: undefined, engScal: undefined })
+const engScalMap = { '1': '大(1)型', '2': '大(2)型', '3': '中型', '4': '小(1)型', '5': '小(2)型' }
 const statsVisible = ref(false); const statsLoading = ref(false); const statsData = ref([])
 function getList() { loading.value = true; getWcoReservoirList(queryParams).then(res => { list.value = res.rows; total.value = res.total; loading.value = false }) }
 function handleQuery() { queryParams.pageNum = 1; getList() }
-function resetQuery() { queryParams.resName = undefined; handleQuery() }
+function resetQuery() { queryParams.resName = undefined; queryParams.key = undefined; queryParams.engScal = undefined; handleQuery() }
 function handleDetail(row) { getReservoir(row.resCode).then(res => { form.value = res.data; title.value = '详情'; open.value = true }) }
 function handleDelete(row) { proxy.$modal.confirm('确认删除水库"' + row.resName + '"?').then(() => { delReservoir(row.resCode).then(() => { proxy.$modal.msgSuccess('删除成功'); getList() }) }) }
 function handleStats() { statsVisible.value = true; statsLoading.value = true; getReservoirCountType().then(res => { statsData.value = res.data || []; statsLoading.value = false }).catch(() => { statsLoading.value = false }) }

+ 75 - 25
gw-ui/src/views/slgc/wco/waga/index.vue

@@ -3,12 +3,20 @@
     <el-card shadow="never">
       <template #header>
         <div class="card-header">
-          <el-space><el-button text @click="$router.back()">← 返回</el-button><span>WCO 泵站工程</span></el-space>
+          <el-space><el-button text @click="$router.back()">← 返回</el-button><span>WCO 水闸工程</span></el-space>
         </div>
       </template>
       <el-form :model="queryParams" :inline="true" size="small">
-        <el-form-item label="泵站名称">
-          <el-input v-model="queryParams.wagaName" placeholder="请输入泵站名称" clearable @keyup.enter="handleQuery" />
+        <el-form-item label="水闸名称">
+          <el-input v-model="queryParams.wagaName" placeholder="请输入水闸名称" clearable @keyup.enter="handleQuery" />
+        </el-form-item>
+        <el-form-item label="所在位置">
+          <el-input v-model="queryParams.key" placeholder="请输入所在位置" clearable @keyup.enter="handleQuery" />
+        </el-form-item>
+        <el-form-item label="工程等别">
+          <el-select v-model="queryParams.engGrad" placeholder="请选择工程等别" clearable @keyup.enter="handleQuery">
+            <el-option label="Ⅰ" value="1" /><el-option label="Ⅱ" value="2" /><el-option label="Ⅲ" value="3" /><el-option label="Ⅳ" value="4" /><el-option label="Ⅴ" value="5" />
+          </el-select>
         </el-form-item>
         <el-form-item>
           <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
@@ -18,9 +26,11 @@
       </el-form>
       <el-table v-loading="loading" :data="list" stripe>
         <el-table-column prop="wagaCode" label="编码" width="100" />
-        <el-table-column prop="wagaName" label="泵站名称" min-width="140" />
+        <el-table-column prop="wagaName" label="水闸名称" min-width="140" />
         <el-table-column prop="wagaLoc" label="位置" width="140" />
-        <el-table-column prop="wagaType" label="类型" width="80" />
+        <el-table-column prop="wagaType" label="类型" width="80">
+          <template #default="s">{{ wagaTypeMap[s.row.wagaType] || s.row.wagaType }}</template>
+        </el-table-column>
         <el-table-column prop="insPow" label="功率(kW)" width="100" />
         <el-table-column prop="engStat" label="状态" width="80">
           <template #default="s"><el-tag :type="s.row.engStat === '1' ? 'success' : 'warning'" size="small">{{ s.row.engStat === '1' ? '正常' : s.row.engStat === '0' ? '停用' : s.row.engStat }}</el-tag></template>
@@ -35,35 +45,74 @@
       <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
     </el-card>
 
-    <el-dialog :title="title" v-model="open" width="720px" append-to-body>
-      <el-form :model="form" label-width="130px">
+    <el-dialog :title="title" v-model="open" width="900px" append-to-body>
+      <el-form :model="form" label-width="140px">
+        <el-divider content-position="left">基本信息</el-divider>
+        <el-row>
+          <el-col :span="8"><el-form-item label="水闸名称"><el-input v-model="form.wagaName" disabled /></el-form-item></el-col>
+          <el-col :span="8"><el-form-item label="水闸代码"><el-input v-model="form.wagaCode" disabled /></el-form-item></el-col>
+          <el-col :span="8"><el-form-item label="水闸所在位置"><el-input v-model="form.wagaLoc" disabled /></el-form-item></el-col>
+        </el-row>
+        <el-row>
+          <el-col :span="8"><el-form-item label="水闸类型">
+            <el-select v-model="form.wagaType" disabled>
+              <el-option label="分(泄)洪闸" value="1" /><el-option label="节制闸" value="2" /><el-option label="排(退)水闸" value="3" /><el-option label="引(进)水闸" value="4" /><el-option label="挡潮闸" value="5" /><el-option label="船闸" value="6" /><el-option label="其他" value="9" />
+            </el-select>
+          </el-form-item></el-col>
+          <el-col :span="8"><el-form-item label="水闸用途"><el-input v-model="form.wagaUse" disabled /></el-form-item></el-col>
+          <el-col :span="8"><el-form-item label="工程等别">
+            <el-select v-model="form.engGrad" disabled>
+              <el-option label="Ⅰ" value="1" /><el-option label="Ⅱ" value="2" /><el-option label="Ⅲ" value="3" /><el-option label="Ⅳ" value="4" /><el-option label="Ⅴ" value="5" />
+            </el-select>
+          </el-form-item></el-col>
+        </el-row>
+        <el-row>
+          <el-col :span="8"><el-form-item label="工程规模">
+            <el-select v-model="form.engScal" disabled>
+              <el-option label="大(1)型" value="1" /><el-option label="大(2)型" value="2" /><el-option label="中型" value="3" /><el-option label="小(1)型" value="4" /><el-option label="小(2)型" value="5" />
+            </el-select>
+          </el-form-item></el-col>
+          <el-col :span="8"><el-form-item label="最大过闸流量(m³/s)"><el-input v-model="form.lockDisc" disabled /></el-form-item></el-col>
+          <el-col :span="8"><el-form-item label="闸孔数量(孔)"><el-input v-model="form.gaorNum" disabled /></el-form-item></el-col>
+        </el-row>
+
+        <el-divider content-position="left">经纬度信息</el-divider>
         <el-row>
-          <el-col :span="12"><el-form-item label="泵站编码"><el-input v-model="form.wagaCode" disabled /></el-form-item></el-col>
-          <el-col :span="12"><el-form-item label="泵站名称"><el-input v-model="form.wagaName" disabled /></el-form-item></el-col>
+          <el-col :span="6"><el-form-item label="起点经度"><el-input v-model="form.startLong" disabled /></el-form-item></el-col>
+          <el-col :span="6"><el-form-item label="起点纬度"><el-input v-model="form.startLat" disabled /></el-form-item></el-col>
+          <el-col :span="6"><el-form-item label="终点经度"><el-input v-model="form.endLong" disabled /></el-form-item></el-col>
+          <el-col :span="6"><el-form-item label="终点纬度"><el-input v-model="form.endLat" disabled /></el-form-item></el-col>
         </el-row>
+
+        <el-divider content-position="left">装机参数</el-divider>
         <el-row>
-          <el-col :span="12"><el-form-item label="位置"><el-input v-model="form.wagaLoc" disabled /></el-form-item></el-col>
-          <el-col :span="12"><el-form-item label="类型"><el-input v-model="form.wagaType" disabled /></el-form-item></el-col>
+          <el-col :span="8"><el-form-item label="装机功率(kW)"><el-input v-model="form.insPow" disabled /></el-form-item></el-col>
+          <el-col :span="8"><el-form-item label="设计总装机(kW)"><el-input v-model="form.desTotInsCap" disabled /></el-form-item></el-col>
         </el-row>
+
+        <el-divider content-position="left">建设与管理</el-divider>
         <el-row>
-          <el-col :span="12"><el-form-item label="装机功率(kW)"><el-input-number v-model="form.insPow" :min="0" :step="0.1" disabled /></el-form-item></el-col>
-          <el-col :span="12"><el-form-item label="设计总装机(kW)"><el-input-number v-model="form.desTotInsCap" :min="0" :step="0.1" disabled /></el-form-item></el-col>
+          <el-col :span="8"><el-form-item label="工程建设情况">
+            <el-select v-model="form.engStat" disabled>
+              <el-option label="未建" value="0" /><el-option label="在建" value="1" /><el-option label="已建" value="2" />
+            </el-select>
+          </el-form-item></el-col>
+          <el-col :span="8"><el-form-item label="开工时间"><el-input v-model="form.startDate" disabled /></el-form-item></el-col>
+          <el-col :span="8"><el-form-item label="建成时间"><el-input v-model="form.compDate" disabled /></el-form-item></el-col>
         </el-row>
         <el-row>
-          <el-col :span="12">
-            <el-form-item label="工程状态">
-              <el-select v-model="form.engStat" placeholder="请选择状态" disabled>
-                <el-option label="正常" value="1" /><el-option label="停用" value="0" />
-              </el-select>
-            </el-form-item>
-          </el-col>
-          <el-col :span="12"><el-form-item label="管理单位"><el-input v-model="form.admDep" disabled /></el-form-item></el-col>
+          <el-col :span="8"><el-form-item label="归口管理部门">
+            <el-select v-model="form.admDep" disabled>
+              <el-option label="水利部门" value="1" /><el-option label="电力部门" value="2" /><el-option label="农业部门" value="3" /><el-option label="林业部门" value="4" /><el-option label="城建部门" value="5" /><el-option label="航运部门" value="6" /><el-option label="环保部门" value="7" /><el-option label="其他部门" value="9" />
+            </el-select>
+          </el-form-item></el-col>
+          <el-col :span="16"><el-form-item label="备注"><el-input v-model="form.note" disabled /></el-form-item></el-col>
         </el-row>
       </el-form>
       <template #footer><el-button @click="cancel">关 闭</el-button></template>
     </el-dialog>
 
-    <el-dialog v-model="statsVisible" title="泵站类型分布统计" width="600px" append-to-body>
+    <el-dialog v-model="statsVisible" title="水闸类型分布统计" width="600px" append-to-body>
       <el-table :data="statsData" stripe v-loading="statsLoading">
         <el-table-column prop="name" label="类型" min-width="120" />
         <el-table-column prop="value" label="数量" width="100" />
@@ -78,13 +127,14 @@ import { getWaga, delWaga, getWagaCountType } from '@/api/slgc/waga/index'
 const { proxy } = getCurrentInstance()
 const loading = ref(true); const total = ref(0); const open = ref(false); const title = ref('')
 const list = ref([]); const form = ref({})
-const queryParams = reactive({ pageNum: 1, pageSize: 10, wagaName: undefined })
+const queryParams = reactive({ pageNum: 1, pageSize: 10, wagaName: undefined, key: undefined, engGrad: undefined })
+const wagaTypeMap = { '1': '分(泄)洪闸', '2': '节制闸', '3': '排(退)水闸', '4': '引(进)水闸', '5': '挡潮闸', '6': '船闸', '9': '其他' }
 const statsVisible = ref(false); const statsLoading = ref(false); const statsData = ref([])
 function getList() { loading.value = true; getWcoWagaList(queryParams).then(res => { list.value = res.rows; total.value = res.total; loading.value = false }) }
 function handleQuery() { queryParams.pageNum = 1; getList() }
-function resetQuery() { queryParams.wagaName = undefined; handleQuery() }
+function resetQuery() { queryParams.wagaName = undefined; queryParams.key = undefined; queryParams.engGrad = undefined; handleQuery() }
 function handleDetail(row) { getWaga(row.wagaCode).then(res => { form.value = res.data; title.value = '详情'; open.value = true }) }
-function handleDelete(row) { proxy.$modal.confirm('确认删除泵站"' + row.wagaName + '"?').then(() => { delWaga(row.wagaCode).then(() => { proxy.$modal.msgSuccess('删除成功'); getList() }) }) }
+function handleDelete(row) { proxy.$modal.confirm('确认删除水闸"' + row.wagaName + '"?').then(() => { delWaga(row.wagaCode).then(() => { proxy.$modal.msgSuccess('删除成功'); getList() }) }) }
 function handleStats() { statsVisible.value = true; statsLoading.value = true; getWagaCountType().then(res => { statsData.value = res.data || []; statsLoading.value = false }).catch(() => { statsLoading.value = false }) }
 function cancel() { open.value = false }
 onMounted(() => { getList() })

+ 4 - 4
gw-ui/src/views/slgc/zggc/detail/index.vue

@@ -147,19 +147,19 @@ const stationPhotos = ref([
     key: 'tpz',
     name: '太浦闸',
     src: '/profile/station/tpz.jpg',
-    desc: '太浦闸位于江苏省苏州市吴江区,是太浦河控制工程的主体建筑物,承担防洪、排涝和水资源调度等功能。闸门总净宽120米,共30孔,是太湖流域重要的水利枢纽工程。'
+    desc: '10孔,每孔净宽12m,总净宽120m,闸室长70m,设计流量985m³/s。太浦闸位于江苏省苏州市吴江区,是太浦河控制工程的主体建筑物,承担防洪、排涝和水资源调度等功能。'
   },
   {
     key: 'wt',
     name: '望亭枢纽',
     src: '/profile/station/wt.jpg',
-    desc: '望亭枢纽位于江苏省苏州市相城区望亭镇,是望虞河入太湖口的控制性工程,具有防洪、排涝、引水、通航等综合功能,是太湖流域综合治理的关键节点。'
+    desc: '上槽下洞,槽宽60m,9孔,7.0×6.5m,设计流量400m³/s。望亭枢纽位于江苏省苏州市相城区望亭镇,是望虞河入太湖口的控制性工程,具有防洪、排涝、引水、通航等综合功能。'
   },
   {
     key: 'cs',
     name: '常熟水利枢纽',
     src: '/profile/station/cs.jpg',
-    desc: '常熟水利枢纽位于江苏省苏州市常熟市,是望虞河沿线重要的水利控制工程,主要承担区域防洪、排涝及水资源调配任务,为太湖水环境综合治理提供有力支撑。'
+    desc: '6×8m节制闸,16×190m船闸,9×20m³/s泵站。常熟水利枢纽位于江苏省苏州市常熟市,是望虞河沿线重要的水利控制工程,主要承担区域防洪、排涝及水资源调配任务。'
   }
 ])
 
@@ -208,7 +208,7 @@ onMounted(() => {
     id: query.id || 1, name: query.name || 'XX水库除险加固工程', type: query.type || '水库',
     contractor: 'XX建设公司', startDate: '2026-01-15', endDate: '2026-12-30',
     progress: query.progress || '65%', status: query.status || '施工中',
-    overview: '该工程是重点水利建设项目,建成后将有效提升区域防洪能力和水资源配置能力。'
+    overview: '重点水利建设项目,包含太浦闸(10孔,总净宽120m,设计流量985m³/s)、望亭枢纽(上槽下洞,槽宽60m,9孔,设计流量400m³/s)、常熟水利枢纽(6×8m节制闸、16×190m船闸、9×20m³/s泵站),建成后将有效提升区域防洪能力和水资源配置能力。'
   }
   loadMonitor()
 })

+ 75 - 30
gw-ui/src/views/slgc/zggc/portal.vue

@@ -20,7 +20,7 @@
           <el-card shadow="never" class="station-card">
             <template #header>
               <div class="station-card-header" :style="{ background: st.status === '超警' ? '#F56C6C' : '#086560' }">
-                <span class="station-name">{{ st.name }}</span>
+                <span class="station-name" @click="openStationVideo(st)">{{ st.name }}</span>
                 <el-tag :type="st.status === '超警' ? 'danger' : 'success'" size="small" effect="dark" class="status-tag">
                   {{ st.status }}
                 </el-tag>
@@ -58,7 +58,7 @@
               <el-divider style="margin:8px 0" />
               <div class="year-water">
                 <div class="year-item">
-                  <span class="year-label">今年引水量</span>
+                  <span class="year-label">{{ st.yearEnterWaterLabel }}</span>
                   <span class="year-value">{{ st.yearEnterWater }}</span>
                   <span class="year-unit">亿m³</span>
                 </div>
@@ -74,6 +74,9 @@
                 <span class="dispatch-label">当前调度令:</span>
                 <span class="dispatch-value">{{ st.dispatchOrder }}</span>
               </div>
+              <div style="margin-top:8px;text-align:right">
+                <el-button type="primary" link size="small" @click="openWeekData(st)">查看周历史数据</el-button>
+              </div>
             </div>
           </el-card>
         </el-col>
@@ -95,13 +98,26 @@
         <div id="viewDiv" class="map-container" />
       </el-card>
     </div>
+
+    <!-- Week history data dialog -->
+    <el-dialog v-model="weekDataVisible" :title="weekDataTitle" width="700px" destroy-on-close>
+      <el-table :data="weekDataList" v-loading="weekDataLoading" stripe border size="small" style="width:100%">
+        <el-table-column prop="time" label="时间" width="180" />
+        <el-table-column prop="waterLevel" label="水位(m)" align="center" />
+        <el-table-column prop="yearInflow" label="年引水量(亿m³)" align="center" />
+        <el-table-column prop="yearOutflow" label="年排水量(亿m³)" align="center" />
+      </el-table>
+      <template #footer>
+        <el-button @click="weekDataVisible = false">关闭</el-button>
+      </template>
+    </el-dialog>
   </div>
 </template>
 
 <script setup name="ZggcPortal">
 import { Monitor, VideoCamera, Folder, Refresh, WarningFilled, Notification } from '@element-plus/icons-vue'
-import { getPortalData } from '@/api/slgc/zggc/index'
-import { useArcGisMap } from '@/composables/useArcGisMap'
+import { getPortalData, getWeeklyData } from '@/api/slgc/zggc/index'
+import { useMap } from '@/composables/useMap'
 
 const router = useRouter()
 
@@ -109,10 +125,14 @@ const loading = ref(true)
 const refreshing = ref(false)
 const stations = ref([])
 
-const { map, view, initMap, createGraphicsLayer, createPointMarker, createCircleSymbol, addBasinBoundary } = useArcGisMap()
+const weekDataVisible = ref(false)
+const weekDataLoading = ref(false)
+const weekDataTitle = ref('周历史数据')
+const weekDataList = ref([])
+
+const { map, view, initMap, createGraphicsLayer, createPointMarker, createCircleSymbol, openPopup, closePopup, addBasinBoundary } = useMap()
 
 let graphicsLayer = null
-let currentHitGraphic = null
 
 const warningLevels = {
   '69642': 3.8,
@@ -146,6 +166,7 @@ function mapStation(s) {
     dispatchOrder: d.nowOrder || d.dispatchOrder || '-',
     yearEnterWater: d.yearEnterWater || d.yrEnter || '-',
     yearCommWater: d.yearCommWater || d.yrComm || '-',
+    yearEnterWaterLabel: s.stcd === 'tpz' || s.name === '太浦闸' ? '今年供水量' : '今年引水量',
     waterLevelWarn: !isNaN(wl) && !isNaN(warnLevel) ? wl > warnLevel : false,
     status: (!isNaN(wl) && !isNaN(warnLevel) && wl > warnLevel) ? '超警' : '正常',
   }
@@ -163,6 +184,28 @@ function loadPortalData() {
   })
 }
 
+function openWeekData(station) {
+  weekDataTitle.value = `${station.name} - 周历史数据`
+  weekDataVisible.value = true
+  weekDataLoading.value = true
+  weekDataList.value = []
+  getWeeklyData({ stcd: station.stcd }).then(res => {
+    const data = res.data || res.rows || res
+    if (Array.isArray(data)) {
+      weekDataList.value = data.map(item => ({
+        time: item.time || item.tm || '-',
+        waterLevel: item.waterLevel || item.upz || '-',
+        yearInflow: item.yearInflow || item.yrEnter || '-',
+        yearOutflow: item.yearOutflow || item.yrComm || '-',
+      }))
+    }
+  }).catch(() => {
+    ElMessage.warning('暂无周历史数据')
+  }).finally(() => {
+    weekDataLoading.value = false
+  })
+}
+
 function buildPopupContent(st) {
   const isWarn = st.waterLevelWarn
   return `
@@ -187,7 +230,7 @@ function buildPopupContent(st) {
 }
 
 function updateMapMarkers() {
-  if (!graphicsLayer || !view.value) return
+  if (!graphicsLayer || !map.value) return
   graphicsLayer.removeAll()
   stations.value.forEach(st => {
     const coords = stationCoords[st.name]
@@ -205,28 +248,20 @@ function updateMapMarkers() {
 }
 
 function setupHover() {
-  if (!view.value) return
-  view.value.on('pointer-move', (event) => {
-    view.value.hitTest(event).then(response => {
-      const results = response.results.filter(r =>
-        r.graphic && r.graphic.layer && r.graphic.layer.id === 'portal-markers'
-      )
-      if (results.length > 0) {
-        const graphic = results[0].graphic
-        if (graphic !== currentHitGraphic) {
-          currentHitGraphic = graphic
-          const attrs = graphic.attributes
-          view.value.popup.open({
-            title: attrs._popupTitle,
-            content: attrs._popupContent,
-            location: graphic.geometry,
-          })
-        }
-      } else if (currentHitGraphic) {
-        currentHitGraphic = null
-        view.value.popup.close()
-      }
+  if (!map.value) return
+  let currentFeature = null
+  map.value.on('pointermove', (event) => {
+    if (event.dragging) return
+    const feature = map.value.forEachFeatureAtPixel(event.pixel, f => f, {
+      layerFilter: l => l === graphicsLayer._layer,
     })
+    if (feature && feature !== currentFeature) {
+      currentFeature = feature
+      openPopup(map.value, feature.get('_popupTitle'), feature.get('_popupContent'), event.coordinate)
+    } else if (!feature && currentFeature) {
+      currentFeature = null
+      closePopup()
+    }
   })
 }
 
@@ -234,9 +269,8 @@ async function initPortalMap() {
   await initMap('viewDiv', [31.4, 120.55], 10)
   await addBasinBoundary()
   graphicsLayer = createGraphicsLayer('portal-markers')
-  map.value.add(graphicsLayer)
   updateMapMarkers()
-  setTimeout(() => view.value.resize(), 300)
+  setTimeout(() => map.value?.updateSize(), 300)
   setupHover()
 }
 
@@ -244,6 +278,10 @@ function goVideoAll() {
   router.push({ path: '/slgc/zggc/video', query: { name: '指挥中心' } })
 }
 
+function openStationVideo(station) {
+  router.push({ path: '/slgc/zggc/video', query: { stcd: station.stcd, name: station.name } })
+}
+
 function goFileAll() {
   router.push({ path: '/slgc/zggc/file', query: { stcd: '0' } })
 }
@@ -339,6 +377,13 @@ onMounted(() => {
   font-size: 15px;
   font-weight: 700;
   letter-spacing: 1px;
+  cursor: pointer;
+  transition: opacity 0.2s;
+}
+
+.station-name:hover {
+  opacity: 0.8;
+  text-decoration: underline;
 }
 
 .status-tag {

+ 134 - 7
gw-ui/src/views/slgc/zggc/video/index.vue

@@ -17,6 +17,7 @@
             <el-button size="small" @click="handleSnap">预览抓图</el-button>
             <el-button size="small" @click="osdVisible=true">字符叠加</el-button>
             <el-button size="small" @click="playbackVisible=true">历史回放</el-button>
+            <el-button size="small" @click="settingsVisible=true">设置</el-button>
             <el-button size="small" type="danger" @click="stopAllPreview">停止全部预览</el-button>
           </el-space>
         </div>
@@ -96,6 +97,73 @@
         <el-button @click="playbackVisible=false">关 闭</el-button>
       </template>
     </el-dialog>
+    <!-- 插件设置对话框 -->
+    <el-dialog v-model="settingsVisible" title="插件设置" width="520px">
+      <el-form :model="pluginSettings" label-width="120px" size="small">
+        <el-divider content-position="left">连接配置</el-divider>
+        <el-form-item label="AppKey">
+          <el-input v-model="pluginSettings.appkey" placeholder="请输入AppKey" />
+        </el-form-item>
+        <el-form-item label="Secret">
+          <el-input v-model="pluginSettings.secret" placeholder="请输入Secret" show-password />
+        </el-form-item>
+        <el-form-item label="服务器IP">
+          <el-input v-model="pluginSettings.ip" placeholder="请输入服务器IP" />
+        </el-form-item>
+        <el-form-item label="端口">
+          <el-input-number v-model="pluginSettings.port" :min="1" :max="65535" style="width:100%" />
+        </el-form-item>
+        <el-divider content-position="left">预览参数</el-divider>
+        <el-form-item label="预览模式">
+          <el-radio-group v-model="pluginSettings.previewMode">
+            <el-radio :value="0">空闲预览</el-radio>
+            <el-radio :value="1">选中窗口预览</el-radio>
+            <el-radio :value="2">指定窗口预览</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="码流类型">
+          <el-radio-group v-model="pluginSettings.streamMode">
+            <el-radio :value="0">主码流</el-radio>
+            <el-radio :value="1">子码流</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="传输协议">
+          <el-radio-group v-model="pluginSettings.transMode">
+            <el-radio :value="1">TCP</el-radio>
+            <el-radio :value="0">UDP</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="GPU硬解码">
+          <el-radio-group v-model="pluginSettings.gpuMode">
+            <el-radio :value="0">不启用</el-radio>
+            <el-radio :value="1">启用</el-radio>
+          </el-radio-group>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button type="primary" @click="applySettings">应用设置</el-button>
+        <el-button @click="settingsVisible=false">关 闭</el-button>
+      </template>
+    </el-dialog>
+    <!-- 调试信息 -->
+    <div class="debug-panel">
+      <div class="debug-header" @click="debugPanelVisible = !debugPanelVisible">
+        <span>调试信息 ({{ debugLogs.length }})</span>
+        <span class="debug-toggle">{{ debugPanelVisible ? '▲' : '▼' }}</span>
+      </div>
+      <div v-show="debugPanelVisible" class="debug-body">
+        <div class="debug-toolbar">
+          <el-button size="small" @click="clearDebugLogs">清空</el-button>
+        </div>
+        <div ref="debugLogsRef" class="debug-logs">
+          <div v-for="(log, i) in debugLogs" :key="i" class="debug-line">
+            <span class="debug-ts">[{{ log.ts }}]</span>
+            <span class="debug-msg">{{ log.msg }}</span>
+          </div>
+          <div v-if="!debugLogs.length" class="debug-empty">暂无调试日志</div>
+        </div>
+      </div>
+    </div>
   </div>
 </template>
 <script setup name="VideoMonitor">
@@ -107,10 +175,35 @@ const initCount = ref(0)
 const playHeight = ref(500)
 let pubKey = ''
 
+// 调试信息
+const debugLogs = ref([])
+const debugPanelVisible = ref(false)
+function cbIntegrationCallBack(info) {
+  const msg = typeof info === 'object' ? JSON.stringify(info) : String(info)
+  const now = new Date()
+  const ts = `${now.getFullYear()}-${String(now.getMonth()+1).padStart(2,'0')}-${String(now.getDate()).padStart(2,'0')} ${String(now.getHours()).padStart(2,'0')}:${String(now.getMinutes()).padStart(2,'0')}:${String(now.getSeconds()).padStart(2,'0')}.${String(now.getMilliseconds()).padStart(3,'0')}`
+  debugLogs.value.push({ ts, msg })
+  if (debugLogs.value.length > 500) debugLogs.value.splice(0, debugLogs.value.length - 500)
+}
+function clearDebugLogs() {
+  debugLogs.value = []
+}
+
 const osdVisible = ref(false)
 const osdForm = ref({ text: '', x: 0, y: 0, r: 255, g: 0, b: 0, wndId: 0 })
 const playbackVisible = ref(false)
 const playbackForm = ref({ startTime: '', endTime: '', streamMode: 0 })
+const settingsVisible = ref(false)
+const pluginSettings = ref({
+  appkey: '20671975',
+  secret: 'vBsmHRbAgkiWdrenmwC8',
+  ip: '10.8.8.201',
+  port: 443,
+  previewMode: 0,
+  streamMode: 0,
+  transMode: 1,
+  gpuMode: 0,
+})
 
 const cameraGroups = ref([
   { name: '太浦闸', cameras: [
@@ -142,7 +235,13 @@ function startPreview(code) {
   if (!window.oWebControl) return
   window.oWebControl.JS_RequestInterface({
     funcName: 'startPreview',
-    argument: JSON.stringify({ cameraIndexCode: code, streamMode: 0, transMode: 1, gpuMode: 0, wndId: -1 })
+    argument: JSON.stringify({
+      cameraIndexCode: code,
+      streamMode: pluginSettings.value.streamMode,
+      transMode: pluginSettings.value.transMode,
+      gpuMode: pluginSettings.value.gpuMode,
+      wndId: -1,
+    })
   })
 }
 function stopAllPreview() {
@@ -180,7 +279,7 @@ function startPlayback() {
       startTime: formatTime(playbackForm.value.startTime),
       endTime: formatTime(playbackForm.value.endTime),
       streamMode: playbackForm.value.streamMode,
-      transMode: 1, gpuMode: 0, wndId: -1
+      transMode: pluginSettings.value.transMode, gpuMode: pluginSettings.value.gpuMode, wndId: -1
     })
   }).then(() => { ElMessage.success('开始回放') })
 }
@@ -189,6 +288,17 @@ function stopPlayback() {
   window.oWebControl.JS_RequestInterface({ funcName: 'stopAllPlayback' })
     .then(() => ElMessage.success('已停止回放'))
 }
+function applySettings() {
+  settingsVisible.value = false
+  if (window.oWebControl) {
+    window.oWebControl.JS_Disconnect().then(() => {
+      window.oWebControl = null
+      pluginReady.value = false
+      initPlugin()
+    })
+  }
+  ElMessage.success('设置已应用,插件将重新初始化')
+}
 function setEncrypt(value) {
   if (!window.JSEncrypt || !pubKey) return value
   const encrypt = new window.JSEncrypt()
@@ -204,10 +314,16 @@ function initVideo() {
     window.oWebControl.JS_RequestInterface({
       funcName: 'init',
       argument: JSON.stringify({
-        appkey: '20671975', secret: setEncrypt('vBsmHRbAgkiWdrenmwC8'),
-        ip: setEncrypt('10.8.8.201'), playMode: 0, port: 443,
-        snapDir: 'D:\\SnapDir', videoDir: 'D:\\VideoDir',
-        layout: layout.value, enableHTTPS: 1, encryptedFields: 'secret,ip'
+        appkey: pluginSettings.value.appkey,
+        secret: setEncrypt(pluginSettings.value.secret),
+        ip: setEncrypt(pluginSettings.value.ip),
+        playMode: pluginSettings.value.previewMode,
+        port: pluginSettings.value.port,
+        snapDir: 'D:\\SnapDir',
+        videoDir: 'D:\\VideoDir',
+        layout: layout.value,
+        enableHTTPS: 1,
+        encryptedFields: 'secret,ip',
       })
     }).then(() => { pluginReady.value = true; startPreview(currentCamera.value) })
   })
@@ -228,7 +344,8 @@ function initPlugin() {
       pluginReady.value = false; window.oWebControl = null
       if (initCount.value < 3) setTimeout(initPlugin, 3000)
     },
-    cbConnectClose: () => { window.oWebControl = null }
+    cbConnectClose: () => { window.oWebControl = null },
+    cbIntegrationCallBack,
   })
 }
 function handleResize() {
@@ -258,4 +375,14 @@ onBeforeUnmount(() => {
 .download-link { color: #086560; font-size: 13px; }
 .play-wnd { width: 100%; border: 1px solid #e5e5e5; position: relative; background: #000; }
 .plugin-tip { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); }
+.debug-panel { margin-top: 12px; border: 1px solid #dcdfe6; border-radius: 4px; }
+.debug-header { padding: 8px 16px; background: #f5f7fa; cursor: pointer; display: flex; justify-content: space-between; align-items: center; font-size: 14px; font-weight: 500; user-select: none; }
+.debug-header:hover { background: #ecf5ff; }
+.debug-toggle { font-size: 12px; color: #909399; }
+.debug-toolbar { padding: 6px 12px; border-top: 1px solid #ebeef5; border-bottom: 1px solid #ebeef5; display: flex; justify-content: flex-end; }
+.debug-logs { max-height: 300px; overflow-y: auto; background: #1e1e1e; padding: 8px 12px; }
+.debug-line { font-family: 'Consolas', 'Courier New', monospace; font-size: 12px; line-height: 1.6; white-space: pre-wrap; word-break: break-all; }
+.debug-ts { color: #569cd6; margin-right: 8px; }
+.debug-msg { color: #d4d4d4; }
+.debug-empty { color: #666; font-family: monospace; font-size: 12px; text-align: center; padding: 20px; }
 </style>