77681 před 1 dnem
rodič
revize
2cdeaec8ad
40 změnil soubory, kde provedl 2313 přidání a 1064 odebrání
  1. 46 0
      gw-slgcrun/src/main/java/com/goldenwater/slgc/controller/CmsArticleController.java
  2. 47 0
      gw-slgcrun/src/main/java/com/goldenwater/slgc/controller/DocFileController.java
  3. 43 0
      gw-slgcrun/src/main/java/com/goldenwater/slgc/controller/WaterUnitController.java
  4. 8 3
      gw-slgcrun/src/main/java/com/goldenwater/slgc/controller/proxy/ZggcController.java
  5. 13 0
      gw-slgcrun/src/main/java/com/goldenwater/slgc/domain/CmsArticle.java
  6. 11 0
      gw-slgcrun/src/main/java/com/goldenwater/slgc/domain/SlgcWaterUnit.java
  7. 9 0
      gw-slgcrun/src/main/java/com/goldenwater/slgc/mapper/SlgcWaterUnitMapper.java
  8. 9 0
      gw-slgcrun/src/main/java/com/goldenwater/slgc/service/CmsArticleService.java
  9. 1 0
      gw-slgcrun/src/main/java/com/goldenwater/slgc/service/WaterUnitService.java
  10. 34 0
      gw-slgcrun/src/main/java/com/goldenwater/slgc/service/impl/CmsArticleServiceImpl.java
  11. 43 0
      gw-slgcrun/src/main/java/com/goldenwater/slgc/service/impl/WaterUnitServiceImpl.java
  12. 7 1
      gw-slgcrun/src/main/resources/mapper/slgc/SlgcWaterUnitMapper.xml
  13. 5 2
      gw-ui/.env.development
  14. 5 2
      gw-ui/.env.production
  15. 2 2
      gw-ui/.env.staging
  16. 4 0
      gw-ui/src/api/slgc/cms/index.js
  17. 3 0
      gw-ui/src/api/slgc/doc/index.js
  18. 4 0
      gw-ui/src/api/slgc/law/index.js
  19. 5 0
      gw-ui/src/api/slgc/unit/index.js
  20. 57 6
      gw-ui/src/composables/useMap.js
  21. 1 1
      gw-ui/src/layout/components/Navbar.vue
  22. 78 119
      gw-ui/src/layout/components/Sidebar/index.vue
  23. 11 2
      gw-ui/src/layout/components/TopNavbar/index.vue
  24. 70 21
      gw-ui/src/layout/index.vue
  25. 26 0
      gw-ui/src/router/modules/slgc.js
  26. 6 0
      gw-ui/src/store/modules/permission.js
  27. 1 1
      gw-ui/src/utils/request.js
  28. 225 35
      gw-ui/src/views/slgc/cms/article/index.vue
  29. 86 197
      gw-ui/src/views/slgc/dike/index.vue
  30. 145 15
      gw-ui/src/views/slgc/doc/index.vue
  31. 85 52
      gw-ui/src/views/slgc/gis/index.vue
  32. 2 2
      gw-ui/src/views/slgc/gis/wiu.vue
  33. 206 12
      gw-ui/src/views/slgc/law/index.vue
  34. 335 120
      gw-ui/src/views/slgc/monitor/index.vue
  35. 100 108
      gw-ui/src/views/slgc/pust/index.vue
  36. 82 196
      gw-ui/src/views/slgc/reservoir/index.vue
  37. 1 1
      gw-ui/src/views/slgc/unit/file.vue
  38. 361 51
      gw-ui/src/views/slgc/unit/index.vue
  39. 111 111
      gw-ui/src/views/slgc/waga/index.vue
  40. 25 4
      gw-ui/vite.config.js

+ 46 - 0
gw-slgcrun/src/main/java/com/goldenwater/slgc/controller/CmsArticleController.java

@@ -9,16 +9,20 @@ import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.PutMapping;
 import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 import com.goldenwater.common.annotation.Log;
 import com.goldenwater.common.core.controller.BaseController;
 import com.goldenwater.common.core.domain.AjaxResult;
 import com.goldenwater.common.core.page.TableDataInfo;
 import com.goldenwater.common.enums.BusinessType;
+import com.goldenwater.common.utils.poi.ExcelUtil;
 import com.goldenwater.slgc.domain.CmsArticle;
 import com.goldenwater.slgc.service.CmsArticleService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.web.multipart.MultipartFile;
 
 /**
  * 内容文章管理
@@ -146,4 +150,46 @@ public class CmsArticleController extends BaseController {
     public AjaxResult getArticlesPublicNum() {
         return success(cmsArticleService.selectTodayPublishedCount());
     }
+
+    /**
+     * 导出内容文章列表
+     *
+     * @param cmsArticle 查询条件
+     */
+    @Operation(summary = "13.10 导出内容文章")
+    @Log(title = "内容文章管理", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public void export(CmsArticle cmsArticle, HttpServletResponse response) {
+        List<CmsArticle> list = cmsArticleService.selectCmsArticleList(cmsArticle);
+        ExcelUtil<CmsArticle> util = new ExcelUtil<CmsArticle>(CmsArticle.class);
+        util.exportExcel(response, list, "内容文章数据");
+    }
+
+    /**
+     * 导入内容文章列表
+     *
+     * @param file 上传的Excel文件
+     * @param cateId 目标分类ID(可选,导入时统一归类)
+     * @return 导入结果
+     */
+    @Operation(summary = "13.11 导入内容文章")
+    @Log(title = "内容文章管理", businessType = BusinessType.IMPORT)
+    @PostMapping("/importData")
+    public AjaxResult importData(MultipartFile file,
+                                 @RequestParam(value = "cateId", required = false) String cateId) throws Exception {
+        ExcelUtil<CmsArticle> util = new ExcelUtil<CmsArticle>(CmsArticle.class);
+        List<CmsArticle> articleList = util.importExcel(file.getInputStream());
+        String message = cmsArticleService.importCmsArticle(articleList, cateId);
+        return success(message);
+    }
+
+    /**
+     * 下载导入模板
+     */
+    @Operation(summary = "13.12 下载内容文章导入模板")
+    @GetMapping("/importTemplate")
+    public void importTemplate(HttpServletResponse response) {
+        ExcelUtil<CmsArticle> util = new ExcelUtil<CmsArticle>(CmsArticle.class);
+        util.importTemplateExcel(response, "内容文章数据");
+    }
 }

+ 47 - 0
gw-slgcrun/src/main/java/com/goldenwater/slgc/controller/DocFileController.java

@@ -1,9 +1,17 @@
 package com.goldenwater.slgc.controller;
 
+import java.io.File;
 import java.math.BigDecimal;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
 import java.util.Date;
 import java.util.List;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+import jakarta.servlet.http.HttpServletResponse;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.MediaType;
 import org.springframework.web.bind.annotation.DeleteMapping;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.PathVariable;
@@ -20,6 +28,7 @@ import com.goldenwater.common.core.controller.BaseController;
 import com.goldenwater.common.core.domain.AjaxResult;
 import com.goldenwater.common.core.page.TableDataInfo;
 import com.goldenwater.common.enums.BusinessType;
+import com.goldenwater.common.utils.StringUtils;
 import com.goldenwater.common.utils.file.FileUploadUtils;
 import com.goldenwater.common.utils.uuid.IdUtils;
 import org.apache.commons.io.FilenameUtils;
@@ -173,4 +182,42 @@ public class DocFileController extends BaseController {
             return AjaxResult.error("上传失败: " + e.getMessage());
         }
     }
+
+    /**
+     * 批量下载文档文件(打包为ZIP)
+     *
+     * @param filePaths 文件路径列表(数据库 filePath 字段)
+     * @param response 响应
+     */
+    @Operation(summary = "16.08 批量下载文档(ZIP)")
+    @Log(title = "文档文件管理", businessType = BusinessType.EXPORT)
+    @PostMapping("/batchDownload")
+    public void batchDownload(@RequestBody List<String> filePaths, HttpServletResponse response) throws Exception {
+        if (filePaths == null || filePaths.isEmpty()) {
+            throw new com.goldenwater.common.exception.ServiceException("请选择要下载的文件");
+        }
+        String zipName = "文档批量下载.zip";
+        response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
+        response.setCharacterEncoding("utf-8");
+        String encodedName = URLEncoder.encode(zipName, StandardCharsets.UTF_8.name()).replaceAll("\\+", "%20");
+        response.setHeader("Content-Disposition", "attachment;filename*=utf-8''" + encodedName);
+
+        try (ZipOutputStream zos = new ZipOutputStream(response.getOutputStream())) {
+            int count = 0;
+            for (String filePath : filePaths) {
+                if (StringUtils.isEmpty(filePath)) continue;
+                // 拼接本地实际路径(与 /common/download 一致)
+                String localPath = RuoYiConfig.getProfile() + com.goldenwater.common.utils.file.FileUtils.stripPrefix(filePath);
+                File file = new File(localPath);
+                if (!file.exists() || !file.isFile()) continue;
+                // zip 内文件名:取原文件名,重名时加序号
+                String entryName = filePath.substring(filePath.lastIndexOf("/") + 1);
+                if (entryName.isEmpty()) entryName = "file_" + count;
+                zos.putNextEntry(new ZipEntry(entryName));
+                Files.copy(file.toPath(), zos);
+                zos.closeEntry();
+                count++;
+            }
+        }
+    }
 }

+ 43 - 0
gw-slgcrun/src/main/java/com/goldenwater/slgc/controller/WaterUnitController.java

@@ -15,12 +15,15 @@ import com.goldenwater.common.core.controller.BaseController;
 import com.goldenwater.common.core.domain.AjaxResult;
 import com.goldenwater.common.core.page.TableDataInfo;
 import com.goldenwater.common.enums.BusinessType;
+import com.goldenwater.common.utils.poi.ExcelUtil;
 import com.goldenwater.slgc.domain.SlgcWaterUnit;
 import com.goldenwater.slgc.domain.SlgcWaterUnitRecord;
 import com.goldenwater.slgc.service.WaterUnitService;
 import com.goldenwater.slgc.service.WaterUnitRecordService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.web.multipart.MultipartFile;
 
 /**
  * 水利考核管理
@@ -101,6 +104,46 @@ public class WaterUnitController extends BaseController {
         return toAjax(waterUnitService.deleteWaterUnitByIds(ids));
     }
 
+    /**
+     * 导出水管单位列表
+     *
+     * @param slgcWaterUnit 查询条件
+     */
+    @Operation(summary = "11.11 导出水管单位")
+    @Log(title = "水利考核管理", businessType = BusinessType.EXPORT)
+    @GetMapping("/export")
+    public void export(SlgcWaterUnit slgcWaterUnit, HttpServletResponse response) {
+        List<SlgcWaterUnit> list = waterUnitService.selectWaterUnitList(slgcWaterUnit);
+        ExcelUtil<SlgcWaterUnit> util = new ExcelUtil<SlgcWaterUnit>(SlgcWaterUnit.class);
+        util.exportExcel(response, list, "用水单位数据");
+    }
+
+    /**
+     * 导入水管单位列表
+     *
+     * @param file 上传的Excel文件
+     * @return 导入结果
+     */
+    @Operation(summary = "11.12 导入水管单位")
+    @Log(title = "水利考核管理", businessType = BusinessType.IMPORT)
+    @PostMapping("/importData")
+    public AjaxResult importData(MultipartFile file) throws Exception {
+        ExcelUtil<SlgcWaterUnit> util = new ExcelUtil<SlgcWaterUnit>(SlgcWaterUnit.class);
+        List<SlgcWaterUnit> unitList = util.importExcel(file.getInputStream());
+        String message = waterUnitService.importWaterUnit(unitList);
+        return success(message);
+    }
+
+    /**
+     * 下载导入模板
+     */
+    @Operation(summary = "11.13 下载水管单位导入模板")
+    @GetMapping("/importTemplate")
+    public void importTemplate(HttpServletResponse response) {
+        ExcelUtil<SlgcWaterUnit> util = new ExcelUtil<SlgcWaterUnit>(SlgcWaterUnit.class);
+        util.importTemplateExcel(response, "用水单位数据");
+    }
+
     /**
      * 查询审核记录列表
      *

+ 8 - 3
gw-slgcrun/src/main/java/com/goldenwater/slgc/controller/proxy/ZggcController.java

@@ -28,12 +28,17 @@ public class ZggcController extends BaseController {
 
     @Operation(summary = "22.01 查询水情数据")
     @GetMapping("/station")
-    public AjaxResult station(@RequestParam(defaultValue = "0") int statCode, String date) {
+    public AjaxResult station(@RequestParam(defaultValue = "0") int statCode,
+                              @RequestParam(required = false) String stcd, String date) {
         String d = date != null ? date : new java.text.SimpleDateFormat("yyyy-MM-dd").format(new java.util.Date());
         String[] codes = {"69642", "69485", "69462"};
-        String stcd = statCode >= 0 && statCode < 3 ? codes[statCode] : "69642";
+        // 优先使用前端传入的 stcd(直管工程站点详情),否则按索引取
+        String finalStcd = stcd;
+        if (finalStcd == null || finalStcd.isEmpty()) {
+            finalStcd = statCode >= 0 && statCode < 3 ? codes[statCode] : "69642";
+        }
         try {
-            String xml = restTemplate.getForObject(apiProperties.getZggc() + "/SYSW?strTime=" + d + "&stcd=" + stcd, String.class);
+            String xml = restTemplate.getForObject(apiProperties.getZggc() + "/SYSW?strTime=" + d + "&stcd=" + finalStcd, String.class);
             return success(parseDiffgram(xml));
         } catch (Exception e) {
             return success(Collections.emptyList());

+ 13 - 0
gw-slgcrun/src/main/java/com/goldenwater/slgc/domain/CmsArticle.java

@@ -1,5 +1,6 @@
 package com.goldenwater.slgc.domain;
 
+import com.goldenwater.common.annotation.Excel;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 
@@ -13,26 +14,35 @@ public class CmsArticle {
     private String aid;
     @Schema(description = "业务部门")
     private String bizDept;
+    @Excel(name = "分类ID")
     @Schema(description = "分类ID")
     private String cateId;
+    @Excel(name = "标题")
     @Schema(description = "标题")
     private String title;
+    @Excel(name = "副标题")
     @Schema(description = "副标题")
     private String subtitle;
+    @Excel(name = "关键词")
     @Schema(description = "关键词")
     private String keyword;
     @Schema(description = "是否图片新闻")
     private String isImage;
     @Schema(description = "缩略图")
     private String thumbnail;
+    @Excel(name = "来源")
     @Schema(description = "来源")
     private String source;
+    @Excel(name = "摘要")
     @Schema(description = "摘要")
     private String summary;
+    @Excel(name = "状态")
     @Schema(description = "状态")
     private String status;
+    @Excel(name = "发布人")
     @Schema(description = "发布人")
     private String pubUser;
+    @Excel(name = "发布日期", dateFormat = "yyyy-MM-dd")
     @Schema(description = "发布日期")
     private Date pubDate;
     @Schema(description = "截止日期")
@@ -47,16 +57,19 @@ public class CmsArticle {
     private String isDel;
     @Schema(description = "是否允许评论")
     private String isComment;
+    @Excel(name = "浏览次数")
     @Schema(description = "浏览次数")
     private Integer viewCount;
     @Schema(description = "评论次数")
     private Integer commentCount;
     @Schema(description = "文章内容")
     private String content;
+    @Excel(name = "发布平台")
     @Schema(description = "发布平台")
     private String pubPlatform;
     @Schema(description = "标题颜色")
     private String titleColor;
+    @Excel(name = "类型")
     @Schema(description = "类型")
     private String type;
 

+ 11 - 0
gw-slgcrun/src/main/java/com/goldenwater/slgc/domain/SlgcWaterUnit.java

@@ -1,5 +1,6 @@
 package com.goldenwater.slgc.domain;
 
+import com.goldenwater.common.annotation.Excel;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 
@@ -10,22 +11,31 @@ import java.util.Date;
 public class SlgcWaterUnit {
     @Schema(description = "主键ID")
     private String id;
+    @Excel(name = "单位名称")
     @Schema(description = "单位名称")
     private String name;
+    @Excel(name = "省份")
     @Schema(description = "省份")
     private String province;
+    @Excel(name = "城市")
     @Schema(description = "城市")
     private String city;
+    @Excel(name = "传真")
     @Schema(description = "传真")
     private String fax;
+    @Excel(name = "经度")
     @Schema(description = "经度")
     private String longitude;
+    @Excel(name = "纬度")
     @Schema(description = "纬度")
     private String latitude;
+    @Excel(name = "地址")
     @Schema(description = "地址")
     private String address;
+    @Excel(name = "联系人")
     @Schema(description = "联系人")
     private String contacts;
+    @Excel(name = "联系电话")
     @Schema(description = "联系电话")
     private String telphone;
     @Schema(description = "创建时间")
@@ -34,6 +44,7 @@ public class SlgcWaterUnit {
     private Date updateTime;
     @Schema(description = "是否删除(0-正常 1-删除)")
     private String isDelete;
+    @Excel(name = "最近考核日期")
     @Schema(description = "最近考核日期")
     private String approvalDate;
 }

+ 9 - 0
gw-slgcrun/src/main/java/com/goldenwater/slgc/mapper/SlgcWaterUnitMapper.java

@@ -3,6 +3,7 @@ package com.goldenwater.slgc.mapper;
 import java.util.List;
 import com.goldenwater.slgc.domain.SlgcWaterUnit;
 import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
 
 /** 水管单位 Mapper */
 @Mapper
@@ -53,4 +54,12 @@ public interface SlgcWaterUnitMapper {
      * @return 最大ID
      */
     public Integer findMaxId();
+
+    /**
+     * 按单位名称查询(导入时判重)
+     *
+     * @param unitName 单位名称
+     * @return 水管单位
+     */
+    public SlgcWaterUnit selectByUnitName(@Param("unitName") String unitName);
 }

+ 9 - 0
gw-slgcrun/src/main/java/com/goldenwater/slgc/service/CmsArticleService.java

@@ -67,4 +67,13 @@ public interface CmsArticleService {
      * @return 发布数
      */
     public int selectTodayPublishedCount();
+
+    /**
+     * 导入内容文章(按标题判重)
+     *
+     * @param articleList 文章列表
+     * @param cateId 目标分类ID(可为空,为空则保留Excel中的分类)
+     * @return 导入结果信息
+     */
+    public String importCmsArticle(List<CmsArticle> articleList, String cateId);
 }

+ 1 - 0
gw-slgcrun/src/main/java/com/goldenwater/slgc/service/WaterUnitService.java

@@ -44,4 +44,5 @@ public interface WaterUnitService {
      * @return 删除行数
      */
     public int deleteWaterUnitByIds(String[] ids);
+    public String importWaterUnit(List<SlgcWaterUnit> unitList);
 }

+ 34 - 0
gw-slgcrun/src/main/java/com/goldenwater/slgc/service/impl/CmsArticleServiceImpl.java

@@ -54,4 +54,38 @@ public class CmsArticleServiceImpl implements CmsArticleService {
     public int selectTodayPublishedCount() {
         return cmsArticleMapper.selectTodayPublishedCount();
     }
+
+    @Override
+    public String importCmsArticle(List<CmsArticle> articleList, String cateId) {
+        if (com.goldenwater.common.utils.StringUtils.isNull(articleList) || articleList.size() == 0) {
+            throw new RuntimeException("导入数据不能为空!");
+        }
+        int successNum = 0;
+        int failureNum = 0;
+        StringBuilder successMsg = new StringBuilder();
+        StringBuilder failureMsg = new StringBuilder();
+        for (CmsArticle article : articleList) {
+            try {
+                if (com.goldenwater.common.utils.StringUtils.isEmpty(article.getTitle())) {
+                    throw new RuntimeException("标题不能为空");
+                }
+                // 指定了目标分类则统一归类(如法规导入到"法律法规"分类)
+                if (com.goldenwater.common.utils.StringUtils.isNotEmpty(cateId)) {
+                    article.setCateId(cateId);
+                }
+                article.setIsDel("0");
+                cmsArticleMapper.insertCmsArticle(article);
+                successNum++;
+                successMsg.append("<br/>" + successNum + "、文章 " + article.getTitle() + " 导入成功");
+            } catch (Exception e) {
+                failureNum++;
+                failureMsg.append("<br/>" + failureNum + "、文章 " + article.getTitle() + " 导入失败:" + e.getMessage());
+            }
+        }
+        if (failureNum > 0) {
+            failureMsg.insert(0, "很抱歉,导入失败!共 " + failureNum + " 条数据格式不正确,错误如下:");
+            throw new RuntimeException(failureMsg.toString());
+        }
+        return successMsg.insert(0, "恭喜您,数据已全部导入成功!共 " + successNum + " 条,数据如下:").toString();
+    }
 }

+ 43 - 0
gw-slgcrun/src/main/java/com/goldenwater/slgc/service/impl/WaterUnitServiceImpl.java

@@ -4,6 +4,7 @@ import java.util.Date;
 import java.util.List;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
+import com.goldenwater.common.utils.StringUtils;
 import com.goldenwater.slgc.domain.SlgcWaterUnit;
 import com.goldenwater.slgc.mapper.SlgcWaterUnitMapper;
 import com.goldenwater.slgc.service.WaterUnitService;
@@ -45,4 +46,46 @@ public class WaterUnitServiceImpl implements WaterUnitService {
     public int deleteWaterUnitByIds(String[] ids) {
         return slgcWaterUnitMapper.deleteSlgcWaterUnitByIds(ids);
     }
+
+    @Override
+    public String importWaterUnit(List<SlgcWaterUnit> unitList) {
+        if (StringUtils.isNull(unitList) || unitList.size() == 0) {
+            throw new RuntimeException("导入数据不能为空!");
+        }
+        int successNum = 0;
+        int failureNum = 0;
+        StringBuilder successMsg = new StringBuilder();
+        StringBuilder failureMsg = new StringBuilder();
+        for (SlgcWaterUnit unit : unitList) {
+            try {
+                if (StringUtils.isEmpty(unit.getName())) {
+                    throw new RuntimeException("单位名称不能为空");
+                }
+                // 已存在同名单位则更新,否则新增
+                SlgcWaterUnit exist = slgcWaterUnitMapper.selectByUnitName(unit.getName());
+                unit.setIsDelete("0");
+                unit.setUpdateTime(new Date());
+                if (exist != null) {
+                    unit.setId(exist.getId());
+                    slgcWaterUnitMapper.updateSlgcWaterUnit(unit);
+                } else {
+                    unit.setCreateTime(new Date());
+                    Integer maxId = slgcWaterUnitMapper.findMaxId();
+                    unit.setId(String.valueOf(maxId));
+                    slgcWaterUnitMapper.insertSlgcWaterUnit(unit);
+                }
+                successNum++;
+                successMsg.append("<br/>" + successNum + "、单位 " + unit.getName() + " 导入成功");
+            } catch (Exception e) {
+                failureNum++;
+                String msg = "<br/>" + failureNum + "、单位 " + unit.getName() + " 导入失败:" + e.getMessage();
+                failureMsg.append(msg);
+            }
+        }
+        if (failureNum > 0) {
+            failureMsg.insert(0, "很抱歉,导入失败!共 " + failureNum + " 条数据格式不正确,错误如下:");
+            throw new RuntimeException(failureMsg.toString());
+        }
+        return successMsg.insert(0, "恭喜您,数据已全部导入成功!共 " + successNum + " 条,数据如下:").toString();
+    }
 }

+ 7 - 1
gw-slgcrun/src/main/resources/mapper/slgc/SlgcWaterUnitMapper.xml

@@ -26,7 +26,9 @@
     </sql>
 
     <select id="selectSlgcWaterUnitList" parameterType="SlgcWaterUnit" resultMap="SlgcWaterUnitResult">
-        select swu.*, SWUR.APPROVAL_DATE as APPROVAL_DATE
+        select swu.ID, swu.NAME, swu.PROVINCE, swu.CITY, swu.FAX, swu.LONGITUDE, swu.LATITUDE,
+               swu.ADDRESS, swu.CONTACTS, swu.TELPHONE, swu.CREATE_TIME, swu.UPDATE_TIME, swu.IS_DELETE,
+               SWUR.APPROVAL_DATE as APPROVAL_DATE
         from SLGC_WATER_UNIT swu
         LEFT JOIN (
             select UNIT_ID, MAX(APPROVAL_DATE) AS APPROVAL_DATE
@@ -109,4 +111,8 @@
         select COALESCE(max(to_number(id)) + 1, 1) from SLGC_WATER_UNIT
     </select>
 
+    <select id="selectByUnitName" parameterType="String" resultMap="SlgcWaterUnitResult">
+        select * from SLGC_WATER_UNIT where NAME = #{unitName} and IS_DELETE = '0' fetch first 1 rows only
+    </select>
+
 </mapper>

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

@@ -6,12 +6,15 @@ VITE_APP_ENV = 'development'
 #vue启动端口
 VITE_APP_PORT = 25022
 # 前端基础路径
-VITE_APP_BASE_TITLE = '/gw'
+VITE_APP_BASE_TITLE = '/slgc-run'
 # 北京金水管理系统/开发环境
-VITE_SERVICE_BASE_TITLE = '/gw-api'
+VITE_SERVICE_BASE_TITLE = '/slgc-run-api'
 
 # 后端接口基础路径
 VITE_SERVER_URL = 'http://127.0.0.1:8901/slgc-run'
 
 # GIS地图引擎: openlayers
 VITE_GIS_ENGINE = 'openlayers'
+
+# 天地图Key
+VITE_TIANDITU_KEY = '2d46608fc30b02c5855c5dc662e7d33a'

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

@@ -6,9 +6,9 @@ VITE_APP_ENV = 'production'
 #vue启动端口
 VITE_APP_PORT = 25022
 # 前端基础路径
-VITE_APP_BASE_TITLE = '/gw'
+VITE_APP_BASE_TITLE = '/slgc-run'
 # 北京金水管理系统/生产环境
-VITE_SERVICE_BASE_TITLE = '/gw-api'
+VITE_SERVICE_BASE_TITLE = '/slgc-run-api'
 
 # 后端接口基础路径
 VITE_SERVER_URL = 'http://39.98.38.2:18055/gw-api'
@@ -16,5 +16,8 @@ VITE_SERVER_URL = 'http://39.98.38.2:18055/gw-api'
 # GIS地图引擎: openlayers
 VITE_GIS_ENGINE = 'openlayers'
 
+# 天地图Key
+VITE_TIANDITU_KEY = '2d46608fc30b02c5855c5dc662e7d33a'
+
 # 是否在打包时开启压缩,支持 gzip 和 brotli
 VITE_BUILD_COMPRESS = gzip

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

@@ -7,10 +7,10 @@ VITE_APP_ENV = 'staging'
 #vue启动端口
 VITE_APP_PORT = 25022
 # 前端基础路径
-VITE_APP_BASE_TITLE = '/gw'
+VITE_APP_BASE_TITLE = '/slgc-run'
 
 # 北京金水管理系统/生产环境
-VITE_SERVICE_BASE_TITLE = '/gw-api'
+VITE_SERVICE_BASE_TITLE = '/slgc-run-api'
 
 # 后端接口基础路径
 VITE_SERVER_URL = 'http://39.98.38.2:18055/gw-api'

+ 4 - 0
gw-ui/src/api/slgc/cms/index.js

@@ -8,6 +8,10 @@ export function delArticle(aids) { return request({ url: '/slgc/cms-article/' +
 export function batchAddArticle(data) { return request({ url: '/slgc/cms-article/batch', method: 'post', data }) }
 export function batchUpdateArticle(data) { return request({ url: '/slgc/cms-article/batch', method: 'put', data }) }
 
+// 导入导出
+export function exportArticle(params) { return request({ url: '/slgc/cms-article/export', method: 'get', params, responseType: 'blob' }) }
+export function importTemplateUrl() { return '/slgc/cms-article/importTemplate' }
+
 export function getCateList(params) { return request({ url: '/slgc/cms-cate/list', method: 'get', params }) }
 export function getCateTree() { return request({ url: '/slgc/cms-cate/tree', method: 'get' }) }
 export function getCate(id) { return request({ url: '/slgc/cms-cate/' + id, method: 'get' }) }

+ 3 - 0
gw-ui/src/api/slgc/doc/index.js

@@ -18,3 +18,6 @@ export function moveDocFile(data) {
   params.append('fileCate', data.fileCate)
   return request({ url: '/slgc/doc-file/move', method: 'post', params, paramsSerializer: params => params.toString() })
 }
+export function batchDownloadDoc(data) {
+  return request({ url: '/slgc/doc-file/batchDownload', method: 'post', data, responseType: 'blob' })
+}

+ 4 - 0
gw-ui/src/api/slgc/law/index.js

@@ -10,3 +10,7 @@ export function getLawList(params) { return request({ url: '/slgc/cms-article/li
 export function getLawDetail(aid) { return request({ url: '/slgc/cms-article/' + aid, method: 'get' }) }
 export function saveLaw(data) { return data.aid ? request({ url: '/slgc/cms-article', method: 'put', data }) : request({ url: '/slgc/cms-article', method: 'post', data }) }
 export function delLaw(aids) { return request({ url: '/slgc/cms-article/' + aids, method: 'delete' }) }
+
+// 导入导出(复用 cms-article 接口)
+export function exportLaw(params) { return request({ url: '/slgc/cms-article/export', method: 'get', params, responseType: 'blob' }) }
+export function importTemplateUrl() { return '/slgc/cms-article/importTemplate' }

+ 5 - 0
gw-ui/src/api/slgc/unit/index.js

@@ -5,6 +5,11 @@ export function getUnit(id) { return request({ url: '/slgc/water-unit/' + id, me
 export function saveUnit(data) { return data.id ? request({ url: '/slgc/water-unit', method: 'put', data }) : request({ url: '/slgc/water-unit', method: 'post', data }) }
 export function delUnit(ids) { return request({ url: '/slgc/water-unit/' + ids, method: 'delete' }) }
 
+// 导入导出
+export function exportUnit(params) { return request({ url: '/slgc/water-unit/export', method: 'get', params, responseType: 'blob' }) }
+export function importUnit(data) { return request({ url: '/slgc/water-unit/importData', method: 'post', data }) }
+export function importTemplateUrl() { return '/slgc/water-unit/importTemplate' }
+
 export function getRecordList(params) { return request({ url: '/slgc/water-unit/record/list', method: 'get', params }) }
 export function getRecord(id) { return request({ url: '/slgc/water-unit/record/' + id, method: 'get' }) }
 export function saveRecord(data) { return data.id ? request({ url: '/slgc/water-unit/record', method: 'put', data }) : request({ url: '/slgc/water-unit/record', method: 'post', data }) }

+ 57 - 6
gw-ui/src/composables/useMap.js

@@ -1,10 +1,10 @@
-import { ref, onBeforeUnmount } from 'vue'
+import { ref, onBeforeUnmount } from 'vue'
 import Map from 'ol/Map.js'
 import View from 'ol/View.js'
 import TileLayer from 'ol/layer/Tile.js'
 import VectorLayer from 'ol/layer/Vector.js'
 import VectorSource from 'ol/source/Vector.js'
-import OSM from 'ol/source/OSM.js'
+import XYZ from 'ol/source/XYZ.js'
 import Feature from 'ol/Feature.js'
 import Point from 'ol/geom/Point.js'
 import Polygon from 'ol/geom/Polygon.js'
@@ -19,7 +19,16 @@ import { DEFAULT_CENTER, DEFAULT_ZOOM, TBA_BASIN_PATHS } from '@/utils/gis-confi
 proj4.defs('EPSG:4490', '+proj=longlat +ellps=GRS80 +no_defs +type=crs')
 register(proj4)
 
-const PROJ = 'EPSG:4490'
+// 视图投影:使用 EPSG:3857(Web墨卡托),与天地图瓦片一致,无需重投影
+const PROJ = 'EPSG:3857'
+
+// 天地图Key(环境变量优先,未配置时使用默认值)
+const TIANDITU_KEY = import.meta.env.VITE_TIANDITU_KEY || '2d46608fc30b02c5855c5dc662e7d33a'
+
+// 天地图瓦片URL(WMTS,EPSG:3857)—— 固定 t0 子域,避免 {s} 替换问题
+function tiandituUrl(layer) {
+  return `https://t0.tianditu.gov.cn/${layer}_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=${layer}&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${TIANDITU_KEY}`
+}
 
 export function useMap() {
   const map = ref(null)
@@ -50,22 +59,64 @@ export function useMap() {
   async function initMap(containerId, center, zoom) {
     const container = typeof containerId === 'string' ? document.getElementById(containerId) : containerId
 
+    // 天地图矢量底图 + 注记图层(国内可访问,EPSG:3857 与视图一致,直接渲染)
     const baseLayer = new TileLayer({
-      source: new OSM(),
+      source: new XYZ({
+        url: tiandituUrl('vec'),
+        maxZoom: 18,
+      }),
+    })
+    // 天地图矢量注记(地名标注层)
+    const labelLayer = new TileLayer({
+      source: new XYZ({
+        url: tiandituUrl('cva'),
+        maxZoom: 18,
+      }),
+    })
+
+    // 瓦片加载诊断日志(兼容不同OL版本,绝不让日志抛错中断渲染)
+    const getTileSrc = (e) => {
+      try {
+        const t = e.tile
+        if (!t) return 'unknown'
+        if (typeof t.getSrc === 'function') return t.getSrc()
+        if (t.src) return t.src
+        if (t.getImage && typeof t.getImage === 'function') {
+          const img = t.getImage()
+          return img && img.src ? img.src : 'no-src'
+        }
+        return 'unknown-tile'
+      } catch (err) {
+        return 'log-error'
+      }
+    }
+    baseLayer.getSource().on('tileloaderror', (e) => {
+      console.error('[Map] 底图瓦片加载失败:', getTileSrc(e))
+    })
+    labelLayer.getSource().on('tileloaderror', (e) => {
+      console.error('[Map] 注记瓦片加载失败:', getTileSrc(e))
     })
 
     const olView = new View({
       projection: PROJ,
-      center: center ? fromLonLat(center, PROJ) : fromLonLat(DEFAULT_CENTER, PROJ),
+      // center 参数约定为 [纬度, 经度],内部转换为 fromLonLat 需要的 [经度, 纬度]
+      center: fromLonLat([center[1], center[0]], PROJ),
       zoom: zoom ?? DEFAULT_ZOOM,
     })
 
     const olMap = new Map({
       target: container,
-      layers: [baseLayer],
+      layers: [baseLayer, labelLayer],
       view: olView,
     })
 
+    // 确保容器渲染完成后刷新尺寸(解决容器高度为0时瓦片不显示的问题)
+    setTimeout(() => {
+      olMap.updateSize()
+      console.log('[Map] 容器尺寸:', container.clientWidth, 'x', container.clientHeight)
+      console.log('[Map] 地图尺寸:', olMap.getSize())
+    }, 200)
+
     olMap.on('pointermove', (evt) => {
       if (evt.dragging) return
       const ll = toLonLat(evt.coordinate, PROJ)

+ 1 - 1
gw-ui/src/layout/components/Navbar.vue

@@ -105,7 +105,7 @@ function logout() {
     type: 'warning'
   }).then(() => {
     userStore.logOut().then(() => {
-      location.href = '/index'
+      location.href = import.meta.env.VITE_APP_BASE_TITLE + '/index'
     })
   }).catch(() => { })
 }

+ 78 - 119
gw-ui/src/layout/components/Sidebar/index.vue

@@ -1,7 +1,8 @@
 <!-- layout/components/Sidebar/index.vue -->
 <template>
-  <div class="sidebar-container" :class="{ collapsed: isCollapse }">
+  <div class="sidebar-container" :class="{ collapsed: isCollapse, empty: !menuList.length }">
     <el-menu
+      v-if="menuList.length > 0"
       :default-active="activeMenu"
       :collapse="isCollapse"
       background-color="#ffffff"
@@ -11,59 +12,31 @@
       unique-opened
       class="sidebar-menu"
     >
-      <template v-for="item in menuList" :key="item.path">
+      <template v-for="item in menuList" :key="item._fullPath">
         <!-- 有子菜单的项 -->
         <el-sub-menu
           v-if="item.children && item.children.length > 0"
-          :index="getFullPath(item.path)"
+          :index="item._fullPath"
         >
           <template #title>
             <svg-icon v-if="item.meta?.icon" :icon-class="item.meta.icon" />
             <span>{{ item.meta?.title }}</span>
           </template>
-          <!-- 二级菜单 -->
-          <template v-for="child in item.children" :key="child.path">
-            <el-sub-menu
-              v-if="child.children && child.children.length > 0"
-              :index="getFullPath(child.path, item.path)"
-            >
-              <template #title>
-                <svg-icon
-                  v-if="child.meta?.icon"
-                  :icon-class="child.meta.icon"
-                />
-                <span>{{ child.meta?.title }}</span>
-              </template>
-              <!-- 三级菜单 -->
-              <el-menu-item
-                v-for="subChild in child.children"
-                :key="subChild.path"
-                :index="getFullPath(subChild.path, child.path, item.path)"
-                @click="handleMenuClick(subChild, child, item)"
-              >
-                <svg-icon
-                  v-if="subChild.meta?.icon"
-                  :icon-class="subChild.meta.icon"
-                />
-                <span>{{ subChild.meta?.title }}</span>
-              </el-menu-item>
-            </el-sub-menu>
-            <!-- 二级菜单(无子菜单) -->
-            <el-menu-item
-              v-else
-              :index="getFullPath(child.path, item.path)"
-              @click="handleMenuClick(child, item)"
-            >
-              <svg-icon v-if="child.meta?.icon" :icon-class="child.meta.icon" />
-              <span>{{ child.meta?.title }}</span>
-            </el-menu-item>
-          </template>
+          <el-menu-item
+            v-for="child in item.children"
+            :key="child._fullPath"
+            :index="child._fullPath"
+            @click="handleMenuClick(child)"
+          >
+            <svg-icon v-if="child.meta?.icon" :icon-class="child.meta.icon" />
+            <span>{{ child.meta?.title }}</span>
+          </el-menu-item>
         </el-sub-menu>
 
         <!-- 没有子菜单的一级菜单项 -->
         <el-menu-item
           v-else
-          :index="getFullPath(item.path)"
+          :index="item._fullPath"
           @click="handleMenuClick(item)"
         >
           <svg-icon v-if="item.meta?.icon" :icon-class="item.meta.icon" />
@@ -105,101 +78,84 @@ export default {
       return route.path;
     });
 
+    // 给路由添加完整路径标记
+    function enrichWithFullPath(routes, basePath) {
+      return routes.map((item) => {
+        const itemPath = item.path || "";
+        // 如果子项路径已经包含斜杠(绝对路径),直接使用;否则拼接
+        const fullPath = itemPath.startsWith("/")
+          ? itemPath
+          : basePath
+          ? basePath + "/" + itemPath
+          : "/" + itemPath;
+        const enriched = { ...item, _fullPath: fullPath };
+        if (item.children && item.children.length > 0) {
+          enriched.children = enrichWithFullPath(item.children, fullPath);
+        }
+        return enriched;
+      });
+    }
+
     // 获取菜单列表
     const menuList = computed(() => {
-      const routes = permissionStore.sidebarRouters || [];
-      console.log('[Sidebar] sidebarRouters:', routes.length, routes);
-      console.log('[Sidebar] topMenuKey:', props.topMenuKey, typeof props.topMenuKey);
-      console.log('[Sidebar] route.path:', route.path);
-
-      if (!routes.length) return [];
-
-      // 1. 尝试按topMenuKey匹配(仅当topMenuKey是字符串且匹配到路由时)
-      if (props.topMenuKey && typeof props.topMenuKey === 'string' && props.topMenuKey.startsWith('/')) {
-        const current = routes.find((item) => item.path === props.topMenuKey);
-        if (current && current.children) {
-          console.log('[Sidebar] matched by topMenuKey:', current.path);
-          return current.children.filter((child) => !child.hidden);
+      const allRoutes = permissionStore.sidebarRouters || [];
+      if (!allRoutes.length) return [];
+
+      // 归一化路径:去掉前导斜杠用于比较
+      const normalize = (p) => (p || "").replace(/^\//, "");
+
+      // 1. 按 topMenuKey 匹配
+      if (props.topMenuKey) {
+        const normKey = normalize(props.topMenuKey);
+        const found = allRoutes.find(
+          (r) => normalize(r.path) === normKey
+        );
+        if (found && found.children) {
+          const items = found.children.filter((c) => !c.hidden);
+          return enrichWithFullPath(items, "/" + normKey);
         }
       }
 
-      // 2. 找当前路由所在的第一级父路由,返回其子菜单
-      const currentSegs = route.path.split("/").filter(Boolean);
-      if (currentSegs.length > 0) {
-        const firstLevel = "/" + currentSegs[0];
-        console.log('[Sidebar] looking for firstLevel:', firstLevel);
-        const parentRoute = routes.find((r) => r.path === firstLevel);
-        if (parentRoute && parentRoute.children) {
-          console.log('[Sidebar] found parentRoute:', parentRoute.path, 'children:', parentRoute.children.length);
-          return parentRoute.children.filter((child) => !child.hidden);
-        } else {
-          console.log('[Sidebar] no parentRoute found for:', firstLevel, 'available paths:', routes.map(r => r.path));
+      // 2. 按当前路由第一级路径匹配
+      const segs = route.path.split("/").filter(Boolean);
+      if (segs.length > 0) {
+        const firstSeg = segs[0];
+        const found = allRoutes.find(
+          (r) => normalize(r.path) === firstSeg
+        );
+        if (found && found.children) {
+          const items = found.children.filter((c) => !c.hidden);
+          return enrichWithFullPath(items, "/" + firstSeg);
         }
       }
 
       // 3. 兜底:返回第一个有子菜单的路由
-      for (const r of routes) {
+      for (const r of allRoutes) {
         if (r.children && r.children.length) {
-          console.log('[Sidebar] fallback route:', r.path);
-          return r.children.filter((child) => !child.hidden);
+          const items = r.children.filter((c) => !c.hidden);
+          return enrichWithFullPath(items, "/" + normalize(r.path));
         }
       }
 
       return [];
     });
 
-    // 基准路径:以当前顶层菜单为基础,保证首页点击菜单时路径正确
-    const basePath = computed(() => {
-      if (props.topMenuKey && props.topMenuKey.startsWith('/')) {
-        return props.topMenuKey;
-      }
-      const segs = route.path.split("/").filter(Boolean);
-      return segs.length > 0 ? "/" + segs[0] : "";
-    });
-
-    // 获取完整路径
-    const getFullPath = (path, parentPath = "", grandParentPath = "") => {
-      if (path.startsWith("/")) {
-        return path;
-      }
-
-      // 三级菜单
-      if (grandParentPath && parentPath) {
-        return basePath.value + "/" + grandParentPath + "/" + parentPath + "/" + path;
+    // 点击菜单(支持 query 参数)
+    const handleMenuClick = (item) => {
+      if (typeof item === "string") {
+        router.push(item);
+        return;
       }
-
-      // 二级菜单
-      if (parentPath) {
-        return basePath.value + "/" + parentPath + "/" + path;
-      }
-
-      // 一级菜单
-      return basePath.value + "/" + path;
-    };
-
-    // 点击菜单
-    const handleMenuClick = (
-      item,
-      parentItem = null,
-      grandParentItem = null,
-    ) => {
-      let fullPath = "";
-
-      if (grandParentItem && parentItem) {
-        fullPath =
-          basePath.value +
-          "/" +
-          grandParentItem.path +
-          "/" +
-          parentItem.path +
-          "/" +
-          item.path;
-      } else if (parentItem) {
-        fullPath = basePath.value + "/" + parentItem.path + "/" + item.path;
-      } else {
-        fullPath = basePath.value + "/" + item.path;
+      const fullPath = item._fullPath || item.path;
+      if (item.query) {
+        try {
+          const query = JSON.parse(item.query);
+          router.push({ path: fullPath, query });
+          return;
+        } catch (e) {
+          // query 解析失败则直接跳转
+        }
       }
-
       router.push(fullPath);
     };
 
@@ -212,7 +168,6 @@ export default {
       menuList,
       isCollapse,
       activeMenu,
-      getFullPath,
       handleMenuClick,
       toggleCollapse,
     };
@@ -229,6 +184,10 @@ export default {
   border-right: 1px solid #e4e7ed;
 }
 
+.sidebar-container.empty {
+  display: none;
+}
+
 .sidebar-menu {
   flex: 1;
   border: none;

+ 11 - 2
gw-ui/src/layout/components/TopNavbar/index.vue

@@ -10,7 +10,7 @@
       <div
         v-for="item in topMenus"
         :key="item.path"
-        :class="['menu-link', { active: activeMenu === item.path }]"
+        :class="['menu-link', { active: isActive(item.path) }]"
         @click="handleMenuClick(item.path)"
       >
         {{ item.meta?.title }}
@@ -99,6 +99,14 @@ export default {
       return routes.filter((item) => item.meta?.title && !item.hidden);
     });
 
+    // 判断菜单是否激活(兼容有无前导斜杠)
+    const isActive = (menuPath) => {
+      if (!props.activeMenu) return false;
+      const active = props.activeMenu.startsWith('/') ? props.activeMenu : '/' + props.activeMenu;
+      const menu = menuPath.startsWith('/') ? menuPath : '/' + menuPath;
+      return active === menu;
+    };
+
     const handleMenuClick = (path) => {
       emit("menu-click", path);
     };
@@ -133,7 +141,7 @@ export default {
       })
         .then(() => {
           userStore.logOut().then(() => {
-            location.href = "/index";
+            location.href = import.meta.env.VITE_APP_BASE_TITLE + "/index";
           });
         })
         .catch(() => {});
@@ -144,6 +152,7 @@ export default {
       userStore,
       settingsStore,
       isFullscreen,
+      isActive,
       handleMenuClick,
       handleCommand,
     };

+ 70 - 21
gw-ui/src/layout/index.vue

@@ -18,9 +18,9 @@
 
     <!-- 下方区域:左右结构 -->
     <div class="main-layout">
-      <!-- 左侧二三级菜单 -->
-      <div class="sidebar-wrapper" :class="{ collapsed: isCollapse }">
-        <sidebar v-if="!sidebar.hide" :top-menu-key="activeTopMenu" />
+      <!-- 左侧二三级菜单(页面内部面板的模块隐藏系统导航) -->
+      <div class="sidebar-wrapper" :class="{ collapsed: isCollapse, hidden: !showSystemSidebar }">
+        <sidebar v-if="!sidebar.hide && showSystemSidebar" :top-menu-key="activeTopMenu" />
       </div>
 
       <!-- 右侧内容区域 -->
@@ -50,12 +50,18 @@ const router = useRouter();
 
 // 首页路径
 const HOME_PATH = "/index";
+// 默认业务入口(工程基本信息 - 内部首页)
+const DEFAULT_BIZ_PATH = "/proj/index";
 
 // 计算属性
 const theme = computed(() => settingsStore.theme);
 const sidebar = computed(() => appStore.sidebar);
 const isCollapse = computed(() => appStore.sidebar.isCollapse);
 
+// 使用页面内部面板的顶级菜单(隐藏系统左侧导航)
+const INTERNAL_PANEL_MENUS = ["/proj", "/zggc", "/gis", "/assess", "/standard", "/legal"];
+const showSystemSidebar = computed(() => !INTERNAL_PANEL_MENUS.includes(activeTopMenu.value));
+
 // 当前选中的顶级菜单
 const activeTopMenu = ref("");
 
@@ -63,18 +69,26 @@ const activeTopMenu = ref("");
 const getCurrentTopMenu = () => {
   const currentPath = route.path;
 
-  // 首页统一映射到业务根菜单,使侧边栏固定显示业务模块
-  if (currentPath === HOME_PATH) {
-    return "/slgc";
+  // 首页统一映射到工程基本信息
+  if (currentPath === HOME_PATH || currentPath === "/") {
+    return "/proj";
   }
 
   try {
     const routes = permissionStore.topbarRouters || [];
-    const firstLevel = "/" + currentPath.split("/")[1];
-    const matched = routes.find((r) => r.path === firstLevel);
-    return matched?.path || "/slgc";
+    // 取第一段路径
+    const segs = currentPath.split("/").filter(Boolean);
+    if (segs.length === 0) return "/proj";
+    const firstSegment = segs[0];
+    // 归一化比较
+    const normSeg = firstSegment.toLowerCase();
+    const matched = routes.find((r) => {
+      const rp = (r.path || "").replace(/^\//, "").toLowerCase();
+      return rp === normSeg;
+    });
+    return matched ? ("/" + matched.path.replace(/^\//, "")) : "/proj";
   } catch (error) {
-    return "/slgc";
+    return "/proj";
   }
 };
 
@@ -84,15 +98,40 @@ function handleTopMenuClick(menuPath) {
 
   // 跳转到第一个子路由
   const routes = permissionStore.topbarRouters || [];
-  const targetRoute = routes.find((r) => r.path === menuPath);
-  if (targetRoute && targetRoute.children && targetRoute.children.length > 0) {
-    const firstChild = targetRoute.children.find((child) => !child.hidden);
-    if (firstChild) {
-      let fullPath = firstChild.path;
-      if (!fullPath.startsWith("/")) {
-        fullPath = menuPath + "/" + fullPath;
+  // 归一化路径比较
+  const normPath = (p) => (p || "").replace(/^\//, "");
+  const normClick = normPath(menuPath);
+
+  // 页面内部面板模块:固定跳转内部首页(路由在前端静态注册,不依赖后端菜单)
+  const INTERNAL_HOME = { proj: "/proj/index", zggc: "/zggc/index" };
+  if (INTERNAL_HOME[normClick]) {
+    router.push(INTERNAL_HOME[normClick]);
+    return;
+  }
+
+  const targetRoute = routes.find((r) => normPath(r.path) === normClick);
+  if (targetRoute) {
+    if (targetRoute.children && targetRoute.children.length > 0) {
+      // 有子菜单的目录路由 → 跳转到第一个子路由(含隐藏的内部首页)
+      const firstChild = targetRoute.children.find((child) => !child.hidden) || targetRoute.children[0];
+      if (firstChild) {
+        let childPath = firstChild.path;
+        if (!childPath.startsWith("/")) {
+          childPath = "/" + normClick + "/" + childPath;
+        }
+        if (firstChild.query) {
+          try {
+            router.push({ path: childPath, query: JSON.parse(firstChild.query) });
+            return;
+          } catch (e) {
+            // query 解析失败则直接跳转
+          }
+        }
+        router.push(childPath);
       }
-      router.push(fullPath);
+    } else {
+      // 叶节点路由(如直管工程)→ 直接跳转
+      router.push("/" + normClick);
     }
   }
 }
@@ -115,9 +154,9 @@ watch(
 );
 
 onMounted(() => {
-  // 如果当前是根路径,跳转到首页
-  if (route.path === "/") {
-    router.push(HOME_PATH);
+  // 如果当前是根路径或首页,跳转到工程基本信息-堤防管理(默认页面)
+  if (route.path === "/" || route.path === HOME_PATH) {
+    router.push(DEFAULT_BIZ_PATH);
   }
   activeTopMenu.value = getCurrentTopMenu();
 });
@@ -142,6 +181,16 @@ onMounted(() => {
   flex-shrink: 0;
 }
 
+/* 页面内部面板的模块(工程基本信息/直管工程)隐藏系统左侧导航 */
+.sidebar-wrapper.hidden {
+  width: 0;
+}
+
+/* 侧边栏无菜单时自动隐藏 */
+.sidebar-wrapper:has(.sidebar-container.empty) {
+  width: 0;
+}
+
 .sidebar-wrapper.collapsed {
   width: 64px;
 }

+ 26 - 0
gw-ui/src/router/modules/slgc.js

@@ -2,6 +2,32 @@
 import Layout from '@/layout'
 
 const slgcRoutes = [
+  {
+    path: '/proj/index',
+    component: Layout,
+    hidden: true,
+    children: [
+      {
+        path: '',
+        component: () => import('@/views/slgc/proj/index'),
+        name: 'ProjIndex',
+        meta: { title: '工程基本信息', icon: 'guide' },
+      },
+    ],
+  },
+  {
+    path: '/zggc/index',
+    component: Layout,
+    hidden: true,
+    children: [
+      {
+        path: '',
+        component: () => import('@/views/slgc/monitor/index'),
+        name: 'ZggcHome',
+        meta: { title: '直管工程', icon: 'monitor' },
+      },
+    ],
+  },
   {
     path: '/slgc/gis/wiu',
     component: Layout,

+ 6 - 0
gw-ui/src/store/modules/permission.js

@@ -44,6 +44,12 @@ const usePermissionStore = defineStore(
             const defaultRoutes = filterAsyncRouter(defaultData)
             const asyncRoutes = filterDynamicRoutes(dynamicRoutes)
             asyncRoutes.forEach(route => { router.addRoute(route) })
+            // 确保顶级路由路径以 "/" 开头(后端可能返回不带斜杠的路径)
+            rewriteRoutes.forEach(route => {
+              if (route.path && !route.path.startsWith('/') && !route.path.startsWith('http')) {
+                route.path = '/' + route.path
+              }
+            })
             this.setRoutes(rewriteRoutes)
             this.setSidebarRouters(constantRoutes.concat(sidebarRoutes))
             this.setDefaultRoutes(sidebarRoutes)

+ 1 - 1
gw-ui/src/utils/request.js

@@ -89,7 +89,7 @@ service.interceptors.response.use(res => {
         ElMessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', { confirmButtonText: '重新登录', cancelButtonText: '取消', type: 'warning' }).then(() => {
           isRelogin.show = false
           useUserStore().logOut().then(() => {
-            location.href = "/gw/index";
+            location.href = import.meta.env.VITE_APP_BASE_TITLE + "/index";
           })
       }).catch(() => {
         isRelogin.show = false

+ 225 - 35
gw-ui/src/views/slgc/cms/article/index.vue

@@ -23,11 +23,15 @@
         </el-card>
       </el-col>
       <el-col :span="18">
-    <el-card shadow="never">
+    <el-card shadow="never" class="content-card">
       <template #header>
         <div class="card-header">
           <span>内容管理</span>
-          <el-button v-if="queryParams.status === '0' || queryParams.status === '1'" type="primary" size="small" @click="handleAdd">新增文章</el-button>
+          <div class="header-actions">
+            <el-button size="small" icon="Download" @click="handleExport">导出</el-button>
+            <el-button size="small" icon="Upload" @click="openImportDialog">导入</el-button>
+            <el-button v-if="queryParams.status === '0' || queryParams.status === '1'" type="primary" size="small" @click="handleAdd">新增文章</el-button>
+          </div>
         </div>
       </template>
 
@@ -36,22 +40,22 @@
         <el-tab-pane label="全部" name="undefined" />
         <el-tab-pane :name="'0'">
           <template #label>
-            <span>草稿 <el-badge :value="statusCounts[0]" :max="999" class="tab-badge" /></span>
+            <span>草稿 <el-badge :value="statusCounts[0]" :max="999" :hidden="!statusCounts[0]" class="tab-badge" /></span>
           </template>
         </el-tab-pane>
         <el-tab-pane :name="'1'">
           <template #label>
-            <span>已发布 <el-badge :value="statusCounts[1]" :max="999" class="tab-badge" /></span>
+            <span>已发布 <el-badge :value="statusCounts[1]" :max="999" :hidden="!statusCounts[1]" class="tab-badge" /></span>
           </template>
         </el-tab-pane>
         <el-tab-pane :name="'2'">
           <template #label>
-            <span>已撤回 <el-badge :value="statusCounts[2]" :max="999" class="tab-badge" /></span>
+            <span>已撤回 <el-badge :value="statusCounts[2]" :max="999" :hidden="!statusCounts[2]" class="tab-badge" /></span>
           </template>
         </el-tab-pane>
         <el-tab-pane :name="'3'">
           <template #label>
-            <span>已删除 <el-badge :value="statusCounts[3]" :max="999" class="tab-badge" type="info" /></span>
+            <span>已删除 <el-badge :value="statusCounts[3]" :max="999" :hidden="!statusCounts[3]" class="tab-badge" type="info" /></span>
           </template>
         </el-tab-pane>
       </el-tabs>
@@ -118,7 +122,6 @@
         ref="tableRef"
       >
         <el-table-column type="selection" width="45" />
-        <el-table-column prop="aid" label="ID" width="60" />
         <el-table-column prop="title" label="标题" min-width="200" show-overflow-tooltip>
           <template #default="s">
             <span :style="{ color: s.row.titleColor || '' }">{{ s.row.title }}</span>
@@ -129,7 +132,7 @@
           <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="120">
+        <el-table-column prop="pubPlatform" label="发布平台" width="100">
           <template #default="s">
             {{ pubPlatformLabel(s.row.pubPlatform) }}
           </template>
@@ -141,33 +144,38 @@
             </el-tag>
           </template>
         </el-table-column>
-        <el-table-column prop="pubDate" label="发布日期" width="140" sortable="custom" />
+        <el-table-column prop="pubDate" label="发布日期" width="110" sortable="custom">
+          <template #default="s">{{ formatDate(s.row.pubDate) }}</template>
+        </el-table-column>
         <el-table-column prop="status" label="状态" width="80">
           <template #default="s">
             <el-tag :type="statusTagType(s.row.status)" size="small">{{ statusLabel(s.row.status) }}</el-tag>
           </template>
         </el-table-column>
         <el-table-column prop="viewCount" label="点击量" width="80" sortable="custom" />
-        <el-table-column label="操作" width="200" fixed="right">
+        <el-table-column label="操作" width="180" fixed="right">
           <template #default="s">
-            <el-button type="primary" link icon="View" @click="handleDetail(s.row)">查看</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>
+            <div class="op-group">
+              <el-button type="primary" link @click="handleDetail(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>
+            </div>
           </template>
         </el-table-column>
       </el-table>
 
-      <pagination
-        v-show="total > 0"
-        :total="total"
-        v-model:page="queryParams.pageNum"
-        v-model:limit="queryParams.pageSize"
-        @pagination="getList"
-      />
+      <div class="gc-pagination">
+        <pagination
+          v-show="total > 0"
+          :total="total"
+          v-model:page="queryParams.pageNum"
+          v-model:limit="queryParams.pageSize"
+          @pagination="getList"
+        />
+      </div>
     </el-card>
       </el-col>
     </el-row>
-
     <!-- 新增/编辑文章对话框 -->
     <el-dialog :title="title" v-model="open" width="900px" append-to-body top="3vh" destroy-on-close>
       <el-form :model="form" :rules="formRules" ref="formRef" label-width="90px">
@@ -247,7 +255,7 @@
               <el-upload
                 ref="thumbUploadRef"
                 class="thumbnail-uploader"
-                action="/slgc/cms-file"
+                action="/common/upload"
                 :show-file-list="false"
                 :on-success="handleThumbSuccess"
                 :before-upload="beforeThumbUpload"
@@ -279,7 +287,7 @@
         <el-form-item label="附件">
           <el-upload
             ref="uploadRef"
-            action="/slgc/cms-file"
+            action="/common/upload"
             :auto-upload="false"
             :on-change="handleFileChange"
             :on-remove="handleFileRemove"
@@ -389,15 +397,61 @@
         <el-button type="primary" :disabled="!moveDialog.targetCateId" @click="confirmMoveOrCopy">确定</el-button>
       </template>
     </el-dialog>
+
+    <!-- 导入文章弹窗 -->
+    <el-dialog title="导入文章" v-model="importVisible" width="420px" append-to-body>
+      <el-form label-width="80px">
+        <el-form-item label="目标分类">
+          <el-tree-select
+            v-model="importCateId"
+            :data="cateTreeData"
+            :props="{ label: 'cateName', value: 'cateId', children: 'children' }"
+            placeholder="请选择分类"
+            check-strictly
+            clearable
+            style="width:100%"
+          />
+        </el-form-item>
+        <el-form-item label="文件">
+          <el-upload
+            ref="importUploadRef"
+            :action="importUrl"
+            :headers="uploadHeaders"
+            :data="{ cateId: importCateId }"
+            accept=".xlsx,.xls"
+            :limit="1"
+            :on-success="handleImportSuccess"
+            :on-error="handleImportError"
+            :show-file-list="true"
+          >
+            <el-button type="primary" size="small" icon="Upload">选择Excel文件</el-button>
+            <template #tip>
+              <div class="el-upload__tip">仅支持 .xlsx/.xls 格式</div>
+            </template>
+          </el-upload>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button size="small" type="primary" icon="Download" @click="handleDownloadTemplate">下载模板</el-button>
+        <el-button @click="importVisible = false">关 闭</el-button>
+      </template>
+    </el-dialog>
   </div>
 </template>
 
 <script setup name="CmsArticle">
 import { Paperclip, Check, Top, RefreshLeft, Delete, DeleteFilled, Plus, CopyDocument, FolderOpened } from '@element-plus/icons-vue'
-import { getArticleList, getArticleDetail, addArticle, updateArticle, delArticle, getCateTree, batchAddArticle, batchUpdateArticle } from '@/api/slgc/cms/index'
+import { getToken } from '@/utils/auth'
+import { getArticleList, getArticleDetail, addArticle, updateArticle, delArticle, getCateTree, batchAddArticle, batchUpdateArticle, exportArticle, importTemplateUrl } from '@/api/slgc/cms/index'
 import Editor from '@/components/Editor/index'
 
 const { proxy } = getCurrentInstance()
+// 导入导出
+const importVisible = ref(false)
+const importUploadRef = ref(null)
+const importUrl = ref('/slgc/cms-article/importData')
+const importCateId = ref('')
+const uploadHeaders = ref({ Authorization: 'Bearer ' + getToken() })
 const loading = ref(true)
 const detailLoading = ref(false)
 const total = ref(0)
@@ -491,6 +545,12 @@ function pubPlatformLabel(val) {
   return map[val] || val || '-'
 }
 
+// 只显示年月日 YYYY-MM-DD
+function formatDate(val) {
+  if (!val) return '-'
+  return String(val).slice(0, 10)
+}
+
 /** 状态工具函数 */
 function statusLabel(status) {
   const map = { '0': '草稿', '1': '已发布', '2': '已撤回', '3': '已删除' }
@@ -525,17 +585,24 @@ function getCates() {
 
 /** 加载各状态数量 */
 function loadStatusCounts() {
-  getArticleList({ pageNum: 1, pageSize: 1 }).then(res => {
-    // 首次加载获取总数,各状态数量需要后端支持或单独请求
-    // 这里简单用 status 参数分别请求获取各状态数量
-    const statuses = ['0', '1', '2', '3']
-    statuses.forEach(s => {
-      getArticleList({ pageNum: 1, pageSize: 1, status: s }).then(r => {
-        statusCounts[s] = r.total || 0
+      getArticleList({ pageNum: 1, pageSize: 1 }).then(res => {
+        // 首次加载获取各状态数量(接口支持 status 过滤则分别获取)
+        const statuses = ['0', '1', '2', '3']
+        statuses.forEach(s => {
+          getArticleList({ pageNum: 1, pageSize: 1, status: s }).then(r => {
+            statusCounts[s] = r.total || 0
+          }).catch(() => {
+            statusCounts[s] = 0
+          })
+        })
+      }).catch(() => {
+        // 统计接口失败时全部置 0,避免角标异常
+        statusCounts['0'] = 0
+        statusCounts['1'] = 0
+        statusCounts['2'] = 0
+        statusCounts['3'] = 0
       })
-    })
-  })
-}
+    }
 
 /** 获取文章列表 */
 function getList() {
@@ -627,6 +694,53 @@ function handleAdd() {
   open.value = true
 }
 
+/** 打开导入弹窗:默认选中当前筛选分类 */
+function openImportDialog() {
+  importCateId.value = queryParams.cateId || ''
+  importVisible.value = true
+}
+
+/** 导出 */
+function handleExport() {
+  proxy.$modal.confirm('确认导出当前筛选条件下的全部数据?').then(() => {
+    const params = { ...queryParams }
+    if (params.status === 'undefined' || params.status === undefined) params.status = undefined
+    exportArticle(params).then(res => {
+      const blob = new Blob([res], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
+      const url = window.URL.createObjectURL(blob)
+      const link = document.createElement('a')
+      link.href = url
+      link.download = '内容文章数据.xlsx'
+      link.click()
+      window.URL.revokeObjectURL(url)
+    })
+  })
+}
+
+/** 下载导入模板 */
+function handleDownloadTemplate() {
+  window.open(importTemplateUrl(), '_blank')
+}
+
+/** 导入成功 */
+function handleImportSuccess(res) {
+  if (res.code === 200) {
+    proxy.$modal.msgSuccess(res.msg || '导入成功')
+    importVisible.value = false
+    getList()
+    loadStatusCounts()
+  } else {
+    proxy.$modal.msgError(res.msg || '导入失败')
+  }
+  if (importUploadRef.value) importUploadRef.value.clearFiles()
+}
+
+/** 导入失败 */
+function handleImportError() {
+  proxy.$modal.msgError('导入失败,请检查文件格式')
+  if (importUploadRef.value) importUploadRef.value.clearFiles()
+}
+
 /** 编辑 */
 function handleEdit(row) {
   getArticleDetail(row.aid).then(res => {
@@ -871,6 +985,68 @@ onMounted(() => {
 </script>
 
 <style scoped>
+/* 页面撑满高度 */
+.app-container {
+  height: calc(100vh - 60px);
+  display: flex;
+  flex-direction: column;
+  padding: 16px;
+  box-sizing: border-box;
+  overflow: hidden;
+}
+.app-container > .el-row {
+  flex: 1;
+  min-height: 0;
+}
+.app-container > .el-row > .el-col {
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+}
+.app-container > .el-row > .el-col > .content-card {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  min-height: 0;
+}
+.app-container > .el-row > .el-col > .content-card :deep(.el-card__body) {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  min-height: 0;
+  overflow: hidden;
+}
+/* 表格撑开 */
+.app-container > .el-row > .el-col > .content-card :deep(.el-table) {
+  flex: 1;
+  min-height: 0;
+}
+/* 非表格元素固定高度,不被压缩 */
+.content-card .status-tabs,
+.content-card :deep(.el-form),
+.content-card .article-toolbar,
+.content-card .batch-toolbar {
+  flex-shrink: 0;
+}
+.gc-pagination {
+  display: flex;
+  justify-content: flex-end;
+  margin-top: 16px;
+  flex-shrink: 0;
+}
+/* 操作栏不换行 */
+.op-group {
+  display: flex;
+  align-items: center;
+  white-space: nowrap;
+}
+.op-group .el-button {
+  margin-left: 0;
+  margin-right: 8px;
+}
+.op-group .el-button + .el-button {
+  margin-left: 0;
+}
 .card-header {
   font-weight: 600;
   font-size: 16px;
@@ -878,6 +1054,11 @@ onMounted(() => {
   justify-content: space-between;
   align-items: center;
 }
+.header-actions {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
 .mt-8 { margin-top: 8px; }
 .mt-16 { margin-top: 16px; }
 
@@ -954,13 +1135,22 @@ onMounted(() => {
 .cate-sidebar {
   height: 100%;
 }
+/* 左侧分类树卡片撑满 */
+.cate-sidebar {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  min-height: 0;
+}
 .cate-sidebar :deep(.el-card__body) {
   padding: 0;
+  flex: 1;
+  overflow: hidden;
 }
 .cate-tree-wrapper {
   padding: 8px 0;
   overflow-y: auto;
-  max-height: calc(100vh - 260px);
+  height: 100%;
 }
 .cate-tree-wrapper :deep(.el-tree-node__content) {
   height: 36px;

+ 86 - 197
gw-ui/src/views/slgc/dike/index.vue

@@ -1,155 +1,76 @@
-<template>
-  <div class="app-container">
-    <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>
-        </div>
-      </template>
-
-      <el-form :model="queryParams" :inline="true" size="small">
-        <el-form-item label="堤防名称">
-          <el-input v-model="queryParams.dikeName" placeholder="请输入堤防名称" clearable @keyup.enter="handleQuery" />
-        </el-form-item>
-        <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>
-          <el-button type="info" icon="DataAnalysis" @click="handleStats">统计</el-button>
-        </el-form-item>
-      </el-form>
-
-      <el-table v-loading="loading" :data="dikeList" stripe>
-        <el-table-column prop="dikeCode" label="编码" width="100" />
-        <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">
-          <template #default="scope">
-            {{ gradeMap[scope.row.dikeGrad] || scope.row.dikeGrad }}
-          </template>
-        </el-table-column>
-        <el-table-column prop="dikeType" label="类型" width="100" />
-        <el-table-column prop="dikePatt" label="堤防型式" width="100">
-          <template #default="scope">
-            {{ 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>
-        <el-table-column prop="updDate" label="更新日期" width="120" />
-        <el-table-column label="操作" width="200" fixed="right">
-          <template #default="scope">
-            <el-button type="primary" link icon="View" @click="handleDetail(scope.row)">查看</el-button>
-            <el-button type="primary" link icon="Edit" @click="handleEdit(scope.row)">编辑</el-button>
-            <el-button type="danger" link icon="Delete" @click="handleDelete(scope.row)">删除</el-button>
-          </template>
-        </el-table-column>
-      </el-table>
+<template>
+  <div class="gc-page">
+    <!-- 顶部工具栏 -->
+    <div class="gc-toolbar">
+      <el-button type="success" icon="DataAnalysis" @click="handleStats">统计</el-button>
+      <div class="gc-toolbar-right">
+        <el-input v-model="queryParams.dikeName" placeholder="请输入名称" clearable style="width: 160px" @keyup.enter="handleQuery" />
+        <el-input v-model="queryParams.startLoc" placeholder="请输入地址(起点终点都可)" clearable style="width: 200px" @keyup.enter="handleQuery" />
+        <el-select v-model="queryParams.dikePatt" placeholder="请选择类别-" clearable style="width: 140px">
+          <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-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+      </div>
+    </div>
+
+    <!-- 数据表格 -->
+    <el-table v-loading="loading" :data="list" stripe border style="width: 100%">
+      <el-table-column prop="dikeName" label="堤防名称" min-width="180" />
+      <el-table-column prop="startLoc" label="起始所在位置" min-width="200" />
+      <el-table-column prop="endLoc" label="终点所在位置" min-width="200" />
+      <el-table-column prop="dikeType" label="堤防类型" width="120" />
+      <el-table-column prop="dikeGrad" label="堤防级别" width="100">
+        <template #default="scope">{{ gradeMap[scope.row.dikeGrad] || scope.row.dikeGrad }}</template>
+      </el-table-column>
+      <el-table-column prop="dikePatt" label="堤防型式" width="120">
+        <template #default="scope">{{ dikePattMap[scope.row.dikePatt] || scope.row.dikePatt }}</template>
+      </el-table-column>
+      <el-table-column label="操作" width="100" fixed="right" align="center">
+        <template #default="scope">
+          <el-button type="primary" link icon="View" @click="handleDetail(scope.row)">查看</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <!-- 分页 -->
+    <div class="gc-pagination">
       <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
-    </el-card>
+    </div>
 
-    <el-dialog :title="title" v-model="open" width="720px" append-to-body>
-      <el-form :model="form" label-width="110px">
+    <!-- 查看详情弹窗 -->
+    <el-dialog :title="form.dikeName" v-model="open" width="720px" append-to-body>
+      <el-form :model="form" label-width="110px" :disabled="true">
         <el-row>
-          <el-col :span="12">
-            <el-form-item label="堤防编码">
-              <el-input v-model="form.dikeCode" :disabled="title === '编辑堤防'" />
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="堤防名称">
-              <el-input v-model="form.dikeName" />
-            </el-form-item>
-          </el-col>
+          <el-col :span="12"><el-form-item label="堤防编码">{{ form.dikeCode }}</el-form-item></el-col>
+          <el-col :span="12"><el-form-item label="堤防名称">{{ form.dikeName }}</el-form-item></el-col>
         </el-row>
         <el-row>
-          <el-col :span="12">
-            <el-form-item label="起始位置">
-              <el-input v-model="form.startLoc" />
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="终点位置">
-              <el-input v-model="form.endLoc" />
-            </el-form-item>
-          </el-col>
+          <el-col :span="12"><el-form-item label="起始位置">{{ form.startLoc }}</el-form-item></el-col>
+          <el-col :span="12"><el-form-item label="终点位置">{{ form.endLoc }}</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" />
-            </el-form-item>
-          </el-col>
-          <el-col :span="12">
-            <el-form-item label="等级">
-              <el-select v-model="form.dikeGrad" 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="长度(km)">{{ form.dikeLen }}</el-form-item></el-col>
+          <el-col :span="12"><el-form-item label="等级">{{ gradeMap[form.dikeGrad] }}</el-form-item></el-col>
         </el-row>
         <el-row>
-          <el-col :span="12">
-            <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-col :span="12"><el-form-item label="堤防型式">{{ dikePattMap[form.dikePatt] }}</el-form-item></el-col>
+          <el-col :span="12"><el-form-item label="堤防类型">{{ form.dikeType }}</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>
-          <el-col :span="12">
-            <el-form-item label="堤防类型">
-              <el-input v-model="form.dikeType" />
-            </el-form-item>
-          </el-col>
+          <el-col :span="12"><el-form-item label="运行状况">{{ runStatMap[form.runStat] }}</el-form-item></el-col>
         </el-row>
       </el-form>
-      <template #footer>
-        <el-button type="primary" @click="submitForm">确 定</el-button>
-        <el-button @click="cancel">取 消</el-button>
-      </template>
     </el-dialog>
 
-    <el-dialog v-model="statsVisible" title="堤防类型分布统计" width="600px" append-to-body>
+    <!-- 统计弹窗 -->
+    <el-dialog title="统计类别" v-model="statsVisible" width="500px" 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" />
+        <el-table-column prop="name" label="类别" min-width="120" />
+        <el-table-column prop="value" label="数量" width="120" />
       </el-table>
     </el-dialog>
   </div>
@@ -157,24 +78,18 @@
 
 <script setup name="Dike">
 import { getDikeList } from '@/api/slgc/gc/index'
-import { getDike, addDike, updateDike, delDike, getDikeCountType } from '@/api/slgc/dike/index'
+import { getDike, 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 dikeList = ref([])
-const queryParams = reactive({
-  pageNum: 1, pageSize: 10,
-  dikeName: undefined, startLoc: undefined, dikePatt: undefined
-})
+const list = ref([])
+const queryParams = reactive({ pageNum: 1, pageSize: 10, dikeName: undefined, startLoc: undefined, dikePatt: undefined })
 const form = ref({})
 const statsVisible = ref(false)
 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': '停用' }
@@ -182,53 +97,13 @@ const runStatMap = { '1': '在用良好', '2': '在用故障', '3': '停用' }
 function getList() {
   loading.value = true
   getDikeList(queryParams).then(res => {
-    dikeList.value = res.rows
-    total.value = res.total
-    loading.value = false
+    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
-  queryParams.dikePatt = undefined
-  handleQuery()
-}
-
 function handleDetail(row) {
-  getDike(row.dikeCode).then(res => {
-    form.value = res.data
-    title.value = '堤防详情'
-    open.value = true
-  })
+  getDike(row.dikeCode).then(res => { form.value = res.data; open.value = true })
 }
-
-function handleEdit(row) {
-  getDike(row.dikeCode).then(res => {
-    form.value = res.data
-    title.value = '编辑堤防'
-    open.value = true
-  })
-}
-
-function submitForm() {
-  const method = form.value.dikeCode && title.value === '编辑堤防' ? updateDike : addDike
-  method(form.value).then(() => {
-    proxy.$modal.msgSuccess('操作成功')
-    open.value = false
-    getList()
-  })
-}
-
-function cancel() { open.value = false }
-
-function handleAdd() {
-  form.value = { runStat: '1' }
-  title.value = '新增堤防'
-  open.value = true
-}
-
 function handleStats() {
   statsVisible.value = true
   statsLoading.value = true
@@ -238,18 +113,32 @@ function handleStats() {
   }).catch(() => { statsLoading.value = false })
 }
 
-function handleDelete(row) {
-  proxy.$modal.confirm('确认删除堤防"' + row.dikeName + '"?').then(() => {
-    delDike(row.dikeCode).then(() => {
-      proxy.$modal.msgSuccess('删除成功')
-      getList()
-    })
-  })
-}
-
 onMounted(() => { getList() })
 </script>
 
 <style scoped>
-.card-header { font-weight: 600; font-size: 16px; }
+.gc-page {
+  padding: 16px;
+  background: #fff;
+  min-height: calc(100vh - 60px);
+  display: flex;
+  flex-direction: column;
+}
+.gc-toolbar {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 16px;
+}
+.gc-toolbar-right {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+.gc-pagination {
+  margin-top: auto;
+  padding-top: 16px;
+  display: flex;
+  justify-content: flex-end;
+}
 </style>

+ 145 - 15
gw-ui/src/views/slgc/doc/index.vue

@@ -2,7 +2,7 @@
   <div class="app-container">
     <el-row :gutter="16">
       <el-col :span="6">
-        <el-card shadow="never">
+        <el-card shadow="never" class="cate-card">
           <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"
@@ -20,22 +20,36 @@
         </el-card>
       </el-col>
       <el-col :span="18">
-        <el-card shadow="never">
+        <el-card shadow="never" class="content-card">
           <template #header><div class="card-header"><span>文档列表</span><el-button type="primary" size="small" @click="handleUpload" v-hasRole="['admin']">上传文档</el-button></div></template>
+          <el-form :inline="true" size="small" style="margin-bottom:10px;">
+            <el-form-item label="文件名">
+              <el-input v-model="searchFileName" placeholder="请输入文件名" clearable @keyup.enter="handleSearch" />
+            </el-form-item>
+            <el-form-item>
+              <el-button type="primary" icon="Search" @click="handleSearch">搜索</el-button>
+              <el-button icon="Refresh" @click="searchFileName = ''; handleSearch()">重置</el-button>
+            </el-form-item>
+          </el-form>
           <el-table :data="fileList" stripe v-loading="loading" @selection-change="handleSelectionChange">
             <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 prop="fileSize" label="大小" width="90">
+              <template #default="s">{{ formatFileSize(s.row.fileSize) }}</template>
+            </el-table-column>
+            <el-table-column prop="fileTime" label="上传时间" width="110">
+              <template #default="s">{{ formatDate(s.row.fileTime) }}</template>
+            </el-table-column>
             <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)" 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>
+                <div class="op-group">
+                  <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)" 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>
+                </div>
               </template>
             </el-table-column>
           </el-table>
@@ -50,6 +64,7 @@
             @current-change="loadFileList"
           />
           <div style="margin-top: 12px" v-if="selectedRows.length > 0">
+            <el-button type="primary" size="small" icon="Download" @click="handleBatchDownload">批量下载 ({{ selectedRows.length }})</el-button>
             <el-button type="warning" size="small" @click="handleBatchMove" v-hasRole="['admin']">批量移动 ({{ selectedRows.length }})</el-button>
           </div>
         </el-card>
@@ -98,19 +113,36 @@
   </div>
 </template>
 <script setup name="Doc">
-import { getDocCateTree, getDocCate, getDocFileList, addDocCate, updateDocCate, delDocCate, delDocFile, moveDocFile, updateDocFile } from '@/api/slgc/doc/index'
+import { getDocCateTree, getDocCate, getDocFileList, addDocCate, updateDocCate, delDocCate, delDocFile, moveDocFile, updateDocFile, batchDownloadDoc } from '@/api/slgc/doc/index'
 const { proxy } = getCurrentInstance()
 const loading = ref(true); const cateTree = ref([]); const fileList = ref([]); const uploadVisible = ref(false)
 const uploadRef = ref(null)
 const uploadForm = ref({ fileDesc: '' })
 const uploadUrl = ref('/slgc/doc-file/upload')
 const currentCateId = ref('')
+const searchFileName = ref('')
 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 })
 const pageNum = ref(1); const pageSize = ref(20); const total = ref(0)
-function handleCateClick(data) { currentCateId.value = data.cateId; pageNum.value = 1; loadFileList() }
+function handleSearch() { pageNum.value = 1; loadFileList() }
+
+// 文件大小格式化
+function formatFileSize(bytes) {
+  if (!bytes && bytes !== 0) return '-'
+  if (bytes < 1024) return bytes + 'B'
+  if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + 'KB'
+  return (bytes / 1024 / 1024).toFixed(1) + 'MB'
+}
+
+// 只显示年月日 YYYY-MM-DD
+function formatDate(val) {
+  if (!val) return '-'
+  return String(val).slice(0, 10)
+}
+
+function handleCateClick(data) { currentCateId.value = data.cateId; searchFileName.value = ''; pageNum.value = 1; loadFileList() }
 function handleAddCate() {
   cateEditForm.value = { cateId: '', cateName: '', cateMemo: '', cateSeq: 0 }
   cateEditTitle.value = '新增分类'; cateEditVisible.value = true
@@ -144,13 +176,41 @@ function handleUploadSuccess(res) {
 function handleUploadError() { ElMessage.error('上传失败') }
 function handleSelectionChange(rows) { selectedRows.value = rows }
 function handleView(row) {
-  window.open(`/common/download?fileName=${encodeURIComponent(row.filePath)}`, '_blank')
+  window.open(`/common/download/resource?resource=${encodeURIComponent(row.filePath)}`, '_blank')
 }
 function handleDownload(row) {
-  const url = `/common/download?fileName=${encodeURIComponent(row.filePath)}`
+  const url = `/common/download/resource?resource=${encodeURIComponent(row.filePath)}`
   const a = document.createElement('a')
   a.href = url; a.download = row.fileName; a.click()
 }
+
+// 批量下载:后端打包ZIP一次性下载
+function handleBatchDownload() {
+  if (!selectedRows.value.length) return
+  ElMessageBox.confirm(`确认下载选中的 ${selectedRows.value.length} 个文件?`, '批量下载', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    type: 'warning',
+  }).then(() => {
+    const filePaths = selectedRows.value.map(r => r.filePath).filter(Boolean)
+    if (!filePaths.length) {
+      ElMessage.warning('所选文件无下载路径')
+      return
+    }
+    batchDownloadDoc(filePaths).then(res => {
+      const blob = new Blob([res], { type: 'application/zip' })
+      const url = window.URL.createObjectURL(blob)
+      const a = document.createElement('a')
+      a.href = url
+      a.download = '文档批量下载.zip'
+      a.click()
+      window.URL.revokeObjectURL(url)
+      ElMessage.success(`已下载 ${filePaths.length} 个文件`)
+    }).catch(() => {
+      ElMessage.error('批量下载失败')
+    })
+  }).catch(() => {})
+}
 function handleRename(row) {
   ElMessageBox.prompt('请输入新的文件名称', '重命名', {
     inputValue: row.fileName,
@@ -183,15 +243,85 @@ function handleDelete(row) {
 function loadFileList() {
   const params = { pageNum: pageNum.value, pageSize: pageSize.value }
   if (currentCateId.value) params.fileCate = currentCateId.value
+  if (searchFileName.value) params.fileName = searchFileName.value
   getDocFileList(params).then(r => { fileList.value = r.rows; total.value = r.total || 0; loading.value = false })
+    .catch(() => { loading.value = false })
 }
 function loadCateTree() { getDocCateTree().then(r => { cateTree.value = r.data || r.rows || [] }) }
 onMounted(() => { loadCateTree(); loadFileList() })
 </script>
 <style scoped>
+/* 页面撑满高度 */
+.app-container {
+  height: calc(100vh - 60px);
+  display: flex;
+  flex-direction: column;
+  padding: 16px;
+  box-sizing: border-box;
+  overflow: hidden;
+}
+.app-container > .el-row {
+  flex: 1;
+  min-height: 0;
+}
+.app-container > .el-row > .el-col {
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+}
+.app-container > .el-row > .el-col > .content-card {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  min-height: 0;
+}
+.app-container > .el-row > .el-col > .content-card :deep(.el-card__body) {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  min-height: 0;
+  overflow: hidden;
+}
+/* 表格撑开 */
+.app-container > .el-row > .el-col > .content-card :deep(.el-table) {
+  flex: 1;
+  min-height: 0;
+}
+/* 左侧分类树卡片撑满 */
+.cate-card {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  min-height: 0;
+}
+.cate-card :deep(.el-card__body) {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  min-height: 0;
+  overflow: hidden;
+}
+.cate-card :deep(.el-tree) {
+  flex: 1;
+  min-height: 0;
+  overflow-y: auto;
+}
 .card-header { font-weight: 600; font-size: 16px; display: flex; justify-content: space-between; align-items: center; }
 .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; }
+.pagination-wrap { margin-top: 12px; display: flex; justify-content: flex-end; flex-shrink: 0; }
+/* 操作栏不换行 */
+.op-group {
+  display: flex;
+  align-items: center;
+  white-space: nowrap;
+}
+.op-group .el-button {
+  margin-left: 0;
+  margin-right: 8px;
+}
+.op-group .el-button + .el-button {
+  margin-left: 0;
+}
 </style>

+ 85 - 52
gw-ui/src/views/slgc/gis/index.vue

@@ -1,50 +1,7 @@
-<template>
+<template>
   <div class="app-container gis-page">
     <div class="gis-layout">
-      <!-- Left: Map Area (85%) -->
-      <div class="map-area">
-        <div class="search-bar">
-          <el-input v-model="searchName" placeholder="站点名称" clearable size="small" class="search-input" />
-          <el-input v-model="searchCity" placeholder="行政区划" clearable size="small" class="search-input" />
-          <el-select v-model="searchType" placeholder="工程类型" clearable size="small" class="search-select">
-            <el-option label="全部" value="" />
-            <el-option label="水库" value="B" />
-            <el-option label="堤防" value="D" />
-            <el-option label="水闸" value="K" />
-            <el-option label="泵站" value="4" />
-          </el-select>
-        </div>
-        <div class="map-header">
-          <span class="map-title">
-            <el-icon style="margin-right:6px"><Location /></el-icon>
-            GIS工程地图
-          </span>
-          <div class="map-header-actions">
-            <span class="coord-display" v-if="coord">
-              经度: {{ coord.lng.toFixed(6) }} &nbsp;&nbsp; 纬度: {{ coord.lat.toFixed(6) }}
-            </span>
-            <el-button size="small" @click="$router.push('/slgc/gis/water')">水质水文监测</el-button>
-            <el-button size="small" icon="Refresh" @click="refreshMap">刷新</el-button>
-          </div>
-        </div>
-        <div class="map-wrapper" v-loading="loading" element-loading-text="工程数据加载中...">
-          <div ref="mapRef" id="viewDiv" class="map-container"></div>
-          <!-- Map Legend Overlay -->
-          <div class="map-legend" v-show="showEng">
-            <div class="legend-title">流域图例</div>
-            <div v-for="z in zoneList" :key="z.value" class="legend-item">
-              <span class="legend-dot" :style="{ background: z.color }"></span>
-              <span class="legend-label">{{ z.label }}</span>
-            </div>
-          </div>
-          <!-- Coordinate bar overlay -->
-          <div class="coord-bar" v-if="coord">
-            经度: {{ coord.lng.toFixed(6) }} &nbsp;|&nbsp; 纬度: {{ coord.lat.toFixed(6) }}
-          </div>
-        </div>
-      </div>
-
-      <!-- Right: Side Panel (15%) -->
+      <!-- Left: Side Panel (图层控制 + 工程列表) -->
       <div class="side-panel">
         <!-- Layer Controls -->
         <div class="panel-section layer-section">
@@ -85,7 +42,7 @@
           </div>
           <el-table
             ref="tableRef"
-            :data="filteredTableData"
+            :data="pagedTableData"
             stripe
             size="small"
             :height="tableHeight"
@@ -103,6 +60,59 @@
               </template>
             </el-table-column>
           </el-table>
+          <div class="gc-pagination">
+            <el-pagination
+              v-model:current-page="tablePageNum"
+              v-model:page-size="tablePageSize"
+              :page-sizes="[10, 20, 50]"
+              :total="filteredTableData.length"
+              layout="total, sizes, prev, pager, next, jumper"
+              small
+            />
+          </div>
+        </div>
+      </div>
+
+      <!-- Right: Map Area -->
+      <div class="map-area">
+        <div class="search-bar">
+          <el-input v-model="searchName" placeholder="站点名称" clearable size="small" class="search-input" />
+          <el-input v-model="searchCity" placeholder="行政区划" clearable size="small" class="search-input" />
+          <el-select v-model="searchType" placeholder="工程类型" clearable size="small" class="search-select">
+            <el-option label="全部" value="" />
+            <el-option label="水库" value="B" />
+            <el-option label="堤防" value="D" />
+            <el-option label="水闸" value="K" />
+            <el-option label="泵站" value="4" />
+          </el-select>
+        </div>
+        <div class="map-header">
+          <span class="map-title">
+            <el-icon style="margin-right:6px"><Location /></el-icon>
+            GIS工程地图
+          </span>
+          <div class="map-header-actions">
+            <span class="coord-display" v-if="coord">
+              经度: {{ coord.lng.toFixed(6) }} &nbsp;&nbsp; 纬度: {{ coord.lat.toFixed(6) }}
+            </span>
+            <el-button size="small" @click="$router.push('/slgc/gis/water')">水质水文监测</el-button>
+            <el-button size="small" icon="Refresh" @click="refreshMap">刷新</el-button>
+          </div>
+        </div>
+        <div class="map-wrapper" v-loading="loading" element-loading-text="工程数据加载中...">
+          <div ref="mapRef" id="viewDiv" class="map-container"></div>
+          <!-- Map Legend Overlay -->
+          <div class="map-legend" v-show="showEng">
+            <div class="legend-title">流域图例</div>
+            <div v-for="z in zoneList" :key="z.value" class="legend-item">
+              <span class="legend-dot" :style="{ background: z.color }"></span>
+              <span class="legend-label">{{ z.label }}</span>
+            </div>
+          </div>
+          <!-- Coordinate bar overlay -->
+          <div class="coord-bar" v-if="coord">
+            经度: {{ coord.lng.toFixed(6) }} &nbsp;|&nbsp; 纬度: {{ coord.lat.toFixed(6) }}
+          </div>
         </div>
       </div>
     </div>
@@ -226,7 +236,22 @@ const filteredTableData = computed(() => {
   }))
 })
 
-const tableHeight = computed(() => `calc(100vh - 480px)`)
+// 工程列表分页
+const tablePageNum = ref(1)
+const tablePageSize = ref(10)
+
+const pagedTableData = computed(() => {
+  const all = filteredTableData.value
+  const start = (tablePageNum.value - 1) * tablePageSize.value
+  return all.slice(start, start + tablePageSize.value)
+})
+
+// 过滤条件变化时重置到第一页
+watch(filteredTableData, () => {
+  tablePageNum.value = 1
+})
+
+const tableHeight = computed(() => `calc(100vh - 540px)`)
 
 const iframeSrc = computed(() => {
   if (!currentMarker.value || !currentMarker.value.name) return ''
@@ -440,7 +465,7 @@ function loadTyphoon() {
             fill: new Fill({ color: '#F56C6C' }),
           }),
         })
-        const markerFeat = new Feature(new Point(fromLonLat([baseLng, baseLat], 'EPSG:4490')))
+        const markerFeat = new Feature(new Point(fromLonLat([baseLng, baseLat], 'EPSG:3857')))
         markerFeat.setStyle(markerStyle)
         markerFeat.set('name', t.TFNAME || '台风' + (i + 1))
         markerFeat.set('_popupTitle', t.TFNAME || '台风' + (i + 1))
@@ -454,7 +479,7 @@ function loadTyphoon() {
           [baseLng + 0.8, baseLat + 0.8],
           [baseLng + 1.5, baseLat + 1.5],
         ]
-        const lineCoords = paths.map(p => fromLonLat(p, 'EPSG:4490'))
+        const lineCoords = paths.map(p => fromLonLat(p, 'EPSG:3857'))
         const lineFeat = new Feature(new LineString(lineCoords))
         lineFeat.setStyle(new Style({
           stroke: new Stroke({ color: '#F56C6C', width: 2, lineDash: [5, 5] }),
@@ -506,7 +531,7 @@ function onToggleTyphoon() {
 
 function onTableRowClick(row) {
   if (!view.value || !row.lng || !row.lat) return
-  view.value.animate({ center: fromLonLat([row.lng, row.lat], 'EPSG:4490'), zoom: 12 })
+  view.value.animate({ center: fromLonLat([row.lng, row.lat], 'EPSG:3857'), zoom: 12 })
   currentMarker.value = row
   detailDialogVisible.value = true
   detailTab.value = 'basic'
@@ -521,7 +546,7 @@ watch([searchName, searchCity, searchType], () => {
 })
 
 async function initGisMap() {
-  await initMap('viewDiv', [120.5, 31.2], 8)
+  await initMap('viewDiv', [31.2, 120.5], 8)
   await addBasinBoundary()
   engLayer = createGraphicsLayer('eng-layer')
   windLayer = createGraphicsLayer('wind-layer')
@@ -565,7 +590,7 @@ onMounted(() => initGisMap())
 
 <style scoped>
 .gis-page {
-  height: calc(100vh - 120px);
+  height: calc(100vh - 60px);
   overflow: hidden;
 }
 
@@ -795,6 +820,14 @@ onMounted(() => initGisMap())
   flex: 1;
 }
 
+/* 工程列表分页(右下角) */
+.gc-pagination {
+  display: flex;
+  justify-content: flex-end;
+  padding: 6px 2px 2px;
+  flex-shrink: 0;
+}
+
 .table-section :deep(.el-table th.el-table__cell) {
   padding: 4px 0;
   font-size: 12px;

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

@@ -1,4 +1,4 @@
-<template>
+<template>
   <div class="app-container gis-page">
     <div class="top-bar">
       <div class="top-bar-left">
@@ -501,7 +501,7 @@ function onSearch() {
 
 function onTableRowClick(row) {
   if (!view.value || !row.lng || !row.lat) return
-  view.value.animate({ center: fromLonLat([row.lng, row.lat], 'EPSG:4490'), zoom: 12 })
+  view.value.animate({ center: fromLonLat([row.lng, row.lat], 'EPSG:3857'), zoom: 12 })
   const fields = POPUP_FIELDS[row.typeKey] || []
   const content = buildPopupContent(row, fields)
   openPopup(map.value, row.stnm, content, [row.lng, row.lat])

+ 206 - 12
gw-ui/src/views/slgc/law/index.vue

@@ -2,7 +2,7 @@
   <div class="app-container">
     <el-row :gutter="16">
       <el-col :span="6">
-        <el-card shadow="never">
+        <el-card shadow="never" class="cate-card">
           <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">
@@ -19,14 +19,35 @@
         </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" v-hasRole="['admin']">新增法规</el-button></div></template>
+        <el-card shadow="never" class="content-card">
+          <template #header>
+            <div class="card-header">
+              <span>法规列表</span>
+              <div class="header-actions">
+                <el-button size="small" icon="Download" @click="handleExport">导出</el-button>
+                <el-button size="small" icon="Upload" @click="openImportDialog">导入</el-button>
+                <el-button type="primary" size="small" @click="handleAdd" v-hasRole="['admin']">新增法规</el-button>
+              </div>
+            </div>
+          </template>
+          <el-form :inline="true" size="small" style="margin-bottom:10px;">
+            <el-form-item label="标题">
+              <el-input v-model="searchTitle" placeholder="请输入标题" clearable @keyup.enter="handleSearch" />
+            </el-form-item>
+            <el-form-item>
+              <el-button type="primary" icon="Search" @click="handleSearch">搜索</el-button>
+              <el-button icon="Refresh" @click="searchTitle = ''; handleSearch()">重置</el-button>
+            </el-form-item>
+          </el-form>
           <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="cateId" label="分类" width="110">
+              <template #default="s">{{ getCateName(s.row.cateId) }}</template>
+            </el-table-column>
+            <el-table-column prop="pubDate" label="发布日期" width="110">
+              <template #default="s">{{ formatDate(s.row.pubDate) }}</template>
+            </el-table-column>
             <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>
@@ -34,9 +55,11 @@
             <el-table-column prop="viewCount" label="点击量" width="70" />
             <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)" v-hasRole="['admin']">编辑</el-button>
-                <el-button type="danger" link @click="handleDelete(s.row)" v-hasRole="['admin']">删除</el-button>
+                <div class="op-group">
+                  <el-button type="primary" link @click="handleView(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>
+                </div>
               </template>
             </el-table-column>
           </el-table>
@@ -96,24 +119,126 @@
         <el-button @click="cateOpen=false">取 消</el-button>
       </template>
     </el-dialog>
+    <!-- 导入法规弹窗 -->
+    <el-dialog title="导入法规" v-model="importVisible" width="420px" append-to-body>
+      <el-form label-width="80px">
+        <el-form-item label="目标分类">
+          <el-select v-model="importCateId" placeholder="请选择分类" style="width:100%">
+            <el-option v-for="c in cateTree" :key="c.cateId" :label="c.cateName" :value="c.cateId" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="文件">
+          <el-upload
+            ref="importUploadRef"
+            :action="importUrl"
+            :headers="uploadHeaders"
+            :data="{ cateId: importCateId }"
+            accept=".xlsx,.xls"
+            :limit="1"
+            :on-success="handleImportSuccess"
+            :on-error="handleImportError"
+            :show-file-list="true"
+          >
+            <el-button type="primary" size="small" icon="Upload">选择Excel文件</el-button>
+            <template #tip>
+              <div class="el-upload__tip">仅支持 .xlsx/.xls 格式</div>
+            </template>
+          </el-upload>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button size="small" type="primary" icon="Download" @click="handleDownloadTemplate">下载模板</el-button>
+        <el-button @click="importVisible = false">关 闭</el-button>
+      </template>
+    </el-dialog>
   </div>
 </template>
 <script setup name="Law">
-import { getLawCateList, getLawCateTree, getLawCate, addLawCate, updateLawCate, delLawCate, getLawList, getLawDetail, saveLaw, delLaw } from '@/api/slgc/law/index'
+import { getLawCateList, getLawCateTree, getLawCate, addLawCate, updateLawCate, delLawCate, getLawList, getLawDetail, saveLaw, delLaw, exportLaw, importTemplateUrl } from '@/api/slgc/law/index'
+import { getToken } from '@/utils/auth'
 import Editor from '@/components/Editor/index'
 const { proxy } = getCurrentInstance()
+// 导入导出
+const importVisible = ref(false)
+const importUploadRef = ref(null)
+const importUrl = ref('/slgc/cms-article/importData')
+const importCateId = ref('')
+const uploadHeaders = ref({ Authorization: 'Bearer ' + getToken() })
 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)
+const searchTitle = ref('')
+
+function handleSearch() { pageNum.value = 1; loadLawList() }
+
+// 打开导入弹窗:默认选中当前分类(或第一个分类)
+function openImportDialog() {
+  importCateId.value = currentCateId.value || (cateTree.value && cateTree.value[0] ? cateTree.value[0].cateId : '')
+  importVisible.value = true
+}
+
+// === 导入导出 ===
+function handleExport() {
+  proxy.$modal.confirm('确认导出当前筛选条件下的全部数据?').then(() => {
+    exportLaw({ title: searchTitle.value }).then(res => {
+      const blob = new Blob([res], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
+      const url = window.URL.createObjectURL(blob)
+      const link = document.createElement('a')
+      link.href = url
+      link.download = '法规数据.xlsx'
+      link.click()
+      window.URL.revokeObjectURL(url)
+    })
+  })
+}
+function handleDownloadTemplate() {
+  window.open(importTemplateUrl(), '_blank')
+}
+function handleImportSuccess(res) {
+  if (res.code === 200) {
+    proxy.$modal.msgSuccess(res.msg || '导入成功')
+    importVisible.value = false
+    loadLawList()
+  } else {
+    proxy.$modal.msgError(res.msg || '导入失败')
+  }
+  if (importUploadRef.value) importUploadRef.value.clearFiles()
+}
+function handleImportError() {
+  proxy.$modal.msgError('导入失败,请检查文件格式')
+  if (importUploadRef.value) importUploadRef.value.clearFiles()
+}
+
+// 分类ID → 分类名称(遍历左侧树)
+function getCateName(cateId) {
+  if (!cateId) return '-'
+  const walk = (list) => {
+    for (const item of list || []) {
+      if (String(item.cateId) === String(cateId)) return item.cateName
+      const sub = walk(item.children)
+      if (sub) return sub
+    }
+    return ''
+  }
+  return walk(cateTree.value) || cateId
+}
+
+// 只显示年月日 YYYY-MM-DD
+function formatDate(val) {
+  if (!val) return '-'
+  return String(val).slice(0, 10)
+}
 
-function handleCateClick(d) { currentCateId.value = d.cateId; pageNum.value = 1; loadLawList() }
+function handleCateClick(d) { currentCateId.value = d.cateId; searchTitle.value = ''; pageNum.value = 1; loadLawList() }
 function loadLawList() {
   const params = { pageNum: pageNum.value, pageSize: pageSize.value }
   if (currentCateId.value) params.cateId = currentCateId.value
+  if (searchTitle.value) params.title = searchTitle.value
   getLawList(params).then(r => { lawList.value = r.rows; total.value = r.total || 0; loading.value = false })
+    .catch(() => { loading.value = false })
 }
 function handleAdd() { form.value = {}; title.value = '新增法规'; open.value = true }
 function handleView(r) {
@@ -149,10 +274,79 @@ onMounted(() => {
 })
 </script>
 <style scoped>
+/* 页面撑满高度 */
+.app-container {
+  height: calc(100vh - 60px);
+  display: flex;
+  flex-direction: column;
+  padding: 16px;
+  box-sizing: border-box;
+  overflow: hidden;
+}
+.app-container > .el-row {
+  flex: 1;
+  min-height: 0;
+}
+.app-container > .el-row > .el-col {
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+}
+.app-container > .el-row > .el-col > .content-card {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  min-height: 0;
+}
+.app-container > .el-row > .el-col > .content-card :deep(.el-card__body) {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  min-height: 0;
+  overflow: hidden;
+}
+/* 表格撑开 */
+.app-container > .el-row > .el-col > .content-card :deep(.el-table) {
+  flex: 1;
+  min-height: 0;
+}
+/* 左侧分类树卡片撑满 */
+.cate-card {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  min-height: 0;
+}
+.cate-card :deep(.el-card__body) {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  min-height: 0;
+  overflow: hidden;
+}
+.cate-card :deep(.el-tree) {
+  flex: 1;
+  min-height: 0;
+  overflow-y: auto;
+}
 .card-header { font-weight:600;font-size:16px;display:flex;justify-content:space-between;align-items:center; }
+.header-actions { display: flex; align-items: center; gap: 8px; }
 .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; }
 .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; }
+.pagination-wrap { margin-top: 12px; display: flex; justify-content: flex-end; flex-shrink: 0; }
+/* 操作栏不换行 */
+.op-group {
+  display: flex;
+  align-items: center;
+  white-space: nowrap;
+}
+.op-group .el-button {
+  margin-left: 0;
+  margin-right: 8px;
+}
+.op-group .el-button + .el-button {
+  margin-left: 0;
+}
 </style>

+ 335 - 120
gw-ui/src/views/slgc/monitor/index.vue

@@ -1,168 +1,383 @@
 <template>
-  <div class="app-container monitor-page">
-    <div class="monitor-layout">
-      <div class="monitor-sidebar">
-        <div class="sidebar-title">站点列表</div>
-        <div
-          v-for="(st, i) in stationList"
-          :key="st.stcd"
-          class="sidebar-item"
-          :class="{ active: activeStation === i }"
-          @click="selectStation(i)"
-        >
-          <span class="station-dot" :class="{ online: st.status !== '离线' }"></span>
-          <span class="station-name">{{ st.name || st.stationName }}</span>
-        </div>
+  <div class="zggc-page">
+    <!-- 左侧内部面板:站点切换(不跳转路由) -->
+    <div class="zggc-sidebar">
+      <div
+        v-for="(st, i) in stations"
+        :key="st.stcd || i"
+        :class="['sidebar-item', { active: activeIdx === i }]"
+        @click="switchStation(i)"
+      >
+        <span class="station-dot" :class="{ online: st.status !== '离线' }"></span>
+        <span>{{ st.name || st.stationName }}</span>
       </div>
-      <div class="monitor-main">
-        <div class="map-wrapper" v-loading="mapLoading">
-          <div id="viewDiv" class="map-container"></div>
-          <div class="station-popup" v-if="activeStation !== null && stationList[activeStation]">
-            <div class="popup-header">{{ stationList[activeStation].name || stationList[activeStation].stationName }}</div>
-            <div class="popup-body">
-              <div class="popup-row"><span class="popup-label">水位</span><span class="popup-value">{{ stationList[activeStation].waterLevel || '-' }}</span></div>
-              <div class="popup-row"><span class="popup-label">流量</span><span class="popup-value">{{ stationList[activeStation].flow || '-' }} m³/s</span></div>
-              <div class="popup-row"><span class="popup-label">孔数</span><span class="popup-value">{{ stationList[activeStation].holeCount || '-' }}</span></div>
-              <div class="popup-row"><span class="popup-label">孔高</span><span class="popup-value">{{ stationList[activeStation].holeHeight || '-' }} m</span></div>
+    </div>
+
+    <!-- 右侧地图 -->
+    <div class="zggc-main">
+      <div class="map-container" v-loading="mapLoading">
+        <div id="zggcMap" class="map-inner"></div>
+      </div>
+
+      <!-- 站点详情弹窗 -->
+      <el-dialog
+        v-model="dialogVisible"
+        :title="currentStation?.name || currentStation?.stationName || '站点详情'"
+        width="860px"
+        append-to-body
+        :close-on-click-modal="true"
+      >
+        <el-tabs v-model="activeTab">
+          <!-- 基本信息 -->
+          <el-tab-pane label="基本信息" name="basic">
+            <div v-if="stationDetail" class="detail-content">
+              <div class="detail-section">
+                <el-descriptions :column="2" border size="small">
+                  <el-descriptions-item label="站名">{{ stationDetail.name || stationDetail.stationName }}</el-descriptions-item>
+                  <el-descriptions-item label="行政区划">{{ stationDetail.area }}</el-descriptions-item>
+                  <el-descriptions-item label="类型">{{ stationDetail.type }}</el-descriptions-item>
+                  <el-descriptions-item label="所属水系">{{ stationDetail.riverSystem }}</el-descriptions-item>
+                  <el-descriptions-item label="经度">{{ stationDetail.lng }}</el-descriptions-item>
+                  <el-descriptions-item label="纬度">{{ stationDetail.lat }}</el-descriptions-item>
+                </el-descriptions>
+                <div v-if="stationDetail.description" class="station-desc">
+                  <p>{{ stationDetail.description }}</p>
+                </div>
+              </div>
             </div>
-            <div style="padding:6px 14px 10px;text-align:right;border-top:1px solid #eee">
-              <a style="color:#409EFF;font-size:12px;text-decoration:none;cursor:pointer"
-                 @click.stop="openHistoryChart(stationList[activeStation])">查看历史图表 &gt;</a>
+          </el-tab-pane>
+
+          <!-- 实时工情 -->
+          <el-tab-pane label="实时工情" name="realtime">
+            <div class="realtime-grid">
+              <div class="metric-card">
+                <div class="metric-label">水位(m)</div>
+                <div class="metric-value">{{ currentStation?.waterLevel || '-' }}</div>
+              </div>
+              <div class="metric-card">
+                <div class="metric-label">流量(m³/s)</div>
+                <div class="metric-value">{{ currentStation?.flow || '-' }}</div>
+              </div>
+              <div class="metric-card">
+                <div class="metric-label">孔数</div>
+                <div class="metric-value">{{ currentStation?.holeCount || '-' }}</div>
+              </div>
+              <div class="metric-card">
+                <div class="metric-label">孔高(m)</div>
+                <div class="metric-value">{{ currentStation?.holeHeight || '-' }}</div>
+              </div>
             </div>
-          </div>
-        </div>
-      </div>
+          </el-tab-pane>
+
+          <!-- 实时监控 -->
+          <el-tab-pane label="实时监控" name="video">
+            <div class="video-section">
+              <el-empty v-if="!videoUrl" description="暂无视频监控" />
+              <video v-else :src="videoUrl" controls style="width:100%;max-height:400px" />
+            </div>
+          </el-tab-pane>
+
+          <!-- 文档资料 -->
+          <el-tab-pane label="文档资料" name="docs">
+            <div class="doc-section">
+              <el-table :data="docList" stripe size="small" v-loading="docLoading">
+                <el-table-column prop="fileName" label="文件名" min-width="200" />
+                <el-table-column prop="createBy" label="上传人" width="100" />
+                <el-table-column prop="createTime" label="上传时间" width="160" />
+                <el-table-column label="操作" width="80" align="center">
+                  <template #default="{ row }">
+                    <el-button type="primary" link icon="Download" @click="downloadDoc(row)">下载</el-button>
+                  </template>
+                </el-table-column>
+              </el-table>
+            </div>
+          </el-tab-pane>
+        </el-tabs>
+      </el-dialog>
     </div>
   </div>
 </template>
 
-<script setup name="MonitorIndex">
-import { getPortalData } from '@/api/slgc/zggc/index'
-import { ref, onMounted, nextTick } from 'vue'
+<script setup name="ZggcMonitor">
+import { ref, onMounted, onBeforeUnmount, onActivated, nextTick } from 'vue'
+import { getPortalData, getStationDetail } from '@/api/slgc/zggc/index'
+import { getDocFileList } from '@/api/slgc/doc/index'
 import { fromLonLat } from 'ol/proj.js'
 import { useMap } from '@/composables/useMap'
 
 const {
-  map,
-  view,
-  initMap,
-  createGraphicsLayer,
-  createPointMarker,
-  createCircleSymbol,
-  addBasinBoundary,
-  openPopup,
+  map, view, initMap, createGraphicsLayer,
+  createPointMarker, createCircleSymbol,
+  addBasinBoundary, openPopup,
 } = useMap()
 
 const mapLoading = ref(true)
-const stationList = ref([])
-const activeStation = ref(null)
-
-const STATION_IDX = '_stIdx'
+const stations = ref([])
+const activeIdx = ref(null)
+const dialogVisible = ref(false)
+const currentStation = ref(null)
+const activeTab = ref('basic')
+const stationDetail = ref(null)
+const videoUrl = ref('')
+const docList = ref([])
+const docLoading = ref(false)
 
-const stationCoords = {
+const STATION_COORDS = {
   太浦闸: [30.98, 120.56],
   望亭枢纽: [31.46, 120.42],
   常熟水利枢纽: [31.65, 120.75],
 }
 
+const DEFAULT_STATIONS = [
+  { stcd: '69642', name: '太浦闸', stationName: '太浦闸', waterLevel: '3.43', flow: '723', holeCount: '30', holeHeight: '4.0', status: '正常' },
+  { stcd: '69485', name: '望亭枢纽', stationName: '望亭枢纽', waterLevel: '3.46', flow: '474', holeCount: '24', holeHeight: '3.5', status: '正常' },
+  { stcd: '69462', name: '常熟水利枢纽', stationName: '常熟水利枢纽', waterLevel: '3.45', flow: '0', holeCount: '16', holeHeight: '3.8', status: '正常' },
+]
+
 const DEFAULT_CENTER = [31.2, 120.6]
 const DEFAULT_ZOOM = 9
 
-function popupContent(st) {
-  const name = st.name || st.stationName
-  return `<div style="font-weight:600;font-size:14px;margin-bottom:6px">${name}</div>
-<div>水位:${st.waterLevel || '-'}</div>
-<div>流量:${st.flow || '-'} m³/s</div>
-<div>孔数:${st.holeCount || '-'}</div>
-<div>孔高:${st.holeHeight || '-'} m</div>
-<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(name)}', '_blank')">
-    查看历史图表 &gt;
-  </a>
-</div>`
-}
-
-function openHistoryChart(st) {
-  const name = st.name || st.stationName
-  window.open(`/slgc/hydrology?stnm=${encodeURIComponent(name)}`, '_blank')
-}
+let stationLayer = null
 
-async function selectStation(i) {
-  activeStation.value = i
-  const st = stationList.value[i]
+// 面板切换:不跳转路由,只切换内容
+function switchStation(i) {
+  activeIdx.value = i
+  const st = stations.value[i]
   if (!st) return
+  currentStation.value = st
+  dialogVisible.value = true
+  activeTab.value = 'basic'
+
   const name = st.name || st.stationName
-  const coords = stationCoords[name]
-  if (!coords || !view.value) return
-  await view.value.animate({ center: fromLonLat(coords, 'EPSG:4490'), zoom: 11 })
-  openPopup(map.value, name, popupContent(st), fromLonLat(coords, 'EPSG:4490'))
+  const coords = STATION_COORDS[name]  // [纬度, 经度]
+  if (coords && view.value) {
+    view.value.animate({ center: fromLonLat([coords[1], coords[0]], 'EPSG:3857'), zoom: 11 })
+  }
+  loadStationDetail(st)
+  loadDocs(st)
 }
 
-async function renderMap() {
-  await initMap('viewDiv', DEFAULT_CENTER, DEFAULT_ZOOM)
-  await addBasinBoundary()
+async function loadStationDetail(st) {
+  stationDetail.value = null
+  try {
+    const res = await getStationDetail({ stcd: st.stcd, name: st.name || st.stationName })
+    stationDetail.value = res.data || st
+  } catch {
+    stationDetail.value = st
+  }
+}
+
+function loadDocs(st) {
+  docLoading.value = true
+  // 与旧版约定一致:附件通过 fileCate = 站点stcd 关联
+  const stcd = st.stcd || ''
+  getDocFileList({ fileCate: stcd }).then(res => {
+    docList.value = res.rows || []
+    docLoading.value = false
+  }).catch(() => {
+    docList.value = []
+    docLoading.value = false
+  })
+}
 
-  const layer = createGraphicsLayer('stations')
+function downloadDoc(row) {
+  if (row.filePath) {
+    window.open(row.filePath, '_blank')
+  }
+}
 
-  stationList.value.forEach((st, i) => {
+// 更新站点标记(数据刷新时调用)
+function updateStationMarkers() {
+  if (!stationLayer) return
+  stationLayer.removeAll()
+  stations.value.forEach((st, i) => {
     const name = st.name || st.stationName
-    const coords = stationCoords[name]
+    const coords = STATION_COORDS[name]
     if (!coords) return
     const [lat, lng] = coords
-    const sym = createCircleSymbol('#409EFF', 16)
-    const g = createPointMarker(lat, lng, sym, { [STATION_IDX]: i })
-    layer.add(g)
+    const sym = createCircleSymbol('#086560', 16)
+    const g = createPointMarker(lat, lng, sym, { _idx: i })
+    stationLayer.add(g)
   })
+}
 
-  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
-      openPopup(map.value, name, popupContent(st), feature.getGeometry().getCoordinates())
-      return true
+async function renderMap() {
+  try {
+    await initMap('zggcMap', DEFAULT_CENTER, DEFAULT_ZOOM)
+    await addBasinBoundary()
+    stationLayer = createGraphicsLayer('zggc-stations')
+    updateStationMarkers()
+
+    // 图层诊断日志
+    console.log('[ZggcMonitor] 站点标记数:', stationLayer._source.getFeatures().length)
+
+    map.value.on('click', (evt) => {
+      map.value.forEachFeatureAtPixel(evt.pixel, (feature) => {
+        const idx = feature.get('_idx')
+        if (idx === undefined || idx === null) return
+        switchStation(idx)
+        return true
+      })
     })
-  })
+  } catch (e) {
+    console.error('[ZggcMonitor] 地图初始化失败:', e)
+  } finally {
+    // 无论成败都结束 loading,保证页面可用
+    mapLoading.value = false
+  }
+}
 
-  mapLoading.value = false
+// 带超时的接口请求(防止外部API挂起导致页面一直loading)
+function fetchPortalData(timeout = 8000) {
+  return Promise.race([
+    getPortalData(),
+    new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), timeout)),
+  ])
 }
 
 onMounted(() => {
-  getPortalData().then(res => {
-    stationList.value = res.data || []
-    nextTick(() => { renderMap() })
+  // 1. 先使用默认站点立即渲染地图(不依赖外部接口)
+  stations.value = [...DEFAULT_STATIONS]
+  nextTick(() => { renderMap() })
+
+  // 2. 异步获取真实站点数据,成功则刷新列表和标记
+  fetchPortalData().then(res => {
+    const data = res.data || []
+    if (data.length > 0) {
+      stations.value = data
+      updateStationMarkers()
+      // 若当前无选中站点,默认选中第一个
+      if (activeIdx.value === null && stations.value.length > 0) {
+        activeIdx.value = 0
+      }
+    }
   }).catch(() => {
-    stationList.value = [
-      { stcd: '69642', name: '太浦闸', stationName: '太浦闸', waterLevel: '3.43', flow: '723', holeCount: '30', holeHeight: '4.0' },
-      { stcd: '69485', name: '望亭枢纽', stationName: '望亭枢纽', waterLevel: '3.46', flow: '474', holeCount: '24', holeHeight: '3.5' },
-      { stcd: '69462', name: '常熟水利枢纽', stationName: '常熟水利枢纽', waterLevel: '3.45', flow: '0', holeCount: '16', holeHeight: '3.8' },
-    ]
-    nextTick(() => { renderMap() })
+    // 外部接口不可用时保持默认站点
+  })
+})
+
+onBeforeUnmount(() => {
+  stationLayer = null
+})
+
+// keep-alive 缓存重新激活时刷新地图尺寸(否则画布尺寸可能为0)
+onActivated(() => {
+  nextTick(() => {
+    if (map.value) map.value.updateSize()
   })
 })
 </script>
 
 <style scoped>
-.monitor-page { height: calc(100vh - 100px); padding: 0; }
-.monitor-layout { display: flex; height: 100%; gap: 0; }
-.monitor-sidebar { width: 180px; flex-shrink: 0; background: #fff; border-right: 1px solid #e4e7ed; overflow-y: auto; padding: 12px 0; }
-.sidebar-title { font-size: 15px; font-weight: 600; padding: 8px 16px; color: #303133; border-bottom: 1px solid #eee; margin-bottom: 8px; }
-.sidebar-item { display: flex; align-items: center; gap: 8px; padding: 10px 16px; cursor: pointer; font-size: 14px; color: #606266; transition: all .2s; border-left: 3px solid transparent; }
-.sidebar-item:hover { background: #f5f7fa; color: #409EFF; }
-.sidebar-item.active { background: #ecf5ff; color: #409EFF; font-weight: 600; border-left-color: #409EFF; }
-.station-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
-.station-dot.online { background: #67C23A; }
-.station-dot:not(.online) { background: #C0C4CC; }
-.monitor-main { flex: 1; position: relative; }
-.map-wrapper { height: 100%; width: 100%; position: relative; }
-.map-container { height: 100%; width: 100%; }
-.station-popup { position: absolute; bottom: 24px; left: 24px; width: 220px; background: #fff; border-radius: 8px; box-shadow: 0 4px 16px rgba(0,0,0,0.12); z-index: 1000; overflow: hidden; }
-.popup-header { background: #409EFF; color: #fff; padding: 10px 14px; font-weight: 600; font-size: 14px; }
-.popup-body { padding: 10px 14px; }
-.popup-row { display: flex; justify-content: space-between; padding: 4px 0; font-size: 13px; }
-.popup-label { color: #909399; }
-.popup-value { color: #303133; font-weight: 500; }
+.zggc-page {
+  display: flex;
+  height: calc(100vh - 60px);
+  overflow: hidden;
+}
+.zggc-sidebar {
+  width: 150px;
+  flex-shrink: 0;
+  background: #fff;
+  border-right: 1px solid #e4e7ed;
+  padding: 8px 0;
+  overflow-y: auto;
+}
+.sidebar-item {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  padding: 14px 16px;
+  cursor: pointer;
+  font-size: 14px;
+  color: #606266;
+  transition: all 0.2s;
+  border-left: 3px solid transparent;
+}
+.sidebar-item:hover {
+  background: #f5f7fa;
+  color: #409eff;
+}
+.sidebar-item.active {
+  background: #ecf5ff;
+  color: #409eff;
+  font-weight: 600;
+  border-left-color: #409eff;
+}
+.station-dot {
+  width: 8px;
+  height: 8px;
+  border-radius: 50%;
+  flex-shrink: 0;
+  background: #67c23a;
+}
+.zggc-main {
+  flex: 1;
+  position: relative;
+  overflow: hidden;
+}
+.map-container {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+}
+.map-inner {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+}
+
+/* 详情弹窗 */
+.detail-content {
+  padding: 8px 0;
+}
+.station-desc {
+  margin-top: 16px;
+  padding: 12px 16px;
+  background: #f5f7fa;
+  border-radius: 4px;
+  color: #606266;
+  font-size: 14px;
+  line-height: 1.8;
+}
+
+/* 实时工情 */
+.realtime-grid {
+  display: grid;
+  grid-template-columns: 1fr 1fr;
+  gap: 16px;
+  padding: 16px 0;
+}
+.metric-card {
+  background: #f5f7fa;
+  border-radius: 6px;
+  padding: 20px;
+  text-align: center;
+}
+.metric-label {
+  font-size: 13px;
+  color: #909399;
+  margin-bottom: 8px;
+}
+.metric-value {
+  font-size: 28px;
+  font-weight: 700;
+  color: #303133;
+}
+
+/* 视频 */
+.video-section {
+  min-height: 200px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+/* 文档 */
+.doc-section {
+  padding: 8px 0;
+}
 </style>

+ 100 - 108
gw-ui/src/views/slgc/pust/index.vue

@@ -1,101 +1,77 @@
-<template>
-  <div class="app-container">
-    <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>
-        </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.admDep" placeholder="请输入" clearable @keyup.enter="handleQuery" /></el-form-item>
-        <el-form-item>
-          <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
-          <el-button icon="Refresh" @click="resetQuery">重置</el-button>
-          <el-button type="info" icon="DataAnalysis" @click="handleStats">统计</el-button>
-        </el-form-item>
-      </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="pustLoc" label="位置" width="140" />
-        <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="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>
-        </el-table-column>
-        <el-table-column prop="updDate" label="更新日期" width="120" />
-        <el-table-column label="操作" width="200" fixed="right">
-          <template #default="s">
-            <el-button type="primary" link icon="View" @click="handleDetail(s.row)">查看</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>
-        </el-table-column>
-      </el-table>
+<template>
+  <div class="gc-page">
+    <div class="gc-toolbar">
+      <el-button type="success" icon="DataAnalysis" @click="handleStats">统计</el-button>
+      <div class="gc-toolbar-right">
+        <el-input v-model="queryParams.pustName" placeholder="请输入名称" clearable style="width: 160px" @keyup.enter="handleQuery" />
+        <el-input v-model="queryParams.pustLoc" placeholder="请输入位置" clearable style="width: 160px" @keyup.enter="handleQuery" />
+        <el-select v-model="queryParams.pustType" placeholder="请选择类别-" clearable style="width: 140px">
+          <el-option label="排水泵站" value="1" />
+          <el-option label="供水泵站" value="2" />
+          <el-option label="供排结合泵站" value="3" />
+        </el-select>
+        <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+      </div>
+    </div>
+
+    <el-table v-loading="loading" :data="list" stripe border>
+      <el-table-column prop="pustName" label="泵站名称" min-width="180" />
+      <el-table-column prop="pustLoc" label="泵站所在位置" min-width="220" />
+      <el-table-column prop="insFlow" label="装机流量(m³/s)" width="150" />
+      <el-table-column label="操作" width="100" fixed="right" align="center">
+        <template #default="s">
+          <el-button type="primary" link icon="View" @click="handleDetail(s.row)">查看</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <div class="gc-pagination">
       <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">
+    </div>
+
+    <!-- 查看详情弹窗 -->
+    <el-dialog :title="form.pustName" v-model="open" width="720px" append-to-body>
+      <el-form :model="form" label-width="130px" :disabled="true">
+        <el-row>
+          <el-col :span="12"><el-form-item label="泵站名称">{{ form.pustName }}</el-form-item></el-col>
+          <el-col :span="12"><el-form-item label="泵站代码">{{ form.pustCode }}</el-form-item></el-col>
+        </el-row>
+        <el-row>
+          <el-col :span="12"><el-form-item label="泵站经度">{{ form.pustLon }}</el-form-item></el-col>
+          <el-col :span="12"><el-form-item label="泵站纬度">{{ form.pustLat }}</el-form-item></el-col>
+        </el-row>
         <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="泵站所在位置">{{ form.pustLoc }}</el-form-item></el-col>
+          <el-col :span="12"><el-form-item label="泵站类型">{{ pustTypeMap[form.pustType] }}</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-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-col :span="12"><el-form-item label="装机流量">{{ form.insFlow }} m³/s</el-form-item></el-col>
+          <el-col :span="12"><el-form-item label="装机功率">{{ form.insPow }}</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-col :span="12"><el-form-item label="水泵数量">{{ form.pumpNum }}</el-form-item></el-col>
+          <el-col :span="12"><el-form-item label="设计扬程">{{ form.designHead }}</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="工程状态">
-            <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="工程等别">{{ engGradMap[form.engGrad] }}</el-form-item></el-col>
+          <el-col :span="12"><el-form-item label="工程规模">{{ form.engScale }}</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>
-          <el-col :span="12"><el-form-item label="开工日期"><el-input v-model="form.startDate" type="date" /></el-form-item></el-col>
+          <el-col :span="12"><el-form-item label="工程建设情况">{{ form.engBuild }}</el-form-item></el-col>
+          <el-col :span="12"><el-form-item label="开工时间">{{ form.startDate }}</el-form-item></el-col>
+        </el-row>
+        <el-row>
+          <el-col :span="12"><el-form-item label="建成时间">{{ form.endDate }}</el-form-item></el-col>
+          <el-col :span="12"><el-form-item label="归口管理部门">{{ form.admDep }}</el-form-item></el-col>
         </el-row>
       </el-form>
-      <template #footer>
-        <el-button type="primary" @click="submitForm">确 定</el-button>
-        <el-button @click="cancel">取 消</el-button>
-      </template>
     </el-dialog>
 
-    <el-dialog v-model="statsVisible" title="泵站类型分布统计" width="600px" append-to-body>
+    <!-- 统计弹窗 -->
+    <el-dialog title="统计类别" v-model="statsVisible" width="500px" 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" />
+        <el-table-column prop="name" label="类" min-width="120" />
+        <el-table-column prop="value" label="数量" width="120" />
       </el-table>
     </el-dialog>
   </div>
@@ -103,40 +79,30 @@
 
 <script setup name="Pust">
 import { getPustList } from '@/api/slgc/gc/index'
-import { getPust, addPust, updatePust, 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('')
+import { getPust, getPustCountType } from '@/api/slgc/pust/index'
+
+const loading = ref(true)
+const total = ref(0)
+const open = ref(false)
 const list = ref([])
-const queryParams = reactive({ pageNum: 1, pageSize: 10, pustName: undefined, admDep: undefined })
+const queryParams = reactive({ pageNum: 1, pageSize: 10, pustName: undefined, pustLoc: undefined, pustType: undefined })
 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 cancel() { open.value = false }
-
-function handleAdd() {
-  form.value = { engStat: '1' }
-  title.value = '新增泵站'
-  open.value = true
-}
 
-function handleDelete(row) {
-  proxy.$modal.confirm('确认删除泵站"' + row.pustName + '"?').then(() => {
-    delPust(row.pustCode).then(() => {
-      proxy.$modal.msgSuccess('删除成功')
-      getList()
-    })
+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 handleDetail(row) {
+  getPust(row.pustCode).then(res => { form.value = res.data; open.value = true })
+}
 function handleStats() {
   statsVisible.value = true
   statsLoading.value = true
@@ -148,4 +114,30 @@ function handleStats() {
 
 onMounted(() => { getList() })
 </script>
-<style scoped>.card-header { font-weight: 600; font-size: 16px; }</style>
+
+<style scoped>
+.gc-page {
+  padding: 16px;
+  background: #fff;
+  min-height: calc(100vh - 60px);
+  display: flex;
+  flex-direction: column;
+}
+.gc-toolbar {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 16px;
+}
+.gc-toolbar-right {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+.gc-pagination {
+  margin-top: auto;
+  padding-top: 16px;
+  display: flex;
+  justify-content: flex-end;
+}
+</style>

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

@@ -1,148 +1,78 @@
-<template>
-  <div class="app-container">
-    <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>
-        </div>
-      </template>
+<template>
+  <div class="gc-page">
+    <!-- 顶部工具栏 -->
+    <div class="gc-toolbar">
+      <el-button type="success" icon="DataAnalysis" @click="handleStats">统计</el-button>
+      <div class="gc-toolbar-right">
+        <el-input v-model="queryParams.resName" placeholder="水库名称" clearable style="width: 160px" @keyup.enter="handleQuery" />
+        <el-input v-model="queryParams.resLoc" placeholder="水库所在位置" clearable style="width: 160px" @keyup.enter="handleQuery" />
+        <el-select v-model="queryParams.resType" placeholder="请选择类别-" clearable style="width: 140px">
+          <el-option label="山丘水库" value="1" />
+          <el-option label="平原水库" value="2" />
+          <el-option label="地下水库" value="3" />
+        </el-select>
+        <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+      </div>
+    </div>
 
-      <el-form :model="queryParams" :inline="true" size="small">
-        <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.admDep" placeholder="请输入管理单位" clearable @keyup.enter="handleQuery" />
-        </el-form-item>
-        <el-form-item>
-          <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
-          <el-button icon="Refresh" @click="resetQuery">重置</el-button>
-          <el-button type="info" icon="DataAnalysis" @click="handleStats">统计</el-button>
-        </el-form-item>
-      </el-form>
+    <!-- 数据表格 -->
+    <el-table v-loading="loading" :data="list" stripe border style="width: 100%">
+      <el-table-column prop="resName" label="水库名称" min-width="160" />
+      <el-table-column prop="resLoc" label="水库所在位置" min-width="200" />
+      <el-table-column prop="resType" label="水库类型" width="120">
+        <template #default="scope">{{ resTypeMap[scope.row.resType] || scope.row.resType }}</template>
+      </el-table-column>
+      <el-table-column prop="engGrad" label="工程等别" width="100">
+        <template #default="scope">{{ engGradMap[scope.row.engGrad] || scope.row.engGrad }}</template>
+      </el-table-column>
+      <el-table-column prop="totCap" label="总库容(10⁴m³)" width="140" />
+      <el-table-column label="操作" width="100" fixed="right" align="center">
+        <template #default="scope">
+          <el-button type="primary" link icon="View" @click="handleDetail(scope.row)">查看</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
 
-      <el-table v-loading="loading" :data="list" stripe>
-        <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="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.runStat === '1' ? 'success' : scope.row.runStat === '2' ? 'warning' : 'info'" size="small">
-              {{ runStatMap[scope.row.runStat] || scope.row.runStat }}
-            </el-tag>
-          </template>
-        </el-table-column>
-        <el-table-column prop="admDep" label="管理单位" width="140" />
-        <el-table-column prop="updDate" label="更新日期" width="120" />
-        <el-table-column label="操作" width="200" fixed="right">
-          <template #default="scope">
-            <el-button type="primary" link icon="View" @click="handleDetail(scope.row)">查看</el-button>
-            <el-button type="primary" link icon="Edit" @click="handleEdit(scope.row)">编辑</el-button>
-            <el-button type="danger" link icon="Delete" @click="handleDelete(scope.row)">删除</el-button>
-          </template>
-        </el-table-column>
-      </el-table>
+    <!-- 分页 -->
+    <div class="gc-pagination">
       <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
-    </el-card>
+    </div>
 
-    <el-dialog :title="title" v-model="open" width="720px" append-to-body>
-      <el-form :model="form" label-width="130px">
+    <!-- 查看详情弹窗 -->
+    <el-dialog :title="form.resName" v-model="open" width="720px" append-to-body>
+      <el-form :model="form" label-width="130px" :disabled="true">
         <el-row>
-          <el-col :span="12"><el-form-item label="水库编码"><el-input v-model="form.resCode" :disabled="title === '编辑水库'" /></el-form-item></el-col>
-          <el-col :span="12"><el-form-item label="水库名称"><el-input v-model="form.resName" /></el-form-item></el-col>
+          <el-col :span="12"><el-form-item label="水库编码">{{ form.resCode }}</el-form-item></el-col>
+          <el-col :span="12"><el-form-item label="水库名称">{{ form.resName }}</el-form-item></el-col>
         </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-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-col :span="12"><el-form-item label="位置">{{ form.resLoc }}</el-form-item></el-col>
+          <el-col :span="12"><el-form-item label="类型">{{ resTypeMap[form.resType] }}</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-col :span="12"><el-form-item label="工程等别">{{ engGradMap[form.engGrad] }}</el-form-item></el-col>
+          <el-col :span="12"><el-form-item label="总库容(万m³)">{{ form.totCap }}</el-form-item></el-col>
         </el-row>
         <el-row>
-          <el-col :span="12"><el-form-item label="防洪库容(万m³)"><el-input-number v-model="form.flcoCap" :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.storFlCap" :min="0" :step="0.01" /></el-form-item></el-col>
+          <el-col :span="12"><el-form-item label="正常库容(万m³)">{{ form.normPoolStagCap }}</el-form-item></el-col>
+          <el-col :span="12"><el-form-item label="防洪库容(万m³)">{{ form.flcoCap }}</el-form-item></el-col>
         </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.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="调洪库容(万m³)">{{ form.storFlCap }}</el-form-item></el-col>
+          <el-col :span="12"><el-form-item label="集水面积(km²)">{{ form.watShedArea }}</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>
-          <el-col :span="12"><el-form-item label="开工日期"><el-input v-model="form.startDate" type="date" /></el-form-item></el-col>
+          <el-col :span="12"><el-form-item label="运行状况">{{ runStatMap[form.runStat] }}</el-form-item></el-col>
+          <el-col :span="12"><el-form-item label="管理单位">{{ form.admDep }}</el-form-item></el-col>
         </el-row>
       </el-form>
-      <template #footer>
-        <el-button v-if="title === '水库详情'" icon="User" @click="handlePerson(form)">责任人信息</el-button>
-        <el-button type="primary" @click="submitForm">确 定</el-button>
-        <el-button @click="cancel">取 消</el-button>
-      </template>
     </el-dialog>
 
-    <el-dialog v-model="personVisible" title="水库责任人信息" width="700px" append-to-body>
-      <el-tabs v-model="personTab">
-        <el-tab-pane label="行政责任人" name="admin">
-          <el-descriptions :column="2" border v-if="personData.adminName">
-            <el-descriptions-item label="姓名">{{ personData.adminName }}</el-descriptions-item>
-            <el-descriptions-item label="职务">{{ personData.adminDuty }}</el-descriptions-item>
-            <el-descriptions-item label="联系方式" :span="2">{{ personData.adminPhone }}</el-descriptions-item>
-          </el-descriptions>
-          <el-empty v-else description="暂无数据" />
-        </el-tab-pane>
-        <el-tab-pane label="技术责任人" name="tech">
-          <el-descriptions :column="2" border v-if="personData.techName">
-            <el-descriptions-item label="姓名">{{ personData.techName }}</el-descriptions-item>
-            <el-descriptions-item label="职务">{{ personData.techDuty }}</el-descriptions-item>
-            <el-descriptions-item label="联系方式" :span="2">{{ personData.techPhone }}</el-descriptions-item>
-          </el-descriptions>
-          <el-empty v-else description="暂无数据" />
-        </el-tab-pane>
-        <el-tab-pane label="巡查责任人" name="patrol">
-          <el-descriptions :column="2" border v-if="personData.patrolName">
-            <el-descriptions-item label="姓名">{{ personData.patrolName }}</el-descriptions-item>
-            <el-descriptions-item label="职务">{{ personData.patrolDuty }}</el-descriptions-item>
-            <el-descriptions-item label="联系方式" :span="2">{{ personData.patrolPhone }}</el-descriptions-item>
-          </el-descriptions>
-          <el-empty v-else description="暂无数据" />
-        </el-tab-pane>
-      </el-tabs>
-      <template #footer>
-        <el-button @click="personVisible = false">关 闭</el-button>
-      </template>
-    </el-dialog>
-
-    <el-dialog v-model="statsVisible" title="水库类型分布统计" width="600px" append-to-body>
+    <!-- 统计弹窗 -->
+    <el-dialog title="统计类别" v-model="statsVisible" width="500px" 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" />
+        <el-table-column prop="name" label="类别" min-width="120" />
+        <el-table-column prop="value" label="数量" width="120" />
       </el-table>
     </el-dialog>
   </div>
@@ -150,15 +80,14 @@
 
 <script setup name="Reservoir">
 import { getReservoirList } from '@/api/slgc/gc/index'
-import { getReservoir, addReservoir, updateReservoir, delReservoir, getReservoirCountType } from '@/api/slgc/reservoir/index'
+import { getReservoir, getReservoirCountType } from '@/api/slgc/reservoir/index'
 
 const { proxy } = getCurrentInstance()
 const loading = ref(true)
 const total = ref(0)
 const open = ref(false)
-const title = ref('')
 const list = ref([])
-const queryParams = reactive({ pageNum: 1, pageSize: 10, resName: undefined, admDep: undefined })
+const queryParams = reactive({ pageNum: 1, pageSize: 10, resName: undefined, resLoc: undefined, resType: undefined })
 const form = ref({})
 const statsVisible = ref(false)
 const statsLoading = ref(false)
@@ -166,10 +95,6 @@ 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({})
-const personLoading = ref(false)
 
 function getList() {
   loading.value = true
@@ -178,34 +103,9 @@ function getList() {
   })
 }
 function handleQuery() { queryParams.pageNum = 1; getList() }
-function resetQuery() { queryParams.resName = undefined; queryParams.admDep = undefined; handleQuery() }
 function handleDetail(row) {
-  getReservoir(row.resCode).then(res => { form.value = res.data; title.value = '水库详情'; open.value = true })
-}
-function handleEdit(row) {
-  getReservoir(row.resCode).then(res => { form.value = res.data; title.value = '编辑水库'; open.value = true })
-}
-function submitForm() {
-  const method = form.value.resCode && title.value === '编辑水库' ? updateReservoir : addReservoir
-  method(form.value).then(() => { proxy.$modal.msgSuccess('操作成功'); open.value = false; getList() })
-}
-function cancel() { open.value = false }
-
-function handleAdd() {
-  form.value = { runStat: '1' }
-  title.value = '新增水库'
-  open.value = true
-}
-
-function handleDelete(row) {
-  proxy.$modal.confirm('确认删除水库"' + row.resName + '"?').then(() => {
-    delReservoir(row.resCode).then(() => {
-      proxy.$modal.msgSuccess('删除成功')
-      getList()
-    })
-  })
+  getReservoir(row.resCode).then(res => { form.value = res.data; open.value = true })
 }
-
 function handleStats() {
   statsVisible.value = true
   statsLoading.value = true
@@ -215,46 +115,32 @@ function handleStats() {
   }).catch(() => { statsLoading.value = false })
 }
 
-function handlePerson(row) {
-  personVisible.value = true
-  personTab.value = 'admin'
-  personData.value = {}
-  personLoading.value = true
-  const names = [row.resName, row.resName.replace(/水库$/, ''), row.resName + '水库']
-  const outFields = '行政责任人姓名,行政责任人职务,行政责任人联系方式,技术责任人姓名,技术责任人职务,技术责任人联系方式,巡查责任人姓名,巡查责任人职务,巡查责任人联系方式'
-  const layers = [5, 6, 7]
-  let found = false
-  layers.reduce((seq, layerId) => {
-    return seq.catch(() => {
-      const where = names.map(n => `水库名称 like '%25${encodeURIComponent(n)}%25'`).join(' or ')
-      const url = `http://10.8.4.128/server/rest/services/OneMap/TH_SLGC/MapServer/${layerId}/query?f=json&where=${where}&returnGeometry=false&outFields=${outFields}&outSR=4490`
-      return fetch(url).then(r => r.json()).then(data => {
-        if (data.features && data.features.length > 0) {
-          found = true
-          const attrs = data.features[0].attributes
-          const d = personData.value
-          d.adminName = attrs['行政责任人姓名'] || '-'
-          d.adminDuty = attrs['行政责任人职务'] || '-'
-          d.adminPhone = attrs['行政责任人联系方式'] || '-'
-          d.techName = attrs['技术责任人姓名'] || '-'
-          d.techDuty = attrs['技术责任人职务'] || '-'
-          d.techPhone = attrs['技术责任人联系方式'] || '-'
-          d.patrolName = attrs['巡查责任人姓名'] || '-'
-          d.patrolDuty = attrs['巡查责任人职务'] || '-'
-          d.patrolPhone = attrs['巡查责任人联系方式'] || '-'
-          return Promise.resolve()
-        }
-        return Promise.reject()
-      })
-    })
-  }, Promise.reject()).catch(() => {
-    if (!found) proxy.$modal.msgWarning('未查询到责任人信息')
-  }).finally(() => { personLoading.value = false })
-}
-
 onMounted(() => { getList() })
 </script>
 
 <style scoped>
-.card-header { font-weight: 600; font-size: 16px; }
+.gc-page {
+  padding: 16px;
+  background: #fff;
+  min-height: calc(100vh - 60px);
+  display: flex;
+  flex-direction: column;
+}
+.gc-toolbar {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 16px;
+}
+.gc-toolbar-right {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+.gc-pagination {
+  margin-top: auto;
+  padding-top: 16px;
+  display: flex;
+  justify-content: flex-end;
+}
 </style>

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

@@ -64,7 +64,7 @@ function loadFiles() {
 }
 
 function getDownloadUrl(file) {
-  return `/common/download?fileName=${encodeURIComponent(file.filePath)}`
+  return `/common/download/resource?resource=${encodeURIComponent(file.filePath)}`
 }
 
 function formatSize(size) {

+ 361 - 51
gw-ui/src/views/slgc/unit/index.vue

@@ -1,51 +1,64 @@
 <template>
-  <div class="app-container">
-    <el-card shadow="never">
+  <div class="gc-page">
+    <el-card shadow="never" class="unit-card">
       <template #header>
         <div class="card-header"><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.name" placeholder="请输入" clearable @keyup.enter="handleQuery" /></el-form-item>
-        <el-form-item label="所在城市"><el-input v-model="queryParams.city" placeholder="请输入" clearable @keyup.enter="handleQuery" /></el-form-item>
-        <el-form-item label="过期筛选">
-          <el-select v-model="queryParams.overdue" placeholder="全部" clearable @change="handleQuery">
-            <el-option label="全部" value="" />
-            <el-option label="已过期(超5年)" value="yes" />
-            <el-option label="未过期" value="no" />
-          </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>
-        </el-form-item>
-      </el-form>
+      <div class="unit-body">
+        <el-form :model="queryParams" :inline="true" size="small">
+          <el-form-item label="查询名称"><el-input v-model="queryParams.name" placeholder="请输入" clearable @keyup.enter="handleQuery" /></el-form-item>
+          <el-form-item label="查询地址"><el-input v-model="queryParams.address" placeholder="请输入" clearable @keyup.enter="handleQuery" /></el-form-item>
+          <el-form-item>
+            <el-select v-model="queryParams.overdue" placeholder="全部数据" clearable @change="handleQuery">
+              <el-option label="全部数据" value="" />
+              <el-option label="已过期(超5年)" value="yes" />
+              <el-option label="未过期" value="no" />
+            </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>
+          </el-form-item>
+        </el-form>
 
-      <el-button type="danger" size="small" :disabled="!ids.length" style="margin-bottom:12px" @click="handleBatchDelete">批量删除</el-button>
-
-      <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" 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="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 icon="Edit" @click="handleEdit(s.row)">编辑</el-button>
-            <el-button type="danger" link icon="Delete" @click="handleDelete(s.row)">删除</el-button>
-          </template>
-        </el-table-column>
-      </el-table>
-      <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
+        <!-- 批量操作(表格上方左侧) -->
+        <div class="table-toolbar">
+          <el-button type="success" size="small" icon="Download" @click="handleExport">导出</el-button>
+          <el-button type="success" size="small" icon="Upload" @click="importVisible = true">导入</el-button>
+          <el-button type="danger" size="small" icon="Delete" :disabled="!ids.length" @click="handleBatchDelete">批量删除</el-button>
+        </div>
+
+        <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="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="longitude" label="经度" width="100" />
+          <el-table-column prop="latitude" label="纬度" width="100" />
+          <el-table-column prop="approvalDate" label="批准日期" width="110">
+            <template #default="s">{{ formatDate(s.row.approvalDate) }}</template>
+          </el-table-column>
+          <el-table-column label="操作" width="300" fixed="right">
+            <template #default="s">
+              <div class="op-group">
+                <el-button type="warning" link @click="openFileDialog(s.row)">附件</el-button>
+                <el-button type="primary" link @click="openRecordDialog(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>
+              </div>
+            </template>
+          </el-table-column>
+        </el-table>
+        <div class="gc-pagination">
+          <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
+        </div>
+      </div>
     </el-card>
 
+    <!-- ====== 编辑单位弹窗 ====== -->
     <el-dialog :title="title" v-model="open" width="600px" append-to-body>
       <el-form :model="form" label-width="100px">
         <el-row>
@@ -76,39 +89,179 @@
         <el-button @click="cancel">取 消</el-button>
       </template>
     </el-dialog>
+
+    <!-- ====== 复核记录弹窗 ====== -->
+    <el-dialog title="复核记录" v-model="recordVisible" width="800px" append-to-body>
+      <div style="margin-bottom:12px;">
+        <el-button type="primary" size="small" icon="Plus" @click="handleAddRecord">添加</el-button>
+        <el-button type="danger" size="small" icon="Delete" :disabled="!recordIds.length" @click="handleBatchDelRecord">批量删除</el-button>
+      </div>
+      <el-table :data="recordList" v-loading="recordLoading" stripe size="small" @selection-change="handleRecordSelectionChange">
+        <el-table-column type="selection" width="40" />
+        <el-table-column prop="id" label="ID" width="50" />
+        <el-table-column prop="approvalNumber" label="复核编号" width="100" />
+        <el-table-column prop="approvalDate" label="复核日期" width="120" />
+        <el-table-column prop="approvalContent" label="复核内容" min-width="200" show-overflow-tooltip />
+        <el-table-column prop="updateTime" label="更新时间" width="160" />
+        <el-table-column label="操作" width="140" fixed="right">
+          <template #default="r">
+            <el-button type="primary" link icon="Edit" @click="handleEditRecord(r.row)">编辑</el-button>
+            <el-button type="danger" link icon="Delete" @click="handleDeleteRecord(r.row)">删除</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <!-- 复核记录编辑弹窗 -->
+      <el-dialog title="复核记录" v-model="recordFormVisible" width="500px" append-to-body>
+        <el-form :model="recordForm" label-width="100px">
+          <el-form-item label="复核编号"><el-input v-model="recordForm.approvalNumber" placeholder="请输入" /></el-form-item>
+          <el-form-item label="复核日期"><el-date-picker v-model="recordForm.approvalDate" type="date" value-format="YYYY-MM-DD" style="width:100%" /></el-form-item>
+          <el-form-item label="复核内容"><el-input v-model="recordForm.approvalContent" type="textarea" :rows="4" placeholder="请输入" /></el-form-item>
+        </el-form>
+        <template #footer>
+          <el-button type="primary" @click="submitRecordForm">确 定</el-button>
+          <el-button @click="recordFormVisible = false">取 消</el-button>
+        </template>
+      </el-dialog>
+    </el-dialog>
+
+    <!-- ====== 附件管理弹窗 ====== -->
+    <el-dialog :title="'附件管理 - ' + fileUnitName" v-model="fileVisible" width="800px" append-to-body>
+      <div style="margin-bottom:12px;">
+        <el-button type="primary" size="small" icon="Upload" @click="handleUploadFile">上传附件</el-button>
+      </div>
+      <el-table :data="fileList" v-loading="fileLoading" stripe size="small">
+        <el-table-column prop="id" label="ID" width="50" />
+        <el-table-column prop="fileName" label="文件名" min-width="200" show-overflow-tooltip />
+        <el-table-column prop="fileSize" label="大小" width="100">
+          <template #default="f">
+            {{ formatFileSize(f.row.fileSize) }}
+          </template>
+        </el-table-column>
+        <el-table-column prop="uploadTime" label="上传时间" width="160" />
+        <el-table-column label="操作" width="120" fixed="right">
+          <template #default="f">
+            <el-button type="primary" link icon="Download" @click="handleDownloadFile(f.row)">下载</el-button>
+            <el-button type="danger" link icon="Delete" @click="handleDeleteFile(f.row)">删除</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <!-- 上传附件对话框 -->
+      <el-dialog title="上传附件" v-model="uploadVisible" width="400px" append-to-body>
+        <el-form label-width="80px">
+          <el-form-item label="文件标题"><el-input v-model="uploadTitle" placeholder="请输入" /></el-form-item>
+          <el-form-item label="选择文件">
+            <el-upload ref="uploadRef" :action="uploadAction" :data="uploadData" :headers="uploadHeaders"
+              :limit="1" :on-success="handleUploadSuccess" :before-upload="beforeUpload" :auto-upload="false">
+              <el-button type="primary" size="small" icon="Upload">选择文件</el-button>
+              <template #tip><div class="el-upload__tip">支持PDF、Word等常见文件</div></template>
+            </el-upload>
+          </el-form-item>
+        </el-form>
+        <template #footer>
+          <el-button type="primary" @click="$refs.uploadRef.submit()">上 传</el-button>
+          <el-button @click="uploadVisible = false">取 消</el-button>
+        </template>
+      </el-dialog>
+    </el-dialog>
+    <!-- ====== 导入单位弹窗 ====== -->
+    <el-dialog title="导入单位" v-model="importVisible" width="420px" append-to-body>
+      <el-form label-width="80px">
+        <el-form-item label="文件">
+          <el-upload
+            ref="importUploadRef"
+            :action="importUrl"
+            :headers="uploadHeaders"
+            accept=".xlsx,.xls"
+            :limit="1"
+            :on-success="handleImportSuccess"
+            :on-error="handleImportError"
+            :show-file-list="true"
+          >
+            <el-button type="primary" size="small" icon="Upload">选择Excel文件</el-button>
+            <template #tip>
+              <div class="el-upload__tip">仅支持 .xlsx/.xls 格式</div>
+            </template>
+          </el-upload>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button size="small" type="primary" icon="Download" @click="handleDownloadTemplate">下载模板</el-button>
+        <el-button @click="importVisible = false">关 闭</el-button>
+      </template>
+    </el-dialog>
   </div>
 </template>
 
 <script setup name="Unit">
-import { getUnitList, getUnit, saveUnit, delUnit } from '@/api/slgc/unit/index'
+import { getToken } from '@/utils/auth'
+import { getUnitList, getUnit, saveUnit, delUnit, getRecordList, saveRecord, delRecord, exportUnit, importTemplateUrl } from '@/api/slgc/unit/index'
+import { getDocFileList, delDocFile } from '@/api/slgc/doc/index'
+
 const { proxy } = getCurrentInstance()
 const loading = ref(true); const total = ref(0); const open = ref(false); const title = ref('')
 const list = ref([]); const ids = ref([])
-const queryParams = reactive({ pageNum: 1, pageSize: 10, name: undefined, city: undefined, overdue: undefined })
+const queryParams = reactive({ pageNum: 1, pageSize: 10, name: undefined, address: undefined, overdue: undefined })
 const form = ref({})
 
+// === 复核记录 ===
+const recordVisible = ref(false)
+const recordFormVisible = ref(false)
+const recordLoading = ref(false)
+const recordList = ref([])
+const recordIds = ref([])
+const recordUnitId = ref(null)
+const recordForm = ref({})
+
+// === 附件管理 ===
+const fileVisible = ref(false)
+const fileLoading = ref(false)
+const fileList = ref([])
+const fileUnitId = ref(null)
+const fileUnitName = ref('')
+const uploadVisible = ref(false)
+const uploadTitle = ref('')
+const uploadAction = ref('')
+const uploadData = ref({})
+const uploadHeaders = ref({ Authorization: 'Bearer ' + getToken() })
+
+// === 导入导出 ===
+const importVisible = ref(false)
+const importUploadRef = ref(null)
+const importUrl = ref('/slgc/water-unit/importData')
+
+function formatFileSize(bytes) {
+  if (!bytes) return '-'
+  if (bytes < 1024) return bytes + 'B'
+  if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + 'KB'
+  return (bytes / 1024 / 1024).toFixed(1) + 'MB'
+}
+
+// 只显示年月日 YYYY-MM-DD
+function formatDate(val) {
+  if (!val) return '-'
+  return String(val).slice(0, 10)
+}
+
+// === 过期判定 ===
 function isOverdue(row) {
   if (!row.approvalDate) return false
   const fiveYearsAgo = new Date()
   fiveYearsAgo.setFullYear(fiveYearsAgo.getFullYear() - 5)
   return new Date(row.approvalDate) <= fiveYearsAgo
 }
+function rowClassName({ row }) { return isOverdue(row) ? 'overdue-row' : '' }
 
-function rowClassName({ row }) {
-  return isOverdue(row) ? 'overdue-row' : ''
-}
-
+// === 单位列表 ===
 function getList() {
   loading.value = true
   getUnitList(queryParams).then(res => {
     list.value = res.rows || []
     total.value = res.total
     loading.value = false
-  })
+  }).catch(() => { loading.value = false })
 }
-
 function handleQuery() { queryParams.pageNum = 1; getList() }
-function resetQuery() { queryParams.name = undefined; queryParams.city = undefined; queryParams.overdue = undefined; handleQuery() }
+function resetQuery() { queryParams.name = undefined; queryParams.address = undefined; queryParams.overdue = undefined; handleQuery() }
 function handleSelectionChange(selection) { ids.value = selection.map(s => s.id) }
 function handleEdit(row) { getUnit(row.id).then(res => { form.value = res.data; title.value = '编辑单位'; open.value = true }) }
 function handleAdd() { form.value = {}; title.value = '新增单位'; open.value = true }
@@ -119,7 +272,6 @@ function handleDelete(row) {
     delUnit(row.id).then(() => { proxy.$modal.msgSuccess('删除成功'); getList() })
   })
 }
-
 function handleBatchDelete() {
   if (!ids.value.length) return
   proxy.$modal.confirm('确认删除选中的 ' + ids.value.length + ' 条记录?').then(() => {
@@ -127,11 +279,169 @@ function handleBatchDelete() {
   })
 }
 
+// === 导入导出 ===
+function handleExport() {
+  proxy.$modal.confirm('确认导出当前筛选条件下的全部数据?').then(() => {
+    exportUnit(queryParams).then(res => {
+      const blob = new Blob([res], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
+      const url = window.URL.createObjectURL(blob)
+      const link = document.createElement('a')
+      link.href = url
+      link.download = '用水单位数据.xlsx'
+      link.click()
+      window.URL.revokeObjectURL(url)
+    })
+  })
+}
+
+function handleDownloadTemplate() {
+  window.open(importTemplateUrl(), '_blank')
+}
+
+function handleImportSuccess(res) {
+  if (res.code === 200) {
+    proxy.$modal.msgSuccess(res.msg || '导入成功')
+    importVisible.value = false
+    getList()
+  } else {
+    proxy.$modal.msgError(res.msg || '导入失败')
+  }
+  if (importUploadRef.value) importUploadRef.value.clearFiles()
+}
+
+function handleImportError() {
+  proxy.$modal.msgError('导入失败,请检查文件格式')
+  if (importUploadRef.value) importUploadRef.value.clearFiles()
+}
+
+// === 复核记录管理(弹窗) ===
+function openRecordDialog(row) {
+  recordUnitId.value = row.id
+  recordVisible.value = true
+  loadRecords(row.id)
+}
+
+function loadRecords(unitId) {
+  recordLoading.value = true
+  getRecordList({ unitId: unitId || recordUnitId.value }).then(res => {
+    recordList.value = res.rows || res.data || []
+    recordLoading.value = false
+  }).catch(() => { recordList.value = []; recordLoading.value = false })
+}
+
+function handleAddRecord() {
+  recordForm.value = {}
+  recordFormVisible.value = true
+}
+function handleEditRecord(row) {
+  recordForm.value = { ...row }
+  recordFormVisible.value = true
+}
+function submitRecordForm() {
+  const data = { ...recordForm.value, unitId: recordUnitId.value }
+  saveRecord(data).then(() => {
+    proxy.$modal.msgSuccess('操作成功')
+    recordFormVisible.value = false
+    loadRecords()
+  })
+}
+function handleDeleteRecord(row) {
+  proxy.$modal.confirm('确认删除该复核记录?').then(() => {
+    delRecord(row.id).then(() => { proxy.$modal.msgSuccess('删除成功'); loadRecords() })
+  })
+}
+function handleRecordSelectionChange(selection) { recordIds.value = selection.map(s => s.id) }
+function handleBatchDelRecord() {
+  if (!recordIds.value.length) return
+  proxy.$modal.confirm('确认删除选中的 ' + recordIds.value.length + ' 条记录?').then(() => {
+    delRecord(recordIds.value.join(',')).then(() => { proxy.$modal.msgSuccess('删除成功'); loadRecords() })
+  })
+}
+
+// === 附件管理(弹窗) ===
+function openFileDialog(row) {
+  fileUnitId.value = row.id
+  fileUnitName.value = row.name || ''
+  fileVisible.value = true
+  loadFiles(row.id)
+}
+
+function loadFiles(unitId) {
+  fileLoading.value = true
+  // 与后端约定:附件通过 fileCate 前缀 'UNIT-' + 单位ID 关联
+  getDocFileList({ fileCate: 'UNIT-' + (unitId || fileUnitId.value) }).then(res => {
+    fileList.value = res.rows || res.data || []
+    fileLoading.value = false
+  }).catch(() => { fileList.value = []; fileLoading.value = false })
+}
+
+function handleUploadFile() {
+  uploadTitle.value = ''
+  // 相对路径,走 vite 代理;fileCate 前缀 UNIT- 关联单位
+  uploadAction.value = '/slgc/doc-file/upload'
+  uploadData.value = { fileCate: 'UNIT-' + fileUnitId.value, fileTitle: '' }
+  uploadVisible.value = true
+}
+
+function beforeUpload(file) {
+  uploadData.value.fileTitle = uploadTitle.value || file.name
+  return true
+}
+
+function handleUploadSuccess(res) {
+  if (res.code === 200) {
+    proxy.$modal.msgSuccess('上传成功')
+    uploadVisible.value = false
+    loadFiles()
+  } else {
+    proxy.$modal.msgError(res.msg || '上传失败')
+  }
+}
+
+function handleDownloadFile(row) {
+  if (row.filePath) {
+    // 与上传目录一致(profile/upload),走资源下载接口
+    window.open(`/common/download/resource?resource=${encodeURIComponent(row.filePath)}`, '_blank')
+  }
+}
+
+function handleDeleteFile(row) {
+  proxy.$modal.confirm('确认删除该附件?').then(() => {
+    delDocFile(row.id).then(() => { proxy.$modal.msgSuccess('删除成功'); loadFiles() })
+  })
+}
+
 onMounted(() => { getList() })
 </script>
 
 <style scoped>
+.gc-page { padding: 16px; height: calc(100vh - 60px); display: flex; flex-direction: column; box-sizing: border-box; }
+.unit-card { flex: 1; display: flex; flex-direction: column; min-height: 0; }
+.unit-card :deep(.el-card__body) { flex: 1; display: flex; flex-direction: column; min-height: 0; overflow: hidden; }
+.unit-body { flex: 1; display: flex; flex-direction: column; min-height: 0; }
+.unit-body .el-table { flex: 1; min-height: 0; }
 .card-header { font-weight: 600; font-size: 16px; }
+/* 表格上方工具栏(批量操作) */
+.table-toolbar {
+  display: flex;
+  align-items: center;
+  margin-bottom: 10px;
+  flex-shrink: 0;
+}
+.gc-pagination { display: flex; justify-content: flex-end; margin-top: 16px; flex-shrink: 0; }
+/* 操作栏不换行 */
+.op-group {
+  display: flex;
+  align-items: center;
+  white-space: nowrap;
+}
+.op-group .el-button {
+  margin-left: 0;
+  margin-right: 8px;
+}
+.op-group .el-button + .el-button {
+  margin-left: 0;
+}
 :deep(.overdue-row) { background-color: #fdf6ec !important; }
 :deep(.overdue-row td) { background-color: #fdf6ec !important; }
 </style>

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

@@ -1,101 +1,86 @@
-<template>
-  <div class="app-container">
-    <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>
-        </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.admDep" placeholder="请输入" clearable @keyup.enter="handleQuery" /></el-form-item>
-        <el-form-item>
-          <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
-          <el-button icon="Refresh" @click="resetQuery">重置</el-button>
-          <el-button type="info" icon="DataAnalysis" @click="handleStats">统计</el-button>
-        </el-form-item>
-      </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="wagaLoc" label="位置" width="140" />
-        <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">
-          <template #default="s">
-            <el-button type="primary" link icon="View" @click="handleDetail(s.row)">查看</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>
-        </el-table-column>
-      </el-table>
+<template>
+  <div class="gc-page">
+    <div class="gc-toolbar">
+      <el-button type="success" icon="DataAnalysis" @click="handleStats">统计</el-button>
+      <div class="gc-toolbar-right">
+        <el-input v-model="queryParams.wagaName" placeholder="请输入名称" clearable style="width: 160px" @keyup.enter="handleQuery" />
+        <el-input v-model="queryParams.wagaLoc" placeholder="请输入位置" clearable style="width: 160px" @keyup.enter="handleQuery" />
+        <el-select v-model="queryParams.wagaType" placeholder="请选择" clearable style="width: 140px">
+          <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-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+      </div>
+    </div>
+
+    <el-table v-loading="loading" :data="list" stripe border>
+      <el-table-column prop="wagaName" label="水闸名称" min-width="180" />
+      <el-table-column prop="wagaLoc" label="水闸所在位置" min-width="220" />
+      <el-table-column prop="engGrad" label="工程等别" width="120">
+        <template #default="s">{{ engGradMap[s.row.engGrad] || s.row.engGrad }}</template>
+      </el-table-column>
+      <el-table-column label="操作" width="100" fixed="right" align="center">
+        <template #default="s">
+          <el-button type="primary" link icon="View" @click="handleDetail(s.row)">查看</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <div class="gc-pagination">
       <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">
+    </div>
+
+    <!-- 查看详情弹窗 -->
+    <el-dialog :title="form.wagaName" v-model="open" width="720px" append-to-body>
+      <el-form :model="form" label-width="130px" :disabled="true">
+        <el-row>
+          <el-col :span="12"><el-form-item label="水闸名称">{{ form.wagaName }}</el-form-item></el-col>
+          <el-col :span="12"><el-form-item label="水闸代码">{{ form.wagaCode }}</el-form-item></el-col>
+        </el-row>
         <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="起点经度">{{ form.startLon }}</el-form-item></el-col>
+          <el-col :span="12"><el-form-item label="起点纬度">{{ form.startLat }}</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-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-col :span="12"><el-form-item label="终点经度">{{ form.endLon }}</el-form-item></el-col>
+          <el-col :span="12"><el-form-item label="终点纬度">{{ form.endLat }}</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³/s)"><el-input-number v-model="form.lockDisc" :min="0" :step="0.1" /></el-form-item></el-col>
+          <el-col :span="12"><el-form-item label="水闸所在位置">{{ form.wagaLoc }}</el-form-item></el-col>
+          <el-col :span="12"><el-form-item label="水闸类型">{{ wagaTypeMap[form.wagaType] }}</el-form-item></el-col>
         </el-row>
         <el-row>
-          <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-col :span="12"><el-form-item label="水闸用途">{{ form.wagaUsage }}</el-form-item></el-col>
+          <el-col :span="12"><el-form-item label="工程等别">{{ engGradMap[form.engGrad] }}</el-form-item></el-col>
+        </el-row>
+        <el-row>
+          <el-col :span="12"><el-form-item label="工程规模">{{ form.engScale }}</el-form-item></el-col>
+          <el-col :span="12"><el-form-item label="最大过闸流量">{{ form.lockDisc }}</el-form-item></el-col>
+        </el-row>
+        <el-row>
+          <el-col :span="12"><el-form-item label="闸孔数量">{{ form.lockNum }}</el-form-item></el-col>
+          <el-col :span="12"><el-form-item label="工程建设情况">{{ form.engBuild }}</el-form-item></el-col>
+        </el-row>
+        <el-row>
+          <el-col :span="12"><el-form-item label="开工时间">{{ form.startDate }}</el-form-item></el-col>
+          <el-col :span="12"><el-form-item label="建成时间">{{ form.endDate }}</el-form-item></el-col>
+        </el-row>
+        <el-row>
+          <el-col :span="12"><el-form-item label="归口管理部门">{{ form.admDep }}</el-form-item></el-col>
         </el-row>
       </el-form>
-      <template #footer>
-        <el-button type="primary" @click="submitForm">确 定</el-button>
-        <el-button @click="cancel">取 消</el-button>
-      </template>
     </el-dialog>
 
-    <el-dialog v-model="statsVisible" title="水闸类型分布统计" width="600px" append-to-body>
+    <!-- 统计弹窗 -->
+    <el-dialog title="统计类别" v-model="statsVisible" width="500px" 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" />
+        <el-table-column prop="name" label="类" min-width="120" />
+        <el-table-column prop="value" label="数量" width="120" />
       </el-table>
     </el-dialog>
   </div>
@@ -103,41 +88,30 @@
 
 <script setup name="Waga">
 import { getWagaList } from '@/api/slgc/gc/index'
-import { getWaga, addWaga, updateWaga, 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('')
+import { getWaga, getWagaCountType } from '@/api/slgc/waga/index'
+
+const loading = ref(true)
+const total = ref(0)
+const open = ref(false)
 const list = ref([])
-const queryParams = reactive({ pageNum: 1, pageSize: 10, wagaName: undefined, admDep: undefined })
+const queryParams = reactive({ pageNum: 1, pageSize: 10, wagaName: undefined, wagaLoc: undefined, wagaType: undefined })
 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 cancel() { open.value = false }
 
-function handleAdd() {
-  form.value = { runStat: '1' }
-  title.value = '新增水闸'
-  open.value = true
-}
-
-function handleDelete(row) {
-  proxy.$modal.confirm('确认删除水闸"' + row.wagaName + '"?').then(() => {
-    delWaga(row.wagaCode).then(() => {
-      proxy.$modal.msgSuccess('删除成功')
-      getList()
-    })
+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 handleDetail(row) {
+  getWaga(row.wagaCode).then(res => { form.value = res.data; open.value = true })
+}
 function handleStats() {
   statsVisible.value = true
   statsLoading.value = true
@@ -149,4 +123,30 @@ function handleStats() {
 
 onMounted(() => { getList() })
 </script>
-<style scoped>.card-header { font-weight: 600; font-size: 16px; }</style>
+
+<style scoped>
+.gc-page {
+  padding: 16px;
+  background: #fff;
+  min-height: calc(100vh - 60px);
+  display: flex;
+  flex-direction: column;
+}
+.gc-toolbar {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 16px;
+}
+.gc-toolbar-right {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+.gc-pagination {
+  margin-top: auto;
+  padding-top: 16px;
+  display: flex;
+  justify-content: flex-end;
+}
+</style>

+ 25 - 4
gw-ui/vite.config.js

@@ -49,14 +49,35 @@ export default defineConfig(({mode, command}) => {
           changeOrigin: true,
           rewrite: (path) => path.replace(new RegExp(`^${VITE_SERVICE_BASE_TITLE}`), ''),
         },
-        // springdoc proxy
+        // springdoc proxy(不含 context-path 的请求)
         '^/v3/api-docs/(.*)': {
           target: VITE_SERVER_URL,
           changeOrigin: true,
         },
-        // Knife4j 内部请求包含 context-path,需透传
-        '/slgc-run': {
-          target: VITE_SERVER_URL.replace('/slgc-run', ''),
+        // Knife4j 透传:前端 base 已改为 /slgc-run,只能用精确匹配避免拦截前端资源
+        '/slgc-run/doc.html': {
+          target: 'http://127.0.0.1:8901',
+          changeOrigin: true,
+        },
+        '/slgc-run/webjars/(.*)': {
+          target: 'http://127.0.0.1:8901',
+          changeOrigin: true,
+        },
+        '/slgc-run/swagger-resources': {
+          target: 'http://127.0.0.1:8901',
+          changeOrigin: true,
+        },
+        '/slgc-run/v3/api-docs': {
+          target: 'http://127.0.0.1:8901',
+          changeOrigin: true,
+        },
+        '/slgc-run/v3/api-docs/(.*)': {
+          target: 'http://127.0.0.1:8901',
+          changeOrigin: true,
+        },
+        // Knife4j UI 页面本身也通过后端代理(让 doc.html 可以同域访问)
+        '/slgc-run/swagger-ui/(.*)': {
+          target: 'http://127.0.0.1:8901',
           changeOrigin: true,
         }
       }