Merge branch 'main' into dev-tw

This commit is contained in:
tangwei 2026-07-15 14:02:03 +08:00
commit 2a7b8d3e3f
104 changed files with 8382 additions and 1133 deletions

View File

@ -24,17 +24,23 @@ public class MyMetaObjectHandler implements MetaObjectHandler {
public void insertFill(MetaObject metaObject) {
Date now = new Date();
// 自动填充创建时间
// 创建时间
this.strictInsertFill(metaObject, "createdAt", Date.class, now);
// 自动填充更新时间
// 更新时间
this.strictInsertFill(metaObject, "updatedAt", Date.class, now);
// 更新时间
this.strictInsertFill(metaObject, "modifyTime", Date.class, now);
try {
String userId = SecurityUtils.getUserId();
// 自动填充更新时间
// 创建人
this.strictInsertFill(metaObject, "createdBy", String.class, userId );
// 自动填充更新时间
//创建人
this.strictInsertFill(metaObject, "recordUser", String.class, userId );
// 更新人
this.strictInsertFill(metaObject, "modifyUser", String.class, userId);
// 更新人
this.strictInsertFill(metaObject, "updatedBy", String.class, userId);
} catch (Exception e) {
log.info("message{}",e.getMessage());
@ -47,12 +53,21 @@ public class MyMetaObjectHandler implements MetaObjectHandler {
*/
@Override
public void updateFill(MetaObject metaObject) {
// 自动填充更新时间
this.strictUpdateFill(metaObject, "updatedAt", Date.class, new Date());
Date date = new Date();
// 更新时间
this.strictUpdateFill(metaObject, "updatedAt", Date.class, date);
// 删除时间
this.strictUpdateFill(metaObject, "deleteTime", Date.class, date);
// 跟新时间
this.strictInsertFill(metaObject, "modifyTime", Date.class, date);
try {
String userId = SecurityUtils.getUserId();
// 自动填充更新人
// 更新人
this.strictInsertFill(metaObject, "updatedBy", String.class, userId);
// 删除人
this.strictInsertFill(metaObject, "deleteUser", String.class, userId);
// 更新人
this.strictInsertFill(metaObject, "modifyUser", String.class, userId);
} catch (Exception e) {
log.info("message{}",e.getMessage());
}

View File

@ -241,6 +241,14 @@ public class SwaggerConfig {
.build();
}
@Bean
public GroupedOpenApi groupSysOqmgApi() {
return GroupedOpenApi.builder()
.group("5.2 倾斜摄影管理")
.packagesToScan("com.yfd.platform.qgc_sys.obliqueImage.controller")
.build();
}
@Bean
public GroupedOpenApi groupLygkApi() {
return GroupedOpenApi.builder()

View File

@ -1,6 +1,7 @@
package com.yfd.platform.qgc_base.domain;
import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.EqualsAndHashCode;
@ -28,6 +29,7 @@ public class SdEngInfoBH implements Serializable {
/**
* 数据时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date tm;
/**
@ -77,6 +79,7 @@ public class SdEngInfoBH implements Serializable {
* 数据接入时间
*/
@TableField("DTIN_TM")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date dtinTm;
/**
@ -182,29 +185,34 @@ public class SdEngInfoBH implements Serializable {
* 规划设计日期
*/
@TableField("STDSDT")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date stdsdt;
/**
* 计划开工日期
*/
@TableField("PSTSTDT")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date pststdt;
/**
* 计划完工日期
*/
@TableField("PESSTDT")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date pesstdt;
/**
* 开工日期
*/
@TableField("SWDT")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date swdt;
/**
* 建成日期
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@TableField("JCDT")
private Date jcdt;
@ -212,12 +220,14 @@ public class SdEngInfoBH implements Serializable {
* 退役/拆除日期
*/
@TableField("WDDT")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date wddt;
/**
* 截流日期
*/
@TableField("JLDT")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date jldt;
/**
@ -404,6 +414,7 @@ public class SdEngInfoBH implements Serializable {
/**
* 编制时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date edttm;
/**
@ -856,16 +867,19 @@ public class SdEngInfoBH implements Serializable {
/**
* 蓄水验收日期
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date sswdt;
/**
* 一期下闸蓄水日期
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date piodt;
/**
* 二期下闸蓄水日期
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date aiodt;
/**
@ -936,16 +950,19 @@ public class SdEngInfoBH implements Serializable {
/**
* 水土保持竣工验收时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date styphpsp;
/**
* 现行环境保护竣工验收时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date xsjdhbys;
/**
* 申报水土保持方案日期
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date sbfascrq;
/**
@ -957,21 +974,25 @@ public class SdEngInfoBH implements Serializable {
/**
* 环境影响后评价时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date hpsprq;
/**
* 竣工环境保护验收时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date jghbysrq;
/**
* 截流阶段环境保护验收时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date jljdsbys;
/**
* 竣工水土保持方案验收时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date jgjdsbys;
/**
@ -1218,6 +1239,7 @@ public class SdEngInfoBH implements Serializable {
/**
* 植被覆盖调查时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date vlsrTm;
/**
@ -1228,6 +1250,7 @@ public class SdEngInfoBH implements Serializable {
/**
* 创建时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date recordTime;
/**
@ -1238,6 +1261,7 @@ public class SdEngInfoBH implements Serializable {
/**
* 更新时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date modifyTime;
/**
@ -1253,6 +1277,7 @@ public class SdEngInfoBH implements Serializable {
/**
* 删除时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date deleteTime;
/**

View File

@ -10,6 +10,7 @@ import com.yfd.platform.config.ResponseResult;
import com.yfd.platform.qgc_base.service.ISdHbrvDicService;
import com.yfd.platform.qgc_env.wt.service.SdWtMonitorService;
import com.yfd.platform.qgc_lygk.along.domain.MsAlongB;
import com.yfd.platform.qgc_lygk.along.domain.PowerDto;
import com.yfd.platform.qgc_lygk.along.service.IMsAlongBService;
import com.yfd.platform.qgc_sys.mapcfg.service.IMapmoduleBService;
import com.yfd.platform.utils.DataSourceRequestUtil;
@ -90,4 +91,12 @@ public class MsAlongBController {
public ResponseResult getWbsbList(@RequestParam String stcd) {
return ResponseResult.successData(msAlongBService.getWbsbList(stcd));
}
@PostMapping("/noAuth/GetKendoPage")
@Operation(summary = "查询水电基地流域字典分页列表")
public ResponseResult getKendoPage(@RequestBody DataSourceRequest dataSourceRequest) {
DataSourceResult<PowerDto> result = msAlongBService.getKendoPage(dataSourceRequest);
return ResponseResult.successData(result);
}
}

View File

@ -84,4 +84,12 @@ public class MsAlongB implements Serializable {
/** 排序 */
@TableField("ORDER_INDEX")
private Integer orderIndex;
/** 水电基地编码 */
@TableField("BASEID")
private String baseId;
/** 水电基地名称 */
@TableField("BASENAME")
private String baseName;
}

View File

@ -82,4 +82,10 @@ public class MsAlongDetB implements Serializable {
/** 删除时间 */
@TableField("DELETE_TIME")
private Date deleteTime;
/** 出库入库标识 */
@TableField("IO_DIRECTION")
private String ioDirection;
}

View File

@ -14,5 +14,6 @@ public class MsAlongDetTreeDto {
private String stnm; // 仅用于显示不存表
private String rstcd; // 仅用于显示不存表
private String parentId; // 前端用不存表
private String ioDirection; // 前端用不存表
private List<MsAlongDetTreeDto> children; // 子节点
}

View File

@ -1,26 +1,191 @@
package com.yfd.platform.qgc_lygk.along.domain;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
@Data
@TableName("V_MS_STBPRP_T")
public class PowerDto {
// 水电水利工程编码
private String stcd;
// 水电水利工程名称
private String stnm;
// ==================== 基础编码与名称 ====================
@TableField("STCD")
private String stcd; // 水电水利工程编码
// 水电水利工程名称
private String sttp;
@TableField("STNM")
private String stnm; // 水电水利工程名称中文
// 主表实体
private String sttpCode;
@TableField("ENNM")
private String ennm; // 水电水利工程名称英文/简称
// 主表实体
private String sttpName;
// ==================== 测站类型STTP ====================
@TableField("STTP")
private String sttp; // 测站类型代码 WEEQ_5
// 主表实体
private String rstcd;
@TableField("STTP_CODE")
private String sttpCode; // 测站类型编码 STTP 相同
@TableField("STTP_NAME")
private String sttpName; // 测站类型名称 水生生态调查断面基荷发电
@TableField("STTP_FULL_PATH")
private String sttpFullPath; // 测站类型完整路径 ENV,ENVM,WE,
@TableField("STTP_TREE_LEVEL")
private Integer sttpTreeLevel; // 测站类型树层级 3
// ==================== 关联上级 ====================
@TableField("RSTCD")
private String rstcd; // 关联上级测站编码
// ==================== 流域信息BASE ====================
@TableField("BASE_ID")
private String baseId; // 流域ID 02
@TableField("BASE_NAME")
private String baseName; // 流域名称 雅砻江干流
// ==================== 流域水系HBRVCD ====================
@TableField("HBRVCD")
private String hbrvcd; // 流域水系编码
@TableField("HBRVCD_NAME")
private String hbrvcdName; // 流域水系名称
// ==================== 河流/流域RVCD ====================
@TableField("RVCD")
private String rvcd; // 河流/流域编码 SJLY176
@TableField("RVCD_NAME")
private String rvcdName; // 河流/流域名称 雅砻江干流
// ==================== 行政区划ADDVCD ====================
@TableField("ADDVCD")
private String addvcd; // 行政区划编码
@TableField("ADDVNM")
private String addvnm; // 行政区划名称
// ==================== 河流水系HYCD ====================
@TableField("HYCD")
private String hycd; // 河流水系编码
@TableField("HYNM")
private String hynm; // 河流水系名称
@TableField("TOP_HYCD")
private String topHycd; // 顶级河流水系编码
@TableField("TOP_HYNM")
private String topHynm; // 顶级河流水系名称
// ==================== 地理坐标 ====================
@TableField("LGTD")
private BigDecimal lgtd; // 经度 100.780252
@TableField("LTTD")
private BigDecimal lttd; // 纬度 30.26872
// ==================== 站点属性 ====================
@TableField("STLC")
private String stlc; // 站点位置描述
@TableField("USFL")
private Integer usfl; // 是否启用1启用 0禁用
@TableField("DTIN")
private String dtin; // 数据类型代码
@TableField("DTIN_TM")
private Date dtinTm; // 数据类型更新时间
@TableField("BLDSTT_CODE")
private String bldsttCode; // 建设状态代码
// ==================== 描述与附件 ====================
@TableField("INTRODUCE")
private String introduce; // 介绍/描述
@TableField("LOGO")
private String logo; // LOGO 图片
@TableField("INFFILE")
private String inffile; // 附件文件
// ==================== 投资与建设 ====================
@TableField("INV")
private String inv; // 投资方/投资信息
@TableField("INVINMN")
private String invinmn; // 投资单位/法人
@TableField("STDSDT")
private Date stdsdt; // 开工日期
@TableField("PSTSTDT")
private Date pststdt; // 计划竣工日期
@TableField("PESSTDT")
private Date pesstdt; // 实际竣工日期
// ==================== 水文参数 ====================
@TableField("SWDT")
private String swdt; // 水文数据类型
@TableField("JCDT")
private String jcdt; // 监测数据类型
@TableField("WDDT")
private String wddt; // 温度数据类型
@TableField("ORDER_INDEX")
private Integer orderIndex; // 排序序号
@TableField("REMARK")
private String remark; // 备注
@TableField("VLSR")
private String vlsr; // 流速
@TableField("VLSR_TM")
private Date vlsrTm; // 流速时间
// ==================== 数据源与运行状态 ====================
@TableField("DTIN_TYPE")
private String dtinType; // 数据类型分类
@TableField("MWAY")
private String mway; // 测量方式
@TableField("RUN_STATE")
private String runState; // 运行状态 1-运行中
// ==================== 审计字段通用 ====================
@TableField("RECORD_USER")
private String recordUser; // 创建人
@TableField("RECORD_TIME")
private Date recordTime; // 创建时间
@TableField("MODIFY_USER")
private String modifyUser; // 修改人
@TableField("MODIFY_TIME")
private Date modifyTime; // 修改时间
@TableField("IS_DELETED")
private Integer isDeleted; // 软删除0未删 1已删
@TableField("DELETE_USER")
private String deleteUser; // 删除人
@TableField("DELETE_TIME")
private Date deleteTime; // 删除时间
// ==================== 数据来源 ====================
@TableField("DATA_SOURCE")
private String dataSource; // 数据来源 SD_WE_B_H
}

View File

@ -26,6 +26,8 @@ public interface IMsAlongBService extends IService<MsAlongB> {
List<PowerDto> getWbsbList(String stcd);
DataSourceResult<PowerDto> getKendoPage(DataSourceRequest dataSourceRequest);
// /**
// * 保存或更新沿程配置
// *

View File

@ -8,6 +8,7 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.yfd.platform.common.DataSourceLoadOptionsBase;
import com.yfd.platform.common.DataSourceRequest;
import com.yfd.platform.common.DataSourceResult;
import com.yfd.platform.common.MicroservicDynamicSQLMapper;
import com.yfd.platform.qgc_base.domain.SdRvcdDic;
import com.yfd.platform.qgc_base.mapper.SdRvcdDicMapper;
import com.yfd.platform.qgc_lygk.along.domain.MsAlongB;
@ -17,6 +18,9 @@ import com.yfd.platform.qgc_lygk.along.service.IMsAlongBService;
import com.yfd.platform.utils.CodeToNameMetadataBo;
import com.yfd.platform.utils.DataSourceRequestUtil;
import com.yfd.platform.utils.DictCodeToNameConverter;
import com.yfd.platform.common.DataSourceRequest;
import com.yfd.platform.common.DataSourceRequest.FilterDescriptor;
import com.yfd.platform.common.DataSourceRequest.SortDescriptor;
import com.yfd.platform.utils.QgcQueryWrapperUtil;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
@ -37,6 +41,9 @@ public class MsAlongBServiceImpl extends ServiceImpl<MsAlongBMapper, MsAlongB> i
@Resource
private SdRvcdDicMapper sdRvcdDicMapper;
@Resource
private MicroservicDynamicSQLMapper microservicDynamicSQLMapper;
@Override
public DataSourceResult<MsAlongB> getKendoList(DataSourceRequest dataSourceRequest) {
// 1. 构建查询条件
@ -101,6 +108,136 @@ public class MsAlongBServiceImpl extends ServiceImpl<MsAlongBMapper, MsAlongB> i
return powerDtos;
}
@Override
public DataSourceResult<PowerDto> getKendoPage(DataSourceRequest dataSourceRequest) {
// 1. 处理分页参数
DataSourceLoadOptionsBase loadOptions = dataSourceRequest == null ? null : dataSourceRequest.toDevRequest();
// 2. 构建基础 SQL
StringBuilder sql = new StringBuilder("SELECT * FROM V_MS_STBPRP_T WHERE 1=1");
Map<String, Object> paramMap = new HashMap<>();
// 3. 解析过滤条件利用你的 FilterDescriptor
if (dataSourceRequest != null && dataSourceRequest.getFilter() != null) {
String filterSql = parseFilter(dataSourceRequest.getFilter(), paramMap, new int[]{0});
if (StrUtil.isNotBlank(filterSql)) {
sql.append(" AND ").append(filterSql);
}
}
// 4. 解析排序利用你的 SortDescriptor
if (dataSourceRequest != null && CollUtil.isNotEmpty(dataSourceRequest.getSort())) {
List<String> orderList = new ArrayList<>();
for (SortDescriptor sort : dataSourceRequest.getSort()) {
String field = sort.getField();
if (StrUtil.isBlank(field) || !isSafeField(field)) continue;
String direction = "asc".equalsIgnoreCase(sort.getDir()) ? "ASC" : "DESC";
orderList.add(field + " " + direction);
}
if (!orderList.isEmpty()) {
sql.append(" ORDER BY ").append(String.join(", ", orderList));
}
}
// 5. 执行分页查询
Page<?> page = loadOptions == null ? null :
QgcQueryWrapperUtil.buildPage(loadOptions, loadOptions.getSkip(), loadOptions.getTake());
List<PowerDto> list = microservicDynamicSQLMapper.pageAllListWithResultType(
page, sql.toString(), paramMap, PowerDto.class
);
// 6. 封装返回结果
DataSourceResult<PowerDto> result = new DataSourceResult<>();
result.setData(list);
result.setTotal(page != null ? page.getTotal() : list.size());
result.setAggregates(new HashMap<>());
return result;
}
/**
* 递归解析 FilterDescriptor完美适配你的内部类结构
*/
/**
* 递归解析 FilterDescriptor
*/
/**
* 递归解析 FilterDescriptorOracle 兼容版
*/
private String parseFilter(FilterDescriptor filter, Map<String, Object> paramMap, int[] paramIndex) {
if (filter == null) return "";
// 如果有嵌套条件logic + filters
List<FilterDescriptor> childFilters = filter.getFilters();
if (CollUtil.isNotEmpty(childFilters)) {
List<String> childConditions = new ArrayList<>();
for (FilterDescriptor child : childFilters) {
String childSql = parseFilter(child, paramMap, paramIndex);
if (StrUtil.isNotBlank(childSql)) {
childConditions.add(childSql);
}
}
if (childConditions.isEmpty()) return "";
String logic = StrUtil.isNotBlank(filter.getLogic()) ? filter.getLogic() : "and";
String connector = "and".equalsIgnoreCase(logic) ? " AND " : " OR ";
return "(" + String.join(connector, childConditions) + ")";
}
// 叶子节点单个条件
String field = filter.getField();
Object value = filter.getValue();
String operator = filter.getOperator();
if (StrUtil.isBlank(field) || value == null) return "";
if (!isSafeField(field)) return "";
String paramKey = "p" + paramIndex[0]++;
paramMap.put(paramKey, value);
// 根据操作符生成 SQLOracle 使用 || 拼接
if ("eq".equalsIgnoreCase(operator)) {
return field + " = #{map." + paramKey + "}";
} else if ("neq".equalsIgnoreCase(operator)) {
return field + " != #{map." + paramKey + "}";
} else if ("contains".equalsIgnoreCase(operator)) {
// Oracle || 替代 CONCAT
return field + " LIKE '%' || #{map." + paramKey + "} || '%'";
} else if ("startswith".equalsIgnoreCase(operator)) {
return field + " LIKE #{map." + paramKey + "} || '%'";
} else if ("endswith".equalsIgnoreCase(operator)) {
return field + " LIKE '%' || #{map." + paramKey + "}";
} else if ("gt".equalsIgnoreCase(operator)) {
return field + " > #{map." + paramKey + "}";
} else if ("gte".equalsIgnoreCase(operator)) {
return field + " >= #{map." + paramKey + "}";
} else if ("lt".equalsIgnoreCase(operator)) {
return field + " < #{map." + paramKey + "}";
} else if ("lte".equalsIgnoreCase(operator)) {
return field + " <= #{map." + paramKey + "}";
} else if ("isnull".equalsIgnoreCase(operator)) {
paramMap.remove(paramKey);
return field + " IS NULL";
} else if ("isnotnull".equalsIgnoreCase(operator)) {
paramMap.remove(paramKey);
return field + " IS NOT NULL";
}
return "";
}
/**
* 安全字段白名单根据你的视图字段调整
*/
private boolean isSafeField(String field) {
if (StrUtil.isBlank(field)) return false;
// 支持视图中的常用字段
Set<String> validFields = new HashSet<>(Arrays.asList(
"STCD", "STNM", "ENNM", "BASE_ID", "BASE_NAME", "RVCD", "RVCD_NAME",
"ADDVCD", "ADDVNM", "HYCD", "HYNM", "STTP", "STTP_CODE", "STTP_NAME",
"LGTD", "LTTD", "ORDER_INDEX", "RUN_STATE"
));
return validFields.contains(field.toUpperCase());
}
// @Override
// public boolean saveOrUpdate(MsAlongB msAlongB) {
// super.saveOrUpdate(msAlongB);

View File

@ -149,6 +149,7 @@ public class MsAlongDetBServiceImpl extends ServiceImpl<MsAlongDetBMapper, MsAlo
entity.setAlongId(node.getAlongId());
entity.setStcd(node.getStcd());
entity.setSttp(node.getSttp());
entity.setIoDirection(node.getIoDirection());
entity.setRstcd(node.getRstcd());
entity.setRecordUser(SecurityUtils.getUserId()); // 创建人
//entity.setRecordUser(SecurityUtils.getCurrentUsername()); // 创建人

View File

@ -0,0 +1,61 @@
package com.yfd.platform.qgc_sys.obliqueImage.controller;
import cn.hutool.core.util.StrUtil;
import com.yfd.platform.common.DataSourceRequest;
import com.yfd.platform.common.DataSourceResult;
import com.yfd.platform.common.exception.BizException;
import com.yfd.platform.config.ResponseResult;
import com.yfd.platform.qgc_sys.obliqueImage.domain.MsObliqueImageB;
import com.yfd.platform.qgc_sys.obliqueImage.service.IMsObliqueImageBService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.*;
/**
* 倾斜摄影管理控制器
*
* @author migration
*/
@RestController
@RequestMapping("/sys/oblique")
@Tag(name = "倾斜摄影管理控制器")
public class MsObliqueImageBController {
@Resource
private IMsObliqueImageBService msObliqueImageBService;
/**
* 条件过滤数据列表
*/
@Operation(summary = "条件过滤数据列表")
@PostMapping("/GetKendoListCust")
public ResponseResult getKendoListCust(@RequestBody DataSourceRequest dataSourceRequest) {
DataSourceResult<MsObliqueImageB> result = msObliqueImageBService.getKendoList(dataSourceRequest);
return ResponseResult.successData(result);
}
/**
* 新增或修改
*/
@Operation(summary = "新增或修改")
@PostMapping("/addOrUpdate")
public ResponseResult addOrUpdate(@RequestBody MsObliqueImageB msObliqueImageB) {
msObliqueImageBService.addOrUpdate(msObliqueImageB);
return ResponseResult.success();
}
/**
* 删除
*/
@Operation(summary = "删除")
@GetMapping("/delete")
public ResponseResult delete(@RequestParam("id") String id) {
if (StrUtil.isBlank(id)) {
throw new BizException("ID不能为空");
}
msObliqueImageBService.delete(id);
return ResponseResult.success();
}
}

View File

@ -0,0 +1,151 @@
package com.yfd.platform.qgc_sys.obliqueImage.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
/**
* 倾斜摄影管理实体类
*
* @author migration
*/
@Data
@TableName("MS_OBL_PHOTO")
@Schema(description = "倾斜摄影管理实体类")
public class MsObliqueImageB implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.ASSIGN_UUID)
private String id;
/** 站码 */
@TableField("STCD")
private String stcd;
/** 站名 */
@TableField("STNM")
private String stnm;
/** 所属站类ID */
@TableField("STTP")
private String sttp;
/** 所属站类编码 */
@TableField("STTP_CODE")
private String sttpCode;
/** 所属站类名称 */
@TableField("STTP_NAME")
private String sttpName;
/** 所属站类全路径(非数据库字段) */
@TableField(exist = false)
private String sttpFullPath;
/** 所属站类层级(非数据库字段) */
@TableField(exist = false)
private Integer sttpTreeLevel;
/** 所属水电基地编码 */
@TableField("BASE_ID")
private String baseId;
/** 所属水电基地名称 */
@TableField("BASE_NAME")
private String baseName;
/** 所属流域编码 */
@TableField("HBRVCD")
private String hbrvcd;
/** 所属流域名称 */
@TableField("HBRVCD_NAME")
private String hbrvcdName;
/** 所属河段编码非数据库字段与HBRVCD同源 */
@TableField(exist = false)
private String rvcd;
/** 所属河段名称非数据库字段与HBRVCD_NAME同源 */
@TableField(exist = false)
private String rvcdName;
/** 在线地址 */
@TableField("URL")
private String url;
/** 所属电站编码 */
@TableField("RSTCD")
private String rstcd;
/** 所属电站名称 */
@TableField("ENNM")
private String ennm;
/** 电站经度(非数据库字段) */
@TableField("LGTD")
private Double lgtd;
/** 电站纬度(非数据库字段) */
@TableField("LTTD")
private Double lttd;
/** 电站坐标JSON串 */
@TableField("LOCATION")
private String location;
/** 模型精度 */
@TableField("ACCURACY")
private Integer accuracy;
/** 边界范围 */
@TableField("BOUNDARY")
private String boundary;
/** 自定义高程 */
@TableField("HEIGHT")
private Integer height;
/** 备注 */
@TableField("COMMENTS")
private String comments;
/** 创建人 */
@TableField("RECORD_USER")
private String recordUser;
/** 创建人名称(非数据库字段) */
@TableField(exist = false)
private String recordUserName;
/** 创建时间(非数据库字段) */
@TableField(exist = false)
private Date recordTime;
/** 更新人 */
@TableField("MODIFY_USER")
private String modifyUser;
/** 更新时间 */
@TableField("MODIFY_TIME")
private Date modifyTime;
/** 是否已删除0=未删除 1=已删除 */
@TableField("IS_DELETED")
private Integer isDeleted;
/** 删除人 */
@TableField("DELETE_USER")
private String deleteUser;
/** 删除时间 */
@TableField("DELETE_TIME")
private Date deleteTime;
}

View File

@ -0,0 +1,24 @@
package com.yfd.platform.qgc_sys.obliqueImage.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.yfd.platform.qgc_sys.obliqueImage.domain.MsObliqueImageB;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
/**
* 倾斜摄影管理 Mapper 接口
*
* @author migration
*/
@Mapper
public interface MsObliqueImageBMapper extends BaseMapper<MsObliqueImageB> {
@Delete("DELETE FROM MS_OBL_PHOTO WHERE ID = #{id}")
void deleteById(@Param("id") String id);
@Select("SELECT ID FROM SD_STTP_B WHERE STTP_CODE = 'OSGB'")
String getSttp();
}

View File

@ -0,0 +1,36 @@
package com.yfd.platform.qgc_sys.obliqueImage.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.yfd.platform.common.DataSourceRequest;
import com.yfd.platform.common.DataSourceResult;
import com.yfd.platform.qgc_sys.obliqueImage.domain.MsObliqueImageB;
/**
* 倾斜摄影管理 Service 接口
*
* @author migration
*/
public interface IMsObliqueImageBService extends IService<MsObliqueImageB> {
/**
* 条件过滤数据列表Kendo 数据源
*
* @param dataSourceRequest 数据源请求
* @return 数据源结果
*/
DataSourceResult<MsObliqueImageB> getKendoList(DataSourceRequest dataSourceRequest);
/**
* 新增或修改
*
* @param msObliqueImageB 倾斜摄影实体
*/
void addOrUpdate(MsObliqueImageB msObliqueImageB);
/**
* 删除
*
* @param id 主键ID
*/
void delete(String id);
}

View File

@ -0,0 +1,178 @@
package com.yfd.platform.qgc_sys.obliqueImage.service.impl;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.yfd.platform.common.DataSourceLoadOptionsBase;
import com.yfd.platform.common.DataSourceRequest;
import com.yfd.platform.common.DataSourceResult;
import com.yfd.platform.common.exception.BizException;
import com.yfd.platform.qgc_base.domain.SdEngInfoBH;
import com.yfd.platform.qgc_base.service.ISdEngInfoBHService;
import com.yfd.platform.qgc_sys.obliqueImage.domain.MsObliqueImageB;
import com.yfd.platform.qgc_sys.obliqueImage.mapper.MsObliqueImageBMapper;
import com.yfd.platform.qgc_sys.obliqueImage.service.IMsObliqueImageBService;
import com.yfd.platform.utils.DataSourceRequestUtil;
import com.yfd.platform.utils.QgcQueryWrapperUtil;
import com.yfd.platform.utils.QueryWrapperUtil;
import com.yfd.platform.utils.SecurityUtils;
import jakarta.annotation.Resource;
import lombok.Data;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.Date;
import java.util.List;
/**
* 倾斜摄影管理 Service 实现类
*
* @author migration
*/
@Service
public class MsObliqueImageBServiceImpl extends ServiceImpl<MsObliqueImageBMapper, MsObliqueImageB> implements IMsObliqueImageBService {
@Resource
private MsObliqueImageBMapper msObliqueImageBMapper;
@Resource
private ISdEngInfoBHService engInfoBHService;
@Override
public DataSourceResult<MsObliqueImageB> getKendoList(DataSourceRequest dataSourceRequest) {
// 1. 构建查询条件
DataSourceLoadOptionsBase loadOptions = dataSourceRequest == null ? null : dataSourceRequest.toDevRequest();
QueryWrapper<MsObliqueImageB> wrapper = DataSourceRequestUtil.buildQueryWrapper(
dataSourceRequest, MsObliqueImageB.class);
// 固定条件STTP_CODE = 'OSGB' AND IS_DELETED = 0 AND RSTCD IS NOT NULL
wrapper.eq("STTP_CODE", "OSGB");
wrapper.eq("IS_DELETED", 0);
wrapper.isNotNull("RSTCD");
// 旧系统 rvcd 字段映射到 HBRVCD 前端传参保持 rvcd 不变
if (loadOptions != null) {
String rvcd = QueryWrapperUtil.getFilterFieldValue(loadOptions, "rvcd");
if (StrUtil.isNotBlank(rvcd)) {
wrapper.eq("HBRVCD", rvcd);
}
}
// 默认按修改时间倒序
wrapper.orderByDesc("MODIFY_TIME");
// 2. 分页参数处理
Page<MsObliqueImageB> page = loadOptions == null ? null
: (Page<MsObliqueImageB>) QgcQueryWrapperUtil.buildPage(loadOptions, loadOptions.getSkip(), loadOptions.getTake());
// 3. 执行查询
Page<MsObliqueImageB> resultPage;
if (page != null) {
resultPage = this.page(page, wrapper);
} else {
List<MsObliqueImageB> list = this.list(wrapper);
resultPage = new Page<>();
resultPage.setRecords(list);
resultPage.setTotal(list.size());
}
// 4. 构建结果
DataSourceResult<MsObliqueImageB> dataSourceResult = new DataSourceResult<>();
dataSourceResult.setData(resultPage.getRecords());
dataSourceResult.setTotal(resultPage.getTotal());
return dataSourceResult;
}
@Override
public void addOrUpdate(MsObliqueImageB msObliqueImageB) {
if (StrUtil.isNotBlank(msObliqueImageB.getRstcd())){
LambdaQueryWrapper<SdEngInfoBH> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(StrUtil.isNotBlank(msObliqueImageB.getRstcd()), SdEngInfoBH::getStcd, msObliqueImageB.getRstcd())
.eq(SdEngInfoBH::getIsDeleted, 0)
.select(SdEngInfoBH::getLgtd, SdEngInfoBH::getLttd, SdEngInfoBH::getBaseId, SdEngInfoBH::getBaseName)
.orderByAsc(SdEngInfoBH::getBaseId, SdEngInfoBH::getOrderIndex);
SdEngInfoBH sdEngInfoBH = engInfoBHService.getOne(wrapper);
if (sdEngInfoBH.getLgtd() != null){
msObliqueImageB.setLgtd(sdEngInfoBH.getLgtd());
}
if (sdEngInfoBH.getLttd() != null){
msObliqueImageB.setLttd(sdEngInfoBH.getLttd());
}
if (StrUtil.isNotBlank(sdEngInfoBH.getBaseName())){
msObliqueImageB.setBaseName(sdEngInfoBH.getBaseName());
}
if (StrUtil.isNotBlank(sdEngInfoBH.getBaseId())){
msObliqueImageB.setBaseId(sdEngInfoBH.getBaseId());
}
}
if (StrUtil.isBlank(msObliqueImageB.getId())) {
addMsObliqueImageB(msObliqueImageB);
} else {
updateMsObliqueImageB(msObliqueImageB);
}
}
@Override
public void delete(String id) {
msObliqueImageBMapper.deleteById(id);
}
private void addMsObliqueImageB(MsObliqueImageB msObliqueImageB) {
// 查询新增的倾斜摄影是否已存在
String rstcd = msObliqueImageB.getRstcd();
if (StrUtil.isBlank(rstcd)) {
throw new BizException("电站不能为空");
}
String id = "OSGB_" + rstcd;
MsObliqueImageB old = msObliqueImageBMapper.selectById(id);
if (old != null) {
throw new BizException("该电站已存在倾斜摄影");
}
msObliqueImageB.setSttpCode("OSGB");
msObliqueImageB.setSttpName("倾斜摄影");
if (StrUtil.isBlank(msObliqueImageB.getRvcd())) {
msObliqueImageB.setRvcd(msObliqueImageB.getHbrvcd());
}
if (StrUtil.isBlank(msObliqueImageB.getRvcdName())) {
msObliqueImageB.setRvcdName(msObliqueImageB.getHbrvcdName());
}
msObliqueImageB.setStcd("OSGB_" + msObliqueImageB.getRstcd());
msObliqueImageB.setId("OSGB_" + msObliqueImageB.getRstcd());
msObliqueImageB.setSttpFullPath("OSGB,");
msObliqueImageB.setSttpTreeLevel(1);
String sttp = msObliqueImageBMapper.getSttp();
if (StrUtil.isBlank(sttp)) {
throw new BizException("倾斜摄影对应的sttp在SD_STTP_B中找不到");
}
msObliqueImageB.setSttp(sttp);
// 设置通用字段
String userId = currentUserIdOrSystem();
msObliqueImageB.setRecordUser(userId);
Date now = new java.util.Date();
msObliqueImageB.setModifyTime(now);
msObliqueImageB.setModifyUser(userId);
msObliqueImageB.setIsDeleted(0);
msObliqueImageBMapper.insert(msObliqueImageB);
}
private void updateMsObliqueImageB(MsObliqueImageB msObliqueImageB) {
String userId = currentUserIdOrSystem();
msObliqueImageB.setModifyUser(userId);
msObliqueImageB.setModifyTime(new java.util.Date());
this.updateById(msObliqueImageB);
}
private String currentUserIdOrSystem() {
try {
return SecurityUtils.getUserId();
} catch (Exception ex) {
return "system";
}
}
}

View File

@ -4,23 +4,24 @@ import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.yfd.platform.annotation.Log;
import com.yfd.platform.common.DataSourceRequest;
import com.yfd.platform.common.exception.BizException;
import com.yfd.platform.config.ResponseResult;
import com.yfd.platform.system.domain.PushConfig;
import com.yfd.platform.system.domain.PushConfigTargetRequest;
import com.yfd.platform.system.domain.PushHistory;
import com.yfd.platform.system.domain.PushHistoryDetail;
import com.yfd.platform.system.service.IPushConfigService;
import com.yfd.platform.qgc_data.domain.ApprovalMain;
import com.yfd.platform.system.domain.*;
import com.yfd.platform.system.service.*;
import com.yfd.platform.utils.DataSourceRequestUtil;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import static cn.hutool.core.date.DateUtil.parseDate;
@RestController
@RequestMapping("/system/pushConfig")
@ -30,14 +31,32 @@ public class PushConfigController {
@Resource
private IPushConfigService pushConfigService;
@Resource
private IUserService userService;
@Resource
private ISysRoleService roleService;
@Resource
private IPushHistoryService pushHistoryService;
@Resource
private IPushHistoryDetailService pushHistoryDetailService;
@Operation(summary = "查询推送配置列表")
@GetMapping("/getPushConfigList")
public ResponseResult getPushConfigList(Page<PushConfig> page,
String categoryName,
String messageType,
Integer targetType,
Integer status) {
return ResponseResult.successData(pushConfigService.getPushConfigPage(page, categoryName, messageType, targetType, status));
@PostMapping("/getPushConfigList")
public ResponseResult getPushConfigList(@RequestBody DataSourceRequest request) {
Page<PushConfig> pushConfigPage = DataSourceRequestUtil.executeQuery(request, PushConfig.class, pushConfigService);
return ResponseResult.successData(pushConfigPage);
// String categoryName = null;
// String messageType = null;
// Integer targetType = null;
// Integer status = null;
// return ResponseResult.successData(pushConfigService.getPushConfigPage(page, categoryName, messageType, targetType, status));
}
@Operation(summary = "根据ID查询推送配置")
@ -63,7 +82,7 @@ public class PushConfigController {
@Log(module = "推送配置", value = "删除推送配置")
@Operation(summary = "删除推送配置")
@PostMapping("/deletePushConfig")
public ResponseResult deletePushConfig(@RequestParam String ids) {
public ResponseResult deletePushConfig(@RequestBody List<String> ids) {
return pushConfigService.deletePushConfig(ids) ? ResponseResult.success() : ResponseResult.error("删除失败");
}
@ -102,23 +121,78 @@ public class PushConfigController {
}
@Operation(summary = "查询推送历史主表")
@GetMapping("/getPushHistoryList")
public ResponseResult getPushHistoryList(Page<PushHistory> page,
String configId,
Integer status,
String messageTypeCode,
String startTime,
String endTime) {
Date start = StrUtil.isBlank(startTime) ? null : DateUtil.parse(startTime);
Date end = StrUtil.isBlank(endTime) ? null : DateUtil.parse(endTime);
return ResponseResult.successData(pushConfigService.getPushHistoryPage(page, configId, status, messageTypeCode, start, end));
@PostMapping("/postPushHistoryList")
public ResponseResult getPushHistoryList(@RequestBody DataSourceRequest request) {
// 1. 预处理日期过滤器
List<DataSourceRequest.FilterDescriptor> filters = request.getFilter().getFilters();
for (DataSourceRequest.FilterDescriptor filter : filters) {
String field = filter.getField();
// 判断是否为日期字段根据你的实体字段名
if (isDateField(field)) {
Object value = filter.getValue();
if (value != null) {
String strValue = value.toString();
if (StrUtil.isNotBlank(strValue)) {
Date dateValue = parseDate(strValue);
// 关键将转换后的 Date 对象重新设置回 filter
filter.setValue(dateValue);
}
}
}
}
// 2. 调用通用查询
Page<PushHistory> pushHistoryPage = DataSourceRequestUtil.executeQuery(request, PushHistory.class, pushHistoryService);
return ResponseResult.successData(pushHistoryPage);
}
// 辅助方法判断字段是否为日期类型根据你的实体类字段名
private boolean isDateField(String field) {
// 这些字段在 PushHistory 中都是 Date 类型
return "planTime".equals(field)
|| "createdAt".equals(field)
|| "updatedAt".equals(field)
|| "modifyTime".equals(field)
|| "deleteTime".equals(field);
}
// 辅助方法解析日期字符串
private Date parseDate(String dateStr) {
try {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(dateStr);
} catch (ParseException e) {
throw new BizException("日期格式错误,请使用 yyyy-MM-dd HH:mm:ss 格式: " + dateStr);
}
}
@Operation(summary = "查询推送历史明细")
@GetMapping("/getPushHistoryDetailList")
public ResponseResult getPushHistoryDetailList(Page<PushHistoryDetail> page,
String historyId,
Integer status) {
return ResponseResult.successData(pushConfigService.getPushHistoryDetailPage(page, historyId, status));
@PostMapping("/getPushHistoryDetailList")
public ResponseResult getPushHistoryDetailList(@RequestBody DataSourceRequest request) {
Page<PushHistoryDetail> pushHistoryDetailPage = DataSourceRequestUtil.executeQuery(request, PushHistoryDetail.class, pushHistoryDetailService);
return ResponseResult.successData(pushHistoryDetailPage);
}
@GetMapping("/queryUsersList")
@Operation(summary = "查询状态正常的用户列表")
@ResponseBody
public ResponseResult queryUsersList( @RequestParam(value = "name", required = false) String name,@RequestHeader(value = "Tenant_id", required = false) String tenantId) {
List<SysUser> sysUserList = userService.queryUsersList(name,StrUtil.trimToNull(tenantId));
return ResponseResult.successData(sysUserList);
}
@Operation(summary = "根据historyId查推送历史明细详情")
@GetMapping("/getPushHistoryDetailById")
public ResponseResult getPushHistoryDetailById(@RequestParam String historyId) {
return ResponseResult.successData(pushHistoryDetailService.getPushHistoryDetailById(historyId));
}
@GetMapping("/queryRolesList")
@Operation(summary = "查询角色类型列表")
@ResponseBody
public ResponseResult queryRolesList( @RequestParam(value = "roleName", required = false) String roleName,@RequestHeader(value = "Tenant_id", required = false) String tenantId) {
List<SysRole> sysRoleList = roleService.queryRolesList(roleName,StrUtil.trimToNull(tenantId));
return ResponseResult.successData(sysRoleList);
}
}

View File

@ -1,8 +1,6 @@
package com.yfd.platform.system.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.io.Serializable;
@ -15,6 +13,7 @@ public class PushConfig implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.ASSIGN_UUID)
@TableField("ID")
private String id;
private String categoryCode;
@ -41,16 +40,20 @@ public class PushConfig implements Serializable {
private Integer status;
@TableField(fill = FieldFill.INSERT)
private Date createdAt;
private Date updatedAt;
@TableField(fill = FieldFill.INSERT)
private String recordUser;
@TableField(fill = FieldFill.UPDATE)
private String modifyUser;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date modifyTime;
/** 是否已删除0=未删除 1=已删除 */
@TableField("IS_DELETED")
private Integer isDeleted;
private String deleteUser;

View File

@ -12,10 +12,16 @@ public class PushConfigTargetRequest implements Serializable {
private String configId;
// /**
// * 0-用户1-角色
// */
// private Integer type;
/**
* 0-用户1-角色
* 1-所有人2-按人3-角色
*/
private Integer type;
private Integer targetType;
private List<String> userRoleIds;
}

View File

@ -0,0 +1,10 @@
package com.yfd.platform.system.domain;
import lombok.Data;
@Data
public class PushHistoryDetailStatistics {
private Integer pendingCount; // 待推送0
private Integer successCount; // 成功1
private Integer failCount; // 失败2
}

View File

@ -0,0 +1,15 @@
package com.yfd.platform.system.domain;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
@Data
public class PushTargetVo implements Serializable {
private Integer targetType;
private List<String> userRoleIds;
private List<PushConfigTargetVo> pushConfigTargetVoList;
}

View File

@ -0,0 +1,28 @@
package com.yfd.platform.system.domain;
import lombok.Data;
import java.io.Serializable;
@Data
public class SdWtbhVo implements Serializable{
private static final long serialVersionUID = 1L;
private String stcd;
private String stnm;
private String baseId;
private String baseName;
private String sttpCode;
private String sttpName;
private String rstcd;
private String warnState;
}

View File

@ -2,6 +2,20 @@ package com.yfd.platform.system.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.yfd.platform.system.domain.PushHistoryDetail;
import com.yfd.platform.system.domain.PushHistoryDetailStatistics;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
import java.util.Map;
@Mapper
public interface PushHistoryDetailMapper extends BaseMapper<PushHistoryDetail> {
@Select("SELECT " +
"COUNT(CASE WHEN STATUS = 0 THEN 1 END) AS pendingCount, " +
"COUNT(CASE WHEN STATUS = 1 THEN 1 END) AS successCount, " +
"COUNT(CASE WHEN STATUS = 2 THEN 1 END) AS failCount " +
"FROM PUSH_HISTORY_DETAIL " +
"WHERE HISTORY_ID = #{historyId}")
PushHistoryDetailStatistics countByHistoryId(@Param("historyId") String historyId);
}

View File

@ -2,11 +2,7 @@ package com.yfd.platform.system.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.yfd.platform.system.domain.PushConfig;
import com.yfd.platform.system.domain.PushConfigTargetRequest;
import com.yfd.platform.system.domain.PushConfigTargetVo;
import com.yfd.platform.system.domain.PushHistory;
import com.yfd.platform.system.domain.PushHistoryDetail;
import com.yfd.platform.system.domain.*;
import java.util.Date;
import java.util.List;
@ -23,7 +19,7 @@ public interface IPushConfigService extends IService<PushConfig> {
boolean updatePushConfig(PushConfig pushConfig);
boolean deletePushConfig(String ids);
boolean deletePushConfig(List<String> ids);
boolean setPushConfigStatus(String id, Integer status);
@ -31,7 +27,7 @@ public interface IPushConfigService extends IService<PushConfig> {
boolean savePushTargets(PushConfigTargetRequest request);
List<PushConfigTargetVo> getPushTargets(String configId);
PushTargetVo getPushTargets(String configId);
Page<PushHistory> getPushHistoryPage(Page<PushHistory> page,
String configId,

View File

@ -4,4 +4,5 @@ import com.baomidou.mybatisplus.extension.service.IService;
import com.yfd.platform.system.domain.PushHistoryDetail;
public interface IPushHistoryDetailService extends IService<PushHistoryDetail> {
Object getPushHistoryDetailById(String historyId);
}

View File

@ -65,4 +65,6 @@ public interface ISysRoleService extends IService<SysRole> {
boolean setMenuById(String id, String menuIds);
List<SysRole> selectRoleList(String rolename, String tenantId);
List<SysRole> queryRolesList(String roleName, String tenantId);
}

View File

@ -183,4 +183,5 @@ public interface IUserService extends IService<SysUser> {
************************************/
Page<SysUser> queryPendingAuditUsers(Page<SysUser> page, String name, String regStatus, String tenantId);
List<SysUser> queryUsersList(String name, String tenantId);
}

View File

@ -2,10 +2,23 @@ package com.yfd.platform.system.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.yfd.platform.system.domain.PushHistoryDetail;
import com.yfd.platform.system.domain.PushHistoryDetailStatistics;
import com.yfd.platform.system.mapper.PushHistoryDetailMapper;
import com.yfd.platform.system.service.IPushHistoryDetailService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
@Service
public class PushHistoryDetailServiceImpl extends ServiceImpl<PushHistoryDetailMapper, PushHistoryDetail> implements IPushHistoryDetailService {
@Autowired
private PushHistoryDetailMapper pushHistoryDetailMapper;
@Override
public Object getPushHistoryDetailById(String historyId) {
return pushHistoryDetailMapper.countByHistoryId(historyId);
}
}

View File

@ -1,6 +1,8 @@
package com.yfd.platform.system.service.impl;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.yfd.platform.config.ResponseResult;
@ -172,4 +174,15 @@ public class SysRoleServiceImpl extends ServiceImpl<SysRoleMapper, SysRole> impl
return roleMapper.selectRoleList(rolename, tenantId);
}
@Override
public List<SysRole> queryRolesList(String roleName, String tenantId) {
LambdaQueryWrapper<SysRole> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(StrUtil.isNotBlank(tenantId), SysRole::getTenantId, tenantId);
queryWrapper.like(StrUtil.isNotBlank(roleName),SysRole::getRolename, roleName);
queryWrapper.ne(SysRole::getLevel, 1);
queryWrapper.orderByDesc(SysRole::getLevel);
List<SysRole> sysRoleList = this.list( queryWrapper);
return sysRoleList;
}
}

View File

@ -904,4 +904,20 @@ public class UserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impleme
return true;
}
}
@Override
public List<SysUser> queryUsersList(String name, String tenantId) {
LambdaQueryWrapper<SysUser> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(StrUtil.isNotBlank(tenantId), SysUser::getTenantId, tenantId);
queryWrapper.eq(SysUser::getStatus, 1);
queryWrapper.and(StrUtil.isNotBlank(name), wrapper ->
wrapper.like(SysUser::getNickname, name)
.or()
.like(SysUser::getRealName, name)
);
queryWrapper.orderByDesc(SysUser::getRegTime);
List<SysUser> sysUserList = this.list( queryWrapper);
return sysUserList;
}
}

View File

@ -0,0 +1,28 @@
package com.yfd.platform.utils;
import cn.hutool.cron.pattern.CronPattern;
import cn.hutool.cron.pattern.CronPatternUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.DateField;
import java.util.Date;
/**
* Cron 表达式工具类基于 Hutool 6.x
* 支持 6 位或 7 位表达式 星期 []
*/
public class CronUtil {
/**
* 校验 Cron 表达式是否合法
* @param expression Cron 表达式支持 6 7 个字段
* @return true-合法false-非法
*/
public static boolean isValid(String expression) {
try {
CronPattern.of(expression);
return true;
} catch (Exception e) {
return false;
}
}
}

View File

@ -7,14 +7,14 @@ VITE_APP_TITLE = '水电水利建设项目全过程环境管理信息平台'
VITE_APP_PORT = 3000
VITE_APP_BASE_API = '/dev-api'
# 本地环境
# VITE_APP_BASE_URL = 'http://10.84.121.127:8093'
VITE_APP_BASE_URL = 'http://localhost:8093'
# 测试环境
# VITE_APP_BASE_URL = 'http://172.16.21.142:8093'
# VITE_APP_BASE_URL = 'http://172.16.21.142:8096'
# 汤伟
# VITE_APP_BASE_URL = 'http://10.84.111.235:8093'
VITE_APP_BASE_URL = 'http://10.84.111.235:8093'
# 李林
VITE_APP_BASE_URL = 'http://10.84.111.25:8093'
# VITE_APP_BASE_URL = 'http://10.84.111.25:8093'
# 测试环境线上10.84.121.122:
VITE_APP_TEST_ONLINE_URL = 'https://211.99.26.225:12122'

View File

@ -132,7 +132,7 @@
## 5. 分步骤实施计划
### Step 1修正 3D 初始化并默认定位中国
### Step 1修正 3D 初始化并默认定位中国 ✅️
目标:
@ -157,7 +157,7 @@
- 首屏有加载过渡。
- 进入后稳定落在中国。
### Step 2展示 3D 基础底图
### Step 2展示 3D 基础底图 ✅️
目标:
@ -171,6 +171,10 @@
- 实现 `addBaseDataLayer(layer, isShow)`
- 对 `customBaseLayer` 使用 `url_3d`
- 对 `BASEMAP-img`、`BASEMAP-white` 预留兼容能力
- **必须处理 `wmts` 类型图层**2D 通过 `WMTS` source 加载3D 使用 `Cesium.WebMapTileServiceImageryProvider`,从 URL 参数中提取 LAYER/TILEMATRIXSET
- **必须提供 `tileMatrixLabels`**Cesium 默认用纯数字作为 tileMatrix 参数值,但本系统 WMTS 服务器要求 `TileMatrixSet:zoom` 格式(如 `EPSG:3857_hbb_zrbhq_l13:4`)。需构建 `["TileMatrixSet:0", "TileMatrixSet:1", ...]` 传入 `tileMatrixLabels`
- 其他类型:`raster-dem`→XYZ、MapServer→ArcGIS、其余 →WMS
- **注意**:还存在一个 `key = "powerBaseStation"` 的基地底图图层,该图层**初始化时不加载**(已在 `IGNORED_BASE_LAYER_KEYS` 中忽略),但后续可通过图层树手动勾选显示。
- 实现 `controlBaseLayerTreeShowAndHidden()`
- 让图层树勾选能控制 3D 底图显隐
- 根据你的要求UI 层先不开放 `BaseLayerSwitcher` 的 3D 切换行为,只保留默认基础底图加载。
@ -180,7 +184,7 @@
- 切到 3D 后可稳定看到基础底图。
- 图层初始化时 `addBaseDataLayer(this.baseLayerConfig, checked, true)` 的思路在 3D 可落地。
### Step 3打通 3D 锚点展示
### Step 3打通 3D 锚点展示 ✅️
目标:
@ -189,13 +193,14 @@
计划:
- 在 `MapCesium` 内建立点位图层注册结构,建议`layerKey -> entity[]` 或 `CustomDataSource` 管理。
- 在 `MapCesium` 内建立点位图层注册结构,按 `layerKey -> entity[]` 管理。
- 实现 `addInitDataLayer(pointData, layerType)`
- 复用当前点位数据结构
- 解析 `lgtd/lttd/iconCode/titleName/stnm/anchoPointState/popupHtml`
- 复用现有图标资源路径
- Entity ID 使用 `${layerKey}:${item._id || `${iconType || 'point'}_${stcd}`}`,与 2D 对齐,确保同 stcd 不同 sttpMap 的点不被覆盖
- 确保 `eng_point` 仍按现有 `mapStore` 过滤后的数据展示,中型站点只在 `7.5` 以上层级出现
- 为后续缩放驱动的增量显示预留运行态字段,不把所有点简单一次性硬渲染
- 为后续缩放驱动的增量显示预留运行态字段`_legendVisible`、`_regionVisible`、`_densityVisible`,不把所有点简单一次性硬渲染
- 实现 `mdLayerTreeShowOrHidden(layerType, checked)`
- 图层树控制整个点图层显隐
- 实现 `removePointLayer()` / `hasLayer()`
@ -208,7 +213,34 @@
- 取消勾选图层后,对应锚点消失。
- `eng_point` 在低于 `7.5` 时不显示中型水电站,高于 `7.5` 时正常显示。
### Step 4打通 3D popup 与 hover
### Step 4打通 3D 基地裁切jdPanelControlShowAndHidden ✅️
目标:
- 3D 点击基地面板后,与 2D 行为一致。
- 加载基地边界 GeoJSON只显示区域内锚点隐藏区域外锚点。
- 取消基地时恢复显示所有锚点。
计划:
- 在 `MapCesium` 中捕获 `hydropBase` 配置,与 2D 一样在 `addBaseDataLayer` 中记录。
- 实现 `jdPanelControlShowAndHidden(baseid, isAll)`
- `isAll=false`:清除 3D 球面遮罩实体、恢复所有锚点区域可见性、清除裁切状态。
- `isAll=true`:先隐藏所有锚点,异步加载基地边界 GeoJSON执行点在面判断后仅显示区域内锚点同时在球面绘制裁切边界多边形`applyRegionMask`),最后飞行到区域视野。
- 在 `createEntityPropertiesBag` 中新增 `_regionVisible` 属性,初始为 `true`
- 在 `applyPointVisibilityRefresh` 中跳过 `_regionVisible !== false` 的实体,确保裁切后碰撞检测不会把区域外锚点重新显示出来。
- 复用 `extractPolygonCoords` / `isPointInRing` 射线法判断逻辑(与 `region-mask-manager.ts` 一致)。
- 支持 `AbortController` 取消前一次未完成的请求,避免响应顺序错乱。
- `applyRegionMask` 将 GeoJSON 多边形转换为 Cesium `PolygonHierarchy`,在球面绘制半透明填充 + 可见边界的区域多边形,模拟 2D 底图 Canvas 裁切遮罩的视觉效果。
验收:
- 点击基地后3D 只显示该基地区域内的锚点,区域外锚点隐藏。
- 取消基地后,所有锚点恢复正常显示。
- 快速切换基地不会出现旧数据覆盖新数据的问题。
- 多次切换 2D/3D 后基地裁切行为正常。
### Step 5打通 3D popup 与 hover ✅️
目标:
@ -233,7 +265,7 @@
- 样式和 2D 保持一致。
- 移出后 popup 正常消失。
### Step 5:补齐缩放驱动显示与碰撞检测
### Step 6:补齐缩放驱动显示与碰撞检测
目标:
@ -263,7 +295,7 @@
- 放大到具体层级后会看到更多锚点逐步出现。
- 标签和锚点不会大面积重叠到不可读。
### Step 6打通 3D 锚点点击详情
### Step 7打通 3D 锚点点击详情 ✅️
目标:
@ -287,7 +319,7 @@
- `ylfb` 和普通锚点分支行为一致。
- 点击空白区域不会误触发弹框。
### Step 7打通图例联动
### Step 8打通图例联动 ✅️
目标:
@ -305,7 +337,7 @@
- 图例单项勾选/取消时3D 对应类别锚点同步显隐。
- 图例分组勾选/取消时3D 行为与 2D 保持一致。
### Step 8补足收口与稳定性
### Step 9补足收口与稳定性 ✅️
目标:
@ -329,7 +361,7 @@
1. 默认定位到中国 + 启动动画
2. 展示基础底图
3. 展示锚点
3. 展示锚点 3.5. 补基地裁切jdPanelControlShowAndHidden
4. 补 hover popup
5. 补缩放驱动显示与碰撞检测
6. 补锚点点击详情
@ -337,64 +369,6 @@
8. 补图例联动
9. 做切换稳定性收口
## 9. 必须补齐的隐藏逻辑清单
下面这些逻辑不是表面接口能直接看出来的,但如果 `3D` 不补,最终效果就会和 `2D` 明显不一致。
### 9.1 图层与菜单切换
- 菜单切换时,只隐藏旧页点图层,不隐藏底图。
- 图层树勾选前要先经过互斥规则归一化:
- `rare_fish_point``fish_along_point` 互斥
- `stinfo/stinfo_video_point``stinfo_ai_video_point` 互斥
- `facilities``facilities_built` 两个分支互斥
- 点图层取消勾选时只隐藏,不删除缓存;重新勾选时优先从缓存恢复。
### 9.2 图例与点内显隐
- 图例控制的是图层内部锚点是否可见,不是图层本身是否可见。
- 图层恢复显示时,图例要先整层全关,再按当前 checked 状态逐项恢复。
- 环保设施图例组当前是只展示不交互。
### 9.3 缩放驱动显示
- `eng_point` 的中型水电站只在 `ENG_POINT_MEDIUM_VISIBLE_ZOOM = 7.5` 以上显示。
- 水电开发页缩放跨过 `12` 级时,会自动补开一批动态图层;缩回去时再自动收口。
- `2D` 存在“缩放越大显示越多”的行为,不是所有锚点在所有层级都等量显示。
- `2D` 还存在 hover、批量 popup、密度控制等缩放阈值
- hover 低于一定缩放不触发
- 高缩放会进入批量 popup 模式
- 点位会随着缩放提升逐步放开显示限制
### 9.4 锚点碰撞与控量
- `2D` 中锚点是否可见,不只受图层开关控制,还受以下门闩共同影响:
- 图例可见性
- 区域裁切可见性
- 密度阈值
- 图标碰撞结果
- 标签碰撞结果
- 低缩放下,为避免堆叠,部分锚点或标签会被主动隐藏。
- 高缩放下,会逐步放开图标/标签限制,显示更多内容。
- 因此 `3D` 不能简单把所有 entity 永久全开,否则视觉密度会明显失控。
### 9.5 Popup 与点击
- hover popup 不是全时开启的,有缩放门槛。
- 点击和 hover 的命中对象是图标本体,不是任意文字区域。
- `ylfb` 与普通锚点的点击详情弹框走不同业务分支。
### 9.6 基地与区域裁切
- 基地切换不仅影响搜索候选,还会影响地图上的点位可见范围。
- 区域裁切会直接把区域外点位隐藏,不是只改底图遮罩。
### 9.7 筛选器特殊逻辑
- 装机容量筛选本质是批量切 `eng_point` 图例状态,不是简单切图层。
- 鱼类分布模式会接管 `ylfb_point` 的数据、缓存、图例与显隐。
- 时间筛选当前只重载指定时间相关图层,不是全量重载。
## 7. 预计修改文件
- `src/components/gis/map.cesium.ts`
@ -415,3 +389,91 @@
- 默认飞到中国
这一步最小、最容易验证,也能先把“打开 3D 看起来不对”的核心问题解决掉。
## 9. 必须补齐的隐藏逻辑清单
下面这些逻辑不是表面接口能直接看出来的,但如果 `3D` 不补,最终效果就会和 `2D` 明显不一致。
### 9.1 图层与菜单切换 ✅️
- ✅ 菜单切换时,只隐藏旧页点图层,不隐藏底图。(编排器 `handlePageChange` 控制Cesium 被动接收数据)
- ✅ 图层树勾选前要先经过互斥规则归一化store 层 `updateLayerData` 处理Cesium 不感知)
- `rare_fish_point``fish_along_point` 互斥
- `stinfo/stinfo_video_point``stinfo_ai_video_point` 互斥
- `facilities``facilities_built` 两个分支互斥
- ✅ 点图层取消勾选时只隐藏,不删除缓存;重新勾选时优先从缓存恢复。`mdLayerTreeShowOrHidden` 设置 `_layerVisible` 标志,不删除 `pointLayerRegistry` 条目。
- ✅ **图层树勾选时按 `type` 字段走不同分支**(见 `updateLayerData`store 层处理):
- `type === 'pointMap'` → 走 `handlePointMapLayer` → 调用 `mdLayerTreeShowOrHidden`已实现L1624
- `type === 'GISMap'` → 走 `controlBaseLayerTreeShowAndHidden`已实现L1345
- 3D Cesium 已同时实现这两个方法。
### 9.2 图例与点内显隐 ✅️
- ✅ 图例控制的是图层内部锚点是否可见,不是图层本身是否可见。`setLegendPointVisible` 使用 `_legendVisible` 标志,与 `_layerVisible` 独立。
- ✅ 图层恢复显示时,图例要先整层全关,再按当前 checked 状态逐项恢复。支持 `anchoPointState=''` 全关 → 逐项 `(state, true)` 恢复两步模式L1764-1787
- ✅ 环保设施图例组当前是只展示不交互。编排器层面控制Cesium 不感知)
### 9.3 缩放驱动显示 ✅️
- ✅ `eng_point` 的中型水电站只在 `ENG_POINT_MEDIUM_VISIBLE_ZOOM = 7.5` 以上显示。store `filterPointLayerDataForDisplay` 在数据进入 Cesium 前过滤)
- ✅ 水电开发页缩放跨过 `12` 级时,会自动补开一批动态图层;缩回去时再自动收口。(编排器层面处理)
- ✅ `2D` 存在"缩放越大显示越多"的行为。碰撞检测中密度门控逐级放开。
- ✅ `2D` 还存在 hover、批量 popup、密度控制等缩放阈值
- hover 低于 ~zoom 4.5 不触发(`HOVER_POPUP_MAX_HEIGHT = 9_000_000`
- 高缩放进入批量 popup 模式(`BATCH_POPUP_MODE_ZOOM = 14.9`
- 点位随着缩放提升逐步放开密度限制(密度门控 + `DENSITY_ISOLATION_THRESHOLD`
- ✅ **图标动态缩放**`CallbackProperty` 实现 `billboard.scale`,公式 `clamp(0.7 + (zoom - 4.5) * 0.035, 0.4, 1.2)`L1522
- ✅ **字体动态大小**`CallbackProperty` 实现 `label.font`,钳位 `[10, 20]`L1535
- ✅ **文字在图标上方**`CallbackProperty` 实现 `label.pixelOffset`,负 Y 偏移L1549
- ✅ **Cesium 中必须使用 `CallbackProperty` 实现动态缩放**:已使用 3 个 `CallbackProperty`scale/font/offset`isConstant=false` 每帧重算。
### 9.4 锚点碰撞与控量 ✅️
- ✅ `2D` 中锚点是否可见,不只受图层开关控制,还受以下门闩共同影响:
- 图例可见性 `_legendVisible`
- 区域裁切可见性 `_regionVisible`
- 密度阈值(碰撞循环中 inline 计算,逐级放开)
- 图标碰撞结果 `_iconCollisionVisible`
- 标签碰撞结果 `_labelCollisionVisible`
- ✅ 低缩放下,为避免堆叠,部分锚点或标签会被主动隐藏。(`refreshPointLabelVisibility` 碰撞检测L2117-2455
- ✅ 高缩放下,会逐步放开图标/标签限制,显示更多内容。(密度门控 + 碰撞容差随 zoom 调整)
- ✅ 因此 `3D` 不能简单把所有 entity 永久全开。所有 entity 初始 `_iconCollisionVisible = _labelCollisionVisible = true`,碰撞检测运行后动态调整。
### 9.5 Popup 与点击 ✅️
- ✅ hover popup 不是全时开启的,有缩放门槛。相机高度 > `HOVER_POPUP_MAX_HEIGHT` 时不触发 hover。
- ✅ 点击和 hover 的命中对象是图标本体,不是任意文字区域。`ScreenSpaceEventHandler` 基于 `scene.pick` 命中 entity。
- ✅ `ylfb` 与普通锚点的点击详情弹框走不同业务分支。`sttpMap === 'ylfb'` 打开 `ylfbModalVisible`,其他走 `modalVisible`L366-375
### 9.6 基地与区域裁切 ✅️
- ✅ 基地切换不仅影响搜索候选,还会影响地图上的点位可见范围。`jdPanelControlShowAndHidden` 实现L1684-1748
- ✅ 区域裁切会直接把区域外点位隐藏,不是只改底图遮罩。`filterPointsByRegion` 设置 `_regionVisible = false`L1824-1848遮罩多边形 + 裁剪双重处理。
### 9.7 筛选器特殊逻辑 ✅️
- ✅ 装机容量筛选本质是批量切 `eng_point` 图例状态不是简单切图层。store 层处理Cesium 通过 `setLegendPointVisible` 响应)
- ✅ 鱼类分布模式会接管 `ylfb_point` 的数据、缓存、图例与显隐。store 层处理)
- ✅ 时间筛选当前只重载指定时间相关图层不是全量重载。store 层处理)
### 9.8 数据进入 addInitDataLayer 前的预处理 ✅️
- ✅ 所有点位数据在进入 `addInitDataLayer` 前会经过 `normalizePointLayerItems()` 过滤store 层 `map.ts` 执行Cesium 接收时已过滤)
- 没有 `iconCode``code !== 'colorLayer'` 的项会被抛弃
- `baseId == 'all'``anchoPointState` 结尾为 `_nbuilt` 的项会被抛弃
- ✅ `_id` 默认值生成规则store 层生成 `${sttpMap || iconType || 'point'}_${stcd}`
- ✅ **stcd+经纬度重复锚点处理**Cesium entity ID 使用 `${layerKey}:${item._id}`,与 2D 对齐,同 stcd 不同 sttpMap 的点不会互相覆盖。
### 9.9 图标渐进加载机制
- ❌ 2D 中图标不是一次性全加载完。有 `loading → loaded → error` 三种状态跟踪。
- ❌ 只有 `loaded` 状态的图标才参与样式渲染loader 失败的不渲染。
- ❌ 如果在图标加载过程中就触发了样式刷新,需要通过 `requestAnimationFrame` 回刷。
- ❌ 3D Cesium 中当前所有 entity 同步创建billboard `image` URL 直接赋值无渐进加载。Cesium 内部纹理加载无显式状态跟踪。
### 9.10 密度分档与近邻点元数据 ✅️
- ✅ 每个锚点的 `distance` 字段表示密度分档值(不是物理距离),值越大表示越稀疏。(已使用 `getEntityDensityValue``_rawData.distance` 读取L2460
- ✅ 不同 density 值对应不同的 `minZoom` 显示门槛。`getEntityDensityMinZoom` 映射 `nearby-point-rules.ts``densityDisplayRules`L2491
- ✅ 全量数据加载后store 会调用 `attachNearbyPointRuntimeMeta()` 给所有点打上 `_nearbyRule`、`_nearbyGroupId`、`_nearbyPriority` 等元数据。store 层处理Cesium 碰撞检测中读取)
- ✅ **孤立点例外规则**:如果一个点在当前视口内 ~56px 范围内没有同 layerKey 的其他可见点,它可以绕过密度门槛直接显示。`DENSITY_ISOLATION_THRESHOLD = 56` + `shouldBypassDensityGateForIsolatedEntity`L2506-2556

View File

@ -79,6 +79,16 @@ export function vmsstbprptGetKendoList(data: any) {
data: data
});
}
// 环保自动监测工作开展情弹窗
export function overviewvmsstbprptGetKendoList(data: any) {
return request({
url: '/overview/vmsstbprpt/GetKendoList',
method: 'post',
data: data
});
}
// 获取基本信息
export function baseMsstbprptGetKendoList(data: any) {

View File

@ -41,6 +41,14 @@ export function getEngInfoDropdown(data: any) {
data
});
}
// 根据河段获取电站
export function getByRvcd(data: any) {
return request({
url: '/env/engInfo/getByRvcd',
method: 'get',
params:data
});
}
// 根据id获取电站信息
export function getEngInfoById(params: any) {
return request({

View File

@ -3,7 +3,7 @@ import request from '@/utils/request';
// 获取所有倾斜摄影
export function getAllTiltPhotoTree(data: any) {
return request({
url: '/api/wmp-sys-server/sys/oblique/GetKendoListCust',
url: '/sys/oblique/GetKendoListCust',
method: 'post',
data: data
});
@ -11,7 +11,7 @@ export function getAllTiltPhotoTree(data: any) {
// 保存倾斜摄影
export function saveTiltPhoto(data: any) {
return request({
url: '/api/wmp-sys-server/sys/oblique/addOrUpdate',
url: '/sys/oblique/addOrUpdate',
method: 'post',
data: data
});
@ -19,7 +19,7 @@ export function saveTiltPhoto(data: any) {
// 删除倾斜摄影
export function deleteTiltPhoto(data: any) {
return request({
url: '/api/wmp-sys-server/sys/oblique/delete',
url: '/sys/oblique/delete',
method: 'get',
params: data
});

View File

@ -70,7 +70,7 @@ export function setPushConfigSmsSwitch(data: any) {
return request({
url: '/system/pushConfig/setPushConfigSmsSwitch',
method: 'post',
data
params:data
});
}
//获取推送历史列表

View File

@ -468,7 +468,7 @@ const triggerManualValuesChange = (changedKey: string, newValue: any) => {
emit('valuesChange', changedValues, { ...formData });
};
const dataDimensionDataChange = (value: any, fieldName: string = 'baseId') => {
const dataDimensionDataChange = (value: any, fieldName = 'baseId') => {
formData[fieldName] = value;
formData.rstcd = "";
shuJuTianBaoStore.getEngOption(value,'baseId');

View File

@ -192,6 +192,7 @@ const getList = async (filter?: Record<string, any>) => {
total.value = 0;
if (filter !== undefined) {
lastFilter.value = filter;
page.value = 1;
}
try {
// 使使 searchParams

View File

@ -5,7 +5,7 @@
<div class="card_left">
<div class="card_img">
<div class="carousel-wrapper">
<div class="carousel-inner">
<div class="carousel-inner" ref="carouselInnerRef">
<div class="carousel-track" :style="trackStyle">
<div v-for="(item, index) in originalMediaData" :key="index" class="carousel-item">
<a-image :src="item.url" />
@ -43,7 +43,7 @@
</template>
<script lang="ts" setup>
import { ref, computed, onMounted, watch } from 'vue';
import { ref, computed, onMounted, onUnmounted, watch } from 'vue';
import { LeftOutlined, RightOutlined } from '@ant-design/icons-vue';
defineOptions({
@ -67,6 +67,15 @@ interface MediaItem {
const originalMediaData = ref<MediaItem[]>([]);
const currentIndex = ref(0);
const carouselInnerRef = ref<HTMLElement | null>(null);
const itemWidth = ref(0);
let resizeObserver: ResizeObserver | null = null;
const updateItemWidth = () => {
if (carouselInnerRef.value) {
itemWidth.value = carouselInnerRef.value.clientWidth;
}
};
//
const initOriginalData = () => {
@ -89,10 +98,9 @@ const setCurrentIndexFromProps = () => {
}
};
// - React
const trackStyle = computed(() => {
return {
transform: `translateX(calc((68.8px - 48vw) * ${currentIndex.value}))`
transform: `translateX(-${itemWidth.value * currentIndex.value}px)`
};
});
@ -122,12 +130,23 @@ const currentDescription = computed(() => {
const currentTitle = computed(() => {
return originalMediaData.value[currentIndex.value]?.title || '';
});
//
onMounted(() => {
console.log('ArtsDetail mounted - props.index:', props.index, 'props.dataSource.length:', props.dataSource?.length);
initOriginalData();
setCurrentIndexFromProps();
console.log('After setCurrentIndexFromProps - currentIndex.value:', currentIndex.value);
updateItemWidth();
if (carouselInnerRef.value) {
resizeObserver = new ResizeObserver(() => {
updateItemWidth();
});
resizeObserver.observe(carouselInnerRef.value);
}
});
onUnmounted(() => {
if (resizeObserver) {
resizeObserver.disconnect();
resizeObserver = null;
}
});
// dataSource index
@ -196,10 +215,9 @@ watch(
transition: transform 0.3s ease-in-out;
.carousel-item {
width: calc(48vw - 80.8px);
flex: 0 0 calc(48vw - 80.8px);
width: 100%;
flex: 0 0 100%;
height: 100%;
margin-right: 12px;
display: flex;
justify-content: center;
align-items: center;

View File

@ -32,8 +32,10 @@ import { useMapOrchestrator } from '@/modules/map/application/map-orchestrator';
import { MapClass } from './map.class';
import { getQgcRvcd } from '@/api/map';
import { servers } from './mapurlManage';
import { useUiStore } from '@/store/modules/ui';
const mapOrchestrator = useMapOrchestrator();
const uiStore = useUiStore();
const route = useRoute();
const mapClass = MapClass.getInstance();
@ -82,7 +84,11 @@ const init = async () => {
const handleMapController = async (e: any, mapType: any) => {
switch (e) {
case 'dim':
await mapClass.switchView(mapType);
await mapOrchestrator.switchMapType(mapType, {
popupContainer: popupRef.value,
getIsHydroMenu: () => isShuiDianKaiFaMenu.value
});
uiStore.markMapSwitchCompleted();
break;
case 4: //
tlyLayerVisible.value = !tlyLayerVisible.value;

View File

@ -42,9 +42,6 @@ export const legendData2Obj = (data: any[]) => {
if (item?.childrenList && item.childrenList?.length > 0) {
f(item.childrenList);
} else {
if (item.nameEn.includes('_测试')) {
item.nameEn = item.nameEn.split('_测试')[0];
}
_tempData[item.nameEn] = item;
}
});

File diff suppressed because it is too large Load Diff

View File

@ -272,4 +272,8 @@ export class MapClass implements MapClassInterface {
flyTopanto(position: number[], zoom: number): void {
this.service.flyTopanto(position, zoom);
}
getCurrentZoom(): number | undefined {
return this.service.getCurrentZoom();
}
}

View File

@ -124,6 +124,12 @@ export interface MapInterface {
fitBounds(bbox, bearing): void;
flyTopanto(positon, zoom): void;
/**
*
* 2D zoom3D
*/
getCurrentZoom(): number | undefined;
// /**
// * 加载倾斜摄影数据
// * @param HH3DUrlArray

View File

@ -39,6 +39,7 @@ import { PointLayerManager } from './ol/point-layer-manager';
import { PopupManager } from './ol/popup-manager';
import { RegionMaskManager } from './ol/region-mask-manager';
import { getNearbyPointDensityDisplayRules } from '@/modules/map/domain/nearby-point-rules';
import router from '@/router';
const modelStore = useModelStore();
const VITE_APP_MAP_URL = import.meta.env.VITE_APP_MAP_URL;
@ -49,7 +50,7 @@ const MIN_ZOOM = 4.23;
const MAX_ZOOM = 22;
const INITIAL_ZOOM = 4.5;
const BATCH_POPUP_MODE_ZOOM = 15;
const FULL_DISPLAY_NO_COLLISION_ZOOM = 15;
const FULL_DISPLAY_NO_COLLISION_ZOOM = Number.POSITIVE_INFINITY;
// 定义边界 [minX, minY, maxX, maxY] (Web Mercator 坐标)
const BOUNDS_SW = [26.5, -9.99999999999929];
@ -80,6 +81,8 @@ export class MapOl implements MapInterface {
private regionMaskManager: RegionMaskManager;
private isBatchPopupMode = false;
private batchPopupRefreshFrameId: number | null = null;
private batchPopupPendingRebuild = false;
private batchPopupPostRenderListenerKey: any = null;
private pointLayerStyleRefreshFrameId: number | null = null;
private labelVisibilityRefreshFrameId: number | null = null;
private labelVisibilityDebounceTimerId: number | null = null;
@ -167,6 +170,13 @@ export class MapOl implements MapInterface {
this.handleZoomChange();
});
this.map.on('click', evt => {
if (
router.currentRoute.value.path.includes(
'shengTaiLiuLiangManZuQingKuangJiangJu'
)
) {
return;
}
this.popupManager.showPopup(undefined, undefined);
this.popupManager.handleMapClick(evt.pixel, detectedFeature => {
if (detectedFeature.values_.sttpMap == 'ylfb') {
@ -200,13 +210,14 @@ export class MapOl implements MapInterface {
this.map.on('pointerdrag', () => {
if (this.isBatchPopupMode) {
this.requestUpdateBatchPopups();
this.ensureBatchPopupPostRenderSync();
}
});
this.map.on('moveend', () => {
this.clearBatchPopupPostRenderSync();
this.requestRefreshPointLabelVisibility(false, 60);
if (this.isBatchPopupMode) {
this.requestUpdateBatchPopups();
this.requestUpdateBatchPopups(false, true);
}
});
@ -291,8 +302,9 @@ export class MapOl implements MapInterface {
*/
private enableBatchPopupMode() {
this.isBatchPopupMode = true;
this.clearBatchPopupPostRenderSync();
this.popupManager.clearBatchPopups();
this.requestUpdateBatchPopups(true);
this.requestUpdateBatchPopups(true, true);
}
/**
@ -300,6 +312,8 @@ export class MapOl implements MapInterface {
*/
private disableBatchPopupMode() {
this.isBatchPopupMode = false;
this.batchPopupPendingRebuild = false;
this.clearBatchPopupPostRenderSync();
if (this.batchPopupRefreshFrameId !== null) {
cancelAnimationFrame(this.batchPopupRefreshFrameId);
this.batchPopupRefreshFrameId = null;
@ -314,10 +328,39 @@ export class MapOl implements MapInterface {
private updateBatchPopups() {
const features = this.pointLayerManager.getFeaturesInViewport();
// 传入样式检查器,过滤掉 declutter/距离过滤等隐藏的锚点
this.popupManager.showPopupsForFeatures(features, feature => {
this.popupManager.showPopupsForFeatures(
features,
feature => {
const style = this.createPointStyle(feature);
return style !== null;
},
{
rebuild: this.batchPopupPendingRebuild
}
);
}
private ensureBatchPopupPostRenderSync() {
if (!this.map || this.batchPopupPostRenderListenerKey) {
return;
}
this.batchPopupPostRenderListenerKey = this.map.on('postrender', () => {
if (!this.isBatchPopupMode) {
return;
}
this.popupManager.updateBatchPopupPositions(feature => {
const style = this.createPointStyle(feature);
return style !== null;
});
});
}
private clearBatchPopupPostRenderSync() {
if (this.batchPopupPostRenderListenerKey) {
unByKey(this.batchPopupPostRenderListenerKey);
this.batchPopupPostRenderListenerKey = null;
}
}
/**
* ( Leaflet DivIcon )
@ -346,10 +389,11 @@ export class MapOl implements MapInterface {
legendVisible !== false &&
regionVisible !== false &&
densityVisible &&
this.shouldRenderNearbyFeature(feature, currentZoom);
this.shouldRenderNearbyFeature();
const dynamicScale = this.getDynamicPointScale(currentZoom);
const fontSize = this.getPointFontSize(dynamicScale);
const engPriority = this.getFeatureEngRenderPriority(feature);
const formattedLabelText = this.formatPointLabelText(labelText);
const labelLineCount = formattedLabelText
? formattedLabelText.split('\n').length
@ -363,7 +407,10 @@ export class MapOl implements MapInterface {
feature.get('_labelCollisionVisible') === true;
const finalIconVisible = iconTargetVisible && iconCollisionVisible;
const labelTargetVisible =
finalIconVisible && !!formattedLabelText && labelCollisionVisible;
!this.isBatchPopupMode &&
finalIconVisible &&
!!formattedLabelText &&
labelCollisionVisible;
if (!finalIconVisible) {
return null;
@ -373,6 +420,7 @@ export class MapOl implements MapInterface {
iconUrl,
dynamicScale.toFixed(2),
fontSize,
engPriority,
labelOffsetY,
labelTargetVisible ? formattedLabelText : '',
labelTargetVisible ? 1 : 0
@ -384,6 +432,7 @@ export class MapOl implements MapInterface {
const styles: Style[] = [
new Style({
zIndex: engPriority,
image: new Icon({
src: iconUrl,
scale: dynamicScale,
@ -397,7 +446,7 @@ export class MapOl implements MapInterface {
if (formattedLabelText && labelTargetVisible) {
styles.push(
new Style({
zIndex: 101,
zIndex: engPriority + 1,
text: new Text({
text: formattedLabelText,
offsetY: labelOffsetY,
@ -598,6 +647,7 @@ export class MapOl implements MapInterface {
top: number;
bottom: number;
priority: number;
engPriority: number;
densityPriority: number;
id: string;
pixelX: number;
@ -616,6 +666,7 @@ export class MapOl implements MapInterface {
top: number;
bottom: number;
priority: number;
engPriority: number;
densityPriority: number;
id: string;
pixelX: number;
@ -663,7 +714,7 @@ export class MapOl implements MapInterface {
legendVisible: feature.get('_legendVisible') !== false,
regionVisible: feature.get('_regionVisible') !== false,
densityPriority: this.getFeatureDensityPriority(feature),
nearbyVisible: this.shouldRenderNearbyFeature(feature, currentZoom),
nearbyVisible: this.shouldRenderNearbyFeature(),
labelText: this.formatPointLabelText(
feature.get('_labelText') as string
),
@ -808,7 +859,7 @@ export class MapOl implements MapInterface {
const dynamicScale = this.getDynamicPointScale(currentZoom);
// 备注:图标碰撞盒适当小于视觉图标尺寸,允许相邻站点轻微贴近显示,
// 避免像“小浪底/三门峡”这类近点被过早裁掉成只剩一个点。
const iconCollisionSize = Math.max(12, 16 * dynamicScale);
const iconCollisionSize = Math.max(14, 24 * dynamicScale);
const iconPadding = 0;
const iconLeft = entry.pixelX - iconCollisionSize / 2 - iconPadding;
const iconRight = entry.pixelX + iconCollisionSize / 2 + iconPadding;
@ -851,6 +902,7 @@ export class MapOl implements MapInterface {
top: centerY - estimatedHeight / 2,
bottom: centerY + estimatedHeight / 2,
priority: Number(entry.feature.get('_nearbyPriority') || 9999),
engPriority: this.getFeatureEngRenderPriority(entry.feature),
densityPriority: entry.densityPriority,
id: candidateId,
pixelX: entry.pixelX,
@ -863,6 +915,9 @@ export class MapOl implements MapInterface {
});
candidates.sort((left, right) => {
if (left.engPriority !== right.engPriority) {
return right.engPriority - left.engPriority;
}
if (left.priority !== right.priority) {
return left.priority - right.priority;
}
@ -951,6 +1006,9 @@ export class MapOl implements MapInterface {
});
const labelCandidates = [...candidates].sort((left, right) => {
if (left.engPriority !== right.engPriority) {
return right.engPriority - left.engPriority;
}
if (left.priority !== right.priority) {
return left.priority - right.priority;
}
@ -1149,10 +1207,11 @@ export class MapOl implements MapInterface {
}
}
private requestUpdateBatchPopups(immediate = false) {
private requestUpdateBatchPopups(immediate = false, rebuild = true) {
if (!this.isBatchPopupMode) {
return;
}
this.batchPopupPendingRebuild = this.batchPopupPendingRebuild || rebuild;
if (immediate) {
if (this.batchPopupRefreshFrameId !== null) {
@ -1160,6 +1219,7 @@ export class MapOl implements MapInterface {
this.batchPopupRefreshFrameId = null;
}
this.updateBatchPopups();
this.batchPopupPendingRebuild = false;
return;
}
@ -1173,6 +1233,7 @@ export class MapOl implements MapInterface {
return;
}
this.updateBatchPopups();
this.batchPopupPendingRebuild = false;
});
}
@ -1326,6 +1387,47 @@ export class MapOl implements MapInterface {
return densityDisplayRules.at(-1)?.minZoom ?? 0;
}
private isFeatureEng(feature: Feature): boolean {
const layerKey = String(feature.get('_layerKey') || '').toLowerCase();
const sttpMap = String(
feature.get('_sttpMap') || feature.get('sttpMap') || ''
).toUpperCase();
const sttpCode = String(
feature.get('sttpCode') || feature.get('sttp') || ''
).toUpperCase();
return (
layerKey.includes('eng_point') ||
sttpMap === 'ENG' ||
sttpMap === 'ENG2' ||
sttpCode === 'ENG'
);
}
private isFeatureEngAlarm(feature: Feature): boolean {
const layerKey = String(
feature.get('_layerKey') || feature.get('type') || ''
).toLowerCase();
const legendState = String(feature.get('anchoPointState') || '')
.trim()
.toLowerCase();
return (
layerKey.includes('eng_alarm_point') ||
layerKey.includes('alarm_range') ||
legendState.startsWith('alarm_range_') ||
legendState.startsWith('large_eng_built_alarm_range_') ||
legendState.startsWith('mid_eng_built_alarm_range_')
);
}
private getFeatureEngRenderPriority(feature: Feature): number {
if (this.isFeatureEngAlarm(feature)) {
return 300;
}
return this.isFeatureEng(feature) ? 200 : 100;
}
/**
*
* distance
@ -1398,54 +1500,11 @@ export class MapOl implements MapInterface {
}
/**
*
*
*/
private shouldRenderNearbyFeature(
feature: Feature,
currentZoom: number
): boolean {
const nearbyGroupId = feature.get('_nearbyGroupId');
const groupSize = Number(feature.get('_nearbyGroupSize'));
const priority = Number(feature.get('_nearbyPriority'));
const replaceAtZoom = Number(feature.get('_nearbyReplaceAtZoom'));
const expandAtZoom = Number(feature.get('_nearbyExpandAtZoom'));
const showZoomMin = feature.get('_nearbyShowZoomMin');
const showZoomMax = feature.get('_nearbyShowZoomMax');
if (!nearbyGroupId || !Number.isFinite(groupSize) || groupSize < 2) {
private shouldRenderNearbyFeature(): boolean {
return true;
}
if (!Number.isFinite(priority) || priority <= 0) {
return true;
}
if (
showZoomMin !== null &&
showZoomMin !== undefined &&
currentZoom < Number(showZoomMin)
) {
return false;
}
if (
showZoomMax !== null &&
showZoomMax !== undefined &&
currentZoom > Number(showZoomMax)
) {
return false;
}
if (Number.isFinite(expandAtZoom) && currentZoom >= expandAtZoom) {
return true;
}
if (Number.isFinite(replaceAtZoom) && currentZoom >= replaceAtZoom) {
return priority <= Math.min(2, groupSize);
}
return priority === 1;
}
/**
*
* @param layer
@ -2436,6 +2495,10 @@ export class MapOl implements MapInterface {
);
}
getCurrentZoom(): number | undefined {
return this.view?.getZoom();
}
/**
* Canvas
* @param radius
@ -2496,6 +2559,7 @@ export class MapOl implements MapInterface {
window.clearTimeout(this.labelVisibilityDebounceTimerId);
this.labelVisibilityDebounceTimerId = null;
}
this.clearBatchPopupPostRenderSync();
this.removeQueryLayer();
this.regionMaskManager.destroy();

View File

@ -5,8 +5,6 @@ import VectorLayer from 'ol/layer/Vector';
import VectorSource from 'ol/source/Vector';
import { fromLonLat } from 'ol/proj';
import { getIconPath } from '@/utils/index';
import { getNearbyPointConfig } from '@/modules/map/domain/nearby-point-rules';
type PointLayerStyleFactory = (feature: Feature) => any;
type PointLayerManagerOptions = {
@ -80,103 +78,12 @@ export class PointLayerManager {
updateNearbyFeatureLayout(currentZoom?: number): void {
if (!this.map) return;
const resolution = this.map.getView().getResolution();
if (
currentZoom === undefined ||
!Number.isFinite(currentZoom) ||
!resolution
) {
if (currentZoom === undefined || !Number.isFinite(currentZoom)) {
return;
}
const nearbyPointConfig = getNearbyPointConfig();
const { autoRule, layoutRule } = nearbyPointConfig;
const groupMap = new Map<string, Feature[]>();
this.forEachFeature(feature => {
this.resetFeatureToBaseCoordinate(feature);
const groupId = feature.get('_nearbyGroupId');
const groupSize = Number(feature.get('_nearbyGroupSize'));
if (!groupId || !Number.isFinite(groupSize) || groupSize < 2) {
return;
}
const groupFeatures = groupMap.get(groupId) || [];
groupFeatures.push(feature);
groupMap.set(groupId, groupFeatures);
});
groupMap.forEach(features => {
const expandAtZoom = Number(features[0]?.get('_nearbyExpandAtZoom'));
if (!Number.isFinite(expandAtZoom) || currentZoom < expandAtZoom) {
return;
}
const sortedFeatures = [...features].sort((left, right) => {
return (
Number(left.get('_nearbyPriority') || 0) -
Number(right.get('_nearbyPriority') || 0)
);
});
const secondaryFeatures = sortedFeatures.filter(feature => {
const priority = Number(feature.get('_nearbyPriority'));
return Number.isFinite(priority) && priority > 1;
});
secondaryFeatures.forEach((feature, index) => {
const baseCoordinates = feature.get('_baseCoordinates') as
| number[]
| undefined;
const geometry = feature.getGeometry();
if (
!baseCoordinates ||
baseCoordinates.length < 2 ||
!geometry ||
geometry.getType() !== 'Point'
) {
return;
}
const groupSecondaryCount = secondaryFeatures.length;
const baseOffsetPx =
Number(feature.get('_nearbyExpandOffsetPx')) ||
autoRule.expandOffsetPx;
const ringSize = Math.max(1, layoutRule.ringSize);
const ringIndex = Math.floor(index / ringSize);
const ringOffsetIndex = index % ringSize;
const currentRingCount = Math.min(
ringSize,
groupSecondaryCount - ringIndex * ringSize
);
const fanAngleDeg =
currentRingCount <= 1
? 0
: currentRingCount <= 2
? layoutRule.fanAngleForTwoDeg
: currentRingCount <= 4
? layoutRule.fanAngleForFourDeg
: layoutRule.fanAngleForManyDeg;
const angleStepDeg =
currentRingCount <= 1 ? 0 : fanAngleDeg / (currentRingCount - 1);
const startAngleDeg = -90 - fanAngleDeg / 2;
const expandAngleDeg = startAngleDeg + ringOffsetIndex * angleStepDeg;
const expandAngleRad = (expandAngleDeg * Math.PI) / 180;
const expandOffsetPx =
baseOffsetPx +
ringIndex *
Math.max(
layoutRule.ringGapPx,
baseOffsetPx * layoutRule.ringGapFactor
);
const offsetX = Math.cos(expandAngleRad) * expandOffsetPx * resolution;
const offsetY = Math.sin(expandAngleRad) * expandOffsetPx * resolution;
feature.set('_nearbyExpandAngleDeg', expandAngleDeg, true);
(geometry as Point).setCoordinates([
baseCoordinates[0] + offsetX,
baseCoordinates[1] - offsetY
]);
});
});
}
@ -318,7 +225,7 @@ export class PointLayerManager {
layerKey: string,
matchValue: string,
checked: boolean,
fieldKey: string = 'anchoPointState'
fieldKey = 'anchoPointState'
): void {
const vectorLayer = this.layerRegistry.get(layerKey);
if (!vectorLayer) return;
@ -392,7 +299,6 @@ export class PointLayerManager {
if (!isFinite(coord[0]) || !isFinite(coord[1])) {
return null;
}
let iconUrl = iconCode ? getIconPath(iconCode) : '';
if (!iconUrl) {
iconUrl = getIconPath('default') || '';
@ -403,7 +309,9 @@ export class PointLayerManager {
...item,
_layerKey: targetLayerKey
});
feature.setId(item.stcd || item._id);
const rawFeatureId =
item.stcd || item._id || `${lon.toFixed(6)},${lat.toFixed(6)}`;
feature.setId(`${targetLayerKey}:${rawFeatureId}`);
feature.set('_iconUrl', iconUrl);
feature.set(
'_labelText',
@ -457,7 +365,6 @@ export class PointLayerManager {
const valueMap = fieldIndexMap.get(fieldKey);
if (!valueMap) return [];
return valueMap.get(String(matchValue)) || [];
}

View File

@ -4,7 +4,10 @@ import OlMap from 'ol/Map';
import Overlay from 'ol/Overlay';
import VectorLayer from 'ol/layer/Vector';
import VectorSource from 'ol/source/Vector';
import { generatePopupHtml } from '@/utils/popupHtmlGenerator';
import {
generatePopupHtml,
shouldPreferEng2Popup
} from '@/utils/popupHtmlGenerator';
type PopupManagerOptions = {
map: OlMap | null;
@ -27,6 +30,11 @@ export class PopupManager {
private map: OlMap | null;
private popupOverlay: Overlay | null = null;
private popupElement: HTMLElement | null = null;
private batchPopupContainer: HTMLDivElement | null = null;
private batchPopupItems: Array<{
feature: Feature;
element: HTMLDivElement;
}> = [];
private lastHoveredId: string | number | null = null;
private animationFrameId: number | null = null;
private getPointLayers: () => VectorLayer<VectorSource>[];
@ -49,6 +57,11 @@ export class PopupManager {
// 备注:初始化 Popup Overlay 并挂载到地图实例。
initPopupOverlay(container: HTMLElement) {
this.popupElement = container;
this.popupElement.style.display = 'none';
this.popupElement.style.removeProperty('position');
this.popupElement.style.removeProperty('transform');
this.popupElement.style.removeProperty('left');
this.popupElement.style.removeProperty('top');
this.popupElement.style.setProperty('pointer-events', 'none', 'important');
this.popupMouseEnterHandler = () => {
this.forceHidePopup();
@ -190,7 +203,7 @@ export class PopupManager {
if (feature && coordinate) {
const props = feature.getProperties();
const popupHtml = props.popupHtml || generatePopupHtml(props);
const popupHtml = this.getPopupHtml(props);
if (popupHtml) {
this.popupElement.innerHTML = popupHtml;
@ -204,22 +217,37 @@ export class PopupManager {
this.popupElement.style.display = 'none';
}
private getPopupHtml(props: Record<string, any>) {
return props.popupHtml || generatePopupHtml(props);
}
// 备注:批量显示多个要素的 Popup使用绝对定位的 div 而非 Overlay。
showPopupsForFeatures(
features: Feature[],
styleChecker?: (feature: Feature) => boolean
styleChecker?: (feature: Feature) => boolean,
options?: {
rebuild?: boolean;
}
) {
if (!this.map || !this.popupElement) return;
// 清除之前批量显示的 popups
this.clearBatchPopups();
const shouldRebuild = options?.rebuild !== false;
// 过滤:只显示图例可见的要素
const visibleFeatures = features.filter(
f => f.get('_legendVisible') !== false
);
if (visibleFeatures.length === 0) return;
if (visibleFeatures.length === 0) {
this.clearBatchPopups();
return;
}
if (!shouldRebuild && this.batchPopupItems.length > 0) {
this.updateBatchPopupPositions(styleChecker);
return;
}
this.clearBatchPopups();
const mapElement = this.map.getTargetElement();
const batchContainer = document.createElement('div');
@ -234,6 +262,7 @@ export class PopupManager {
z-index: 1000;
`;
batchContainer.id = 'batch-popup-container';
this.batchPopupContainer = batchContainer;
// 先添加容器到 DOM以便后续元素能正确测量尺寸
mapElement.appendChild(batchContainer);
@ -245,12 +274,6 @@ export class PopupManager {
top: number;
bottom: number;
}> = [];
let styleHiddenCount = 0;
let blockedCount = 0;
let renderedCount = 0;
const renderedIds: Array<string | number | null> = [];
const blockedIds: Array<string | number | null> = [];
visibleFeatures.forEach(feature => {
const geom = feature.getGeometry();
if (!geom || geom.getType() !== 'Point') return;
@ -261,12 +284,15 @@ export class PopupManager {
// 检查样式是否隐藏declutter、距离过滤等
if (styleChecker && !styleChecker(feature)) {
styleHiddenCount += 1;
return;
}
const props = feature.getProperties();
const popupHtml = props.popupHtml || generatePopupHtml(props);
const popupHtml = shouldPreferEng2Popup(props)
? generatePopupHtml(props, { forceEng2: true }) ||
props.popupHtml ||
generatePopupHtml(props)
: this.getPopupHtml(props);
if (!popupHtml) return;
// 创建与原始 popupElement 完全相同的样式
@ -314,8 +340,10 @@ export class PopupManager {
popupEl.style.visibility = 'visible';
popupEl.style.left = `${popupX}px`;
popupEl.style.top = `${popupY}px`;
renderedCount += 1;
renderedIds.push((feature.getId?.() as string | number | null) ?? null);
this.batchPopupItems.push({
feature,
element: popupEl
});
placedPopups.push({
left: popupRect.left,
@ -325,15 +353,76 @@ export class PopupManager {
});
} else {
// 有碰撞,移除该 popup
blockedCount += 1;
blockedIds.push((feature.getId?.() as string | number | null) ?? null);
batchContainer.removeChild(popupEl);
}
});
}
updateBatchPopupPositions(styleChecker?: (feature: Feature) => boolean) {
if (
!this.map ||
!this.batchPopupContainer ||
this.batchPopupItems.length === 0
) {
return;
}
const mapSize = this.map.getSize();
const viewportWidth = mapSize?.[0] ?? 0;
const viewportHeight = mapSize?.[1] ?? 0;
const viewportPadding = 48;
this.batchPopupItems.forEach(({ feature, element }) => {
if (feature.get('_legendVisible') === false) {
element.style.display = 'none';
return;
}
if (styleChecker && !styleChecker(feature)) {
element.style.display = 'none';
return;
}
const geom = feature.getGeometry();
if (!geom || geom.getType() !== 'Point') {
element.style.display = 'none';
return;
}
const coords = (geom as Point).getCoordinates();
const pixel = this.map?.getPixelFromCoordinate(coords);
if (!pixel) {
element.style.display = 'none';
return;
}
const popupX = pixel[0];
const popupY = pixel[1] - 10;
if (
popupX < -viewportPadding ||
popupY < -viewportPadding ||
popupX > viewportWidth + viewportPadding ||
popupY > viewportHeight + viewportPadding
) {
element.style.display = 'none';
return;
}
element.style.left = `${popupX}px`;
element.style.top = `${popupY}px`;
element.style.display = 'block';
});
}
// 备注:清除批量显示的 popups。
clearBatchPopups() {
this.batchPopupItems = [];
if (this.batchPopupContainer) {
this.batchPopupContainer.remove();
this.batchPopupContainer = null;
return;
}
const existing = document.getElementById('batch-popup-container');
if (existing) {
existing.remove();

View File

@ -209,7 +209,7 @@ export class RegionMaskManager {
}
// 备注:清除当前底图遮罩并按需恢复默认视野,供切换基地和异常兜底复用。
clearMapMask(resetView: boolean = true): void {
clearMapMask(resetView = true): void {
const maskedLayers = this.maskBindings.map(binding => binding.layer);
this.clearMaskEvents();

View File

@ -110,7 +110,7 @@
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue';
import { ref, computed, watch, nextTick } from 'vue';
import dayjs from 'dayjs';
import { useRoute } from 'vue-router';
import { MapClass } from '@/components/gis/map.class';
@ -118,6 +118,7 @@ import { useMapOrchestrator } from '@/modules/map/application/map-orchestrator';
import { useMapDataStore } from '@/modules/map/stores/map-data.store';
import { useMapViewStore } from '@/modules/map/stores/map-view.store';
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
import { useUiStore } from '@/store/modules/ui';
import { useMapStore } from '@/store/modules/map';
import { DateSetting } from '@/utils/enumeration';
import { getFishList, getFishPointList } from '@/api/map';
@ -126,6 +127,7 @@ const route = useRoute();
const mapOrchestrator = useMapOrchestrator();
const mapDataStore = useMapDataStore();
const mapViewStore = useMapViewStore();
const uiStore = useUiStore();
const jidiSelectEventStore = useJidiSelectEventStore();
const mapStore = useMapStore();
const mapClass = MapClass.getInstance();
@ -156,6 +158,7 @@ const fishPointOptions = ref<any[]>([]);
const fishSpeciesOptions = ref<{ label: string; value: string }[]>([]);
const fishSpeciesLoading = ref(false);
const fishPointLoading = ref(false);
const refocusTimer = ref<ReturnType<typeof setTimeout> | null>(null);
const searchTimeRange = ref([
dayjs(mapViewStore.searchTimeRange[0]).format('YYYY-MM-DD HH:mm:ss'),
@ -623,15 +626,48 @@ const handleCapacityChange = (value: string) => {
mapOrchestrator.changeEngPointCapacity(value);
};
//
const handleAnchorPointChange = (value: string) => {
if (!value) return;
const clearRefocusTimer = () => {
if (!refocusTimer.value) {
return;
}
clearTimeout(refocusTimer.value);
refocusTimer.value = null;
};
const focusSelectedAnchorPoint = () => {
const selectedValue = anchorPointSelectValue.value;
if (!selectedValue) {
return;
}
mapOrchestrator.focusPoint(
value,
selectedValue,
14,
isFishSurveyMode.value ? anchorPointOptions.value : undefined
);
};
//
const handleAnchorPointChange = (value: string) => {
if (!value) return;
focusSelectedAnchorPoint();
};
watch(
() => uiStore.mapSwitchCompletedTick,
async (newVal, oldVal) => {
if (newVal === oldVal || !anchorPointSelectValue.value) {
return;
}
await nextTick();
clearRefocusTimer();
refocusTimer.value = setTimeout(() => {
refocusTimer.value = null;
focusSelectedAnchorPoint();
}, 120);
}
);
</script>
<style lang="scss" scoped>

View File

@ -88,9 +88,7 @@ const hasEnvFac = computed(() =>
const legendItemClick = (item: any) => {
if (item.parentName == '环保设施') return;
if (item.canBeChecked == 0) {
return;
}
if (item.canBeChecked == 0) return;
if (item.nameEn) {
mapOrchestrator.toggleLegend(item.nameEn, item.checked == 0 ? 1 : 0);
}

View File

@ -4,7 +4,7 @@
<a-form ref="formRef" :model="formState" layout="inline" class="search-form">
<a-space wrap>
<a-form-item label="所属基地" name="dataDimensionVal">
<a-select v-model:value="formState.dataDimensionVal" placeholder="请选择" style="width: 120px"
<a-select v-model:value="formState.dataDimensionVal" placeholder="请选择" style="width: 140px"
show-search :filter-option="filterBaseOption" option-filter-prop="label"
@change="handleBaseChange">
<a-select-option v-for="item in baseList" :key="item.wbsCode" :value="item.wbsCode"
@ -32,7 +32,7 @@
</a-form-item>
<a-form-item label="达标状态" name="sfdb">
<a-select v-model:value="formState.sfdb" placeholder="请选择" style="width: 100px" show-search
<a-select v-model:value="formState.sfdb" placeholder="请选择" style="width: 160px" show-search
:filter-option="filterOption" option-filter-prop="label">
<a-select-option value="" label="全部">全部</a-select-option>
<a-select-option value="0" label="不达标">不达标</a-select-option>
@ -44,12 +44,12 @@
<a-form-item label="时间" name="dateValues">
<a-date-picker v-model:value="formState.dateValues"
:format="formState.types === 'month' ? 'YYYY-MM' : 'YYYY'" :picker="formState.types"
placeholder="请选择月份" style="width: 140px" :disabled-date="disabledMonthDate" />
placeholder="请选择月份" style="width: 100px" :disabled-date="disabledMonthDate" />
</a-form-item>
<a-form-item label="断面名称" name="stnm">
<a-input v-model:value="formState.stnm" placeholder="请输入断面名称" style="width: 200px" allow-clear />
<a-input v-model:value="formState.stnm" placeholder="请输入断面名称" style="width: 180px" allow-clear />
</a-form-item>
<a-form-item>
@ -90,7 +90,7 @@
<!-- 详情弹窗 -->
<a-modal v-model:open="detailModalVisible" :title="`${detailData.stnm || ''} 详细信息`" width="1536px" :footer="null"
@cancel="handleDetailModalClose">
<HJMZDTwoLays :tc="{
<HJMZDTwoLays v-if="detailModalVisible" :tc="{
stcd: detailData.stcd || '',
startTime: detailData.tm,
endTime: (typeof detailData.tm === 'string' ? detailData.tm.split(' ')[0] : dayjs(detailData.tm).format('YYYY-MM-DD')) + ' 23:59:59',
@ -231,7 +231,7 @@ const columns = [
title: '断面名称',
dataIndex: 'stnm',
key: 'stnm',
width: 180,
width: 200,
ellipsis: true,
customCell: (record: any, rowIndex: number) => {
const tableData = tableRef.value?.tableData || []
@ -243,13 +243,13 @@ const columns = [
title: '测站类型',
dataIndex: 'sttypeName',
key: 'sttypeName',
width: 120
width: 80
},
{
title: '达标状态',
dataIndex: 'sfdbName',
key: 'sfdbName',
width: 120,
width: 80,
customRender: ({ record }: any) => {
const sfdb = record.sfdbName
let color = '#000000'
@ -267,19 +267,19 @@ const columns = [
title: '水质要求',
dataIndex: 'wwqtgName',
key: 'wwqtgName',
width: 120
width: 80
},
{
title: '当前水质',
dataIndex: 'wqGrdName',
key: 'wqGrdName',
width: 120
width: 80
},
{
title: '去年同期水质',
dataIndex: 'beforeWqGrdName',
key: 'beforeWqGrdName',
width: 150
width: 140
},
{
title: '操作',
@ -674,7 +674,7 @@ const handleReset = () => {
formState.dtinType = ''
formState.types = props.type || 'month'
formState.sfdb = ''
formState.dateValues = dayjs()
formState.dateValues =props.dateValue ? (typeof props.dateValue === 'string' ? dayjs(props.dateValue, props.type === 'month' ? 'YYYY-MM' : 'YYYY') : props.dateValue) : dayjs(),
formState.stnm = ''
pieCode.value = []

View File

@ -13,7 +13,7 @@
v-model:value="formState.sfdb"
placeholder="请选择"
allow-clear
style="width: 150px"
style="width: 160px"
>
<a-select-option value="">全部</a-select-option>
<a-select-option :value="'0'">不达标</a-select-option>
@ -114,7 +114,8 @@ const columnList = [
elementCode: '',
disabledDate: true,
sorter: true,
multiLink: false
multiLink: false,
width: 160
},
{
dataIndexRaw: '',
@ -558,18 +559,18 @@ const transUnit = (value: any, tbCode: string, ys: string) => {
// ==================== ====================
// tc formState.sfdb
watch(
() => [props.tc.stcd, props.tc.startTime, props.tc.endTime, formState.sfdb],
([stcd, startTime, endTime]) => {
if (!stcd) {
return;
}
//
tableRef.value.tableData = [];
getCol({ stcd, startTime, endTime });
},
{ immediate: false } // onMounted
);
// watch(
// () => [props.tc.stcd, props.tc.startTime, props.tc.endTime, formState.sfdb],
// ([stcd, startTime, endTime]) => {
// if (!stcd) {
// return;
// }
// //
// tableRef.value.tableData = [];
// getCol({ stcd, startTime, endTime });
// },
// { immediate: false } // onMounted
// );
//
onMounted(() => {

View File

@ -167,6 +167,7 @@ const columns = [
dataIndex: 'dtinTm',
key: 'dtinTm',
width: 120,
sort: true, //
customRender: ({ record }: any) => {
const date = record.dtinTm
if (!date) return '-'
@ -192,6 +193,7 @@ const columns = [
dataIndex: 'ststdt',
key: 'ststdt',
width: 120,
sort: true, //
customRender: ({ record }: any) => {
const date = record.ststdt
if (!date) return '-'
@ -211,6 +213,7 @@ const columns = [
dataIndex: 'jcdt',
key: 'jcdt',
width: 120,
sort: true, //
customRender: ({ record }: any) => {
const date = record.jcdt
if (!date) return '-'

View File

@ -48,7 +48,7 @@
ref="tableRef"
:scrollY="400"
:columns="columns"
:list-url="vmsstbprptGetKendoList"
:list-url="overviewvmsstbprptGetKendoList"
:search-params="searchParams"
:transform-data="customTransform"
>
@ -73,8 +73,10 @@
</template>
<script lang="ts" setup>
import { ref, computed, onMounted } from 'vue';
import { vmsstbprptGetKendoList } from '@/api/home';
import { ref, computed, onMounted, h } from 'vue';
import { Tooltip } from 'ant-design-vue';
import { QuestionCircleOutlined } from '@ant-design/icons-vue';
import { vmsstbprptGetKendoList,overviewvmsstbprptGetKendoList } from '@/api/home';
import BasicTable from '@/components/BasicTable/index.vue';
import { useModelStore } from '@/store/modules/model';
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
@ -144,10 +146,9 @@ const columns = computed(() => {
},
{
key: 'coenvwState',
title: '监测状态',
title: h(Tooltip, { title: '最近1天有监测数据为在线否则为离线' }, { default: () => h('span', null, ['监测状态', h(QuestionCircleOutlined, { style: { marginLeft: '4px', color: '#000' } })]) }),
dataIndex: 'coenvwState',
width: '120px',
slots: { customRender: 'coenvwState' }
},
{
@ -203,7 +204,7 @@ const columns = computed(() => {
if (tabIndex.value !== '4') {
baseColumns.push({
key: 'coenvwState',
title: '监测状态',
title: h(Tooltip, { title: '最近1天有监测数据为在线否则为离线' }, { default: () => h('span', null, ['监测状态', h(QuestionCircleOutlined, { style: { marginLeft: '4px', color: '#999' } })]) }),
dataIndex: 'coenvwState',
width: '120px',
slots: { customRender: 'coenvwState' }
@ -313,7 +314,7 @@ const getRstcdData = async () => {
],
select: ['stcd', 'stnm']
};
const res = await vmsstbprptGetKendoList(parmas);
const res = await overviewvmsstbprptGetKendoList(parmas);
rstcdOptions.value = res.data.data.map((item) => {
return {
value: item.stcd,

View File

@ -1,7 +1,6 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
const loading = ref(false);
const isOpen = ref(true);
const JidiSelectEventStore = useJidiSelectEventStore();

View File

@ -317,7 +317,7 @@ import dayjs from 'dayjs';
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
import videoCover from '@/assets/images/videFm.jpg';
import { useDraggable } from '@/utils/drag';
;
const baseUrl = import.meta.env.VITE_APP_ATTACHMENT_URL;
// ==================== Props ====================

View File

@ -33,9 +33,12 @@
<a-form-item>
<a-button type="primary" @click="handleSearch">查询</a-button>
<!-- <a-button style="margin-left: 8px" @click="handleReset">重置</a-button> -->
<a-tooltip title="导出(暂未实现)">
<a-button style="margin-left: 8px">导出</a-button>
</a-tooltip>
<a-button
style="margin-left: 8px"
@click="handleExport"
:loading="exportLoading"
>导出</a-button
>
</a-form-item>
</a-form>
@ -71,6 +74,7 @@ import BasicTable from '@/components/BasicTable/index.vue';
import { getStllGaojingDetail } from '@/api/zngj';
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
import { useModelStore } from '@/store/modules/model';
import dayjs, { Dayjs } from 'dayjs';
defineOptions({
name: 'ModeGaoJing'
});
@ -262,6 +266,19 @@ const opentan = (record: any) => {
modelStore.title = record.ennm;
modelStore.params.stcd = record.rstcd;
};
/* 导出逻辑 */
const exportLoading = ref(false);
const handleExport = () => {
exportLoading.value = true;
tableRef.value
.exportTable({
fileName: `生态流量低于限值告警统计_${dayjs().format('YYYY-MM-DD HH-mm-ss')}`
})
.finally(() => {
exportLoading.value = false;
});
};
//
// handleSearch();
onMounted(() => {
@ -283,7 +300,6 @@ onMounted(() => {
.table-wrapper {
flex: 1;
}
}
</style>

View File

@ -32,6 +32,12 @@
</a-form-item>
<a-form-item>
<a-button type="primary" @click="handleSearch">查询</a-button>
<a-button
style="margin-left: 8px"
@click="handleExport"
:loading="exportLoading"
>导出</a-button
>
</a-form-item>
</a-form>
@ -67,7 +73,7 @@ import BasicTable from '@/components/BasicTable/index.vue';
import { getStllGaojingDetail } from '@/api/zngj';
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
import { useModelStore } from '@/store/modules/model';
import dayjs, { Dayjs } from 'dayjs';
defineOptions({
name: 'ModeGaoJing'
});
@ -266,6 +272,19 @@ const opentan = (record: any) => {
modelStore.title = record.ennm;
modelStore.params.stcd = record.rstcd;
};
/* 导出逻辑 */
const exportLoading = ref(false);
const handleExport = () => {
exportLoading.value = true;
tableRef.value
.exportTable({
fileName: `生态流量低于限值告警统计_${dayjs().format('YYYY-MM-DD HH-mm-ss')}`
})
.finally(() => {
exportLoading.value = false;
});
};
onMounted(() => {
handleSearch();
});

View File

@ -1,15 +1,31 @@
<!-- SidePanelItem.vue -->
<template>
<div>
<SidePanelItem title="水质监测数据告警情况" :select="select" @update-values="handlePanelChange">
<SidePanelItem
title="水质监测数据告警情况"
:select="select"
@update-values="handlePanelChange"
>
<a-spin :spinning="dataLoading" tip="加载中...">
<div class="body_box">
<div class="box" v-for="(item, index) in data" :key="index" @click="handleCardClick(item.warnState)" style="cursor: pointer;">
<div
class="box"
v-for="(item, index) in data"
:key="index"
@click="handleCardClick(item.warnState)"
style="cursor: pointer"
>
<div>
<div :class="item.icon" :style="{background:item.background}"></div>
<div
:class="item.icon"
:style="{ background: item.background }"
></div>
<div class="title_text">{{ item.name }}</div>
</div>
<div><span class="text_num">{{ item.num }}</span><span></span></div>
<div>
<span class="text_num">{{ item.num }}</span
><span></span>
</div>
</div>
</div>
</a-spin>
@ -33,15 +49,15 @@
</template>
<script lang="ts" setup>
import { ref, watch } from "vue";
import { ref, watch } from 'vue';
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
import SidePanelItem from "@/components/SidePanelItem/index.vue";
import SidePanelItem from '@/components/SidePanelItem/index.vue';
import { getShuizhiGaojingList } from '@/api/zngj';
import ModeGaoJing from './ModeGaoJing.vue';
import { useDraggable } from '@/utils/drag';
defineOptions({
name: "shuizhijianceshuju",
name: 'shuizhijianceshuju'
});
const JidiSelectEventStore = useJidiSelectEventStore();
@ -68,10 +84,10 @@ const select = ref({
options: [
{ label: '全部', value: '' },
{ label: '国家站', value: '1' },
{ label: '自建站', value: '0' },
{ label: '自建站', value: '0' }
],
width: '140px',
minWidth: '140px',
minWidth: '140px'
});
const data = ref([
@ -80,22 +96,22 @@ const data = ref([
num: '0',
warnState: '1',
icon: 'iconfont icon-map-rgszzDabiao zhuangji',
background: '#78c300',
background: '#78c300'
},
{
name: '告警',
num: '0',
warnState: '2',
icon: 'iconfont icon-map-rgszzDabiao shebeiZhengchang',
background: '#e55555',
background: '#e55555'
},
{
name: '无数据',
num: '0',
warnState: '0',
icon: 'iconfont icon-map-rgszzDabiao shebeiLixian',
background: '#8d8d8d',
},
background: '#8d8d8d'
}
]);
const handlePanelChange = (payload: any) => {
@ -117,26 +133,28 @@ const getCardData = async () => {
field: 'sttpCode',
operator: 'in',
dataType: 'string',
value: ['WQ'],
value: ['WQ']
},
{
field: 'mway',
operator: 'eq',
dataType: 'number',
value: 2,
value: 2
},
{
field: 'usfl',
operator: 'eq',
dataType: 'string',
value: '1',
value: '1'
},
baseid.value !='all'? {
baseid.value != 'all'
? {
field: 'baseId',
operator: 'eq',
dataType: 'string',
value: baseid.value,
}: null,
value: baseid.value
}
: null
].filter(Boolean);
if (defaultValue.value === '') {
@ -144,21 +162,21 @@ const getCardData = async () => {
field: 'dtinType',
operator: 'in',
dataType: 'number',
value: [0, 1],
value: [0, 1]
});
} else {
filters.push({
field: 'dtinType',
operator: 'eq',
dataType: 'number',
value: Number(defaultValue.value),
value: Number(defaultValue.value)
});
}
const params = {
filter: {
logic: 'and',
filters,
filters
},
group: [
{
@ -167,12 +185,12 @@ const getCardData = async () => {
aggregates: [
{
field: 'stcd',
aggregate: 'count',
},
aggregate: 'count'
}
]
}
],
},
],
select: [],
select: []
};
const res = await getShuizhiGaojingList(params);
@ -181,7 +199,7 @@ const getCardData = async () => {
const result = {
normal: 0,
abnormal: 0,
noData: 0,
noData: 0
};
list.forEach((item: any) => {
@ -195,25 +213,25 @@ const getCardData = async () => {
data.value = [
{
name: '正常',
num: result.normal.toString(),
num: '0',
warnState: '1',
icon: 'iconfont icon-shuidianzhan1 zhuangji',
background: '#78c300',
icon: 'iconfont icon-map-rgszzDabiao zhuangji',
background: '#78c300'
},
{
name: '告警',
num: result.abnormal.toString(),
num: '0',
warnState: '2',
icon: 'iconfont icon-shuidianzhanGaojing shebeiZhengchang',
background: '#e55555',
icon: 'iconfont icon-map-rgszzDabiao shebeiZhengchang',
background: '#e55555'
},
{
name: '无数据',
num: result.noData.toString(),
num: '0',
warnState: '0',
icon: 'iconfont icon-shuidianzhanWushujv shebeiLixian',
background: '#8d8d8d',
},
icon: 'iconfont icon-map-rgszzDabiao shebeiLixian',
background: '#8d8d8d'
}
];
} catch (error) {
console.error('获取水质监测告警数据失败:', error);

View File

@ -63,7 +63,7 @@
<script lang="ts" setup>
import { ref, onMounted, onUnmounted, nextTick,watch } from 'vue';
import { ref, watch } from 'vue';
// import { ref, watch } from 'vue';
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
import SidePanelItem from '@/components/SidePanelItem/index.vue';
import LsstjkTk from './LsstjkTk.vue';

View File

@ -43,6 +43,7 @@ const HYDRO_DYNAMIC_LAYER_KEYS = [
];
const ENG_POINT_LAYER_KEY = 'eng_point';
const ENG_POINT_MEDIUM_VISIBLE_ZOOM = 7.5;
const CESIUM_ZOOM_SYNC_DEBOUNCE_MS = 80;
const LARGE_ENG_LEGEND_PREFIX = 'large_eng_';
const MID_ENG_LEGEND_PREFIX = 'mid_eng_';
@ -55,12 +56,20 @@ export const useMapOrchestrator = () => {
const mapViewStore = useMapViewStore();
const jidiSelectEventStore = useJidiSelectEventStore();
let activePageLoadRequestId = 0;
let zoomListenerKey: any = null;
let removeZoomListener: (() => void) | null = null;
let stopBaseSelectionWatch: WatchStopHandle | null = null;
const initializedBaseLayerKeys = new Set<string>();
const baseSelectionDebounceTimer = ref<ReturnType<typeof setTimeout> | null>(
null
);
const zoomSyncDebounceTimer = ref<ReturnType<typeof setTimeout> | null>(null);
let latestHydroMenuGetter: (() => boolean) | null = null;
let currentMapType: '2D' | '3D' = '2D';
let hydroMenuDefaultCheckedKeys = new Set<string>();
let hydroDynamicLayerSyncTask: {
visible: boolean;
promise: Promise<string[] | void>;
} | null = null;
// 备注:统一获取当前地图视图对象,避免在多个组件里重复兼容 2D/3D view 访问方式。
const getCurrentView = () => {
@ -69,7 +78,14 @@ export const useMapOrchestrator = () => {
// 备注:统一获取当前地图缩放级别,供页面切换和缩放监听复用。
const getCurrentZoom = (): number | undefined => {
return getCurrentView()?.getZoom?.();
return mapClass.getCurrentZoom();
};
const clearZoomSyncDebounceTimer = () => {
if (zoomSyncDebounceTimer.value) {
clearTimeout(zoomSyncDebounceTimer.value);
zoomSyncDebounceTimer.value = null;
}
};
// 备注:统一把当前页面中的 GIS 基础图层初始化到地图实例,避免在图层树组件里做配置解析和加层编排。
@ -95,7 +111,20 @@ export const useMapOrchestrator = () => {
}
if (item.key === 'customBaseLayer') {
sessionStorage.setItem('customBaseLayer', JSON.stringify(item.config));
const serializableBaseLayerConfig = {
key: item.config?.key,
id: item.config?.id,
name: item.config?.name,
type: item.config?.type,
url: item.config?.url,
url_3d: item.config?.url_3d,
layers: item.config?.layers,
rasteropacity: item.config?.rasteropacity
};
sessionStorage.setItem(
'customBaseLayer',
JSON.stringify(serializableBaseLayerConfig)
);
}
if (!initializedBaseLayerKeys.has(item.key)) {
@ -154,6 +183,9 @@ export const useMapOrchestrator = () => {
// 先设置图层数据(更新 checkedLayerKeys再设置图例数据
if (layerConfig.length > 0) {
hydroMenuDefaultCheckedKeys = new Set(
mapConfigStore.extractCheckedLayerKeys(layerConfig)
);
mapStore.setLayerData(layerConfig);
ensureBaseLayersInitialized(layerConfig);
@ -229,10 +261,56 @@ export const useMapOrchestrator = () => {
await mapClass.init(container);
if (popupContainer) {
const popupHost = container.parentElement;
if (popupHost && popupContainer.parentElement !== popupHost) {
popupHost.appendChild(popupContainer);
}
popupContainer.style.display = 'none';
mapClass.initPopupOverlay(popupContainer);
}
};
const replayCurrentMapState = async (
options: Pick<InitializeOptions, 'popupContainer'> & {
getIsHydroMenu: () => boolean;
}
) => {
initializedBaseLayerKeys.clear();
const currentZoom = getCurrentZoom();
if (currentZoom !== undefined) {
mapViewStore.setCurrentZoomLevel(currentZoom);
}
if (options.popupContainer) {
const container = document.getElementById('mapContainer');
const popupHost = container?.parentElement || null;
if (popupHost && options.popupContainer.parentElement !== popupHost) {
popupHost.appendChild(options.popupContainer);
}
options.popupContainer.style.display = 'none';
mapClass.initPopupOverlay(options.popupContainer);
}
ensureBaseLayersInitialized(mapStore.layerData || []);
const checkedKeys = mapViewStore.getCheckedLayerKeys();
await mapStore.updateLayerData(checkedKeys, true);
mapStore.setSelectedLegendData();
await syncZoomSensitiveState({
isHydroMenu: options.getIsHydroMenu(),
refreshEngPoint: true
});
const selectedBaseId = mapViewStore.selectedBaseId;
mapClass.jdPanelControlShowAndHidden(
selectedBaseId || 'all',
!!selectedBaseId
);
bindZoomListener(options.getIsHydroMenu);
};
// 备注:统一完成首屏页面配置加载,供挂载阶段和后续页面重载复用。
const initialize = async ({
container,
@ -249,20 +327,18 @@ export const useMapOrchestrator = () => {
// 备注:统一绑定地图缩放监听,让页面组件只负责触发初始化,不再直接处理缩放联动细节。
const bindZoomListener = (getIsHydroMenu: () => boolean) => {
if (zoomListenerKey) {
unByKey(zoomListenerKey);
zoomListenerKey = null;
if (removeZoomListener) {
removeZoomListener();
removeZoomListener = null;
}
const view = getCurrentView();
if (!view?.on) return;
clearZoomSyncDebounceTimer();
const currentZoom = getCurrentZoom();
if (currentZoom !== undefined) {
mapViewStore.setCurrentZoomLevel(currentZoom);
}
zoomListenerKey = view.on('change:resolution', async () => {
const emitZoomChange = async () => {
const zoom = getCurrentZoom();
if (zoom === undefined) return;
@ -271,7 +347,36 @@ export const useMapOrchestrator = () => {
zoom,
getIsHydroMenu()
);
};
if (currentMapType === '3D') {
const removeCesiumListener =
mapClass.view?.camera?.changed?.addEventListener(() => {
clearZoomSyncDebounceTimer();
zoomSyncDebounceTimer.value = setTimeout(() => {
zoomSyncDebounceTimer.value = null;
void emitZoomChange();
}, CESIUM_ZOOM_SYNC_DEBOUNCE_MS);
});
if (typeof removeCesiumListener === 'function') {
removeZoomListener = () => {
clearZoomSyncDebounceTimer();
removeCesiumListener();
};
}
return;
}
const view = getCurrentView();
if (!view?.on) return;
const zoomListenerKey = view.on('change:resolution', () => {
void emitZoomChange();
});
removeZoomListener = () => {
unByKey(zoomListenerKey);
};
};
// 备注:统一绑定基地切换事件,让页面组件不再直接 watch 外部基地选择源。
@ -308,10 +413,34 @@ export const useMapOrchestrator = () => {
getIsHydroMenu: () => boolean;
}
) => {
currentMapType = '2D';
latestHydroMenuGetter = options.getIsHydroMenu;
await initializeMapShell(options);
bindBaseSelection();
bindZoomListener(options.getIsHydroMenu);
await loadPage({ pageKey: options.pageKey, isInitialLoad: true });
await syncZoomSensitiveState({
isHydroMenu: options.getIsHydroMenu(),
refreshEngPoint: true
});
};
const switchMapType = async (
type: '2D' | '3D',
options: Pick<InitializeOptions, 'popupContainer'> & {
getIsHydroMenu?: () => boolean;
} = {}
) => {
const hydroMenuGetter =
options.getIsHydroMenu || latestHydroMenuGetter || (() => false);
latestHydroMenuGetter = hydroMenuGetter;
currentMapType = type;
await mapClass.switchView(type);
await replayCurrentMapState({
popupContainer: options.popupContainer,
getIsHydroMenu: hydroMenuGetter
});
};
// 备注:统一处理菜单切换后的页面配置重载,供页面 watch 直接复用。
@ -361,17 +490,24 @@ export const useMapOrchestrator = () => {
(layer: any) => layer?.url && !mapDataStore.hasPointLayerData(layer.key)
);
for (const layer of layersToLoad) {
await mapStore.loadLayerData(layer);
if (layersToLoad.length === 0) {
return;
}
await Promise.all(layersToLoad.map(layer => mapStore.loadLayerData(layer)));
};
// 备注:统一控制水电开发菜单下的动态图层增删,避免页面组件直接操作加载和勾选细节。
const syncHydroDynamicLayers = async (visible: boolean) => {
if (
hydroDynamicLayerSyncTask &&
hydroDynamicLayerSyncTask.visible === visible
) {
return hydroDynamicLayerSyncTask.promise;
}
const task = (async () => {
const currentCheckedKeys = mapViewStore.getCheckedLayerKeys();
const pageDefaultCheckedKeys = new Set(
mapConfigStore.extractCheckedLayerKeys(mapStore.layerData || [])
);
if (visible) {
const layersToAdd = HYDRO_DYNAMIC_LAYER_KEYS.filter(
@ -392,7 +528,7 @@ export const useMapOrchestrator = () => {
if (!HYDRO_DYNAMIC_LAYER_KEYS.includes(key)) {
return true;
}
return pageDefaultCheckedKeys.has(key);
return hydroMenuDefaultCheckedKeys.has(key);
});
if (nextCheckedKeys.length === currentCheckedKeys.length) {
@ -402,6 +538,43 @@ export const useMapOrchestrator = () => {
return applyLayerSelection({
checkedKeys: nextCheckedKeys
});
})().finally(() => {
if (hydroDynamicLayerSyncTask?.promise === task) {
hydroDynamicLayerSyncTask = null;
}
});
hydroDynamicLayerSyncTask = {
visible,
promise: task
};
return task;
};
// 备注:统一按当前缩放对齐首页菜单的中型站过滤和 12 级动态图层状态。
const syncZoomSensitiveState = async ({
isHydroMenu,
refreshEngPoint = false
}: {
isHydroMenu: boolean;
refreshEngPoint?: boolean;
}) => {
const currentZoom = getCurrentZoom();
if (currentZoom !== undefined) {
mapViewStore.setCurrentZoomLevel(currentZoom);
}
if (
isHydroMenu &&
refreshEngPoint &&
mapViewStore.getCheckedLayerKeys().includes(ENG_POINT_LAYER_KEY)
) {
mapStore.refreshPointLayerDisplayData(ENG_POINT_LAYER_KEY);
}
await syncHydroDynamicLayers(
!!isHydroMenu && currentZoom !== undefined && currentZoom >= 12
);
};
// 备注:统一处理地图缩放联动,收口水电开发菜单下的动态图层阈值切换逻辑。
@ -419,6 +592,7 @@ export const useMapOrchestrator = () => {
currentZoom <= ENG_POINT_MEDIUM_VISIBLE_ZOOM);
if (
isHydroMenu &&
crossedEngPointMediumThreshold &&
mapViewStore.getCheckedLayerKeys().includes(ENG_POINT_LAYER_KEY)
) {
@ -454,13 +628,10 @@ export const useMapOrchestrator = () => {
}
return reloadPage(pageKey).then(async () => {
const currentZoom = getCurrentZoom();
if (isHydroMenu && currentZoom !== undefined && currentZoom >= 12) {
await syncHydroDynamicLayers(true);
return;
}
await syncHydroDynamicLayers(false);
await syncZoomSensitiveState({
isHydroMenu,
refreshEngPoint: true
});
});
};
@ -487,12 +658,15 @@ export const useMapOrchestrator = () => {
// 备注:统一处理单个图例切换命令,供图例组件复用。
const toggleLegend = (nameEn: string, checked?: number) => {
if (!nameEn) return;
console.log('toggleLegend', nameEn, checked);
console.log(mapViewStore.getLegendChecked(nameEn));
const nextChecked =
checked === undefined
? mapViewStore.getLegendChecked(nameEn) === 1
? 0
: 1
: checked;
console.log(nextChecked);
mapStore.updateLegendChecked(nameEn, nextChecked);
};
@ -570,30 +744,47 @@ export const useMapOrchestrator = () => {
mapDataStore.pointData.find(pointMatcher) ||
fallbackPoints.find(pointMatcher);
if (!targetPoint?.lgtd || !targetPoint?.lttd) return;
const legendState = String(targetPoint?.anchoPointState || '');
if (legendState && mapViewStore.getLegendChecked(legendState) !== 1) {
mapStore.updateLegendChecked(legendState, 1);
}
const layerKey = String(targetPoint?.layerKey || '');
if (layerKey && mapClass.hasLayer(layerKey)) {
mapClass.mdLayerTreeShowOrHidden(layerKey, true);
}
mapClass.flyTopanto([targetPoint.lgtd, targetPoint.lttd], zoom);
};
// 备注:统一释放地图页面生命周期里注册的监听,避免页面卸载后残留联动。
const unmountView = () => {
if (zoomListenerKey) {
unByKey(zoomListenerKey);
zoomListenerKey = null;
if (removeZoomListener) {
removeZoomListener();
removeZoomListener = null;
}
clearZoomSyncDebounceTimer();
if (stopBaseSelectionWatch) {
stopBaseSelectionWatch();
stopBaseSelectionWatch = null;
}
if (baseSelectionDebounceTimer.value) {
clearTimeout(baseSelectionDebounceTimer.value);
baseSelectionDebounceTimer.value = null;
}
latestHydroMenuGetter = null;
currentMapType = '2D';
initializedBaseLayerKeys.clear();
hydroMenuDefaultCheckedKeys.clear();
};
return {
initialize,
mountView,
switchMapType,
unmountView,
loadPage,
reloadPage,

View File

@ -74,19 +74,19 @@ export const DEFAULT_NEARBY_POINT_DENSITY_DISPLAY_RULES: NearbyPointDensityDispl
},
{
minDensityValue: 800000,
minZoom: 6.1
minZoom: 0
},
{
minDensityValue: 400000,
minZoom: 8.1
minZoom: 0
},
{
minDensityValue: 50000,
minZoom: 10.6
minZoom: 2.0
},
{
minDensityValue: 0,
minZoom: 12.2
minZoom: 4.0
}
];
@ -121,6 +121,37 @@ type GroupCandidate = {
layerKey: string;
};
const isAlarmRangeLegendState = (legendState: string): boolean => {
return (
legendState.startsWith('alarm_range_') ||
legendState.startsWith('large_eng_built_alarm_range_') ||
legendState.startsWith('mid_eng_built_alarm_range_')
);
};
const getPointRenderPriority = (point: Record<string, any>): number => {
const layerKey = String(point.layerKey || point.type || '').toLowerCase();
const legendState = String(point.anchoPointState || '').trim().toLowerCase();
if (
layerKey.includes('eng_alarm_point') ||
layerKey.includes('alarm_range') ||
isAlarmRangeLegendState(legendState)
) {
return 300;
}
if (
layerKey.includes('eng_point') ||
legendState.startsWith('large_eng_') ||
legendState.startsWith('mid_eng_')
) {
return 200;
}
return 100;
};
const normalizeText = (value: unknown): string => {
return String(value ?? '')
.trim()
@ -207,6 +238,12 @@ const compareGroupCandidates = (
left: GroupCandidate,
right: GroupCandidate
): number => {
const leftRenderPriority = getPointRenderPriority(left.point);
const rightRenderPriority = getPointRenderPriority(right.point);
if (leftRenderPriority !== rightRenderPriority) {
return rightRenderPriority - leftRenderPriority;
}
const leftDensityValue = toFiniteNumber(left.point.distance);
const rightDensityValue = toFiniteNumber(right.point.distance);
@ -232,21 +269,18 @@ const buildRuntimeMeta = (
displayName: string,
rule: NearbyPointAutoRule
): NearbyPointRuntimeMeta => {
const displayMode: NearbyPointDisplayMode =
priority === 1 ? 'default' : priority === 2 ? 'replace' : 'expand';
return {
groupId,
displayName,
groupSize,
priority,
showZoomMin: priority === 1 ? null : rule.replaceAtZoom,
showZoomMin: null,
showZoomMax: null,
replaceAtZoom: rule.replaceAtZoom,
expandAtZoom: rule.expandAtZoom,
expandOffsetPx: rule.expandOffsetPx,
expandAngleDeg: priority === 1 ? 0 : (priority - 2) * rule.angleStepDeg,
displayMode
expandAngleDeg: 0,
displayMode: 'default'
};
};
@ -279,23 +313,12 @@ const buildGroupCandidates = (
return candidates;
};
const buildLayerScopedNearbyGroups = (
const buildNearbyGroups = (
candidates: GroupCandidate[],
rule: NearbyPointAutoRule
): GroupCandidate[][] => {
const layerScopedCandidates = new Map<string, GroupCandidate[]>();
candidates.forEach(candidate => {
const layerKey = candidate.layerKey || '__empty_layer__';
const layerItems = layerScopedCandidates.get(layerKey) || [];
layerItems.push(candidate);
layerScopedCandidates.set(layerKey, layerItems);
});
const result: GroupCandidate[][] = [];
layerScopedCandidates.forEach(layerItems => {
const sortedCandidates = [...layerItems].sort(compareGroupCandidates);
const sortedCandidates = [...candidates].sort(compareGroupCandidates);
const consumedIndexes = new Set<number>();
sortedCandidates.forEach((seedCandidate, seedIndex) => {
@ -334,7 +357,6 @@ const buildLayerScopedNearbyGroups = (
result.push(groupItems);
}
});
});
return result;
};
@ -354,7 +376,7 @@ export const attachNearbyPointRuntimeMeta = (
if (candidates.length < rule.minGroupSize) {
return points;
}
const groupedCandidates = buildLayerScopedNearbyGroups(candidates, rule);
const groupedCandidates = buildNearbyGroups(candidates, rule);
let groupSeed = 0;
groupedCandidates.forEach(groupItems => {

View File

@ -28,10 +28,9 @@ export const useMapConfigStore = defineStore('map-config', () => {
const legendLoading = ref(false);
const lastLoadOptions = ref<MapConfigLoadOptions | null>(null);
// 备注:统一规范图例 nameEn避免 `_测试` 后缀影响索引命中。
const normalizeLegendNameEn = (nameEn?: string): string => {
if (!nameEn) return '';
return nameEn.includes('_测试') ? nameEn.split('_测试')[0] : nameEn;
return nameEn;
};
// 备注:重建图层配置索引,后续按 key 读取图层配置时不再全量递归。

View File

@ -55,7 +55,9 @@ const select = ref({
value: 'all',
options: jiDiList.value,
picker: undefined,
format: undefined
format: undefined,
width: '160px',
minWidth:'160px'
});
const baseid = ref('');
//

View File

@ -39,7 +39,7 @@ const baseid = ref('');
const modelStore = useModelStore();
// Loading
const loading = ref(false);
const loading = ref(true);
const hasData = ref(true); // true initChart
const select = ref({
show: true,
@ -844,4 +844,7 @@ watch(
z-index: 10;
}
}
:deep(.ant-spin-nested-loading >div>.ant-spin){
background: rgba(255, 255, 255) !important;
}
</style>

View File

@ -41,7 +41,7 @@ let chartInstance: echarts.ECharts | null = null;
const baseid = ref('');
// Loading
const loading = ref(false);
const loading = ref(true);
const hasData = ref(true); // true initChart
//
@ -732,4 +732,7 @@ onUnmounted(() => {
z-index: 10;
}
}
:deep(.ant-spin-nested-loading >div>.ant-spin){
background: rgba(255, 255, 255) !important;
}
</style>

View File

@ -587,12 +587,12 @@ const fetchTableData = async () => {
{
field: 'stnm',
operator: 'contains',
value: '12'
value: params.value.stnm
},
{
field: 'ennm',
operator: 'contains',
value: '12'
value: params.value.stnm
}
]
}

View File

@ -501,7 +501,7 @@ const updateChart = () => {
center: ['35%', '50%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderRadius: 5,
borderColor: '#fff',
borderWidth: 2
},

View File

@ -14,7 +14,7 @@ import {
getMsstbprptKendoList
} from '@/api/home';
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
import dayjs from 'dayjs';
const props = defineProps<{
dataDimensionVal?: string;
name?: string;
@ -337,17 +337,27 @@ const customTransform = (res: any) => ({
total: res?.data?.total || 0
});
const exportLoading = ref(false);
function handleExport() {
if (exportLoading.value) return;
exportLoading.value = true;
const currentTab = tabKey.value;
if (
tableRefs.value[currentTab] &&
typeof tableRefs.value[currentTab].exportTable === 'function'
) {
tableRefs.value[currentTab].exportTable({
fileName: '环境监测数据接入情况'
tableRefs.value[currentTab]
.exportTable({
fileName: `水电开发情况_${dayjs().format('YYYY-MM-DD HH-mm-ss')}`
})
.finally(() => {
exportLoading.value = false;
});
} else {
message.info('导出功能待实现');
message.info('当前Tab暂无导出数据');
exportLoading.value = false;
}
}
@ -640,6 +650,7 @@ const searchList = computed(() => [
type: 'Input',
name: 'stnm',
label: '',
placeholder:'请输入电站名称',
fieldProps: {
allowClear: true,
placeholder: '请输入电站名称'
@ -866,7 +877,7 @@ onUnmounted(() => {
@reset="handleReset"
>
<template #actions>
<a-button @click="handleExport">
<a-button @click="handleExport" :loading ="exportLoading" >
<template #icon><DownloadOutlined /></template>
导出
</a-button>

View File

@ -15,7 +15,7 @@ import {
getBaseWbsb
} from '@/api/home';
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
import dayjs from 'dayjs';
const props = defineProps<{
defaultJidiInfo?: Record<string, string | number | boolean>;
seriesName?: string;
@ -300,12 +300,27 @@ const customTransform = (res: any) => ({
records: res?.data?.data || res?.data?.items || [],
total: res?.data?.total || 0
});
const exportLoading = ref(false);
function handleExport() {
if (tableRef.value && typeof tableRef.value.export === 'function') {
tableRef.value.export();
if (exportLoading.value) return;
exportLoading.value = true;
const currentTab = tabKey.value;
if (
tableRefs.value[currentTab] &&
typeof tableRefs.value[currentTab].exportTable === 'function'
) {
tableRefs.value[currentTab]
.exportTable({
fileName: `水电开发情况_${dayjs().format('YYYY-MM-DD HH-mm-ss')}`
})
.finally(() => {
exportLoading.value = false;
});
} else {
message.info('导出功能待实现');
message.info('当前Tab暂无导出数据');
exportLoading.value = false;
}
}
@ -575,6 +590,7 @@ const searchList = computed(() => [
type: 'Input',
name: 'stnm',
label: '',
placeholder: '请输入电站名称',
fieldProps: {
allowClear: true,
placeholder: '请输入电站名称'
@ -946,7 +962,7 @@ onUnmounted(() => {
@reset="handleReset"
>
<template #actions>
<a-button @click="handleExport">
<a-button @click="handleExport" :loading ="exportLoading" >
<template #icon><DownloadOutlined /></template>
导出
</a-button>

View File

@ -404,7 +404,7 @@ const getselectTwo = async (code: any) => {
"filters": [
code != 'all' ? {
"field": "baseId",
"operator": "contains",
"operator": "eq",
"dataType": "string",
"value": code
} : null,

View File

@ -56,11 +56,28 @@ const paramsOne = {
tm: []
};
const dataOne = ref('');
// 使 computed 使 prompts dataOne.value
const prompts = computed(() => ({
// minTm
const rawTableData = ref<any[]>([]);
// 使 computed 使 prompts minTm
const prompts = computed(() => {
//
if (!rawTableData.value || rawTableData.value.length === 0) {
return { show: true, value: '' };
}
// minTm
const minTm = rawTableData.value[0]?.minTm;
// minTm
if (!minTm) {
return { show: true, value: '' };
}
return {
show: true,
value: `注:最新数据时间为${dataOne.value || getCurrentHourTime()}`
}));
value: `注:最新数据时间为${minTm}`
};
});
const showemit = ref(false);
const loading = ref(false);
// const selectOptions: any = ref([])
@ -308,6 +325,9 @@ const init = async () => {
let res = await getKendoListCust(params);
let data = res.data.data || res.data;
// minTm
rawTableData.value = data;
if (data.length > 0) {
showemit.value = true;
} else {

View File

@ -23,7 +23,7 @@ export const useJidiSelectEventStore = defineStore('jidiSelectEvent', () => {
};
const getJidiData = async () => {
loading.value = true;
let params = {
const params = {
filter: {
logic: 'and',
filters: [
@ -50,7 +50,7 @@ export const useJidiSelectEventStore = defineStore('jidiSelectEvent', () => {
select: ['wbsCode', 'wbsName']
};
const res = await wbsbGetKendoList(params);
let data = res?.data?.data || [
const data = res?.data?.data || [
{
wbsCode: 'all',
wbsName: '当前全部',

View File

@ -16,6 +16,7 @@ import { useMapViewStore } from '@/modules/map/stores/map-view.store';
import request from '@/utils/request';
const mapClass = MapClass.getInstance();
const ENG_POINT_LAYER_KEY = 'eng_point';
const ENG_ALARM_POINT_LAYER_KEY = 'eng_alarm_point';
const YLFB_POINT_LAYER_KEY = 'ylfb_point';
const ENG_POINT_MEDIUM_VISIBLE_ZOOM = 7.5;
const ENG_POINT_LARGE_STATES = [
@ -29,6 +30,22 @@ const ENG_POINT_MEDIUM_STATES = [
'mid_eng_nbuilt'
];
const isEngPointRuntimeItem = (item: any): boolean => {
const runtimeLayerKey = String(
item?.layerKey || item?.type || item?._layerKey || ''
).toLowerCase();
const sttpMap = String(
item?._sttpMap || item?.sttpMap || item?.popupSttpMap || ''
).toUpperCase();
const sttpCode = String(item?.sttpCode || item?.sttp || '').toUpperCase();
if (runtimeLayerKey) {
return runtimeLayerKey.includes(ENG_POINT_LAYER_KEY);
}
return sttpMap === 'ENG' || sttpMap === 'ENG2' || sttpCode === 'ENG';
};
const normalizeCachePayload = (value: any): any => {
if (Array.isArray(value)) {
return value.map(item => normalizeCachePayload(item));
@ -105,19 +122,48 @@ const normalizeRequestParams = (rawParams: any) => {
};
};
const parseAnchorParamFilters = (anchorParamJson?: string) => {
if (!anchorParamJson) return [];
const parseAnchorParamJson = (anchorParamJson?: string) => {
const result: { filters: any[]; orders: any[] | null } = {
filters: [],
orders: null
};
if (!anchorParamJson) return result;
try {
const parsedParams = JSON.parse(anchorParamJson);
if (!parsedParams || typeof parsedParams !== 'object') {
return [];
const parsed = JSON.parse(anchorParamJson);
if (!parsed || typeof parsed !== 'object') return result;
// 解析 params -> filters兼容旧格式如果没有 params 字段,整个对象即为 params
const paramsObj = parsed.params || parsed;
if (
paramsObj &&
typeof paramsObj === 'object' &&
!Array.isArray(paramsObj)
) {
result.filters = buildFiltersFromParamsObject(paramsObj);
}
return buildFiltersFromParamsObject(parsedParams);
// 解析 orders -> sort 数组
if (parsed.orders) {
let ordersObj: Record<string, any> = {};
if (typeof parsed.orders === 'string') {
ordersObj = JSON.parse(parsed.orders);
} else if (typeof parsed.orders === 'object') {
ordersObj = parsed.orders;
}
if (ordersObj && typeof ordersObj === 'object') {
result.orders = Object.entries(ordersObj).map(([field, dir]) => ({
field,
dir
}));
}
}
return result;
} catch (error) {
console.error('解析 anchorParamJson 失败:', error);
return [];
return result;
}
};
@ -183,6 +229,10 @@ export const useMapStore = defineStore('map', () => {
const allowMedium = currentZoom > ENG_POINT_MEDIUM_VISIBLE_ZOOM;
return sourceData.filter((item: any) => {
if (!isEngPointRuntimeItem(item)) {
return true;
}
const state = String(item?.anchoPointState || '');
if (ENG_POINT_LARGE_STATES.includes(state)) {
return true;
@ -985,14 +1035,21 @@ export const useMapStore = defineStore('map', () => {
layerKey: string,
nameEn: string
): string[] => {
void layerKey;
const normalizedNameEn = normalizeLegendNameEn(nameEn);
if (!normalizedNameEn) {
return [];
}
if (normalizedNameEn.includes('alarm_range_')) {
return [`large_eng_built_${normalizedNameEn}`];
const candidates: string[] = [];
if (layerKey === ENG_ALARM_POINT_LAYER_KEY) {
candidates.push(`mid_eng_built_${normalizedNameEn}`);
candidates.push(`large_eng_built_${normalizedNameEn}`);
} else {
candidates.push(`large_eng_built_${normalizedNameEn}`);
candidates.push(`mid_eng_built_${normalizedNameEn}`);
}
return candidates;
}
return [normalizedNameEn];
@ -1130,9 +1187,12 @@ export const useMapStore = defineStore('map', () => {
requestParams = normalizeRequestParams(requestParams);
const anchorParamFilters = parseAnchorParamFilters(anchorParamJson);
if (anchorParamFilters.length) {
requestParams.filters.push(...anchorParamFilters);
const anchorParamResult = parseAnchorParamJson(anchorParamJson);
if (anchorParamResult.filters.length) {
requestParams.filters.push(...anchorParamResult.filters);
}
if (anchorParamResult.orders) {
requestOrders = anchorParamResult.orders;
}
if (timeRangeLayerKeys.includes(layerKey)) {
@ -1166,7 +1226,7 @@ export const useMapStore = defineStore('map', () => {
}
}
if (layer?.orders) {
if (!requestOrders && layer?.orders) {
try {
const ordersObj = JSON.parse(layer.orders);
const ordersArray = Object.entries(ordersObj).map(([field, dir]) => ({

View File

@ -66,7 +66,7 @@ export const useShuJuTianBaoStore = defineStore('shuJuTianBao', () => {
if (type == 'baseId') {
param = rvcd === 'all' ? {} : { baseId: rvcd };
} else {
param = rvcd === 'all' ? {} : { reachcd: rvcd };
param = rvcd === 'all' ? {} : { rvcd: rvcd };
}
const res = await getEngInfoDropdown(param);
if (res.data && Array.isArray(res.data)) {

View File

@ -5,6 +5,7 @@ export const useUiStore = defineStore('ui', () => {
// 右侧抽屉状态
const drawerOpen = ref(true);
const mapType = ref('2D');
const mapSwitchCompletedTick = ref(0);
// 切换抽屉状态
const toggleDrawer = () => {
@ -16,10 +17,16 @@ export const useUiStore = defineStore('ui', () => {
drawerOpen.value = open;
};
const markMapSwitchCompleted = () => {
mapSwitchCompletedTick.value += 1;
};
return {
drawerOpen,
toggleDrawer,
setDrawerOpen,
mapType
mapType,
mapSwitchCompletedTick,
markMapSwitchCompleted
};
});

View File

@ -2451,10 +2451,10 @@ const themecc_list = [
}
];
const getThemee = (fieldName?: string) => {
let themeecList = themecc_list;
const themeecList = themecc_list;
let getitem: any = {};
if (fieldName && themeecList.length > 0) {
let getitemList = themeecList.filter(
const getitemList = themeecList.filter(
(i: any) => i.fieldName.toUpperCase() == fieldName.toUpperCase()
);
if (getitemList.length > 0) {
@ -2642,16 +2642,19 @@ const timegj = [
* @param props -
* @returns HTML
*/
export function generatePopupHtml(props: any): string {
export function generatePopupHtml(
props: any,
options?: {
forceEng2?: boolean;
}
): string {
const p = props;
const dzFdl = getUnitConfigByCode('Other', 'FDLYKW')?.unit;
const dzZjrl = getUnitConfigByCode('Other', 'ZJRL')?.unit;
const title = p?.ennm || p?.stnm;
const titleClass = getTitleClass(title);
let newsttpMap = p.sttpMap;
console.log('newsttpMap', newsttpMap);
if (p.qecInterval) {
if (options?.forceEng2) {
newsttpMap = 'eng2';
}
let html = '';
@ -2714,7 +2717,7 @@ export function generatePopupHtml(props: any): string {
} else {
lastTmEngEqDataVo = p.lastTmEngEqDataVo;
}
if (!lastTmEngEqDataVo?.qo && !lastTmEngEqDataVo?.qi) return '';
// if (!lastTmEngEqDataVo?.qo && !lastTmEngEqDataVo?.qi) return '';
const qecLimitVal = lastTmEngEqDataVo?.qecLimit;
let qecLimitDisplay = '-';
@ -3587,3 +3590,29 @@ export function generatePopupHtml(props: any): string {
return html ? `<div>${html}</div>` : '';
}
export function getNormalizedPopupSttpMap(props: any): string {
return String(props?._sttpMap || props?.sttpMap || props?.popupSttpMap || '')
.trim()
.toUpperCase();
}
export function isEngPopupFamily(props: any): boolean {
return getNormalizedPopupSttpMap(props).startsWith('ENG');
}
export function shouldPreferEng2Popup(
props: any,
options?: {
forceEng2?: boolean;
}
): boolean {
if (options?.forceEng2) {
return true;
}
return (
isEngPopupFamily(props) &&
(!!props?.lastTmEngEqDataVo || !!props?.qecInterval)
);
}

View File

@ -58,7 +58,6 @@ service.interceptors.response.use(
message.error(response.data.msg || '请求失败');
setTimeout(() => {
localStorage.clear();
router.push('/login');
window.location.href = '/';
}, 1000);
return;

View File

@ -246,6 +246,23 @@ const buildSearchParams = (values: any) => {
return {
logic: 'and',
filters: [
values.rvcd && values.rvcd !== 'all'
? {
field: 'rvcd',
operator: 'contains',
dataType: 'string',
value: values.rvcd
}
: null,
values.rstcd
? {
field: 'stcd',
operator: 'eq',
dataType: 'string',
value: values.rstcd
}
: null,
values.basin
? {
field: 'basin',

View File

@ -510,6 +510,23 @@ const buildSearchParams = (values: any) => {
return {
logic: 'and',
filters: [
values.rvcd && values.rvcd !== 'all'
? {
field: 'rvcd',
operator: 'contains',
dataType: 'string',
value: values.rvcd
}
: null,
values.rstcd
? {
field: 'stcd',
operator: 'eq',
dataType: 'string',
value: values.rstcd
}
: null,
values.ptype && values.ptype !== 'all'
? {
field: 'ptype',

View File

@ -22,7 +22,7 @@ let initialLeft = 0;
let initialTop = 0;
const onMouseDown = (e: MouseEvent) => {
;
isDragging = true;
startX = e.clientX;
startY = e.clientY;

View File

@ -238,6 +238,9 @@ const statusFilter = ref<"online" | "offline">("online");
//
const videoList = ref<any[]>(Array(9).fill(null));
//
const pendingVideoQueue = ref<any[]>([]);
//
const removeVideo = (index: number, nodeKey?: string) => {
const videoData = videoList.value[index];
@ -255,6 +258,19 @@ const removeVideo = (index: number, nodeKey?: string) => {
}
//
videoList.value[index] = null;
//
fillEmptySlot(index);
};
//
const fillEmptySlot = (emptyIndex: number) => {
if (pendingVideoQueue.value.length > 0) {
const nextVideo = pendingVideoQueue.value.shift();
if (nextVideo) {
videoList.value[emptyIndex] = nextVideo;
}
}
};
//
@ -269,18 +285,49 @@ const addVideo = (videoData: any) => {
} else if (videoList.value.filter((v, i) => i < maxCount).length < maxCount) {
videoList.value.push(videoData);
} else {
//
videoList.value[0] = videoData;
//
if (!pendingVideoQueue.value.find((v) => v.key === videoData.key)) {
pendingVideoQueue.value.push(videoData);
}
}
};
//
watch(viewLayout, (newLayout) => {
const maxCount = parseInt(newLayout);
watch(viewLayout, (newLayout, oldLayout) => {
const newMaxCount = parseInt(newLayout);
const oldMaxCount = oldLayout ? parseInt(oldLayout) : newMaxCount;
//
if (newMaxCount < oldMaxCount) {
for (let i = newMaxCount; i < oldMaxCount && i < videoList.value.length; i++) {
const video = videoList.value[i];
if (video && !pendingVideoQueue.value.find((v) => v.key === video.key)) {
pendingVideoQueue.value.push(video);
}
videoList.value[i] = null;
}
}
//
while (videoList.value.length < maxCount) {
while (videoList.value.length < newMaxCount) {
videoList.value.push(null);
}
//
if (newMaxCount > oldMaxCount) {
const availableSlots = newMaxCount - oldMaxCount;
for (let i = 0; i < availableSlots && pendingVideoQueue.value.length > 0; i++) {
const emptyIndex = videoList.value.findIndex(
(v, i) => i < newMaxCount && (v === null || v === undefined)
);
if (emptyIndex !== -1) {
const nextVideo = pendingVideoQueue.value.shift();
if (nextVideo) {
videoList.value[emptyIndex] = nextVideo;
}
}
}
}
});
//
@ -408,8 +455,8 @@ const handlePlayRecord = (record: any) => {
if (videoPath) {
const maxCount = parseInt(viewLayout.value);
//
const existingIndex = videoList.value.findIndex((v) => v && v.flnm === record.flnm);
//
const existingIndex = videoList.value.findIndex((v) => v && v.url === videoPath);
if (existingIndex !== -1) {
return;
@ -560,10 +607,11 @@ const initTree = async () => {
},
};
//
//
videoList.value = Array(9).fill(null);
checkedKeys.value = [];
expandedKeys.value = [];
pendingVideoQueue.value = [];
treeLoading.value = true;
try {
@ -679,6 +727,7 @@ const handleMonitorTypeChange = () => {
//
videoList.value = Array(9).fill(null);
checkedKeys.value = [];
pendingVideoQueue.value = [];
}
// 线
@ -729,11 +778,15 @@ const handleTreeSelect = (checkedKeysValue: any[], info: any) => {
// checkedKeys
addParentKeys(nodeData.key);
} else {
// 使 setTimeout Tree
setTimeout(() => {
// 使 filter
checkedKeys.value = checkedKeys.value.filter((key) => key !== nodeData.key);
}, 50);
//
if (!pendingVideoQueue.value.find((v) => v.key === nodeData.key)) {
pendingVideoQueue.value.push({
flnm: nodeData.name,
stnm: nodeData.stnm,
url: videoUrl,
...nodeData,
});
}
}
} else {
}
@ -744,6 +797,17 @@ const handleTreeSelect = (checkedKeysValue: any[], info: any) => {
} else {
//
if (!nodeData.children || nodeData.children.length === 0) {
//
const queueIndex = pendingVideoQueue.value.findIndex(
(v) => v.key === nodeData.key
);
if (queueIndex !== -1) {
//
pendingVideoQueue.value.splice(queueIndex, 1);
removeUnnecessaryParentKeys(nodeData.key);
return;
}
//
removeVideoByNodeKey(nodeData.key);
@ -912,16 +976,23 @@ const addVideosFromNodeWithLimit = (node: any) => {
const remainingSlots = maxCount - currentCount;
if (remainingSlots <= 0) {
//
setTimeout(() => {
cancelNodeCheck(node);
}, 100);
//
allLeafNodes.forEach((leafNode: any) => {
if (!pendingVideoQueue.value.find((v) => v.key === leafNode.key)) {
pendingVideoQueue.value.push({
flnm: leafNode.name,
stnm: leafNode.stnm,
url: leafNode.url || leafNode.videoUrl || leafNode.playUrl,
...leafNode,
});
}
});
return;
}
// remainingSlots
const nodesToAdd = allLeafNodes.slice(0, remainingSlots);
const nodesToCancel = allLeafNodes.slice(remainingSlots);
const nodesToQueue = allLeafNodes.slice(remainingSlots);
//
nodesToAdd.forEach((leafNode: any) => {
@ -938,37 +1009,26 @@ const addVideosFromNodeWithLimit = (node: any) => {
}
});
//
if (nodesToCancel.length > 0) {
setTimeout(() => {
cancelNodesCheck(nodesToCancel);
//
nodesToAdd.forEach((leafNode: any) => {
addParentKeys(leafNode.key);
//
if (nodesToQueue.length > 0) {
nodesToQueue.forEach((leafNode: any) => {
if (!pendingVideoQueue.value.find((v) => v.key === leafNode.key)) {
pendingVideoQueue.value.push({
flnm: leafNode.name,
stnm: leafNode.stnm,
url: leafNode.url || leafNode.videoUrl || leafNode.playUrl,
...leafNode,
});
}, 50);
} else {
//
setTimeout(() => {
nodesToAdd.forEach((leafNode: any) => {
addParentKeys(leafNode.key);
});
}, 50);
}
};
//
const cancelNodeCheck = (node: any) => {
// 使 filter
checkedKeys.value = checkedKeys.value.filter((key) => key !== node.key);
};
//
const cancelNodesCheck = (nodes: any[]) => {
nodes.forEach((node: any) => {
cancelNodeCheck(node);
});
}
//
setTimeout(() => {
nodesToAdd.forEach((leafNode: any) => {
addParentKeys(leafNode.key);
});
}, 50);
};
//
@ -986,9 +1046,20 @@ const removeVideosFromNode = (node: any) => {
// key
const removeVideoByNodeKey = (nodeKey: string) => {
//
const queueIndex = pendingVideoQueue.value.findIndex(
(v) => v.key === nodeKey
);
if (queueIndex !== -1) {
pendingVideoQueue.value.splice(queueIndex, 1);
return;
}
const index = videoList.value.findIndex((video) => video && video.key === nodeKey);
if (index !== -1) {
videoList.value[index] = null;
//
fillEmptySlot(index);
}
};

View File

@ -354,8 +354,8 @@ export const handleCellEdit = (params: any, fWorksheet: any, year: string) => {
);
// 重建该组的所有单元格引用
let groupCells = [];
let currentDayCount = groupStartDayOfYear;
const groupCells = [];
const currentDayCount = groupStartDayOfYear;
let currentMonth = 1;
let cumulativeDays = 0;
@ -389,7 +389,7 @@ export const handleCellEdit = (params: any, fWorksheet: any, year: string) => {
console.log('组中单元格:', groupCells);
// 构建公式
let formula = `=ROUND(AVERAGE(${groupCells.join(',')}), 1)`;
const formula = `=ROUND(AVERAGE(${groupCells.join(',')}), 1)`;
// 找到该组的结束位置用于显示公式
let endMonth = 1;
@ -446,7 +446,7 @@ export const getParams = (searchData: any, Psort?: any, Rstcd?: boolean) => {
return result.slice(1, -1);
};
// if (searchData) {
let params: any = [];
const params: any = [];
// if (sttpCode) {
// params.push(
// {
@ -783,7 +783,7 @@ export const getParams = (searchData: any, Psort?: any, Rstcd?: boolean) => {
].filter((el: any) => el != null);
}
let filter = {
const filter = {
logic: 'and',
filters: params
};
@ -917,7 +917,7 @@ export const calculateCrossMonthAverages = async (
let groupValues: string[] = []; // 存储单元格引用
let groupPositions: { month: number; day: number }[] = []; // 记录每个值的位置
let validDataCount = 0; // 记录当前组中有效数据的数量
let protectedRanges: any[] = [];
const protectedRanges: any[] = [];
for (let month = 1; month <= 12; month++) {
const dataRow = month * 2 + 1; // 当前月份的数据行
const daysInMonth = getDaysInMonth(year, month); // 根据年份获取该月天数
@ -958,7 +958,7 @@ export const calculateCrossMonthAverages = async (
if (validDataCount > 0) {
// 构建平均值公式
const range = groupValues.join(',');
let formula = `=ROUND(AVERAGE(${range}), 1)`;
const formula = `=ROUND(AVERAGE(${range}), 1)`;
// 在组的最后一天显示平均值
const lastPosition = groupPositions[groupValues.length - 1];
@ -989,7 +989,7 @@ export const calculateCrossMonthAverages = async (
if (groupValues.length > 0 && groupValues.length < 5 && validDataCount > 0) {
// 构建平均值公式
const range = groupValues.join(',');
let formula = `=ROUND(AVERAGE(${range}), 1)`;
const formula = `=ROUND(AVERAGE(${range}), 1)`;
// 在组的最后一天显示平均值
const lastPosition = groupPositions[groupValues.length - 1];

View File

@ -0,0 +1,828 @@
<template>
<a-modal
:title="isEdit ? '编辑沿程配置' : '新增沿程配置'"
v-model:open="modalVisible"
:confirm-loading="localLoading"
width="1200px"
:destroy-on-close="true"
ok-text="保存"
@cancel="handleCancel"
@ok="handleOk"
>
<a-form
ref="formRef"
:model="formData"
:rules="rules"
name="edit_form"
:labelCol="{ span: 5 }"
>
<a-row>
<a-col :span="12">
<a-form-item label="沿程配置名称" name="name">
<a-input
v-model:value="formData.name"
style="width: 100%"
placeholder="请输入沿程配置名称"
/> </a-form-item
></a-col>
<a-col :span="12">
<a-form-item label="沿程配置编码" name="code">
<a-input
v-model:value="formData.code"
style="width: 100%"
placeholder="请输入沿程配置编码"
/>
</a-form-item>
</a-col>
</a-row>
<a-row>
<a-col :span="12">
<a-form-item label="所在河段" name="rvcd">
<a-select
v-model:value="formData.rvcd"
show-search
style="width: 100%"
placeholder="请选择所在河段"
:options="riverOptions"
:filter-option="filterOption"
@change="handleRiverSelect"
:disabled="isEdit"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="备注" name="remark">
<a-textarea
:rows="4"
v-model:value="formData.remark"
style="width: 100%"
placeholder="请输入备注"
/>
</a-form-item>
</a-col>
</a-row>
</a-form>
<div class="config-tree-container">
<!-- 新增一级节点区域 -->
<div v-if="addingNewNode" class="add-node-row">
<a-select
v-model:value="newNodeData.stcd"
show-search
placeholder="请选择站点"
style="width: 300px"
:filter-option="filterOption"
@change="handleNodeSelect"
>
<a-select-option
v-for="item in stationList"
:key="item.stcd"
:value="item.stcd"
>
{{ item.stnm }}
</a-select-option>
</a-select>
<a-button
type="primary"
@click="handleSaveNewNode"
style="margin-left: 8px"
>
保存
</a-button>
<a-button @click="handleCancelAdd" style="margin-left: 8px">
<MinusOutlined />
</a-button>
</div>
<a-button v-else class="mb-4" type="primary" @click="handleAdd">
<PlusOutlined /> 新增一级节点
</a-button>
<!-- 树形结构 -->
<a-tree
v-if="treeData.length > 0"
:tree-data="treeData"
:default-expand-all="false"
:selectable="false"
block-node
style="width: 400px"
>
<template #title="{ dataRef }">
<!-- 编辑状态只显示编辑区域 -->
<div
v-if="editingNode && editingNodeKey === dataRef.key"
class="add-child-node-row w-[300px]"
>
<a-select
v-model:value="editNodeData.stcd"
show-search
placeholder="请选择站点"
style="width: 300px"
:filter-option="filterOption"
@change="() => handleEditNodeSelect(dataRef)"
>
<a-select-option
v-for="item in dataRef.parentId
? childStationList
: stationList"
:key="item.stcd"
:value="item.stcd"
>
{{ dataRef.parentId ? item.stnm : item.ennm }}
</a-select-option>
</a-select>
<a-button
type="primary"
@click.stop="handleSaveEditNode"
style="margin-left: 8px"
>
保存
</a-button>
<a-button
@click.stop="handleCancelEditNode"
style="margin-left: 8px"
>
<MinusOutlined />
</a-button>
</div>
<!-- 非编辑状态显示正常节点内容 -->
<template v-else>
<div class="tree-node-wrapper">
<span class="tree-node-title"> {{ getNodeTitle(dataRef) }} </span>
<div class="tree-node-actions">
<!-- 一级节点支持新增子节点编辑排序删除 -->
<template v-if="!dataRef.parentId">
<a-button
type="link"
size="small"
@click.stop="handleAddChild(dataRef)"
:title="
dataRef.sttpName
? `子级添加-${dataRef.sttpName}`
: '子级添加'
"
>
<PlusOutlined />
</a-button>
<a-button
type="link"
size="small"
@click.stop="handleEditNode(dataRef)"
title="编辑"
>
<EditOutlined />
</a-button>
<a-button
type="link"
size="small"
@click.stop="handleMoveUp(dataRef)"
title="上移"
:disabled="isFirstNode(dataRef)"
>
<ArrowUpOutlined />
</a-button>
<a-button
type="link"
size="small"
@click.stop="handleMoveDown(dataRef)"
title="下移"
:disabled="isLastNode(dataRef)"
>
<ArrowDownOutlined />
</a-button>
<a-button
type="link"
size="small"
danger
@click.stop="handleDeleteNode(dataRef)"
title="删除"
>
<DeleteOutlined />
</a-button>
</template>
<!-- 二级节点只支持编辑排序删除 -->
<template v-else>
<a-button
type="link"
size="small"
@click.stop="handleEditNode(dataRef)"
title="编辑"
>
<EditOutlined />
</a-button>
<a-button
type="link"
size="small"
@click.stop="handleMoveUp(dataRef)"
title="上移"
:disabled="isFirstNode(dataRef)"
>
<ArrowUpOutlined />
</a-button>
<a-button
type="link"
size="small"
@click.stop="handleMoveDown(dataRef)"
title="下移"
:disabled="isLastNode(dataRef)"
>
<ArrowDownOutlined />
</a-button>
<a-button
type="link"
size="small"
danger
@click.stop="handleDeleteNode(dataRef)"
title="删除"
>
<DeleteOutlined />
</a-button>
</template>
</div>
</div>
<!-- 子节点添加区域 -->
<div
v-if="addingChildNode && currentParentKey === dataRef.key"
class="add-child-node-row"
>
<a-select
v-model:value="childNodeData.stcd"
show-search
placeholder="请选择站点"
style="width: 300px"
:filter-option="filterOption"
@change="handleChildNodeSelect"
>
<a-select-option
v-for="item in childStationList"
:key="item.stcd"
:value="item.stcd"
>
{{ item.stnm }}-{{ item.sttpName }}
</a-select-option>
</a-select>
<a-button
type="primary"
@click.stop="handleSaveChildNode"
style="margin-left: 8px"
>
保存
</a-button>
<a-button
@click.stop="handleCancelAddChild"
style="margin-left: 8px"
>
<MinusOutlined />
</a-button>
</div>
</template>
</template>
</a-tree>
<a-empty v-else description="暂无数据" style="margin-top: 20px" />
</div>
</a-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, watch } from 'vue';
import { message, Modal } from 'ant-design-vue';
import type { Rule } from 'ant-design-vue/es/form';
import dayjs from 'dayjs';
import {
PlusOutlined,
MinusOutlined,
EditOutlined,
DeleteOutlined,
ArrowUpOutlined,
ArrowDownOutlined
} from '@ant-design/icons-vue';
import { getEngInfoDropdown, getAddvcdDropdownByRvcd } from '@/api/select';
interface Props {
visible: boolean;
initialValues?: any | null;
loading?: boolean;
riverOptions?: any[];
}
const localLoading = ref(false);
const props = withDefaults(defineProps<Props>(), {
visible: false,
initialValues: null,
loading: false,
riverOptions: () => []
});
const modalVisible = computed({
get: () => props.visible,
set: val => emit('update:visible', val)
});
const emit = defineEmits<{
(e: 'update:visible', value: boolean): void;
(e: 'cancel'): void;
(e: 'ok', values: any): void;
}>();
const formRef = ref();
const defaultFormData = reactive({
id: undefined,
name: undefined,
code: undefined,
rvcd: undefined,
remark: undefined
});
const formData: any = reactive({ ...defaultFormData });
const rules: Record<string, Rule[]> = {
name: [{ required: true, message: '请输入沿程配置名称', trigger: 'blur' }],
code: [{ required: true, message: '请输入配置编码', trigger: 'blur' }],
rvcd: [{ required: true, message: '请选择所在河段', trigger: 'change' }]
};
const handleRiverSelect = async (value: string) => {
const res = await getEngInfoDropdown({ rvcd: value });
stationList.value = (res.data || []).map((item: any) => ({
...item,
stnm: item.ennm
}));
};
const isEdit = computed(() => !!props.initialValues);
//
const treeData = ref<any[]>([]);
//
const addingNewNode = ref(false);
const newNodeData = ref({
stcd: undefined,
ennm: '',
sttpName: ''
});
//
const addingChildNode = ref(false);
const currentParentKey = ref<string | null>(null);
const childNodeData = ref({
stcd: undefined,
ennm: '',
sttpName: ''
});
//
const editingNode = ref(false);
const editingNodeKey = ref<string | null>(null);
const editNodeData = ref({
stcd: undefined,
ennm: '',
sttpName: '',
originalData: null as any
});
const stationList = ref([]);
const childStationList = ref([]);
// ID
let nodeIdCounter = 0;
const generateNodeId = () => `node_${Date.now()}_${++nodeIdCounter}`;
//
const getNodeTitle = (dataRef: any) => {
if (!dataRef.parentId) {
return dataRef.ennm || '';
} else {
return `${dataRef.ennm || ''} ${dataRef.sttpName || ''}`;
}
};
//
const filterOption = (inputValue: string, option: any) => {
const label = option.label || option.name || '';
const reachcdName = option.reachcdName || '';
const keyword = inputValue || '';
return label.indexOf(keyword) !== -1 || reachcdName.indexOf(keyword) !== -1;
};
//
const isFirstNode = (dataRef: any) => {
const siblings = getSiblings(dataRef);
return siblings.length > 0 && siblings[0].key === dataRef.key;
};
//
const isLastNode = (dataRef: any) => {
const siblings = getSiblings(dataRef);
return (
siblings.length > 0 && siblings[siblings.length - 1].key === dataRef.key
);
};
//
const getSiblings = (dataRef: any) => {
if (!dataRef.parentId) {
//
return treeData.value;
} else {
// children
const parent = findNodeByKey(treeData.value, dataRef.parentId);
return parent?.children || [];
}
};
// key
const findNodeByKey = (nodes: any[], key: string): any => {
for (const node of nodes) {
if (node.key === key) {
return node;
}
if (node.children && node.children.length > 0) {
const found = findNodeByKey(node.children, key);
if (found) return found;
}
}
return null;
};
//
const handleAdd = () => {
if (!formData.rvcd) {
message.warning('请先选择所在河段');
return;
}
addingNewNode.value = true;
newNodeData.value = {
stcd: undefined,
ennm: '',
sttpName: ''
};
};
//
const handleNodeSelect = (value: string) => {
const station = stationList.value.find(item => item.stcd === value);
if (station) {
newNodeData.value.ennm = station.stnm;
newNodeData.value.sttpName = station.sttpName;
}
};
//
const handleSaveNewNode = () => {
if (!newNodeData.value.stcd) {
message.error('请选择站点');
return;
}
const newNode = {
key: generateNodeId(),
stcd: newNodeData.value.stcd,
ennm: newNodeData.value.ennm,
sttpName: newNodeData.value.sttpName,
parentId: null,
children: []
};
treeData.value.push(newNode);
addingNewNode.value = false;
message.success('添加成功');
};
//
const handleCancelAdd = () => {
addingNewNode.value = false;
newNodeData.value = {
stcd: undefined,
ennm: '',
sttpName: ''
};
};
//
const handleAddChild = async (parentData: any) => {
addingChildNode.value = true;
currentParentKey.value = parentData.key;
childNodeData.value = {
stcd: undefined,
ennm: '',
sttpName: ''
};
const res = await getAddvcdDropdownByRvcd({ stcd: parentData.stcd });
childStationList.value = res.data || [];
};
//
const handleChildNodeSelect = (value: string) => {
const station = childStationList.value.find(item => item.stcd === value);
if (station) {
childNodeData.value.ennm = station.stnm;
childNodeData.value.sttpName = station.sttpName;
}
};
//
const handleSaveChildNode = () => {
if (!childNodeData.value.stcd) {
message.error('请选择站点');
return;
}
const parent = findNodeByKey(treeData.value, currentParentKey.value!);
if (!parent) {
message.error('父节点不存在');
return;
}
const newChild = {
key: generateNodeId(),
stcd: childNodeData.value.stcd,
ennm: childNodeData.value.ennm,
sttpName: childNodeData.value.sttpName,
parentId: parent.key,
children: []
};
if (!parent.children) {
parent.children = [];
}
//
parent.children.unshift(newChild);
addingChildNode.value = false;
currentParentKey.value = null;
message.success('添加子节点成功');
};
//
const handleCancelAddChild = () => {
addingChildNode.value = false;
currentParentKey.value = null;
childNodeData.value = {
stcd: undefined,
ennm: '',
sttpName: ''
};
};
//
const handleEditNode = async (dataRef: any) => {
editingNode.value = true;
editingNodeKey.value = dataRef.key;
//
editNodeData.value = {
stcd: dataRef.stcd,
ennm: dataRef.ennm,
sttpName: dataRef.sttpName,
originalData: { ...dataRef }
};
if (dataRef.parentId) {
const parent = findNodeByKey(treeData.value, dataRef.parentId);
if (parent) {
const res = await getAddvcdDropdownByRvcd({ stcd: parent.stcd });
childStationList.value = res.data || [];
}
}
};
//
const handleEditNodeSelect = (dataRef: any) => {
const value = editNodeData.value.stcd;
const list = dataRef.parentId ? childStationList.value : stationList.value;
const station = list.find(item => item.stcd === value);
if (station) {
editNodeData.value.ennm = station.stnm;
editNodeData.value.sttpName = station.sttpName;
editNodeData.value.stcd = station.stcd;
}
};
//
const handleSaveEditNode = () => {
if (!editNodeData.value.stcd) {
message.error('请选择站点');
return;
}
//
const node = findNodeByKey(treeData.value, editingNodeKey.value!);
if (!node) {
message.error('节点不存在');
return;
}
//
node.stcd = editNodeData.value.stcd;
node.ennm = editNodeData.value.ennm;
node.sttpName = editNodeData.value.sttpName;
editingNode.value = false;
editingNodeKey.value = null;
message.success('编辑成功');
};
//
const handleCancelEditNode = () => {
editingNode.value = false;
editingNodeKey.value = null;
editNodeData.value = {
stcd: undefined,
ennm: '',
sttpName: '',
originalData: null
};
};
//
const handleMoveUp = (dataRef: any) => {
const siblings = getSiblings(dataRef);
const index = siblings.findIndex(item => item.key === dataRef.key);
if (index > 0) {
//
[siblings[index - 1], siblings[index]] = [
siblings[index],
siblings[index - 1]
];
message.success('上移成功');
}
};
//
const handleMoveDown = (dataRef: any) => {
const siblings = getSiblings(dataRef);
const index = siblings.findIndex(item => item.key === dataRef.key);
if (index < siblings.length - 1) {
//
[siblings[index], siblings[index + 1]] = [
siblings[index + 1],
siblings[index]
];
message.success('下移成功');
}
};
//
const handleDeleteNode = (dataRef: any) => {
Modal.confirm({
title: '确认删除',
content: `确定要删除节点"${dataRef.ennm}"吗?`,
okText: '确定',
cancelText: '取消',
onOk: () => {
if (!dataRef.parentId) {
//
const index = treeData.value.findIndex(
item => item.key === dataRef.key
);
if (index > -1) {
treeData.value.splice(index, 1);
}
} else {
//
const parent = findNodeByKey(treeData.value, dataRef.parentId);
if (parent && parent.children) {
const index = parent.children.findIndex(
item => item.key === dataRef.key
);
if (index > -1) {
parent.children.splice(index, 1);
}
}
}
message.success('删除成功');
}
});
};
const handleOk = async () => {
try {
await formRef.value.validate();
const submitValues = {
...formData,
captureDate: formData.captureDate
? dayjs(formData.captureDate).format('YYYY-MM-DD')
: undefined,
configNodes: treeData.value //
};
emit('ok', submitValues);
} catch (error) {
console.error('Validate Failed:', error);
message.error('请检查表单填写是否正确');
}
};
const initForm = async () => {
if (props.initialValues) {
const values = props.initialValues;
Object.keys(formData).forEach(key => {
if (values.hasOwnProperty(key)) {
if (key === 'captureDate' && values[key]) {
formData[key] = dayjs(values[key]);
} else {
formData[key] = values[key];
}
}
});
//
if (values.configNodes && Array.isArray(values.configNodes)) {
treeData.value = values.configNodes.map((node: any) => ({
...node,
children:
node.children?.map((child: any) => ({
...child
})) || []
}));
} else {
treeData.value = [];
}
//
if (formData.rvcd) {
const res = await getEngInfoDropdown({ rvcd: formData.rvcd });
stationList.value = (res.data || []).map((item: any) => ({
...item,
stnm: item.ennm
}));
}
} else {
resetForm();
}
};
const resetForm = () => {
if (formRef.value) {
formRef.value.resetFields();
}
Object.assign(formData, defaultFormData);
treeData.value = [];
addingNewNode.value = false;
};
const handleCancel = () => {
emit('update:visible', false);
emit('cancel');
resetForm();
};
watch(
() => props.visible,
newVisible => {
if (newVisible) {
initForm();
}
},
{ immediate: false }
);
defineExpose({
localLoading
});
</script>
<style scoped lang="scss">
.config-tree-container {
margin-top: 16px;
.add-node-row {
display: flex;
align-items: center;
margin-bottom: 16px;
}
}
.tree-node-wrapper {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
padding-right: 8px;
.tree-node-title {
flex: 1;
}
.tree-node-actions {
opacity: 0;
transition: opacity 0.2s;
}
}
//
.tree-node-wrapper:hover {
.tree-node-actions {
opacity: 1;
}
}
//
.add-child-node-row {
display: flex;
align-items: center;
padding: 8px 0 8px 24px;
margin-top: 4px;
background-color: #f5f5f5;
border-radius: 4px;
}
</style>

View File

@ -0,0 +1,83 @@
<template>
<div class="content-search">
<BasicSearch
ref="basicSearchRef"
:searchList="searchList"
:initial-values="initSearchData"
@reset="handleReset"
@finish="onSearchFinish"
@values-change="onValuesChange"
>
<template #actions>
<a-tooltip title="新增配置">
<a-button @click="props.handleAdd" type="primary">
新增沿程配置
</a-button>
</a-tooltip>
</template>
</BasicSearch>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, onMounted } from 'vue';
import BasicSearch from '@/components/BasicSearch/index.vue';
interface Props {
handleAdd: () => void;
riverOptions: any[];
riverOptionsLoad: boolean;
}
const props = defineProps<Props>();
const emit = defineEmits<{
(e: 'reset', values: any): void;
(e: 'searchFinish', values: any): void;
}>();
const initSearchData = {
name: null,
rvcd: null
};
const searchData = ref<any>({ ...initSearchData });
const searchList: any = computed(() => [
{
type: 'Input',
name: 'name',
label: '沿程配置名称',
fieldProps: {
allowClear: true
}
},
{
type: 'Select',
name: 'rvcd',
label: '所在河段',
width: 300,
fieldProps: {
allowClear: true
},
options: props.riverOptions,
loading: props.riverOptionsLoad
}
]);
const onSearchFinish = (values: any) => {
emit('searchFinish', values);
};
const onValuesChange = (changedValues: any, allValues: any) => {
searchData.value = { ...searchData.value, ...allValues };
};
const handleReset = () => {
emit('reset', initSearchData);
};
onMounted(() => {
emit('searchFinish', initSearchData);
});
</script>
<style lang="scss"></style>

View File

@ -0,0 +1,379 @@
<!-- d:\wordpack\WholeProcessPlatform\frontend\src\views\system\map\components\ConfigManagement\index.vue -->
<template>
<div class="content">
<ConfigManagementSearch
ref="configManagementSearch"
:handle-add="handleAdd"
:river-options="riverOptions"
:river-options-load="riverOptionsLoad"
@reset="handleReset"
@search-finish="onSearchFinish"
/>
<BasicTable
ref="basicTable"
:columns="columns"
:list-url="getAllConfigTree"
:search-params="searchParams"
>
<template #action="{ record }">
<div class="flex gap-[6px]">
<a-button
class="!p-0"
type="link"
size="small"
@click="handleEdit(record)"
>编辑</a-button
>
<a-button
class="!p-0"
type="link"
size="small"
danger
@click="handleDelete(record)"
>删除</a-button
>
</div>
</template>
</BasicTable>
<ConfigManagementForm
ref="configManagementForm"
v-model:visible="editModalVisible"
:initial-values="currentRecord"
:river-options="riverOptions"
:river-options-load="riverOptionsLoad"
@cancel="editModalCancel"
@ok="handleEditSubmit"
/>
</div>
</template>
<script setup lang="ts">
import { ref, nextTick, onMounted } from 'vue';
import BasicTable from '@/components/BasicTable/index.vue';
import ConfigManagementSearch from './ConfigManagementSearch.vue';
import ConfigManagementForm from './ConfigManagementForm.vue';
import { message, Modal } from 'ant-design-vue';
import {
getAllConfigTree,
deleteBaseWbsb,
saveBaseWbsbChild,
saveBaseWbsbChildDetail,
getChildConfigTree
} from '@/api/system/map/ConfigManagement';
import { getRvcdDropdown } from '@/api/select';
import dayjs from 'dayjs';
//
const basicTable = ref<any>(null);
//
const columns = [
{
title: '序号',
dataIndex: 'index',
key: 'index',
width: 60,
align: 'center',
customRender: ({ index }) => index + 1
},
{
title: '沿程配置名称',
dataIndex: 'name',
key: 'name',
width: 150
},
{
title: '配置编码',
key: 'code',
dataIndex: 'code',
width: 120
},
{
title: '所在河段',
dataIndex: 'rvnm',
key: 'rvnm',
width: 300,
ellipsis: true
},
{
title: '创建人',
key: 'recordUser',
width: 120,
dataIndex: 'recordUser'
},
{
title: '创建时间',
key: 'recordTime',
dataIndex: 'recordTime',
width: 150
},
{
title: '更新时间',
key: 'modifyTime',
dataIndex: 'modifyTime',
width: 150
},
{
title: '备注',
dataIndex: 'remark',
key: 'remark',
width: 360
},
{
title: '操作',
key: 'action',
width: 120,
fixed: 'right',
slots: { customRender: 'action' }
}
];
//
const searchParams = ref({
"sort": [
{
"field": "recordTime",
"dir": "desc"
}
],
});
//
const currentRecord = ref<any | null>(null);
const editModalVisible = ref(false);
//
const handleAdd = () => {
currentRecord.value = null;
editModalVisible.value = true;
};
// '-'
const filterRecord = (record: any) => {
const filtered: any = {};
Object.keys(record).forEach(key => {
if (record[key] !== '-') {
filtered[key] = record[key];
}
});
return filtered;
};
//
const handleEdit = async (record: any) => {
currentRecord.value = filterRecord(record);
const params = {
filter: {
logic: 'and',
filters: [
{
field: 'alongId',
operator: 'eq',
value: record.id
}
]
},
sort: [
{
field: 'sort',
dir: 'asc'
}
]
};
const res = await getChildConfigTree(params);
if (res.code == 0) {
const data = res.data.data || [];
const firstLevelNodes = data.filter((item: any) => !item.rstcd);
const secondLevelNodes = data.filter((item: any) => item.rstcd);
const configNodes = firstLevelNodes.map((node: any, index: number) => {
const children = secondLevelNodes.filter(
(child: any) => child.rstcd === node.stcd
);
return {
key: `parent-${index}`,
stcd: node.stcd,
ennm: node.stnm,
sttpName: node.sttpName,
parentId: null,
children: children.map((child: any, childIndex: number) => ({
key: `child-${index}-${childIndex}`,
stcd: child.stcd,
ennm: child.stnm,
sttpName: child.sttpName,
parentId: `parent-${index}`,
children: []
}))
};
});
currentRecord.value.configNodes = configNodes;
}
editModalVisible.value = true;
};
//
const handleDelete = (record: any) => {
Modal.confirm({
title: '确认删除',
content: '确定要删除选中的记录吗?',
zIndex: 2002,
onOk: async () => {
try {
let res = await deleteBaseWbsb([record.id]);
if (res.code == 0) {
message.success('删除成功');
basicTable.value.refresh();
} else {
message.error('删除失败');
}
} catch (error) {
message.error('删除失败');
return;
}
}
});
};
//
const onSearchFinish = (values: any) => {
const params = {
logic: 'and',
filters: [
values.rvcd
? {
field: 'rvcd',
operator: 'eq',
dataType: 'string',
value: values.rvcd
}
: null,
values.name
? {
field: 'name',
operator: 'contains',
dataType: 'string',
value: values.name
}
: null
].filter(Boolean)
};
basicTable.value.getList(params);
};
//
const handleReset = () => {
basicTable.value.getList({
logic: 'and',
filters: []
});
};
//
const editModalCancel = () => {
currentRecord.value = null;
editModalVisible.value = false;
};
//
const handleEditSubmit = async (values: any) => {
try {
const formData = {
...currentRecord.value,
...values,
modifyTime: dayjs().format('YYYY-MM-DD HH:mm:ss')
};
delete formData.configNodes;
Object.keys(formData).forEach(key => {
if (key.includes('-')) {
delete formData[key];
}
});
let res = await saveBaseWbsbChild(formData);
if (res.code == 0) {
const alongId = res.data;
const treeData = values.configNodes || [];
if (treeData.length > 0) {
const formatTreeData = (
nodes: any[],
parentId: string | null = null,
parentStcd: string | null = null
) => {
return nodes.map(node => ({
alongId,
stcd: node.stcd,
stnm: node.ennm || node.stnm,
sttpName: node.sttpName,
parentId: parentId ? parentId : null,
rstcd: parentStcd || null,
children:
node.children && node.children.length > 0
? formatTreeData(node.children, node.key, node.stcd)
: []
}));
};
const detailData = formatTreeData(treeData);
const detailRes = await saveBaseWbsbChildDetail(detailData);
if (detailRes.code == 0) {
message.success(`保存成功`);
editModalVisible.value = false;
basicTable.value.refresh();
} else {
message.error(`保存子节点失败`);
}
} else {
message.success(`保存成功`);
editModalVisible.value = false;
basicTable.value.refresh();
}
} else {
message.error(`保存失败`);
}
} catch (error) {
message.error(`保存失败`);
editModalVisible.value = false;
return;
}
};
const riverOptions = ref<any>([]);
const riverOptionsLoad = ref(false);
const initOption = () => {
const params = {};
riverOptionsLoad.value = true;
getRvcdDropdown(params).then((res: any) => {
res.data.forEach((item: any) => {
item.value = item.rvcd;
item.label = item.rvnm;
});
riverOptions.value = res.data || [];
riverOptionsLoad.value = false;
});
};
onMounted(() => {
initOption();
});
</script>
<style scoped lang="scss">
.content {
position: relative;
z-index: 900;
pointer-events: all;
width: 100%;
height: 100%;
background-color: #ffffff;
display: flex;
flex-direction: column;
gap: 10px;
box-sizing: border-box;
overflow: hidden;
}
.config-management {
height: 100%;
padding: 16px;
}
</style>

View File

@ -0,0 +1,160 @@
<template>
<a-modal
:title="isEdit ? '编辑电站' : '新增电站'"
v-model:open="modalVisible"
:confirm-loading="localLoading"
width="600px"
:destroy-on-close="true"
@cancel="handleCancel"
@ok="handleOk"
>
<a-form
ref="formRef"
:model="formData"
:rules="rules"
name="edit_form"
:labelCol="{ span: 5 }"
>
<a-form-item label="电站名称" name="stationName">
<a-input
v-model:value="formData.stationName"
style="width: 100%"
placeholder="请输入电站名称"
/>
</a-form-item>
<a-form-item label="所属流域" name="basin">
<a-input
v-model:value="formData.basin"
style="width: 100%"
placeholder="请输入所属流域"
/>
</a-form-item>
<a-form-item label="装机容量(MW)" name="capacity">
<a-input-number
v-model:value="formData.capacity"
style="width: 100%"
placeholder="请输入装机容量"
:min="0"
/>
</a-form-item>
<a-form-item label="状态" name="status">
<a-select v-model:value="formData.status" style="width: 100%">
<a-select-option value="running">运行中</a-select-option>
<a-select-option value="stopped">停运</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="位置坐标" name="coordinates">
<a-input
v-model:value="formData.coordinates"
style="width: 100%"
placeholder="请输入位置坐标"
/>
</a-form-item>
</a-form>
</a-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, watch } from 'vue';
import { message } from 'ant-design-vue';
import type { Rule } from 'ant-design-vue/es/form';
interface Props {
visible: boolean;
initialValues?: any | null;
loading?: boolean;
}
const localLoading = ref(false);
const props = withDefaults(defineProps<Props>(), {
visible: false,
initialValues: null,
loading: false
});
const modalVisible = computed({
get: () => props.visible,
set: val => emit('update:visible', val)
});
const emit = defineEmits<{
(e: 'update:visible', value: boolean): void;
(e: 'cancel'): void;
(e: 'ok', values: any): void;
}>();
const formRef = ref();
const defaultFormData = reactive({
id: undefined,
stationName: undefined,
basin: undefined,
capacity: undefined,
status: undefined,
coordinates: undefined
});
const formData: any = reactive({ ...defaultFormData });
const rules: Record<string, Rule[]> = {
stationName: [{ required: true, message: '请输入电站名称', trigger: 'blur' }],
basin: [{ required: true, message: '请输入所属流域', trigger: 'blur' }],
capacity: [{ required: true, message: '请输入装机容量', trigger: 'blur' }]
};
const isEdit = computed(() => !!props.initialValues);
const handleOk = async () => {
try {
await formRef.value.validate();
const submitValues = {
...formData
};
emit('ok', submitValues);
} catch (error) {
console.error('Validate Failed:', error);
message.error('请检查表单填写是否正确');
}
};
const initForm = () => {
if (props.initialValues) {
const values = props.initialValues;
Object.keys(formData).forEach(key => {
if (values.hasOwnProperty(key)) {
formData[key] = values[key];
}
});
} else {
resetForm();
}
};
const resetForm = () => {
if (formRef.value) {
formRef.value.resetFields();
}
Object.assign(formData, defaultFormData);
};
const handleCancel = () => {
emit('update:visible', false);
emit('cancel');
resetForm();
};
watch(
() => props.visible,
newVisible => {
if (newVisible) {
initForm();
}
},
{ immediate: false }
);
defineExpose({
localLoading
});
</script>
<style scoped lang="scss"></style>

View File

@ -0,0 +1,67 @@
<template>
<div class="content-search">
<BasicSearch
ref="basicSearchRef"
:searchList="searchList"
:initial-values="initSearchData"
@reset="handleReset"
@finish="onSearchFinish"
@values-change="onValuesChange"
>
<template #actions>
<a-tooltip title="新增电站">
<a-button @click="props.handleAdd" type="primary"> 新增电站 </a-button>
</a-tooltip>
</template>
</BasicSearch>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, onMounted } from "vue";
import BasicSearch from "@/components/BasicSearch/index.vue";
interface Props {
handleAdd: () => void;
}
const props = defineProps<Props>();
const emit = defineEmits<{
(e: "reset", values: any): void;
(e: "searchFinish", values: any): void;
}>();
const initSearchData = {
stationName: "",
};
const searchData = ref<any>({ ...initSearchData });
const searchList: any = computed(() => [
{
type: "Input",
name: "stationName",
label: "电站名称",
fieldProps: {
allowClear: true,
},
},
]);
const onSearchFinish = (values: any) => {
emit("searchFinish", values);
};
const onValuesChange = (changedValues: any, allValues: any) => {
searchData.value = { ...searchData.value, ...allValues };
};
const handleReset = () => {
emit("reset", initSearchData);
};
onMounted(() => {
emit("searchFinish", initSearchData);
});
</script>
<style lang="scss"></style>

View File

@ -0,0 +1,203 @@
<!-- d:\wordpack\WholeProcessPlatform\frontend\src\views\system\map\components\StationManagement\index.vue -->
<template>
<div class="content">
<StationManagementSearch
ref="stationManagementSearch"
:handle-add="handleAdd"
@reset="handleReset"
@search-finish="onSearchFinish"
/>
<BasicTable
:columns="columns"
:data="dataSource"
:list-url="fetchStationData"
:search-params="searchParams"
:enable-row-selection="true"
@selection-change="handleSelectionChange"
>
<template #action="{ column, record }">
<a-space>
<a-button type="link" size="small" @click="handleEdit(record)"
>编辑</a-button
>
<a-button
type="link"
size="small"
danger
@click="handleDelete(record)"
>删除</a-button
>
</a-space>
</template>
<template #status="{ column, record }">
<a-tag :color="record.status === 'running' ? 'green' : 'red'">
{{ record.status === 'running' ? '运行中' : '停运' }}
</a-tag>
</template>
</BasicTable>
<StationManagementForm
ref="stationManagementForm"
v-model:visible="editModalVisible"
:initial-values="currentRecord"
@cancel="editModalCancel"
@ok="handleEditSubmit"
/>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import BasicTable from '@/components/BasicTable/index.vue';
import StationManagementSearch from './StationManagementSearch.vue';
import StationManagementForm from './StationManagementForm.vue';
import { message } from 'ant-design-vue';
//
const columns = [
{
title: '电站名称',
dataIndex: 'stationName',
key: 'stationName',
width: 150
},
{
title: '所属流域',
dataIndex: 'basin',
key: 'basin',
width: 120
},
{
title: '装机容量(MW)',
dataIndex: 'capacity',
key: 'capacity',
width: 120
},
{
title: '状态',
key: 'status',
dataIndex: 'status',
width: 100,
slots: { customRender: 'status' }
},
{
title: '位置坐标',
dataIndex: 'coordinates',
key: 'coordinates',
ellipsis: true
},
{
title: '操作',
key: 'action',
width: 150,
fixed: 'right',
slots: { customRender: 'action' }
}
];
//
const searchParams = ref({});
//
const currentRecord = ref<any | null>(null);
const editModalVisible = ref(false);
//
const dataSource = ref([]);
//
const fetchStationData = (params: any) => {
return new Promise(resolve => {
setTimeout(() => {
resolve({
data: {
records: [
{
id: 1,
stationName: '三峡电站',
basin: '长江流域',
capacity: 22500,
status: 'running',
coordinates: '111.29,30.83'
},
{
id: 2,
stationName: '葛洲坝电站',
basin: '长江流域',
capacity: 2715,
status: 'running',
coordinates: '111.28,30.75'
},
{
id: 3,
stationName: '溪洛渡电站',
basin: '金沙江流域',
capacity: 13860,
status: 'stopped',
coordinates: '103.65,28.25'
}
],
total: 3
}
});
}, 500);
});
};
const handleAdd = () => {
currentRecord.value = null;
editModalVisible.value = true;
};
//
const handleEdit = (record: any) => {
currentRecord.value = { ...record };
editModalVisible.value = true;
};
//
const handleDelete = (record: any) => {
message.warning(`删除电站: ${record.stationName}`);
};
//
const onSearchFinish = (values: any) => {
console.log(values);
};
//
const handleReset = (params: any) => {};
//
const handleSelectionChange = (selectedRows: any[]) => {
console.log('选中的行:', selectedRows);
};
//
const editModalCancel = () => {
editModalVisible.value = false;
};
//
const handleEditSubmit = (values: any) => {
console.log(values);
};
</script>
<style scoped lang="scss">
.content {
position: relative;
z-index: 900;
pointer-events: all;
width: 100%;
height: 100%;
background-color: #ffffff;
display: flex;
flex-direction: column;
gap: 10px;
box-sizing: border-box;
overflow: hidden;
}
.station-management {
height: 100%;
padding: 16px;
}
</style>

View File

@ -0,0 +1,249 @@
<template>
<a-modal
:title="isEdit ? '编辑倾斜摄影' : '新增倾斜摄影'"
v-model:open="modalVisible"
:confirm-loading="localLoading"
width="600px"
:destroy-on-close="true"
ok-text="保存"
@cancel="handleCancel"
@ok="handleOk"
>
<a-form
ref="formRef"
:model="formData"
:rules="rules"
name="edit_form"
:labelCol="{ span: 5 }"
>
<a-form-item label="倾斜影像名称" name="stnm">
<a-input
v-model:value="formData.stnm"
style="width: 100%"
placeholder="请输入倾斜影像名称"
/>
</a-form-item>
<a-form-item label="所在河段" name="hbrvcd">
<a-select
v-model:value="formData.hbrvcd"
show-search
style="width: 100%"
placeholder="请选择所在河段"
:options="riverOptions"
:filter-option="filterOption"
:disabled="isEdit"
@change="handleRiverSelect"
/>
</a-form-item>
<a-form-item label="所属电站" name="rstcd">
<a-select
v-model:value="formData.rstcd"
show-search
style="width: 100%"
placeholder="请选择所属电站"
:options="ennmOptions"
:filter-option="filterOption"
:disabled="isEdit"
/>
<!-- <a-input
v-model:value="formData.ennm"
style="width: 100%"
placeholder="请输入所属电站"
/> -->
</a-form-item>
<a-form-item label="倾斜影像URL" name="url">
<a-input
v-model:value="formData.url"
style="width: 100%"
placeholder="请输入倾斜影像URL"
/>
</a-form-item>
<a-form-item label="模型坐标" name="location">
<a-textarea
v-model:value="formData.location"
:rows="4"
style="width: 100%"
placeholder="请输入模型坐标"
/>
</a-form-item>
<a-form-item label="模型精度" name="accuracy">
<a-input-number
v-model:value="formData.accuracy"
style="width: 100%"
placeholder="请输入模型精度"
:min="0"
/>
</a-form-item>
<a-form-item label="边界范围" name="boundary">
<a-textarea
v-model:value="formData.boundary"
:rows="4"
style="width: 100%"
placeholder="请输入边界范围"
/>
</a-form-item>
<a-form-item label="模型高度" name="height">
<a-input-number
v-model:value="formData.height"
style="width: 100%"
placeholder="请输入模型高度"
:min="0"
/>
</a-form-item>
<a-form-item label="备注" name="comments">
<a-textarea
v-model:value="formData.comments"
:rows="4"
style="width: 100%"
placeholder="请输入备注"
/>
</a-form-item>
</a-form>
</a-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, watch } from 'vue';
import { message } from 'ant-design-vue';
import type { Rule } from 'ant-design-vue/es/form';
import dayjs from 'dayjs';
import { getEngInfoDropdown } from '@/api/select';
interface Props {
visible: boolean;
initialValues?: any | null;
loading?: boolean;
riverOptions?: any[];
}
const localLoading = ref(false);
const props = withDefaults(defineProps<Props>(), {
visible: false,
initialValues: null,
loading: false,
riverOptions: () => []
});
const modalVisible = computed({
get: () => props.visible,
set: val => emit('update:visible', val)
});
const emit = defineEmits<{
(e: 'update:visible', value: boolean): void;
(e: 'cancel'): void;
(e: 'ok', values: any): void;
}>();
const formRef = ref();
const defaultFormData = reactive({
id: undefined,
stnm: undefined,
hbrvcd: undefined,
rstcd: undefined,
url: undefined,
location: undefined,
accuracy: undefined,
boundary: undefined,
height: undefined,
comments: undefined
});
const formData: any = reactive({ ...defaultFormData });
const rules: Record<string, Rule[]> = {
stnm: [{ required: true, message: '请输入倾斜影像名称', trigger: 'blur' }],
hbrvcd: [{ required: true, message: '请输入所在河段', trigger: 'blur' }],
rstcd: [{ required: true, message: '请输入所属电站', trigger: 'blur' }],
url: [{ required: true, message: '请输入倾斜影像URL', trigger: 'blur' }],
location: [{ required: true, message: '请输入模型坐标', trigger: 'blur' }]
};
const isEdit = computed(() => !!props.initialValues);
const handleOk = async () => {
try {
await formRef.value.validate();
const ennmItem = ennmOptions.value.find(item => item.value == formData.rstcd);
const riverItem = props.riverOptions.find(item => item.value == formData.hbrvcd);
const submitValues = {
...formData,
ennm: ennmItem?.ennm || '',
hbrvcdName: riverItem?.label || '',
captureDate: formData.captureDate
? dayjs(formData.captureDate).format('YYYY-MM-DD')
: undefined,
};
emit('ok', submitValues);
} catch (error) {
console.error('Validate Failed:', error);
message.error('请检查表单填写是否正确');
}
};
const initForm = () => {
if (props.initialValues) {
const values = props.initialValues;
Object.keys(formData).forEach(key => {
if (values.hasOwnProperty(key)) {
if (key === 'captureDate' && values[key]) {
formData[key] = dayjs(values[key]);
} else {
formData[key] = values[key];
}
}
});
} else {
resetForm();
}
};
const resetForm = () => {
if (formRef.value) {
formRef.value.resetFields();
}
Object.assign(formData, defaultFormData);
};
const handleCancel = () => {
emit('update:visible', false);
emit('cancel');
resetForm();
};
//
const filterOption = (inputValue: string, option: any) => {
const label = option.label || option.name || '';
const reachcdName = option.reachcdName || '';
const keyword = inputValue || '';
return label.indexOf(keyword) !== -1 || reachcdName.indexOf(keyword) !== -1;
};
const ennmOptions = ref([]);
const handleRiverSelect = async (value: any) => {
let res: any = await getEngInfoDropdown({ reachcd: value });
if (res?.data?.length > 0) {
res.data.forEach((item: any) => {
item.value = item.stcd;
item.label = item.ennm;
});
ennmOptions.value = res.data;
} else {
ennmOptions.value = [];
}
};
watch(
() => props.visible,
newVisible => {
if (newVisible) {
initForm();
handleRiverSelect('');
}
},
{ immediate: false }
);
defineExpose({
localLoading
});
</script>
<style scoped lang="scss"></style>

View File

@ -0,0 +1,81 @@
<template>
<div class="content-search">
<BasicSearch
ref="basicSearchRef"
:searchList="searchList"
:initial-values="initSearchData"
@reset="handleReset"
@finish="onSearchFinish"
@values-change="onValuesChange"
>
<template #actions>
<a-tooltip title="新增倾斜摄影">
<a-button @click="props.handleAdd" type="primary"> 新增倾斜摄影 </a-button>
</a-tooltip>
</template>
</BasicSearch>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, onMounted } from "vue";
import BasicSearch from "@/components/BasicSearch/index.vue";
interface Props {
handleAdd: () => void;
riverOptions: any[];
riverOptionsLoad: boolean;
}
const props = defineProps<Props>();
const emit = defineEmits<{
(e: "reset", values: any): void;
(e: "searchFinish", values: any): void;
}>();
const initSearchData = {
stnm: "",
rvcd:""
};
const searchData = ref<any>({ ...initSearchData });
const searchList: any = computed(() => [
{
type: "Input",
name: "stnm",
label: "倾斜影像名称",
fieldProps: {
allowClear: true,
},
},
{
type: 'Select',
name: 'rvcd',
label: '所在河段',
width: 300,
fieldProps: {
allowClear: true
},
options: props.riverOptions,
loading: props.riverOptionsLoad
}
]);
const onSearchFinish = (values: any) => {
emit("searchFinish", values);
};
const onValuesChange = (changedValues: any, allValues: any) => {
searchData.value = { ...searchData.value, ...allValues };
};
const handleReset = () => {
emit("reset", initSearchData);
};
onMounted(() => {
emit("searchFinish", initSearchData);
});
</script>
<style scoped lang="scss"></style>

View File

@ -0,0 +1,269 @@
<!-- d:\wordpack\WholeProcessPlatform\frontend\src\views\system\map\components\TiltPhotoManagement\index.vue -->
<template>
<div class="content">
<TiltPhotoManagementSearch
ref="tiltPhotoManagementSearch"
:river-options="riverOptions"
:river-options-load="riverOptionsLoad"
:handle-add="handleAdd"
@reset="handleReset"
@search-finish="onSearchFinish"
/>
<BasicTable
ref="basicTable"
:columns="columns"
:list-url="getAllTiltPhotoTree"
:search-params="searchParams"
>
<template #action="{ column, record }">
<div class="flex gap-[6px]">
<a-button
class="!p-0"
type="link"
size="small"
@click="handleEdit(record)"
>编辑</a-button
>
<a-button
class="!p-0"
type="link"
size="small"
danger
@click="handleDelete(record)"
>删除</a-button
>
</div>
</template>
</BasicTable>
<TiltPhotoManagementForm
ref="tiltPhotoManagementForm"
v-model:visible="editModalVisible"
:river-options="riverOptions"
:river-options-load="riverOptionsLoad"
:initial-values="currentRecord"
@cancel="editModalCancel"
@ok="handleEditSubmit"
/>
</div>
</template>
<script setup lang="ts">
import { ref, nextTick, onMounted } from 'vue';
import BasicTable from '@/components/BasicTable/index.vue';
import TiltPhotoManagementSearch from './TiltPhotoManagementSearch.vue';
import TiltPhotoManagementForm from './TiltPhotoManagementForm.vue';
import { message, Modal } from 'ant-design-vue';
import {
getAllTiltPhotoTree,
saveTiltPhoto,
deleteTiltPhoto
} from '@/api/system/map/TiltPhotoManagement';
import { getRvcdDropdown } from '@/api/select';
//
const columns = [
{
title: '序号',
dataIndex: 'index',
key: 'index',
width: 60,
align: 'center',
customRender: ({ text, record, index }) => index + 1
},
{
title: '倾斜影像名称',
dataIndex: 'stnm',
key: 'stnm',
width: 150
},
{
title: 'URL',
dataIndex: 'url',
key: 'url'
// width: 360
},
{
title: '所在河段',
dataIndex: 'hbrvcdName',
key: 'hbrvcdName',
width: 280
},
{
title: '所属电站',
key: 'ennm',
dataIndex: 'ennm',
width: 140
},
{
title: '创建人',
key: 'recordUser',
dataIndex: 'recordUser',
width: 150
},
{
title: '创建时间',
key: 'recordTime',
dataIndex: 'recordTime',
width: 150
},
{
title: '更新时间',
key: 'modifyTime',
dataIndex: 'modifyTime',
width: 150
},
{
title: '操作',
key: 'action',
width: 120,
fixed: 'right',
slots: { customRender: 'action' }
}
];
//
const basicTable = ref<any>(null);
//
const searchParams = ref({});
//
const currentRecord = ref<any | null>(null);
const editModalVisible = ref(false);
//
const handleAdd = () => {
currentRecord.value = null;
editModalVisible.value = true;
};
//
const handleEdit = (record: any) => {
currentRecord.value = { ...record };
editModalVisible.value = true;
};
//
const handleDelete = (record: any) => {
Modal.confirm({
title: '确认删除',
content: '确定要删除选中的记录吗?',
zIndex: 2002,
onOk: async () => {
try {
let res = await deleteTiltPhoto({ id: record.id });
message.success('删除成功');
basicTable.value.refresh();
} catch (error) {
message.error('删除失败');
return;
}
}
});
};
//
const onSearchFinish = (values: any) => {
const params = {
logic: 'and',
filters: [
values.parentId
? {
field: 'parentId',
operator: 'eq',
dataType: 'string',
value: values.parentId
}
: null,
values.stnm
? {
field: 'stnm',
operator: 'contains',
dataType: 'string',
value: values.stnm
}
: null,
values.rvcd
? {
field: 'hbrvcd',
operator: 'contains',
dataType: 'string',
value: values.rvcd
}
: null
].filter(Boolean)
};
basicTable.value.getList(params);
};
//
const handleReset = () => {
nextTick(() => {
basicTable.value.getList({
logic: 'and',
filters: []
});
});
};
//
const editModalCancel = () => {
currentRecord.value = null;
editModalVisible.value = false;
};
//
const handleEditSubmit = async (values: any) => {
try {
let res = await saveTiltPhoto({
...currentRecord.value,
...values,
recordTime:currentRecord.value.recordTime != '-' ? currentRecord.value.recordTime : null
});
message.success(`保存成功`);
editModalVisible.value = false;
basicTable.value.refresh();
} catch (error) {
message.error(`保存失败`);
editModalVisible.value = false;
return;
}
};
const riverOptions = ref<any>([]);
const riverOptionsLoad = ref(false);
const initOption = () => {
const params = {};
riverOptionsLoad.value = true;
getRvcdDropdown(params).then((res: any) => {
res.data.forEach((item: any) => {
item.value = item.rvcd;
item.label = item.rvnm;
});
riverOptions.value = res.data || [];
riverOptionsLoad.value = false;
});
};
onMounted(() => {
initOption();
});
</script>
<style scoped lang="scss">
.tilt-photo-management {
height: 100%;
padding: 16px;
}
.content {
position: relative;
z-index: 900;
pointer-events: all;
width: 100%;
height: 100%;
background-color: #ffffff;
display: flex;
flex-direction: column;
gap: 10px;
box-sizing: border-box;
overflow: hidden;
padding: 16px;
}
</style>

View File

@ -20,6 +20,7 @@
v-model:value="formData.title"
style="width: 100%"
placeholder="请输入图层名称"
:maxlength="50"
/>
</a-form-item>
<a-form-item label="图层编码" name="code">

View File

@ -265,7 +265,10 @@ const handleDelete = async (record: any) => {
onOk: async () => {
try {
let res = await deleteMapLayer([record.id]);
if(res.code == 0){
message.success('删除成功');
}
basicTable.value.refresh();
} catch (error) {
message.error('删除失败');

View File

@ -1,4 +1,4 @@
<!-- d:\wordpack\WholeProcessPlatform\frontend\src\views\system\map\components\LegendData\index.vue -->
<!-- d:\wordpack\WholeProcessPlatform\frontend\src\views\system\map\componentsData\index.vue -->
<template>
<div class="content">
<LegendDataSearch

View File

@ -250,7 +250,9 @@ const handleDelete = (record: any) => {
onOk: async () => {
try {
let res = await deleteMapLegend([record.id]);
if (res.code == 0) {
message.success('删除成功');
}
basicTable.value.refresh();
} catch (error) {
message.error('删除失败');
@ -375,11 +377,13 @@ const handleEditSubmit = async (values: any, type: string) => {
...values,
parentId: parentId.value
});
if (res.code == 0) {
if (type === 'switch') {
message.success('修改状态成功');
} else {
message.success(`保存成功`);
}
}
editModalVisible.value = false;
basicTable.value.refresh();
} catch (error) {

View File

@ -23,19 +23,33 @@
placeholder="请输入倾斜影像名称"
/>
</a-form-item>
<a-form-item label="所在河段" name="hbrvcdName">
<a-input
v-model:value="formData.hbrvcdName"
<a-form-item label="所在河段" name="hbrvcd">
<a-select
v-model:value="formData.hbrvcd"
show-search
style="width: 100%"
placeholder="请输入所在河段"
placeholder="请选择所在河段"
:options="riverOptions"
:filter-option="filterOption"
:disabled="isEdit"
@change="handleRiverSelect"
/>
</a-form-item>
<a-form-item label="所属电站" name="ennm">
<a-input
<a-form-item label="所属电站" name="rstcd">
<a-select
v-model:value="formData.rstcd"
show-search
style="width: 100%"
placeholder="请选择所属电站"
:options="ennmOptions"
:filter-option="filterOption"
:disabled="isEdit"
/>
<!-- <a-input
v-model:value="formData.ennm"
style="width: 100%"
placeholder="请输入所属电站"
/>
/> -->
</a-form-item>
<a-form-item label="倾斜影像URL" name="url">
<a-input
@ -76,9 +90,9 @@
:min="0"
/>
</a-form-item>
<a-form-item label="备注" name="description">
<a-form-item label="备注" name="comments">
<a-textarea
v-model:value="formData.description"
v-model:value="formData.comments"
:rows="4"
style="width: 100%"
placeholder="请输入备注"
@ -89,33 +103,34 @@
</template>
<script lang="ts" setup>
import { ref, reactive, computed, watch } from "vue";
import { message } from "ant-design-vue";
import type { Rule } from "ant-design-vue/es/form";
import dayjs from "dayjs";
import { ref, reactive, computed, watch } from 'vue';
import { message } from 'ant-design-vue';
import type { Rule } from 'ant-design-vue/es/form';
import dayjs from 'dayjs';
import { getEngInfoDropdown } from '@/api/select';
interface Props {
visible: boolean;
initialValues?: any | null;
loading?: boolean;
riverOptions?: any[];
}
const localLoading = ref(false);
const props = withDefaults(defineProps<Props>(), {
visible: false,
initialValues: null,
loading: false,
riverOptions: () => []
});
const modalVisible = computed({
get: () => props.visible,
set: (val) => emit("update:visible", val),
set: val => emit('update:visible', val)
});
const emit = defineEmits<{
(e: "update:visible", value: boolean): void;
(e: "cancel"): void;
(e: "ok", values: any): void;
(e: 'update:visible', value: boolean): void;
(e: 'cancel'): void;
(e: 'ok', values: any): void;
}>();
const formRef = ref();
@ -123,23 +138,23 @@ const formRef = ref();
const defaultFormData = reactive({
id: undefined,
stnm: undefined,
hbrvcdName: undefined,
ennm: undefined,
hbrvcd: undefined,
rstcd: undefined,
url: undefined,
location: undefined,
accuracy: undefined,
boundary: undefined,
height: undefined,
description: undefined,
comments: undefined
});
const formData: any = reactive({ ...defaultFormData });
const rules: Record<string, Rule[]> = {
stnm: [{ required: true, message: "请输入倾斜影像名称", trigger: "blur" }],
hbrvcdName: [{ required: true, message: "请输入所在河段", trigger: "blur" }],
ennm: [{ required: true, message: "请输入所属电站", trigger: "blur" }],
url: [{ required: true, message: "请输入倾斜影像URL", trigger: "blur" }],
location: [{ required: true, message: "请输入模型坐标", trigger: "blur" }],
stnm: [{ required: true, message: '请输入倾斜影像名称', trigger: 'blur' }],
hbrvcd: [{ required: true, message: '请输入所在河段', trigger: 'blur' }],
rstcd: [{ required: true, message: '请输入所属电站', trigger: 'blur' }],
url: [{ required: true, message: '请输入倾斜影像URL', trigger: 'blur' }],
location: [{ required: true, message: '请输入模型坐标', trigger: 'blur' }]
};
const isEdit = computed(() => !!props.initialValues);
@ -147,25 +162,33 @@ const isEdit = computed(() => !!props.initialValues);
const handleOk = async () => {
try {
await formRef.value.validate();
const ennmItem = ennmOptions.value.find(item => item.value == formData.rstcd);
const riverItem = props.riverOptions.find(item => item.value == formData.hbrvcd);
const submitValues = {
...formData,
ennm: ennmItem?.ennm || '',
lgtd: ennmItem?.lgtd || '',
lttd: ennmItem?.lttd || '',
hbrvcdName: riverItem?.label || '',
baseId: riverItem?.baseId || '',
baseName: riverItem?.baseName || '',
captureDate: formData.captureDate
? dayjs(formData.captureDate).format("YYYY-MM-DD")
: undefined,
? dayjs(formData.captureDate).format('YYYY-MM-DD')
: undefined
};
emit("ok", submitValues);
emit('ok', submitValues);
} catch (error) {
console.error("Validate Failed:", error);
message.error("请检查表单填写是否正确");
console.error('Validate Failed:', error);
message.error('请检查表单填写是否正确');
}
};
const initForm = () => {
if (props.initialValues) {
const values = props.initialValues;
Object.keys(formData).forEach((key) => {
Object.keys(formData).forEach(key => {
if (values.hasOwnProperty(key)) {
if (key === "captureDate" && values[key]) {
if (key === 'captureDate' && values[key]) {
formData[key] = dayjs(values[key]);
} else {
formData[key] = values[key];
@ -185,23 +208,44 @@ const resetForm = () => {
};
const handleCancel = () => {
emit("update:visible", false);
emit("cancel");
emit('update:visible', false);
emit('cancel');
resetForm();
};
//
const filterOption = (inputValue: string, option: any) => {
const label = option.label || option.name || '';
const reachcdName = option.reachcdName || '';
const keyword = inputValue || '';
return label.indexOf(keyword) !== -1 || reachcdName.indexOf(keyword) !== -1;
};
const ennmOptions = ref([]);
const handleRiverSelect = async (value: any) => {
let res: any = await getEngInfoDropdown({ reachcd: value });
if (res?.data?.length > 0) {
res.data.forEach((item: any) => {
item.value = item.stcd;
item.label = item.ennm;
});
ennmOptions.value = res.data;
} else {
ennmOptions.value = [];
}
};
watch(
() => props.visible,
(newVisible) => {
newVisible => {
if (newVisible) {
initForm();
handleRiverSelect('');
}
},
{ immediate: false }
);
defineExpose({
localLoading,
localLoading
});
</script>

View File

@ -23,6 +23,8 @@ import BasicSearch from "@/components/BasicSearch/index.vue";
interface Props {
handleAdd: () => void;
riverOptions: any[];
riverOptionsLoad: boolean;
}
const props = defineProps<Props>();
@ -34,6 +36,7 @@ const emit = defineEmits<{
const initSearchData = {
stnm: "",
rvcd:""
};
const searchData = ref<any>({ ...initSearchData });
@ -47,13 +50,16 @@ const searchList: any = computed(() => [
},
},
{
type: "Select",
name: "hbrvcdName",
label: "所在河段",
type: 'Select',
name: 'rvcd',
label: '所在河段',
width: 300,
fieldProps: {
allowClear: true,
},
allowClear: true
},
options: props.riverOptions,
loading: props.riverOptionsLoad
}
]);
const onSearchFinish = (values: any) => {

Some files were not shown because too many files have changed in this diff Show More